Beej's Bit Bucket

 ⚡ Tech and Programming Fun

jQuery Plugins and Scrolling Regions

2012-07-10, 2013-01-30
On smaller screens, you can grab the border of the app, below, and scroll it to the right. Yes, I know it should be rebuilt to be responsive...

How do you make scrolling panes, like the one above? Also, how do you make them easy to code and reuse?

I use jQuery a lot since it's a great library for keeping things working cross-browser. One of the cool things about it is you can write plugins to extend the functionality, and we'll use that extensibility here to help bring the scrolling region to web pages.

Note: there are jQuery plugins for just about everything, including this. This is just an exercise for fun and learning!

This post assumes you're familiar with jQuery. If you're not, you might want to read some of the many tutorials and get some practice before coming back here. But don't let me dissuade you in any case!

Scrolling Regions

Let's tackle the HTML/CSS side first.

Basically what we have is CSS and nested divs. The outermost we'll call the “frame”, which is the unmoving window on the screen through which you peer down on the “container”; the container slides around under the frame so it appears to move, and it holds all the content.

How the map is masked out under the frame.

The setup is something like this (excuse the HTML and CSS crammed together):

CSS/HTML pseudocode
.scrollregion-frame { position: relative; overflow: hidden; } .scrollregion-container { position: absolute; /* top: -20px; */ /* <- set these to make it scroll */ /* left: -40px; */ } <div class="scrollregion-frame"> <div class="scrollregion-container">I can be scrolled around!</div> </div>

And with that, we can “scroll” the inner div by setting its left and top CSS properties! Imagine the container is a sheet of paper you're sliding around below the window of the surrounding frame; this is why we need to set top and left to negative values to shift the container up and left.

There are a few CSS forces at play, here. The easy one is overflow: hidden. This causes anything that would normally either be outside the parent container (or cause scrollbars to appear) simply gets cropped off, instead. The rest of the map in the demo, above, is still there; it's just hidden away outside the parent container.

The other thing is in .scrollregion-container, we have position: absolute. This allows us to move the div around "out of flow", which means you can move it independently of the rest of the items that are being automatically positioned on the page. Despite the fact that it is called "absolute", it is still absolute relative to the parent container (in this case, the scroll frame.)

And you might notice a position: relative in the .scrollregion-frame. This is because the position: absolute in the container div won't work unless the position is relative (or absolute) on the parent container.

Lastly, you'll want to explicitly set the width and height of the scroll frame in CSS. Since the inner scroll container is out of flow, the outer container won't consider it when deciding what size to make itself; it will default to zero height and 100% width.

Another thing you do is have a CSS reset for your stuff, and make sure you have a valid DOCTYPE on your HTML. Without these things, potentially Weird Things can happen.

At this point, you can just use jQuery's animate() method to change the CSS left and top to pan the map around, like in the demo!

But what if you want to make that code reusable? Read on!

jQuery Plugin Functionality

The basic concept here is that we're going to monkey patch jQuery to do our bidding. When we've done that, we'll be able to use our functionality in the JQuery Way, and other people will be able to, as well!

What is our bidding? Well, I'd like to take the HTML in the previous section, and make it so you can write that with a few lines of JavaScript and have a full-on scrolling region.

For example:

Listing A: CSS/HTML/JS
<style type="text/css"> #scrollregion1 { width: 400px; /* set size of region */ height: 300px; border: 1px solid gray; } </style> <!-- set up container and thing to be scrolled, usmap.jpg --> <div id="scrollregion1"> <div><img src="usmap.jpg"></div> </div> <script type="text/javascript"> $(function() { // init scroll region and set position in pixels $('#scrollregion1').scrollregion('init');1 $('#scrollregion1').scrollregion('setPosition', -250, -200);2 }); </script>

That's how we set up a scrolling region and set the position in the demo at the top of the page (basically). Note that at points 1 and 2 in Listing A, above, it looks like we've added a new method, scrollregion() to the jQuery library, with maybe an argument to tell it what function to perform! How do we do that?

Writing a jQuery Plugin

Assuming you're familiar with jQuery, before we begin, read jQuery plugin authoring best-practices. It's short and has lots of good info. I'm just going to expand on it a tiny bit.

Back already? Did you really read it just now? };-)

In order to get along with jQuery and other plugins, plugins are expected to follow certain rules, which we'll tackle here.

Namespace your JavaScript

As a seasoned JS dev, you know you're not supposed to pollute the global namespace, and you sagely wrap your stuff up in a function to hide it from the outside world.

A cute little trick is that you can pass an argument to that function. In this case, it's convenient to pass the jQuery main object in as an argument. It's even more convenient to name the parameter $ so you can use it in regular jQuery fashion in your code:

JavaScript, inside the plugin
(function($) { // entire plug-in goes here })(jQuery);

As long as you include the jQuery source JavaScript in your HTML before you include the plugin, this will work fine.

Adding a new method

jQuery keeps virtually all the methods is knows in $.fn. So we'll add it there:

JavaScript, inside the plugin
(function($) { $.fn.scrollregion = function(methodName) { // this gets called with $('#foo').scrollregion() }; })(jQuery);

Returning a value

Here's one option:

JavaScript, inside the plugin
(function($) { $.fn.scrollregion = function() { return 4; }; })(jQuery);

That would make this true:

JavaScript, outside the plugin
$('#foo').scrollregion() === 4; // TRUE

Totally valid, but not very exciting.

Here's a slightly more exciting option that returns the width of the element using the jQuery width() function:

JavaScript, inside the plugin
(function($) { $.fn.scrollregion = function() { return this.width(); }; })(jQuery);

It's important to notice that this in this context refers to the jQuery object in question that have been matched by the selector. So if we have:

JavaScript, outside the plugin
$('div').scrollregion();

then this in the plugin will refer to the set of all divs in the document. (However, the width() method is programmed to only return the width of the first element in the set.)

Chaining calls

One of the gorgeous things about jQuery is that you can chain calls on the same line, like in this example where we call css() followed by on(). Both methods are applied to every div, since that's what's in the selector:

JavaScript
$('div').css('top':'0px').on('click', clickHandler);

Not all methods chain like this. Obviously the width() method returns a width and that's the end of it. But lots of them do chain like this, so how do we code the plugin to allow it?

When you call $('div'), that function returns a jQuery object that represents all the divs in your document. That jQuery object has a number of methods, like css() or on() that can be called. And each of the chainable methods returns that same jQuery object! This is why, in the example above, we were able to call on() directly, because css() returned the same jQuery object as it was operating on, namely the one originally created with $('div')!

Let's make a chainable jQuery method that does nothing:

JavaScript, inside the plugin
(function($) { $.fn.nop = function() { return this; }; })(jQuery);

And we that we can:

JavaScript, outside the plugin
$('div').nop().css('top':'0px');

Since nop() just returns the same jQuery object as $('div'), the css() method uses it as though nop() weren't even there.

If you want your plugin call to be chainable, return the jQuery object (namely, this) so that the next method in the chain can be applied to it.

Working with sets of elements

Sometimes jQuery objects match a set of DOM objects, not just one. For instance:

JavaScript
var foo = $('div');

there foo is a jQuery object that refers to all the divs in the document. You can iterate through all of them with the jQuery method each(), which calls a function for every member in the set of DOM elements:

JavaScript
var divs = $('div'); divs.each(function() { // Note that inside the each() handler function, "this" // refers to the individual DOM element, not a jQuery // object. But you can always make a jQuery object from // a DOM element using $() as follows, where jElement is // a jQuery object representing this single DOM element: var jElement = $(this); // do complicated plugin stuff here to each div });

And we can make this chainable, too, since the each() method conveniently returns the jQuery object! So you'll commonly see something like this:

JavaScript, inside the plugin
function jQueryPluginFunction() { return this.each(function() { // do complicated plugin stuff here to each element }); }

Naming methods with the first parameter

jQuery best-practices recommend strongly against adding multiple functions to $.fn for the same plugin. The plugin should define one function, and the first parameter to that function should dictate what the method is and what the plugin should do. Like we've already seen, here is something that sets up a scroll region on a div with ID myscrollarea:

JavaScript, outside the plugin
$('#myscrollarea').scrollregion('init');

That says, "Hey, scrollregion plugin! Call your init method on these elements I've selected.

In order to make this work, we'll follow the template outlined in the best-practices document:

JavaScript, inside the plugin
var methods = { "init": initHandler, "options": optionsHandler, "getContainer": getContainerHandler, "setPosition": setPositionHandler, "setPositionPercent": setPositionPercentHandler, "destroy": destroyHandler }; // stolen from jQuery's sample code: $.fn.scrollregion = function(method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { $.error('Method ' + method + ' does not exist on jQuery.scrollregion'); } };

The first part of the code defines a methods object. This matches the human-readable name of a method (such as 'init') to its handler (such as initHandler.)

Then, at the “stolen from” section of code, we set up our plugin function, scrollregion(). This function takes a single named parameter, which will be a string that is the method to call, such as 'init'.

The code below that looks up the method, and calls it with the remaining arguments in the arguments array. Or it calls 'init' by default. Else it throws an error.

We can use this state of affairs to set up an 'init' call that takes some options, so we could call it like this:

JavaScript, outside the plugin
var options = { 'stayCool': true }; $('#myscrollbar').scrollbar('init', options);

It's a common technique to pass options in an object like that.

And since the boilerplate, above, just passes all remaining arguments (after the first one which is 'init') to initHandler, that function would look like the following, which would be called when the method scrollbar('init') was executed:

JavaScript, inside the plugin
function initHandler(options) { // do something with the options and init };

Setting Plugin Options

As we saw in the previous section, we can lump all the options for a particular plugin into the same object, and pass that object in to the 'init' method, or later in some kind of 'setOptions' method.

Internally, we can store that object and just refer to it when we need it.

Where is a good place to store it?

Well, since we tend to operate on DOM elements, it might actually be good to store the data on the DOM element to which it belongs. This beats storing it globally since each DOM element can have its own state. For example, some scrolling regions might support a certain optional behavior, but other ones on the same page do not. In that case we can't just save the option globally, and must store along with the DOM element.

Fortunately, jQuery provides a very useful method for storing a data object in a DOM element: data(). We can use this to our advantage:

JavaScript, outside the plugin
var options = { 'stayCool': true }; $('#myscrollbar').scrollbar('init', options);
JavaScript, inside the plugin
function initHandler(options) { return this.each(function() { var jelement = $(this); jelement.data(options); // store options on the element }); };

And, actually, we can do it a bit better than that. Let's say you have default values you want in your options, and you'd like the user-specified values to override your default values. What you need is a way to merge the user options object into your default options object.

And again, jQuery has a method for you, this time built on the base jQuery object (AKA $): $.extend().

JavaScript, inside the plugin
function initHandler(user_options) { return this.each(function() { var jelement = $(this); var options = { 'animal': 'goat', 'color': 'orange' }; $.extend(options, user_options); // fold user_options into options jelement.data(options); // store options on the element }); };

Other Plugin Considerations

In the best practices doc, they talk about namespacing your events. So do that.

Also, one common way of naming jQuery plugin sources is jquery.plugInName.js or jquery.plugInName-version.js. This way people know that's what it is.

You should also google for “jquery plugin pluginname” and make sure it doesn't exist yet. When the official plugin site is operational, this will be much easier to check.

Scrolling Regions

Oh yeah, that's what we're talking about. Let's put it to practice, and make a plugin that takes this HTML:

HTML
<div id="scrollregion1"> <div id="scrollcontainer1"> I can be scrolled around! </div> </div>

and turns it into this when the user runs $("scrollregion1").scrollregion('init'):

HTML
<div id="scrollregion1" class="scrollregion-frame"> <div id="scrollcontainer1" class="scrollregion-container"> I can be scrolled around! </div> </div>

All we're doing is adding the class for some CSS, but as we saw at the beginning of the document, a little CSS was all it took for a scrolling region to exist. We'll define the CSS in an external file that must be included by the end user.

And here's how:

Listing B: JavaScript, in the plugin
(function($) { 1 /** * Initialize the scrolling region */ function initHandler(options2) { return this.each3(function() { var frame = $(this); frame.addClass('jquery-scrollregion-frame'); 4 var container = frame.children().first(); container.addClass('jquery-scrollregion-container'); }); }; // list of registered methods this plugin supports: var methods = { 5 "init": initHandler, }; $.fn.scrollregion6 = function(method) { // boilerplate if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { $.error('Method ' + method + ' does not exist on jQuery.scrollregion'); } }; })(jQuery); 7

Here are some of the highlights, marked by numbers in the code, above:

  1. Here's our namespace closure, with $ as a handy parameter for accessing jQuery functionality.
  2. I allow an options parameter to be passed in to 'init'; it's unused in this snippet.
  3. Here's the common return this.each() line.
  4. This is where we add the CSS classes to the elements in question.
  5. Everything from here down is boilerplate from the best practices doc.
  6. When we add a property to jQuery's method list in $.fn is really the moment the plugin is born.
  7. And lastly, here we call our namespace closure function, passing in jQuery, which will be in the $ parameter in the plugin.

Here is a complete use case:

HTML/JavaScript, outside the plugin
<!DOCTYPE html> <html> <head> <!-- include jQuery first, then the plugins --> <script src="jquery-1.7.2.min.js" type="text/javascript"></script> <script src="jquery.scrollregion.js" type="text/javascript"></script> <link href="jquery.scrollregion.css" rel="stylesheet" type="text/css"/> <script type="text/javascript"> (function() { $(function() { // on ready $('scrollregion1').scrollregion('init'); }); })(); </script> </head> <body> <div id="scrollregion1"> <div id="scrollcontainer1"> I can be scrolled! </div> </div> </body> </html>

You're going to want to set up some CSS on scrollregion1 so that it has a width and height and border or whatever. Also you're going to want some on scrollcontainer1 so that it has a bigger width and height and can be scrolled around.

Scroll it how, though? You'll want to add a new method to the plugin called 'setPosition' that takes an x and y value and uses them to set the left and top CSS properties of scrollcontainer1. A sample call would be like this:

JavaScript, outside the plugin
$('scrollregion1').scrollregion('setPosition', 80, 90);

And I'll leave it as an exercise to the reader to implement it.

You might also notice the scroll region at the top of this page supports mouse motion, touch events, and animated scrolling. So there are more exercises for the reader.

Oh, ok. I'll put it all on github, too. I've named my variant GoatScroller.

Sharing your Plugin

But after you've made it, how do you share it? The jQuery guys used to have a whole system set up, but they ditched it and have set up a new one. This new system for the future uses github, and has its own set up procedures and protocols to use, roughly outlined below.

  1. Get a github account. The new system will be tied to it, and you really should have an account, anyway.
  2. Put your project in a github repo (you can start with it there, or add it later).
  3. Follow the steps in the publishing documentation, and add a post-receive hook on your repo to hit http://plugins.jquery.com/postreceive-hook.
  4. Choose a version number for this release. It must be in the form X.Y.Z, a conformant semantic version number.
  5. Create a manifest which is a JSON file describing the characteristics of your plugin.
  6. Tag your release commit with the version number.
  7. Commit and push!

The jQuery folks will track your commits, check out your manifest, and add your plugin to the master list.

Links and References

License

The code attached to this article is licensed under the MIT open source license.

Share me!

Comments

blog comments powered by Disqus
Blog  ⚡  Email beej@beej.us  ⚡  Home page