2012-07-10, 2013-01-30
jQuery Plugins and Scrolling Regions
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 div
s. 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):
.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 theposition: absolute
in the containerdiv
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:
<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'); // [MARK 1]
$('#scrollregion1').scrollregion('setPosition', -250, -200); // [MARK 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 [MARK 1]
and
[MARK 2]
in the listing, 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 inside the plugin:
// 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 the plugin:
// Inside the plugin
(function($) {
$.fn.scrollregion = function(methodName) {
// this gets called with $('#foo').scrollregion()
};
})(jQuery);
Returning a value
Here's one option for returning a value from the plugin:
// Inside the plugin
(function($) {
$.fn.scrollregion = function() {
return 4;
};
})(jQuery);
That would make this true:
$('#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:
// 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 this:
// Outside the plugin
$('div').scrollregion();
then this
in the plugin will refer to the set of all div
s 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:
// Outside the plugin
$('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 div
s 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:
// Inside the plugin
(function($) {
$.fn.nop = function() {
return this;
};
})(jQuery);
And we that we can:
// 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:
var foo = $('div');
there foo
is a jQuery object that refers to all the div
s 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:
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:
// 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
:
// 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:
// 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:
// 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:
// 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:
// Outside the plugin
var options = { 'stayCool': true };
$('#myscrollbar').scrollbar('init', options);
// 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()
.
// 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:
<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')
:
<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:
// Inside the plugin
(function($) { // [MARK 1]
/**
* Initialize the scrolling region
*/
function initHandler(options) { // [MARK 2]
return this.each(function() { // [MARK 3]
var frame = $(this);
frame.addClass('jquery-scrollregion-frame'); // [MARK 4]
var container = frame.children().first();
container.addClass('jquery-scrollregion-container');
});
};
// list of registered methods this plugin supports:
var methods = { // [MARK 5]
"init": initHandler,
};
$.fn.scrollregion = function(method) { // boilerplate [MARK 6]
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); // [MARK 7]
Here are some of the highlights, marked by numbers in the code, above:
-
Here's our namespace closure, with
$
as a handy parameter for accessing jQuery functionality. -
I allow an
options
parameter to be passed in to'init'
; it's unused in this snippet. -
Here's the common
return this.each()
line. -
This is where we add the CSS classes to the elements in question.
-
Everything from here down is boilerplate from the best practices doc.
-
When we add a property to jQuery's method list in
$.fn
is really the moment the plugin is born. -
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:
<!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:
// 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.
-
Get a github account. The new system will be tied to it, and you really should have an account, anyway.
-
`Put your project in a github repo](https://help.github.com/) (you can start with it there, or add it later).
-
Follow the steps in the publishing documentation, and add a post-receive hook on your repo to hit
http://plugins.jquery.com/postreceive-hook
. -
Choose a version number for this release. It must be in the form X.Y.Z, a conformant semantic version number.
-
Create a manifest which is a JSON file describing the characteristics of your plugin.
-
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.