You are here: Home | teaching | Spring 2012 | Week 13

PopUp MP3 Player (New Window)

CMP 342-Internet Programming

Week 13

Class 24 - April 30


 

The jQuery Plugins API

We already dealt with plugins last week when we created the slideshow for our Farm at Richville site. In that exercise, we placed the plugin in our script.js document. From now on, we will place any custom plugins into a plugins.jsfile to keep them separate and manageable. 

Plugins are simply reusable, modular collections of objects and custom methods that work together and that anyone can write, share and add to their sites. Because of the vast network of developes who write plugins, there are now entire plugin libraries in existence for adding really interesting functionality to your code. In fact, many of the methods we will be using in the jQuery UI library are plugins.


 

Just a Note: the .data() method

Today's lesson makes use of a previously unintroduced jQuery method called .data(). Like any method, .data() works differently depending on the parameters passed to it, but its real advantage lies in the ability to attach data values to elements in the DOM, thereby avoiding global variables, circular references and memory leaks. Here are examples of how it is used, from the jQuery API reference website:

Usage 1.) $('body').data('some_value', 98);

Attaches an arbitrary value name 'foo' and its value '98'.

Usage 2.) $('body').data('bar', { myType: 'test', count: 40 });

Attaches an arbitrary value name and a group, or 'map' of values.

Usage 3.) $('body').data('some_value');

Retrieves the value for 'foo'. When the above snippet evalutes, the return result will be 98.

Usage 4.$('body').data();

 The above snippet will simply return all data key/value, i.e. {some_value: 98, bar: { myType: 'test', count: 40 }}

Here's a quick example of how it works:

  1. <!DOCTYPE html>
  2.   <style>
  3.   div { color:blue; }
  4.   span { color:red; }
  5.   </style>
  6.   [removed][removed]
  7. </head>
  8.   <div>
  9.     The values stored were
  10.     <span></span>
  11.     and
  12.     <span></span>
  13.   </div>
  14. [removed]
  15. $("div").data("test", { first: 16, last: "pizza!" });
  16. $("span:first").text($("div").data("test").first);
  17. $("span:last").text($("div").data("test").last);
  18. [removed]
  19.  
  20. </body>
  21. </html>

 That's Awesome...Now How Do I Make a Plugin?

The overall requirements for creating a plugin are rather simple. Essentially, every plugin extends the jQuery object with new children objects and methods. Therefore, to properly create a jQuery plugin, one must use the $.fn.extend() method. Let's build a simple plugin that adds a context-specific menu when a user right clicks somewhere in the browser window.

[Click here for a demo of this plugin] [Click here for the exercise files]

We'll start with some simple HTML. I'll just show the important bits in the body:

  1.     <div>
  2.       <p>
  3.         jQuery plugins give you the ability to extend jQuery's functionality,
  4.         quickly and seamlessly.  In this example you see how to make a context
  5.         menu plugin, that handles everything you need to make a context menu
  6.         widget in self-contained jQuery plugin.
  7.       </p>
  8.       <ul class="contextMenu">
  9.         <li>This is  a context menu item.</li>
  10.       </ul>
  11.      
  12.       <ul>
  13.        <li>This is just a regular list</li>
  14.       </ul>
  15.     </div>

Just some basic text and a &lt;ul> tag with a "contextMenu" class. Then some basic CSS:

  1. @charset "UTF-8";
  2. /* CSS Document */
  3. body {
  4.     font: 12px "Lucida Grande", Arial, sans-serif;
  5.     background: #fff;
  6.     color: rgb(50, 50, 50);
  7. }
  8. body,
  9. html {
  10.     width: 100%;
  11.     height: 100%;
  12.     margin: 0;
  13.     padding: 0;
  14. }
  15. div {
  16.     position: absolute;
  17.     width: 100%;
  18.     height: 100%;
  19. }
  20. {
  21.     padding: 5px;    
  22. }
  23. ul.tmpContextMenu {
  24.     list-style: none;
  25.     margin: 0;
  26.     padding: 5px;
  27.     border: 1px solid rgb(200, 200, 200);
  28.     position: absolute;
  29.     top: 0;
  30.     left: 0;
  31.     background: lightblue;
  32.     width: 200px;
  33.     min-height: 200px;
  34.     display: none;
  35. }
  36. li {
  37.     padding: 3px;
  38. }

And finally we need to place this code in our plugins.js document:

  1. // JavaScript Document
  2. $.fn.extend({
  3.   ContextMenu: function() {  
  4.     this.each(
  5.       function() {
  6.         $(this).addClass('tmpContextMenu');
  7.  
  8.         $(this).hover(
  9.           function() {
  10.             $.data(this, 'ContextMenu', true);
  11.           },
  12.           function() {
  13.             $.data(this, 'ContextMenu', false);
  14.           }    
  15.         );
  16.  
  17.         // Only attach the following event once.
  18.         if (!$.data(document, 'MouseDown')) {
  19.           $.data(document, 'MouseDown', true);
  20.           $(document).mousedown(
  21.             function() {
  22.               $('.tmpContextMenu').each(
  23.                 function() {
  24.                   if (!$.data(this, 'ContextMenu')) {
  25.                     $(this).hide();  
  26.                   }
  27.                 }
  28.               );
  29.             }
  30.           );
  31.         }
  32.  
  33.         $(this).parent().bind(
  34.           'contextmenu',
  35.           function($e) {
  36.             $e.preventDefault();
  37.  
  38.             // FYI: The contextmenu doesn't work in Opera.
  39.             var $menu = $(this).find('.tmpContextMenu');
  40.  
  41.             $menu.show();
  42.  
  43.             // The following bit gets the dimensions of the viewport
  44.             var $vpx, $vpy;
  45.          
  46.             if (self.innerHeight) {
  47.               // all except Explorer
  48.               $vpx = self.innerWidth;
  49.               $vpy = self.innerHeight;
  50.             } else if (document.documentElement && document.documentElement.clientHeight) {
  51.               // Explorer 6 Strict Mode
  52.               $vpx = document.documentElement.clientWidth;
  53.               $vpy = document.documentElement.clientHeight;
  54.             } else if (document.body) {
  55.               // other Explorers
  56.               $vpx = document.body.clientWidth;
  57.               $vpy = document.body.clientHeight;
  58.             }
  59.          
  60.             // Reset offset values to their defaults
  61.             $menu.css({
  62.               top:    'auto',
  63.               right:  'auto',
  64.               bottom: 'auto',
  65.               left:   'auto'
  66.             });
  67.  
  68.             /**
  69.             * If the height or width of the context menu is greater than the amount
  70.             * of pixels from the point of click to the right or bottom edge of the
  71.             * viewport adjust the offset accordingly
  72.             */
  73.             if ($menu.outerHeight() > ($vpy - $e.pageY)) {
  74.               $menu.css('bottom', ($vpy - $e.pageY) + 'px');
  75.             } else {
  76.               $menu.css('top', $e.pageY + 'px');
  77.             }
  78.  
  79.             if ($menu.outerWidth() > ($vpx - $e.pageX)) {
  80.               $menu.css('right',  ($vpx - $e.pageX) + 'px');
  81.             } else {
  82.               $menu.css('left', $e.pageX + 'px');
  83.             }
  84.           }
  85.         );
  86.       }
  87.     );
  88.  
  89.     return $(this);
  90.   },
  91.   MyPlugin: {
  92.     Ready: function() {
  93.       $('ul.contextMenu').ContextMenu();
  94.     }
  95.   }
  96. });
  97.  
  98. $(document).ready(
  99.   function() {
  100.     $.fn.MyPlugin.Ready();
  101.   }
  102. );
Phew! Tha was long. Let's unpack it all, shall we?  The first line of code is important because it calls the jQuery extending function $.fn.extend(...), establishing that what comes next is a plugin. When this plugin is instantiated later on in the code, it run through the remaining code: a method ContextMenu() is declared and an initial function is immediately called:
  1. $.fn.extend({
  2.   ContextMenu: function() {  
  3.     this.each(
  4.       function() {

Using the jQuery .each() method, this function iterates over all the items in the this keyword. In this case, this is referencing whatever HTML elements were selected upon calling ContextMenu() later on in the plugin. It could be multiple items, or just one.

The next lines add a .tmpContextMenu class each element in this and sets an arbitrary data value to determine whether the mouse is hovering over the selected elements or not:

  1.         $(this).addClass('tmpContextMenu');
  2.  
  3.         $(this).hover(
  4.           function() {
  5.             $.data(this, 'ContextMenu', true);
  6.           },
  7.           function() {
  8.             $.data(this, 'ContextMenu', false);
  9.           }    
  10.         );

$.data() is a temporary method for storing a value that takes three arguments: 1.) a reference to the element the value is associated with, 2.) a name for the value and 3.) the value itself. We won't be creating a boolean value and setting it with .hover(). A global variable would only allow single context menu. By using the temporary $.data() method, we associate individual true/false values with any number of contex menus.

The next few lines are sandwiched between an if statement:

  1.         // Only attach the following event once.
  2.         if (!$.data(document, 'MouseDown')) {
  3.           $.data(document, 'MouseDown', true);
  4.           $(document).mousedown(
  5.             function() {
  6.               $('.tmpContextMenu').each(
  7.                 function() {
  8.                   if (!$.data(this, 'ContextMenu')) {
  9.                     $(this).hide();  
  10.                   }
  11.                 }
  12.               );
  13.             }
  14.           );
  15.         }

 

The initial if statement tests to see if a 'MouseDown' flag has been attached to he document via $.data(). The $.data(document, 'MouseDown') conditional will evaluate as false because 'MouseDown' hasn't been created yet, but the not operator (!) in front of it will make it true, thus the initial run through this if statement will execute the code inside. Once we're in the if statement, we immediately create and set 'document MouseDown' to true via the $.data()  method, ensuring this if statement will hereafter break without running. This is all to ensure that the mousedown() event handler within is only attached to the document once.

The mousedown event $(document).mousedown() fires during a mouse click, iterates over each element with the .tmpContextMenu class, checks to see if the mouse is over it, and if it isn't, hides the element. This will hide any open context menus with a single mouse click. The next bit of code is important too and is related to the right-click functionality of our context menu:

  1.         $(this).parent().bind(
  2.           'contextmenu',
  3.           function($e) {
  4.             $e.preventDefault();

The'contextmenu'  is JavaScript event keyword, in the same category as mousedown or click, and it fires whenever the user right clicks. By binding this event hanlder to the parent of whatever element(s) we're attaching the ContextMenu plugin to, and preventing the default action, we're essentially hijacking the right-click. Argh!!

We then create a variable called $menu and pass it a reference to any element with the .tmpContextMenu attached to it (i.e. each element we attached our ContextMenu() method to):

  1.             var $menu = $(this).find('.tmpContextMenu');
  2.  
  3.             $menu.show();

The rest of the code merely deals with positioning the context menu popup inside the viewport of the browser window. This isn't all that exciting, but becomes important if a user clicks near the edge of the window. Try doing this and you'll see what I mean. Finally, we end our ContextMenu() function by returning the jQuery object, with the this keyword selected.

  1.     return $(this);

In other words, we're returning the object that ContextMenu() was called upon to begin with. This comes in handy if a developer wants to pass or chain the $('some_element').ContextMenu() with additional jQuery code.  Before ending this plugin, we to create the main plugin object, whose responsibility it is to call plugin methods like ContextMenu:

  1.   MyPlugin: {
  2.     Ready: function() {
  3.       $('ul.contextMenu').ContextMenu();
  4.     }
  5.   }
 

MyPlugin: {...} creates the object, and Ready: function(){...} is an object method that initializes the whole thing. All Ready does is attach our ContextMenu() method to every &lt;ul> tag with the .contextMenu class associated with it to distinguish them from regular unnumbered lists in the document. This MyPlugin: {...} code is really more of a constructor, however. It just sits there, like an idiot, waiting to be called. So let's call it!

We'll invoke the MyPlugin object (which, in turn, calls  ContextMenu(method) by calling $.fn.MyPlugin.Ready() method directly when the document finishes loading :

  1. $(document).ready(
  2.   function() {
  3.     $.fn.MyPlugin.Ready();
  4.   }
  5. );

And that is all you need to know to create your own plugins! Oh wait, there's actually one more guideline: always write your plugins to account for one or more items to be passed in, and to always return the jQuery object, whenever you can. 

You are here: Home | teaching | Spring 2012 | Week 13