Plugins - The jQuery API - Web Development with jQuery (2015)

Web Development with jQuery (2015)

Part I. The jQuery API

Chapter 9. Plugins

Beyond making many scripting tasks much easier, jQuery also makes itself easy to extend with new functionality. This is done with an easy-to-understand Plugin API. Using jQuery's Plugin API, you can make your own chainable jQuery methods and even write entire complex client-side applications completely as jQuery plugins.

There are a lot of things you can do with plugins. Some of the more useful and prominent examples of jQuery plugins are found in the jQuery UI library, which I discuss in more detail in Chapter 12, “Draggable and Droppable.” Plugins in the jQuery UI library help you to implement functionality like drag-and-drop or selecting elements, and a variety of other functionality. There is also a thriving third-party development community for jQuery that produces plugins for just about anything you can think of. You'll examine a few third-party jQuery plugins and even write one in Part II, “jQuery UI,” of this book. jQuery's thriving plugin community exists largely thanks to how ridiculously easy it is to write plugins for jQuery.

This chapter demonstrates how to use jQuery's Plugin API and covers the basic concepts you need to understand to start writing plugins of your own. Beyond what you learn about jQuery plugin basics in this chapter, you also see more examples that use jQuery's Plugin API later in the book.

Writing a Plugin

jQuery plugins are easy to implement. All you need to do is pass an object literal containing the methods you want to extend jQuery with to the $.fn.extend() method.

Writing a Simple jQuery Plugin

Example 9-1 demonstrates how to write a simple jQuery plugin. If you would like to try this example for yourself, you can find it in the Chapter 9 folder with the rest of the book's examples that you can download from www.wrox.com/go/webdevwithjquery.

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv="content-type"

content="application/xhtml+xml; charset=utf-8" />

<meta http-equiv="content-language" content="en-us" />

<title>John Candy Movies</title>

<script type='text/javascript' src='../jQuery.js'></script>

<script type='text/javascript' src='Example 9-1.js'></script>

<link type='text/css' href='Example 9-1.css' rel='stylesheet' />

</head>

<body>

<h2>John Candy Movies</h2>

<ul class='movieList'>

<li>The Great Outdoors</li>

<li>Uncle Buck</li>

<li>Who’s Harry Crumb?</li>

<li>Canadian Bacon</li>

<li>Home Alone</li>

<li>Spaceballs</li>

<li>Planes, Trains, and Automobiles</li>

</ul>

<p>

<a href='javascript:void(0);' id='movieSelectAll'>Select All</a>

</p>

</body>

</html>

The following CSS sets up some basic styling for your jQuery plugin-enabled XHTML 5 document so that you can visually see what happens when you click items in the movie list:

body {

font: 200 16px Helvetica, Arial, sans-serif;

}

h2 {

font: 200 18px Helvetica, Arial, sans-serif;

text-decoration: underline;

}

ul.movieList {

list-style: none;

margin: 10px;

padding: 0;

}

ul.movieList li {

padding: 3px;

}

ul.movieList li.movieSelected {

background: forestgreen;

color: white;

}

a {

text-decoration: none;

color: green;

}

a:hover {

text-decoration: underline;

}

The following JavaScript provides a simple, to-the-point demonstration of how to use the jQuery Plugin API to write custom plugins for jQuery:

$.fn.extend({

select : function()

{

// In a jQuery plugin; 'this' is already a jQuery ready object

// Performing an operation like addClass() works on one

// or more items, depending on the selection.

return this.addClass('movieSelected');

},

unselect : function()

{

return this.removeClass('movieSelected');

}

});

var movies = {

ready : function()

{

$('a#movieSelectAll').click(

function(event)

{

event.preventDefault();

$('ul.movieList li').select();

}

);

$(document).on(

'click.movieList',

'ul.movieList li',

function()

{

if ($(this).hasClass('movieSelected'))

{

$(this).unselect();

}

else

{

$(this).select();

}

}

);

}

};

$(document).ready(

function()

{

movies.ready();

}

);

The preceding code results in the screen shot that you see in Figure 9.1 when you click individual movie titles.

image

Figure 9.1

In the preceding example, you see how jQuery plugins are written using the $.fn.extend() method. In essence, a jQuery plugin extends what you can do with an object representing an HTML element. In this case, you're creating two jQuery plugins, one for selecting an element and one for unselecting an element. The selection itself is by virtue of applying a class name to one of the <li> elements. If the <li> element is selected, it has the class name movieSelected applied. If the <li> element is not selected, then the class namemovieSelected is withdrawn. And just so you can visually see that an item is selected, the movieSelected class name applies a forestgreen background and makes the text white. This is a really simple example of what can be done using a jQuery plugin; you take one or more HTML elements and do something directly to those elements. In this example, you're doing selection, but you can do much more complicated things with jQuery plugins. You can have a plugin that adds a calendar to an input element; you can have a plugin that makes an element a drop zone for drag and drop actions. You'll see many more examples of jQuery plugins throughout this book; some are plugins developed by the jQuery Foundation, such as jQuery UI, and others are great third-party plugins that add useful functionality, such as the ability to sort a table, which you'll take a look at in Chapter 20, “Creating an Interactive Slideshow.”

The Anatomy of a jQuery Plugin

A jQuery plugin's primary purpose is to do something to an element in the DOM. Within a plugin, the special keyword this represents the element or elements that you are working with. In Example 9-1, you had two plugins, select() and unselect(). These jQuery plugins can be called on any HTML element in the DOM through jQuery. The plugin works on one element or many elements.

$.fn.extend({

select : function()

{

// In a jQuery plugin; 'this' is already a jQuery ready object

// Performing an operation like addClass() works on one

// or more items, depending on the selection.

return this.addClass('movieSelected');

},

unselect : function()

{

return this.removeClass('movieSelected');

}

});

As you've been learning throughout the first part of this book, if you have the following jQuery selection

$('ul.movieList li')

what follows that expression can be any method that jQuery supports. You could call addClass(), for example, and add a class name to every <li> element that selection matches. With jQuery plugins, in addition to all the methods that jQuery supports, such as find(),addClass(), each(), and so on, you can also extend jQuery any way that you see fit with your own custom plugin. In Example 9-1, you saw a remedial example, where you're just adding a class name to every element in a selection. The keyword this represents an array, and that array contains every element in the selection. In the example of John Candy movies, this contained an array of seven <li> elements, so when the following plugin executed

select : function()

{

// In a jQuery plugin; 'this' is already a jQuery ready object

// Performing an operation like addClass() works on one

// or more items, depending on the selection.

return this.addClass('movieSelected');

}

it did so on this collection of <li> elements:

<li>The Great Outdoors</li>

<li>Uncle Buck</li>

<li>Who’s Harry Crumb?</li>

<li>Canadian Bacon</li>

<li>Home Alone</li>

<li>Spaceballs</li>

<li>Planes, Trains, and Automobiles</li>

And it added the movieSelected class name to those elements, depending on whether you clicked a <li> element directly, or selected them all using the <a> element beneath the list.

Inspecting the Document Object Model

In traditional JavaScript, HTML element objects have always had some built-in properties and methods. These properties and methods make it possible to interact with and manipulate the DOM. jQuery sits in the middle, between traditional JavaScript's built-in DOM and the API it provides for interacting with that DOM. As you've already learned, jQuery simplifies the amount of code you have to write to query and manipulate the DOM. Most of the methods jQuery provides have an analogue in the traditional JavaScript DOM API. Some of the methods you're likely to find in the traditional DOM are methods like appendChild(), which adds a new child element or text node just after the last element or text node. Another is getAttribute(), which returns the value of an attribute. In the context of these two examples, jQuery provides similar methods. Instead of appendChild(), you get a whole spectrum of methods for element placement and DOM manipulation such as after(), insertAfter(), before(), insertBefore(), and all the other methods I introduced to you in Chapter 4, “Manipulating Content and Attributes.”

Instead of getAttribute(), setAttribute(), or hasAttributes(), you have attr() and a whole spectrum of CSS attribute selectors. You'll note, however, that the concept of a jQuery plugin builds on the concept of the DOM and the properties and methods that it exposes for working with an element. jQuery plugins extend what you can do with an element and make it possible to define completely custom methods.

In Example 9-2, you tweak what you saw in Example 9-1, using traditional JavaScript. You examine what properties and methods are attached to the <a> element in the HTML document.

You start with this XHTML 5 document:

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv="content-type"

content="application/xhtml+xml; charset=utf-8" />

<meta http-equiv="content-language" content="en-us" />

<title>John Candy Movies</title>

<script type='text/javascript' src='Example 9-2.js'></script>

<link type='text/css' href='Example 9-2.css' rel='stylesheet' />

</head>

<body>

<h2>John Candy Movies</h2>

<ul class='movieList'>

<li>The Great Outdoors</li>

<li>Uncle Buck</li>

<li>Who’s Harry Crumb?</li>

<li>Canadian Bacon</li>

<li>Home Alone</li>

<li>Spaceballs</li>

<li>Planes, Trains, and Automobiles</li>

</ul>

<p>

<a href='javascript:void(0);' id='movieSelectAll'>Select All</a>

</p>

</body>

</html>

It might seem redundant, but next I include the same CSS that you saw in Example 9-1 so that you can see all the components of this document, leaving nothing to mystery.

body {

font: 200 16px Helvetica, Arial, sans-serif;

}

h2 {

font: 200 18px Helvetica, Arial, sans-serif;

text-decoration: underline;

}

ul.movieList {

list-style: none;

margin: 10px;

padding: 0;

}

ul.movieList li {

padding: 3px;

}

ul.movieList li.movieSelected {

background: forestgreen;

color: white;

}

a {

text-decoration: none;

color: green;

}

a:hover {

text-decoration: underline;

}

Finally, we get to the JavaScript document, Example 9-2.js:

document.addEventListener(

'DOMContentLoaded',

function()

{

var a = document.getElementById('movieSelectAll');

for (var property in a)

{

console.log(property);

}

}

);

In Example 9-2, you cast aside jQuery for a moment for the traditional JavaScript Document Object Model. You grab an <a> element from the document and then put it inside a for/in loop to examine what methods and properties are attached to the <a> element.

You can take a look at the structure of jQuery itself by putting something like this (Example 9-3) in a JavaScript file:

$.fn.extend({

select : function()

{

// In a jQuery plugin; 'this' is already a jQuery ready object

// Performing an operation like addClass() works on one

// or more items, depending on the selection.

return this.addClass('movieSelected');

},

unselect : function()

{

return this.removeClass('movieSelected');

}

});

console.log($.fn);

The call to console.log() allows you to examine the structure of jQuery itself, both built-in plugins and custom third-party plugins. In Firefox's web console, when you click the console entry that represents console.log($.fn), you see a list expand in the right column that is filled with the names of jQuery plugins, both built-in plugins and custom plugins that have been added via $.fn.extend().

Writing a Context Menu jQuery Plugin

In Example 9-4, you write a more complicated jQuery plugin with some of the features that you're used to seeing in jQuery plugins, such as being self-contained, and the ability to apply behavior to an element that has been prepared for use with a jQuery plugin through the application of a particular HTML structure and CSS. In this example, you can see how to transform an unordered list into a custom context menu. To start, download or type in the following XHTML 5 document:

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv="content-type"

content="application/xhtml+xml; charset=utf-8" />

<meta http-equiv="content-language" content="en-us" />

<title>Context Menu Plugin</title>

<script type='text/javascript' src='../jQuery.js'></script>

<script type='text/javascript' src='Example 9-4.js'></script>

<link type='text/css' href='Example 9-4.css' rel='stylesheet' />

</head>

<body class='contextMenuContainer'>

<div id='applicationContainer'>

<p>

jQuery plugins give you the ability to extend jQuery's functionality,

quickly and seamlessly. In this example you see how to make a context

menu plugin. It demonstrates some of what you might need to make a

context menu widget as a self-contained jQuery plugin.

</p>

<p class='applicationContextMenuToggles'>

<span id='applicationContextMenuDisable'>Disable Context Menu</span> |

<span id='applicationContextMenuEnable'>Enable Context Menu</span>

</p>

<div id='applicationContextMenu'>

<ul>

<li><span>Open</span></li>

<li class='contextMenuSeparator'><div></div></li>

<li><span>Save</span></li>

<li><span>Save As...</span></li>

<li class='contextMenuSeparator'><div></div></li>

<li class='contextMenuDisabled'><span>Edit</span></li>

</ul>

</div>

</div>

</body>

</html>

The preceding XHTML 5 document sets up the necessary markup structure to begin a context menu plugin. There's a bit of text, a couple of <span> elements that can toggle whether the context menu is enabled, and the structure for the context menu itself. This markup is put together with the following CSS, which styles the context menu to look a lot like a Mac OS X system context menu—which I did because I can.

html,

body {

padding: 0;

margin: 0;

width: 100%;

height: 100%;

}

body {

font: 12px 'Lucida Grande', Helvetica, Arial, sans-serif;

background: #fff;

color: rgb(50, 50, 50);

line-height: 1.5em;

-webkit-user-select: none;

-moz-user-select: none;

-ms-user-select: none;

user-select: none;

}

div#applicationContainer {

width: 400px;

padding: 20px;

}

div.contextMenu {

display: none;

position: absolute;

z-index: 10;

top: 0;

left: 0;

width: 200px;

font-size: 14px;

background: #fff;

background: rgba(255, 255, 255, 0.95);

border: 1px solid rgb(150, 150, 150);

border: 1px solid rgba(150, 150, 150, 0.95);

padding: 4px 0;

box-shadow: 0 5px 25px rgba(100, 100, 100, 0.9);

border-radius: 5px;

border-radius: 5px;

color: #000;

}

div.contextMenu ul {

list-style: none;

margin: 0;

padding: 0;

}

div.contextMenu ul li {

padding: 2px 0 2px 21px;

margin: 0;

height: 15px;

overflow: hidden;

}

div.contextMenu ul li span {

position: relative;

top: -2px;

}

div.contextMenu ul li.contextMenuSeparator {

padding: 5px 0 8px 0;

font-size: 0;

line-height: 0;

height: auto;

}

li.contextMenuSeparator div {

font-size: 0;

line-height: 0;

padding-top: 1px;

background: rgb(200, 200, 200);

margin: 0 1px;

}

body div.contextMenu ul li.contextMenuHover {

/* Old browsers */

background: rgb(82, 117, 243);

/* FF3.6+ */

background: -moz-linear-gradient(top, rgb(82, 117, 243) 0%, rgb(3, 57, 242) 100%);

/* Chrome,Safari4+ */

background: -webkit-gradient(

linear, left top, left bottom,

color-stop(0%, rgb(82, 117, 243)),

color-stop(100%, rgb(3, 57, 242))

);

/* Chrome10+,Safari5.1+ */

background: -webkit-linear-gradient(top, rgb(82,117,243) 0%, rgb(3, 57, 242) 100%);

background: -o-linear-gradient(top, rgb(82, 117, 243) 0%, rgb(3, 57, 242) 100%);

/* IE10+ */

background: -ms-linear-gradient(top, rgb(82, 117, 243) 0%, rgb(3, 57, 242) 100%);

/* W3C */

background: linear-gradient(to bottom, rgb(82, 117, 243) 0%, rgb(3, 57, 242) 100%);

color: white;

}

li.contextMenuDisabled {

opacity: 0.5;

}

p.applicationContextMenuToggles {

color: green;

}

p.applicationContextMenuToggles span:hover {

text-decoration: underline;

cursor: pointer;

}

Finally, the following JavaScript ties everything together and breathes life into this once static, inanimate HTML document.

$.fn.extend({

contextMenu : function()

{

var options = arguments[0] !== undefined ? arguments[0] : {};

var contextMenuIsEnabled = true;

var contextMenu = this;

if (typeof options == 'string')

{

switch (options)

{

case 'disable':

{

contextMenuIsEnabled = false;

break;

}

}

}

else if (typeof options == 'object')

{

// You can pass in an object containing options to

// further customize your context menu.

}

function getViewportDimensions()

{

var x, y;

if (self.innerHeight)

{

x = self.innerWidth;

y = self.innerHeight;

}

else if (document.documentElement &&

document.documentElement.clientHeight)

{

x = document.documentElement.clientWidth;

y = document.documentElement.clientHeight;

}

else if (document.body)

{

x = document.body.clientWidth;

y = document.body.clientHeight;

}

return {

x : x,

y : y

};

}

if (contextMenuIsEnabled)

{

// If this is attaching a context menu to multiple elements,

// iterate over each of them.

this.find('li')

.not('li.contextMenuDisabled, li.contextMenuSeparator')

.bind(

'mouseover.contextMenu',

function()

{

$(this).addClass('contextMenuHover');

}

)

.bind(

'mouseout.contextMenu',

function()

{

$(this).removeClass('contextMenuHover');

}

);

if (!this.data('contextMenu'))

{

this.data('contextMenu', true)

.addClass('contextMenu')

.bind(

'mouseover.contextMenu',

function()

{

$(this).data('contextMenu', true);

}

)

.bind(

'mouseout.contextMenu',

function()

{

$(this).data('contextMenu', false);

}

);

this.parents('.contextMenuContainer:first')

.bind(

'contextmenu.contextMenu',

function(event)

{

event.preventDefault();

var viewport = getViewportDimensions();

contextMenu.show();

contextMenu.css({

top : 'auto',

right : 'auto',

bottom : 'auto',

left : 'auto'

});

if (contextMenu.outerHeight() >

(viewport.y - event.pageY))

{

contextMenu.css(

'bottom',

(viewport.y - event.pageY) + 'px'

);

}

else

{

contextMenu.css(

'top',

event.pageY + 'px'

);

}

if (contextMenu.outerWidth() >

(viewport.x - event.pageX))

{

contextMenu.css(

'right',

(viewport.x - event.pageX) + 'px'

);

}

else

{

contextMenu.css(

'left',

event.pageX + 'px'

);

}

}

);

}

if (!$('body').data('contextMenu'))

{

$('body').data('contextMenu', true);

$(document).bind(

'mousedown.contextMenu',

function()

{

$('div.contextMenu').each(

function()

{

if (!$(this).data('contextMenu'))

{

$(this).hide();

}

}

);

}

);

}

}

else

{

this.find('li')

.not('li.contextMenuDisabled, li.contextMenuSeparator')

.unbind('mouseover.contextMenu')

.unbind('mouseout.contextMenu');

this.data('contextMenu', false)

.removeClass('contextMenu')

.unbind('mouseover.contextMenu')

.unbind('mouseout.contextMenu');

this.parents('.contextMenuContainer:first')

.unbind('contextmenu.contextMenu');

$('body').data('contextMenu', false);

$(document).unbind('mousedown.contextMenu');

}

return this;

}

});

$(document).ready(

function()

{

$('span#applicationContextMenuDisable').click(

function(event)

{

$('div#applicationContextMenu').contextMenu('disable');

$('div#applicationContextMenu').hide();

}

);

$('span#applicationContextMenuEnable').click(

function()

{

$('div#applicationContextMenu').contextMenu();

}

);

$('div#applicationContextMenu').contextMenu();

}

);

With all three documents in place, you get a fine example of jQuery-enabled interactivity when you load this document into a browser that supports and has enabled the contextmenu event. (By default, all do except legacy Presto engine-based Opera, although it's possible to disable this event in Firefox's advanced preferences.) The contextMenu event, as you might have guessed, replaces the menu that comes up by default wherever you might right-click in this web page with a mouse's right button or a context menu gesture. On Macs, the context menu gesture brings up the context menu when you tap with two fingers on an Apple Wireless Trackpad or MacBook Trackpad (assuming you've enabled the gesture in System Preferences Trackpad). The result will look something like what you see in Figure 9.2.

image

Figure 9.2

If you press Disable Context Menu, you should see the default context menu instead. My default context menu is shown in Figure 9.3.

image

Figure 9.3

The remainder of this section picks apart the JavaScript in Example 9-4 line by line and explains how and why it works.

var options = arguments[0] !== undefined ? arguments[0] : {};

var contextMenuIsEnabled = true;

var contextMenu = this;

if (typeof options == 'string')

{

switch (options)

{

case 'disable':

{

contextMenuIsEnabled = false;

break;

}

}

}

else if (typeof options == 'object')

{

// You can pass in an object containing options to

// further customize your context menu.

}

The first chunk of code (preceding this sentence) provides support for passing in some options. To disable the context menu, you pass in the string 'disabled', as in contextMenu('disabled'). To enable the context menu, you call contextMenu() with no arguments.

If you like, you can expand on this example and add some options of your own. The following is the function call that gets everything started:

$('div#applicationContextMenu').contextMenu();

In the HTML, you set up and structure a <div> element that contains a <ul> element, and this becomes the context menu:

<div id='applicationContextMenu'>

<ul>

<li><span>Open</span></li>

<li class='contextMenuSeparator'><div></div></li>

<li><span>Save</span></li>

<li><span>Save As...</span></li>

<li class='contextMenuSeparator'><div></div></li>

<li class='contextMenuDisabled'><span>Edit</span></li>

</ul>

</div>

Along with some CSS, this context menu becomes almost indistinguishable from a real Mac OS X system context menu. Also in the HTML document, you define a boundary for this context menu. The context menu will occur only within the confines of this element. In our document, that container became the <body> element because it was assigned the contextMenuContainer class name. For this plugin to work, the context menu can be placed in any container, so long as it has a parent or ancestor with the contextMenuContainer class name. This is all that you need from the standpoint of the HTML structure, a container element with the contextMenuContainer class name and the <div> element containing a <ul> element. Each <li> element represents a context menu option.

The next block of code in the JavaScript is a reusable function that gets the dimensions of the viewport. This is made compatible with older versions of Internet Explorer, which use different ways of getting the dimensions of the viewport. The method used by most modern, standards-compliant browsers appears in the first block of code:

function getViewportDimensions()

{

var x, y;

if (self.innerHeight)

{

x = self.innerWidth;

y = self.innerHeight;

}

else if (document.documentElement &&

document.documentElement.clientHeight)

{

x = document.documentElement.clientWidth;

y = document.documentElement.clientHeight;

}

else if (document.body)

{

x = document.body.clientWidth;

y = document.body.clientHeight;

}

return {

x : x,

y : y

};

}

The dimensions of the viewport are needed to reposition the context menu depending on where the user clicks inside the viewport. If the user clicks on the left side of the viewport, the context menu is positioned from the left. If the user clicks near the top of the view port, the context menu is positioned from the top, and if the user clicks near the bottom, the context menu is positioned from the bottom. Using a little math, you attempt to avoid a situation in which the element used for the context menu might appear offscreen. Instead, by analyzing where a user clicks and the proximity to the edges of the viewport, you can reposition based on that data.

if (contextMenuIsEnabled)

{

The preceding block of code detects if contextMenu() is called with no arguments, or in other words, when the context menu is enabled.

First, you set up some mouseover and mouseout events for the <li> elements, but only elements that aren't disabled or separators.

this.find('li')

.not('li.contextMenuDisabled, li.contextMenuSeparator')

.bind(

'mouseover.contextMenu',

function()

{

$(this).addClass('contextMenuHover');

}

)

.bind(

'mouseout.contextMenu',

function()

{

$(this).removeClass('contextMenuHover');

}

);

The call to find('li') finds the <li> elements inside the element acting as the context menu. These receive mouseover and mouseout events that are namespaced for our contextMenu plugin. The entire event is mouseover.contextMenu. The mouseover part is the standard event you are attaching, and the contextMenu part is the name of this application of the mouseover event. As you've learned in all the chapters preceding this one, naming your events is good practice because it isolates the events that you apply into namespaces and makes it much easier to selectively enable and disable events, just as you see here where you bind and unbind events based on the event and the event name. So each <li> element, except those with contextMenuDisabled and contextMenuSeparator (filtered out using not()) class names receives a contextMenuHover class name when the user hovers over those <li> elements within the context menu. If you look at the CSS, this causes a CSS gradient to be applied during mouseover.

if (!this.data('contextMenu'))

{

this.data('contextMenu', true)

.addClass('contextMenu')

.bind(

'mouseover.contextMenu',

function()

{

$(this).data('contextMenu', true);

}

)

.bind(

'mouseout.contextMenu',

function()

{

$(this).data('contextMenu', false);

}

);

As explained earlier in this chapter, this refers to the element or elements that are selected. In this case this refers directly to this selection:

$('div#applicationContextMenu')

If you were to do console.log(this) in Safari or Chrome, you'd see the HTML source code of <div id="applicationContextMenu">.The preceding block of code begins by checking to see if data has been attached to that <div> element, under the name contextMenu.

if (!this.data('contextMenu'))

{

If no data is found, data is created.

this.data('contextMenu', true)

Invisibly, data is attached to the <div> element, within jQuery.

Then the <div> element is given the class name contextMenu.

.addClass('contextMenu')

That <div> element now looks like this:

<div id='applicationContainer' class='contextMenu'>

And that same <div> element receives mouseover and mouseout events, which themselves set the same data on the <div> element, which is used to keep track of whether the context menu is active. If the mouse cursor isn't over the context menu, then the context menu is considered inactive, and if a click occurs while the mouse cursor isn't on top of the context menu, the context menu will be hidden.

Next, the contextmenu event is applied, and this happens by traveling up the DOM tree from our <div> element to the first element that has the class name contextMenuContainer.

this.parents('.contextMenuContainer:first')

.bind(

'contextmenu.contextMenu',

function(event)

{

The preceding code binds the contextmenu event namespaced using our plugin's name, contextMenu, to the element with the contextMenuContainer class name. The function provided, of course, executes when the contextmenu event fires. In the first line of that function, the default action, displaying the default system context menu is canceled. The dimensions of the viewport are retrieved from our previously defined function. The contextMenu is set to display with a call to show(); then the four CSS position properties top, right, bottom, and left are all reset to their default value, auto. This is important because your code will set two of the four in pairs, but never all four.

event.preventDefault();

var viewport = getViewportDimensions();

contextMenu.show();

contextMenu.css({

top : 'auto',

right : 'auto',

bottom : 'auto',

left : 'auto'

});

The remaining of the contextmenu event callback function defines the position of the context menu relative to the viewport based on where the user clicked inside that viewport.

if (contextMenu.outerHeight() >

(viewport.y - event.pageY))

{

contextMenu.css(

'bottom',

(viewport.y - event.pageY) + 'px'

);

}

else

{

contextMenu.css(

'top',

event.pageY + 'px'

);

}

if (contextMenu.outerWidth() >

(viewport.x - event.pageX))

{

contextMenu.css(

'right',

(viewport.x - event.pageX) + 'px'

);

}

else

{

contextMenu.css(

'left',

event.pageX + 'px'

);

}

A little bit of math and comparison determines whether it is best to place the context menu from the left or right, and whether from the top or from the bottom is better. The size of the context menu is taken into consideration, along with where the mouse click occurs in relation to the edges of the viewport. The properties event.pageX and event.pageY, obviously provided with all the other event data, are the x,y coordinates of the mouse click in relation to the document. The properties viewport.x and viewport.y contain the width and height of the viewport. Finally, outerHeight() is a jQuery method that gets the height of the context menu including the following CSS properties: height, padding, border-width, and margin. Likewise, outerWidth() provides similar dimensions for width.

Next, an event is attached to the document to track clicks that occur outside the context menu.

if (!$('body').data('contextMenu'))

{

$('body').data('contextMenu', true);

$(document).bind(

'mousedown.contextMenu',

function()

{

$('div.contextMenu').each(

function()

{

if (!$(this).data('contextMenu')

{

$(this).hide();

}

}

);

}

);

}

If the <body> element does not have the data contextMenu, or contextMenu is set to false, then the event is attached. So that the event cannot be attached multiple times, the contextMenu data is set to true on the <body> element to indicate that the event has been attached.

$('body').data('contextMenu', true);

A mousedown event, again namespaced with the name contextMenu, is attached to the document. Whenever a mousedown event occurs, every <div> element with the class name contextMenu is iterated.

$(document).bind(

'mousedown.contextMenu',

function()

{

$('div.contextMenu').each(

function()

{

if (!$(this).data('contextMenu'))

{

$(this).hide();

}

}

);

}

);

If any of those <div> elements doesn't have its contextMenu data set to true, then that <div> element is hidden. Remember earlier in the script that you attached a mouseover and mouseout event to the context menu to track whether the context menu is active, which was done by setting the data contextMenu to a boolean value. This bit of code completes that implementation and makes it possible to close the context menu just by clicking anywhere but on the context menu.

The last block of code is executed when the context menu is disabled, which is done when you click the Disable Context Menu option.

$('span#applicationContextMenuDisable').click(

function(event)

{

$('div#applicationContextMenu').contextMenu('disable');

$('div#applicationContextMenu').hide();

}

);

The preceding code causes the last block of code, which disables the context menu, to execute.

}

else

{

this.find('li')

.not('li.contextMenuDisabled, li.contextMenuSeparator')

.unbind('mouseover.contextMenu')

.unbind('mouseout.contextMenu');

this.data('contextMenu', false)

.removeClass('contextMenu')

.unbind('mouseover.contextMenu')

.unbind('mouseout.contextMenu');

this.parents('.contextMenuContainer:first')

.unbind('contextmenu.contextMenu');

$('body').data('contextMenu', false);

$(document).unbind('mousedown.contextMenu');

}

In this block of code, events are removed and data is set to false, reversing all the document changes that you put in place to enable your context menu. Because your events are namespaced using the contextMenu name, only the events that you explicitly attached are removed. If other people attached click or mouseover or other events using other names in other scripts, their events remain untouched and functional.

Good Practice for jQuery Plugin Development

There are just a few things you should keep in mind while developing your own jQuery plugins:

· It's considered good practice to always expect one or more items to be passed to your plugin (in the jQuery selection preceding the call to your plugin), and to always return the jQuery object, whenever it makes sense and is possible to do so, which makes it possible to chain multiple method calls together.

· If you plan on using third-party jQuery plugins, you'll want to consider name-spacing your own plugins in some way so that your naming choices don't conflict with those of third-party plugins. Oftentimes this is done by prefixing a name of some sort to your plugin name, like the name of your company or organization. In the context of your context menu plugin, if that plugin were developed by your company Example, you might end up calling your plugin exampleContextMenu().

· Avoid polluting the global namespace. Place all your function calls and variables inside your jQuery plugin. As an added benefit, this can also make your APIs private and callable only by you. Preventing other people from using your private APIs gives you the freedom to change them when you need to because you don't have to worry about supporting the people using those APIs.

· If you're interested in developing official third-party jQuery plugins that follow all the recommended best practices published by jQuery's developers, see the document located at http://docs.jquery.com/Plugins/Authoring.

Summary

In this chapter, you learned the basic concepts needed to author your own jQuery plugin. You learned how jQuery plugins are created by passing an object literal to jQuery's $.fn.extend() method.

You learned how jQuery plugins expect to have one or more items passed to them, which are always present in the this keyword.

When you write jQuery plugins, you should return the jQuery object (when it makes sense and is possible to do so) because this preserves jQuery's capability to chain method calls onto one another.

You learned how to write a more complicated, more realistic jQuery plugin example by creating a context menu plugin.

Exercises

1. What method do you use to add a plugin to jQuery?

2. How do you list all the plugins in use within jQuery?

3. How would you create a private API for your own use within your jQuery plugin?

4. How are items you've selected with jQuery accessed in your custom plugin?

5. What value should your plugin return?

6. Why is namespacing your event names a good idea?

7. Customize the jQuery contextMenu plugin example to provide one or more options:

a. Have your contextMenu plugin execute a callback function when the menu is opened and positioned.

b. Have your contextMenu plugin provide options for the list of items used in the context menu itself.