You are on page 1of 287

Thanks

Id like to dedicate this book to my significant other, Patrcia, my family, and everyone else who helped me finish
it. Youre too many to list here, but you know who you are.

I appreciate all your dedication and effort.

Pro jQuery Plugins v1.0.0


page 1
License

The book, like the code, is licensed under CC BY-SA 3.0 ( http://creativecommons.org/licenses/by-sa/3.0/
deed.en_US ).

Pro jQuery Plugins v1.0.0


page 2
Table of Contents

Introduction 6
Objectives 6

Code Download and Base for listings 6

JavaScript first 7

Personal Preferences 8

$ vs jQuery 9

Global variables are bad 9

JSON vs XML 10

Overlooked novelties 10

Template engines 13

Internationalization (i18n) 13

Publishing 13

Chapter 1: The Essentials of jQuery Plugins 14


Section 1: The Why and When of Plugins 14

Section 2: The Skeleton of a Plugin 15

Section 3: Binding Events with jQuery 19

Section 4: Make Your Plugin Extensible 23

Section 5: Identification of Elements on your Plugins 40

Summary 44

Chapter 2: A Simple Input Field Validation Plugin 45


Adding More Options to the Plugin 58

Pro jQuery Plugins v1.0.0


page 3
Using i18n 62

Summary 65

Chapter 3: A Simple Tooltip Plugin 67


Solving Issues 75

Following the mouse 79

Working with Input Field validation 85

Summary 86

Chapter 4: A Simple Lightbox Plugin 87


Building the Base 87

Implementing our To-Dos 92

Adding Gallery Support 100

Adding support for iFrames and HTML 114

Using it with our previous plugins 120

Summary 123

Chapter 5: A Very Complete Slider Plugin Part I 124


Section 1: The Concept 124

Section 2: The HTML & CSS 125

Section 3: The Basic Functionality 130

Making our slider automatic 137

Adding more to our slider 141

Summary 144

Chapter 6: A Very Complete Slider Plugin Part II 146


Section 1: Adding Flexibility 146

Section 2: Making it Mobile-Friendly 170

Summary 176

Chapter 7: A Timeline Plugin Part I 177


Pro jQuery Plugins v1.0.0
page 4
Section 1: Introducing Deferred Objects 177

Section 2: The Concept 182

Section 3: The HTML & CSS 183

Summary 209

Chapter 8: A Timeline Plugin Part II 210


Section 1: The Basic Functionality 210

Section 2: Adding Animations & Flexibility 257

Summary 270

Chapter 9: Extending functionality 271


Extending selector functionality 271

A RegEx :match() selector 273

A :data() selector 275

Summary 278

Chapter 10: Useful Repeating Plugins 279


Summary 282

Appendix A: Fast-paced evolution 283


Appendix B: Complements 284
Appendix C: Interesting Experiments 287

Pro jQuery Plugins v1.0.0


page 5
Introduction

Objectives
I expect you to learn how to properly code a jQuery plugin, considering flexibility, extensibility and different
scenarios while using best practices, trying to get the best performance possible.

I will rely heavily on sample plugins for you to have an idea of a use case when learning about something. I find it
more interesting to learn about something when you see a practical use for it. From chapters 2 to 8 there are 5
sample plugins (the last two 2 separated into 2 chapters each).

This book has lots of code, and many explanations are in the code itself, as comments. I expect you to code
yourself, to obtain a better learning experience.

Code Download and Base for listings


For the following chapters (except Chapter 7 and 8), I've used JSFiddle to code ( http://jsfiddle.net/ ), so I'm not
assuming any "base" CSS reset or markup/JS besides what's available there (you may want to get your own if
you don't want to use JSFiddle). On Chapter 7 I'll suggest HTML5 Boilerplate, since it's a more robust plugin
and sample that will be developed. My favorite and recommended code editor is Sublime Text ( http://
www.sublimetext.com/ ).

While this book was initially written considering jQuery 1.8, I've tested all the code with jQuery 1.9(.1) as well.

All the book's code listings, images, and final code is available at https://github.com/BrunoBernardino/
ProjQueryPlugins. Note this includes links to the JSFiddle samples as well.

Id advice you to clone/fork this repository before starting reading, as there are no code samples inline in this
book.

Issues are bound to exist, even with all the testing, reviews, and dedication I've given this book, so if you find
anything or have any suggestion, please send it to me@brunobernardino.com.

Pro jQuery Plugins v1.0.0


page 6
JavaScript first
For developers like me who "grew up" coding only with JavaScript, creating our own frameworks and libraries,
trying to diminish the number of JavaScript lines we had to write project after project was second nature. When
jQuery came in, it was awesome. Yes, other frameworks existed (Prototype, for example: http://
prototypejs.org/ ), but they weren't as easy to use or as lightweight as jQuery.It was like a life-saver: cross-
browser, lightweight, fast, reliable, complete and flexible.

While this was (and is) great, it also opened a door for developers who knew nothing (or very little) about
JavaScript to start coding applications only with jQuery, leaving them with a big hole in their JavaScript
foundations. jQuery is built in JavaScript, so it only makes sense to know JavaScript before (or together with, at
most) jQuery.

That's why if you are not experienced with JavaScript, it's hard for you to truly understand and be experienced
with jQuery, so I suggest you get enough experience and understand JavaScript properly and deeply first,
before diving into jQuery. This book assumes that you have worked before with JavaScript and jQuery. This
book goes past some basics and assumes you know them, so it's fundamental that you do.

While this book is intended for advanced jQuery developers, it explains and justifies the use of some techniques
over others, to avoid preconceived myths and prevent confusion. It won't only show you how to code jQuery
plugins in the best way, but also JavaScript (and subsequently jQuery) best practices in terms of code structure
and organization.

This book will also enlighten and clarify you about some myths and newer things about jQuery that most
developers aren't aware of, like when to use .attr() or .prop(), the advantages of .data() and advanced uses
of .filter(), for example.

NOTE: Please pay good attention to comments in all the code in this book, as I will explain choices in
there as well as in paragraphs. Also, when showing updated/added code, the unchanged code will be
shown as an ellipsis in a comment ( //... or <!-- ... -->, for example ).

Pro jQuery Plugins v1.0.0


page 7
Personal Preferences
There are some conventions or personal preferences of mine that you will find in this book. Let's take a quick
look at them now.

Tabs & Spaces


I personally prefer tabs over spaces, specially because that gives code editors flexibility (users can define how
many spaces a tab will take in the editor window), but also because it's one character instead of two, four, or
eight, per indentation, which makes the file size smaller (not important for JS or CSS after minification, but
important for bandwidth reasons, once you get hundreds or thousands of files).

Anyway, it's a personal preference and you can use whatever you want, though.

For aligning I use spaces. This is due to the same flexibility mentioned above, because it will look misaligned
depending on the number of spaces your editor renders for you tab, if tabs were used.

Plan before you start, but not too much


On an empty file, before I start coding, I like to make a few lines of comments, each for a "task" or "phase" of
the plugin/functionality, so I can get a clear view of what's left to be done and not get distracted or lost when I
get back to that later. Don't over do it, though, as you will remember new things on the way and those should
be added to the comments as well. You don't want these comments to be constrictions, but rather guidelines
and something flexible.

You'll see me doing this in most of this book's examples.

jQuery's JavaScript Style Guide


You should follow jQuery's JavaScript Style Guide, available at http://contribute.jquery.org/style-guide/js/ .

I'll code according to their style guide, except for quotes. I have a personal preference of using single quoting so
that HTML can have double quotes just like in plain HTML. You're free to use whatever you prefer, though.

Pro jQuery Plugins v1.0.0


page 8
Good, Clean, and Readable Code

Good design is invisible

Quote by Oliver Reichenstein (


http://www.theverge.com/2012/7/24/3177332/ia-oliver-reichenstein-writer-interview-good-design-is-invisi
ble )

I'm a minimalism fan, but sometimes you won't be the only person looking at your code (and in a few months,
you may not even recognize your code. Believe me, we've all been there). Because of this, you need to sacrifice
minimalism over readability most times, which is fine. That's a part of the reason why JavaScript minifiers exist,
so you don't have to worry about writing your code in the minimum lines and characters possible all the time.

Also, remember that when you're coding "for the public", you need to make your code readable and consider
many more scenarios than the ones you encounter.

$ vs jQuery
On the global scope, you should always use jQuery, to avoid conflicts, but for minimalism sake (not
compromising readability), my preference is that you use $ in controlled scopes, so you'll see the examples with
$ instead of jQuery whenever it's not on the global scope.

Global variables are bad


Global variables are bad ( http://c2.com/cgi/wiki?GlobalVariablesAreBad ) and harmful ( http://c2.com/cgi/wiki?
GlobalVariablesConsideredHarmful ). For those reasons mainly, plugins should always be enclosed in closures,
but in the rare cases when you need a global variable, manipulate it using the window object. It's also available
globally and it's safer.

Bear in mind that when I refer to global functions or variables in this book, I'm talking about using the window
object for it.

Pro jQuery Plugins v1.0.0


page 9
JSON vs XML
When working with JavaScript, this is a no-brainer. You should always generate and parse JSON, it's much
smaller and cleaner. It's always easy to use XML instead (or together), though, so don't worry about it. I'll be
using JSON throughout the book.

Overlooked novelties
Here we'll be looking at things that jQuery has that most people aren't aware of, or that are just not properly
used most of the times.

.prop()
.prop() was introduced in jQuery 1.6, but many developers aren't aware of it. It's so much better
than .attr() for many cases, and it's consistent, unlike .attr().

jQuery Documentation ( http://api.jquery.com/prop/ ) does a pretty good job explaining the differences, so allow
me to quote them:

The difference between attributes and properties can be important in specific situations.
() The .prop() method provides a way to explicitly retrieve property values,
while .attr() retrieves attributes.

For example, selectedIndex, tagName, nodeName, nodeType, ownerDocument,


defaultChecked, and defaultSelected should be retrieved and set with the .prop()
method. () These do not have corresponding attributes and are only properties.

With a practical example of the checked attribute for a checkbox:

Pro jQuery Plugins v1.0.0


page 10
According to the W3C forms specification ( http://www.w3.org/TR/html401/interact/
forms.html#h-17.4 ), the checked attribute is a boolean attribute ( http://www.w3.org/TR/
html4/intro/sgmltut.html#h-3.3.4.2 ), which means the corresponding property is true if the
attribute is present at alleven if, for example, the attribute has no value or an empty string
value (...).

In sum, if you want to know if a checkbox is checked or not, you should use $(elem).prop("checked")
and not $(elem).attr("checked"), as the first will return a boolean, whereas the latter will vary from a
string to undefined.

.attr()
With the implementation of .prop() in jQuery 1.6, .attr() changed a bit. Here's a great explanation from
the jQuery Documentation ( http://api.jquery.com/attr/ ):

The .attr() method returns undefined for attributes that have not been set. In
addition, .attr() should not be used on plain objects, arrays, the window, or the
document. To retrieve and change DOM properties, use the .prop() method.

.data()
.data() is the oldest of the novelties I'm mentioning here, but was mostly overlooked because up until jQuery
1.4.3 it wasn't as useful as it is now.

If you have no idea what it does, here's a nice summary from jQuery Documentation ( http://api.jquery.com/
data/ ):

The .data() method allows us to attach data of any type to DOM elements in a way that is
safe from circular references and therefore from memory leaks.

HTML 5 data- attributes ( http://ejohn.org/blog/html-5-data-attributes/ ) will be


automatically pulled in to jQuery's data object.

So, as you can see, it's pretty awesome. It's like an easily-accessible lightweight database in an element that
you can set jQuery to get when the page is loaded (by using the HTML5 data-* attributes), and use/update that.

Pro jQuery Plugins v1.0.0


page 11
.on() / .o()

Before jQuery 1.7, attaching (and detaching) events was not very consistent. There were a lot of different
methods, ways, benefits, disadvantages, etc.

Luckily this all changed with the addition of .on() (and .off()).

Here's a not-so-short explanation of how it works, from the jQuery Documentation ( http://api.jquery.com/on/ ):

The majority of browser events bubble, or propagate, from the deepest, innermost element
(the event target) in the document where they occur all the way up to the body and the
document element. In Internet Explorer 8 and lower, a few events such as change and submit
do not natively bubble but jQuery patches these to bubble and create consistent cross-
browser behavior.

If selector is omitted or is null, the event handler is referred to as direct or directly-bound.


The handler is called every time an event occurs on the selected elements, whether it occurs
directly on the element or bubbles from a descendant (inner) element.

When a selector is provided, the event handler is referred to as delegated. The handler is
not called when the event occurs directly on the bound element, but only for descendants
(inner elements) that match the selector. jQuery bubbles the event from the event target up to
the element where the handler is attached (i.e., innermost to outermost element) and runs the
handler for any elements along that path matching the selector.

Event handlers are bound only to the currently selected elements; they must exist on the
page at the time your code makes the call to .on().

What they're trying to say is that if you want to attach an event handler only to existing elements in the DOM,
use $(elem).on() where elem is a selector of existing elements, but if you want to attach an event handler
for existing and future elements in the DOM, use, for example, $(document).on().

Pro jQuery Plugins v1.0.0


page 12
Template engines
There are many template engines for jQuery available, but no "officially supported" ones yet. jQuery.tmpl ( http://
api.jquery.com/jquery.tmpl/ ) was used for a while but has been discontinued ( http://blog.jquery.com/
2011/04/16/official-plugins-a-change-in-the-roadmap/ ) in favor of a future jsRender ( https://github.com/
BorisMoore/jsrender ) and jsViews ( https://github.com/BorisMoore/jsviews ). Since these aren't even at a beta
stage yet, we'll go with the template engine existing in underscore.js ( http://underscorejs.org ), as it's got some
other very nice utility functions and it's a light library (4kb minified and gzipped). Another option would be
mustache.js ( https://github.com/jonnyreeves/jquery-Mustache ), for example.

In the end, it's up to you to decide which template engine you prefer, and it shouldn't be hard to switch from
one to another.

Internationalization (i18n)
It's very common for jQuery Plugin developers to forget about internationalization and other languages when
they are english, and this is something that should never be overlooked, no matter what programming language
you're working with, if you're developing something for other people to use (or even yourself, when project
requirements change in the future, which happens only all the time).

There are quite a few i18n techniques and plugins for JavaScript, but nothing consensual or even standard, so
I'm going to use what I prefer and like the most, which is jQuery-i18n by Dave Perrett ( https://github.com/
recurser/jquery-i18n ).

Publishing
To publish a jQuery plugin to the jQuery Plugin Registry ( http://plugins.jquery.com ), you need to follow a few
simple steps that you can see here: http://plugins.jquery.com/docs/publish/.

Pro jQuery Plugins v1.0.0


page 13
Chapter 1:
The Essentials of jQuery Plugins

This chapter covers the basics of jQuery plugins, concepts, and best practices.

Section 1: The Why and When of Plugins


Sometimes it's hard to know if you should build a plugin for some functionality or function you need, so here are
a few reasons of why you should build a jQuery Plugin, and when.

Portability
Bundling a common function or functionality in a jQuery plugin will make it more portable, which will allow you to
easily integrate it easily and quickly in other projects.

Reusability
You'll be able to reuse it easily and quickly. This is one of the reasons why it's important to consider multiple
scenarios and conditions; only in very ideal (and extremely rare) conditions will these be the same for all the
elements you're applying the plugin to, so make it flexible.

Abstraction
If you make the plugin flexible enough, it'll be possible for you to use it in a way that will make your code more
readable and easy to manage.

Imagine if you'd have to code a slider every time you needed one or a similar functionality... That would be
insane! Abstraction is related to reusability; you want the plugin to be abstract enough so it can be reused in
other scenarios easily.

Pro jQuery Plugins v1.0.0


page 14
The DRY (Don't Repeat Yourself) principle
If you look at the why's and when's above, you'll see that mostly we're trying to avoid repeating functionality or
the same lines of code repeating several times.

A great advantage to this approach is also that when you need to make a change to how something works
(imagine some sort of validation function or plugin), you just need to do it once, in one place, not everywhere it's
used. This saves tons of time.

Good and Bad Examples


Consider input validation. You'll need to do this for several input fields, probably in more than one form,
validating for different types of input (strings, booleans, numbers, emails, etc.). This should definitely be a plugin.
It'll make your code much more cleaner and easier to read, not to mention lighter.

But what if you need to do something every time an element is removed from your page (like reload or re-render
the elements or something else that needs to interact with them)? In my opinion, this doesn't need to be a
plugin, as it's something very specific and not very flexible, or hard/complex to be made flexible (bear in mind
the view itself and/or the rendering should be independent things as that may need to be executed for many
other reasons, but triggering them doesn't need to).

Now imagine you want check for the current user's browser and its version. This is something that, even though
it only requires a few lines of code, you can use it elsewhere. It shouldn't be a plugin, though, but a "global"
function/method, because it's not usable in many elements. The only reason to be a plugin is if it is *part* of an
utilities plugin (used mainly as a namespace container).

Section 2: The Skeleton of a Plugin


Listing 1-1 shows a very simple but properly structured "Hello World" plugin.

Listing 1-1: Simple "Hello World" plugin

(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {};

options = $.extend( defaults, options );

Pro jQuery Plugins v1.0.0


page 15
return this.each(function() {
var $this = $(this),
data = $this.data( 'helloWorld' );

// If the plugin hasn't been initialized yet


if ( ! data ) {
$this.data( 'helloWorld', {
target : $this
});
methods.print.apply( this );
}
});
},

destroy : function() {
$(window).off( '.helloWorld' );

return this.each(function() {
var $this = $(this),
data = $this.data( 'helloWorld' );

$this.removeData( 'helloWorld' );
});
},

print : function() {
var $this = $(this);
$this.text( 'Hello World!' );
}
};

$.fn.helloWorld = 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.helloWorld' );
}
};
})( jQuery, window );

Pro jQuery Plugins v1.0.0


page 16
NOTE: You've added window to the closure. Whenever you require a variable/method inside window's
scope (global), and/or window itself, you should add it to the closure.

It would be called using $(element).helloWorld(); and it would add the text "Hello World!" in the
element element.

Obviously that something this simple can be converted into the following smaller code (see Listing 1-2), gaining
performance and losing flexibility, which isn't bad as the plugin is only meant to replace an element's text with
"Hello World!", and will hardly ever be more complex than that (but remember this is a very specific and rare
case):

Listing 1-2: Cleaner and smaller "Hello World" plugin

(function( $ ) {
$.fn.helloWorld = function() {
return this.each(function() {
$(this).text( 'Hello World!' );
});
};
})( jQuery );

Remember, though, that the Skeleton of a Plugin is very important for having a great structure and organization
in your plugin, and the one that jQuery uses as an example in their Documentation ( http://docs.jquery.com/
Plugins/Authoring ) is a great one, which I present (pluginSkeleton) here with some slight modifications, just
because it has a few more things handy if the plugin isn't expected to be very simple (see Listing 1-3):

Listing 1-3: Reworked jQuery Skeleton plugin

(function( $, window ) {
var methods = {

init : function( options ) {


var defaults = {
// default properties go here
};

options = $.extend(defaults, options);


Pro jQuery Plugins v1.0.0
page 17
return this.each(function() {
var $this = $(this),
data = $this.data( 'pluginSkeleton' );

// If the plugin hasn't been initialized yet


if ( ! data ) {
$(this).data( 'pluginSkeleton', {
target : $this
// Add some more data
});
}
});
},

destroy : function() {
$(window).off( '.pluginSkeleton' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'pluginSkeleton' );

// That's right, namespaced events!


$this.removeData( 'pluginSkeleton' );
});
}

// Add more methods here


};

$.fn.pluginSkeleton = 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.pluginSkeleton' );
}
};
})( jQuery, window );

Pro jQuery Plugins v1.0.0


page 18
This method is great because it separates logic very well and it's very easy to manage and understand.

There are obviously other valid methods, like this one by Stefan Gabos ( http://stefangabos.ro/jquery/jquery-
plugin-boilerplate-revisited/ ), but the one above (Listing 1-3) has better performance and I think it has better
legibility.

Section 3: Binding Events with jQuery


Events should be bound using .on(), not .live(), .delegate(), or .bind(). Though these are aliases for .on() now,
they're not the correct syntax to use anymore, and .live() is deprecated as of jQuery 1.7.

A good example to bind a click to an element with an ID of `some-random-element` would be this one in Listing
1-4.

Listing 1-4: Good example of Delegated Binding of a Click to an Element

$(document).on( 'click.demo', '#some-random-element', function( event ) {


event.preventDefault();

// Do something here
});

This will bind a click with namespace `demo` to the element with id `some-random-element` whether it exists on
the DOM or not at the time this code is executed, working also for "future" elements (being what in jQuery Docs
is called a *delegated event*).

If you want to bind an event only to existing elements in the DOM (what in jQuery Docs is called a *direct event*),
you just need to call .on() on the element, like as shown in Listing 1-5.

Listing 1-5. Good Example of Direct Binding of a Click to an Element

$('#some-random-element').on( 'click.demo', function( event ) {


event.preventDefault();

// Do something here
});

Pro jQuery Plugins v1.0.0


page 19
Note that with .on() you can also pass data into the event handler function.

A good example is the one at jQuery Docs ( http://api.jquery.com/event.data/ ), also shown in Listing 1-6.

Listing 1-6: Passing data into the Event Handler Function

var logDiv = $('#log');

for ( var i = 0; i < 5; i++ ) {


$('button').eq(i).on( 'click', { value: i }, function( event ) {
var msgs = [
'button = ' + $(this).index(),
'event.data.value = ' + event.data.value,
'i = ' + i
];

logDiv.append( msgs.join(', ') + '<br>' );


});
}

Basically, i will always be 5 when printed because at that time (when a button is clicked), the for loop will have
finished. We can then get the iteration value only by passing it into the event handler function, as an object with
value = i (with the value at the time of execution, not at the time the event is triggered), that you can get on
event.data.value.

Now, for this book I was going to create a benchmark for .live() vs .on() vs .delegate() vs .bind()
specially because I used to use .live() a lot (wrongly, but force of habit), but I found out it was already 99%
done on jsPerf ( http://jsperf.com ) and I just needed to make a few tweaks, add a few cases and click "Run" to
get some results! Figure 1-1 and Figure 1-2 show the results ( http://jsperf.com/jquery-live-vs-on-vs-delegate-
vs-bind/5 ).

Pro jQuery Plugins v1.0.0


page 20
Figure 1-1: Benchmarking event binding using .live(), .on(), .delegate(), and .bind(). Operations per second
(higher the number, the better). Highlighted in green is the best performance; in red is the worst.

Figure 1-2: Same benchmark in a bar graph (the bigger the bar, the better)

Pro jQuery Plugins v1.0.0


page 21
NOTE: The tests above and the following were done with jQuery 1.8 on October 3rd. The 5th revision I'm
linking above existed only to make sure jQuery 1.8 was used instead of 1.9, because .live() was removed
from jQuery 1.9. The results are still valid and so is my point.

As you can see, .live() and .bind() are painfully slow when compared with .on() and .delegate()
(which actually wins for delegated events over .on()). This should be less evident in future versions
because .on() will discard backwards compatibility with .live(), for example.

But you need to remember that was only with one test in one browser. After running a few more tests (and in a
few more browsers), I got the results shown in Figure 1-3 and Figure 1-4.

Figure 1-3: Same benchmark, in a table, with results for more browsers

Pro jQuery Plugins v1.0.0


page 22
Figure 1-4: Same benchmark results as Figure 1-3, in a bar graph

So, basically the .on() wins with the .delegate() in delegated evens creeping out in front for Chrome 23.x
and 24.x, but the point is that .on() is a very decent choice, not only in terms of being future-proof, but also in
terms of current performance.

Section 4: Make Your Plugin Extensible


Since you're programming for the general audience as opposed to yourself, you have to keep certain things in
mind.

1. Provide settings for all the constants you use and assumptions you
do
This means that if you use a 'fast', 'slow', 500 or anything like that for an animation, you should make it a
setting with a default value.

Pro jQuery Plugins v1.0.0


page 23
If you're using a class to identify something, make that a setting with a default value (see Listing 1-7 and Listing
1-8).

Listing 1-7: Bad Example by Not Providing Settings/Options Overrides

(function( $, window ) {
$.fn.sampleSlideDownAndAlert = function() {
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideDownAndAlert' );

if ( ! data ) {
$(this).data( 'sampleSlideDownAndAlert', {
target : $this
});

$this.slideDown( 'fast', function() {// Uh-oh. How will the user


change this 'fast' without changing the code?
window.alert( 'Yup! This is happening... An alert()!' );

$('.show-after-slide-down').show();// What if I want to show


something else? Or do something else?
});
}
});
};
})( jQuery, window );

Listing 1-8: Good Example of Providing Settings/Options Overrides

(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {
'slideSpeed': 'fast',
'onComplete': $.noop,
'showAlert': false,
'logOnConsole': true,
'showElementsOnComplete': true,
Pro jQuery Plugins v1.0.0
page 24
'classToShow': '.show-after-slide-down',
'showSpeed': 0,
'showEasingEffect': 'swing',
'onShowComplete': $.noop
};

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideDownAndAlert' );

if ( ! data ) {
$(this).data( 'sampleSlideDownAndAlert', {
target : $this
});

methods.slide.call( this, options );


}
});
},

destroy : function() {
$(window).off( '.sampleSlideDownAndAlert' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'sampleSlideDownAndAlert' );

$this.removeData( 'sampleSlideDownAndAlert' );
});
},

slide : function( options ) {


$(this).slideDown( options.slideSpeed, function() {
if ( options.showAlert ) {
window.alert( 'Yup! This is happening... An alert()!' );
}

if ( options.logOnConsole && window.console && window.console.log ) {


window.console.log( 'Woohoo! Yes! A proper console.log()!' );
}

Pro jQuery Plugins v1.0.0


page 25
if ( options.showElementsOnComplete ) {
$(options.classToShow).show( options.showSpeed,
options.showEasingEffect, options.onShowComplete );
}

if ( $.isFunction(options.onComplete) ) {
options.onComplete.call( this, options );
}
});
}
};

$.fn.sampleSlideDownAndAlert = 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.sampleSlideDownAndAlert' );

}
};
})( jQuery, window );

As you can see, you've given the users a quick and easy way to override the speed of the animation, its class,
and its animation easing, among other things, providing settings for all constants.

Also, enabling the console.log and callbacks as an option is a good thing because although you don't see
the need to use them, other people might, and that's how you don't make assumptions.

2. Provide callbacks for all the methods that are asynchronous or


delegated or that use animations
This one explains itself, so if you're doing some ajax call or some animation, you should provide an easy way for
someone to add a callback once that task is completed (see Listing 1-9 and Listing 1-10).

Pro jQuery Plugins v1.0.0


page 26
Listing 1-9: Bad Example of Not Providing a Callback

(function( $ ) {
$.fn.sampleLoadPage = function( page ) {
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleLoadPage' );

if ( ! data ) {
$(this).data( 'sampleLoadPage', {
target : $this,
page: page
});

$.get( '/getPage', { page: page }, function( data, textStatus, jqXHR )


{// Uh-oh. What if the user has the page script at some other URL?
$('#new-page-content').html( data.htmlContent ).fadeIn( 'fast' );//
And what if the element has another ID?
});// Guessing the dataType, are we?
}
});
};
})( jQuery );

In this case, there are several issues, such as not providing a callback in case the user wants to do something
after the Ajax request finishes, having to hard-code the ID and Ajax URL, and so on.

Listing 1-10: Good Example of Providing Settings and Callbacks

(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {
'loadPageURL': '/getPage',
'page': false,
'responseDataType': 'json',
'onComplete': $.noop,
'onError': $.noop,
'onSuccess': $.noop,
Pro jQuery Plugins v1.0.0
page 27
'pageWrapper': '#new-page-content',
'fadeSpeed': 'fast',
'fadeEasingEffect': 'swing'
};

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleLoadPage' );

if ( ! data ) {
$(this).data( 'sampleLoadPage', {
target : $this,
options: options
});

methods.loadPage.call( this, options );


}
});
},

destroy : function() {
$(window).off( '.sampleLoadPage' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'sampleLoadPage' );

$this.removeData( 'sampleLoadPage' );
});
},

loadPage : function( options ) {


$.get( options.loadPageURL, { page: options.page }, function( data,
textStatus, jqXHR ) {
if ( data.htmlContent ) {
$
(options.pageWrapper).html( data.htmlContent ).fadeIn( options.fadeSpeed,
options.fadeEasingEffect, function() {
if ( $.isFunction(options.onSuccess) ) {
options.onSuccess.call( this, data );
}
Pro jQuery Plugins v1.0.0
page 28
});
} else if ( $.isFunction(options.onError) ) {
options.onError.call( this, options );
}
}, options.responseDataType ).error(function() {
if ( $.isFunction(options.onError) ) {
options.onError.call( this, options );
}
}).complete(function() {
if ( $.isFunction(options.onComplete) ) {
options.onComplete.call( this, options );
}
});
}
};

$.fn.sampleLoadPage = 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.sampleLoadPage' );
}
};
})( jQuery, window );

3. Don't ignore dierent environments


So you've got jQuery, jQuery UI, Twitter Bootstrap and a couple of other plugins that interact with yours. Do you
really need all of those? What if someone uses a different plugin that will cause a conflict?

A great example is when someone is using a different (or additional) JavaScript library. Obviously, you won't be
coding a different plugin for each library (though that would make a lot of people happy), but degrade gracefully,
maybe send an error, or perhaps make it possible or impossible (but not crashing!) to work with other libraries or
plugins by making those options in your code (see Listing 1-11 and Listing 1-12).

Pro jQuery Plugins v1.0.0


page 29
Listing 1-11: Bad Example of Not Considering Dierent Environments/Scenarios

// Using jQuery Mousewheel 3.0.6 (https://raw.github.com/brandonaaron/jquery-


mousewheel/master/jquery.mousewheel.js) by Brandon Aaron

// Using Twitter Bootstrap's Javascript 2.1.1 (https://github.com/twitter/


bootstrap)

// Using Bootbox 2.4.1 (http://bootboxjs.com/)

(function( $, window ) {
$.fn.sampleSideScroll = function( page ) {
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSideScroll' );

if ( ! data ) {
$(this).data( 'sampleSideScroll', {
target : $this
});

$(this).on( 'mousewheel', function( event, delta ) {// What if there's


no mousewheel?
var widthChange = window.Math.abs( delta ),
currentLeftPosition = window.parseInt( $(this).css('left'), 10 );

if ( delta > 0 ) {// Up/Left


if ( (currentLeftPosition - widthChange) > -500 ) {// A constant
$(this).stop().animate({
'left': ( currentLeftPosition - widthChange )
}, 100 );// Again, speed is not a variable here
} else {
$(this).stop().animate({
'left': -500// Another constant
}, 100 );

$(this).off( 'mousewheel' );

bootbox.alert( 'No more scrolling to the left. Scrolling is now


disabled.' );// What if there's no bootbox?
}

Pro jQuery Plugins v1.0.0


page 30
} else {// Down/Right
if ( (currentLeftPosition + widthChange) < 500 ) {
$(this).stop().animate({
'left': ( currentLeftPosition + widthChange )
}, 100 );
} else {
$(this).stop().animate({
'left': 500
}, 100 );

$(this).off( 'mousewheel' );

bootbox.alert( 'No more scrolling to the right. Scrolling is


now disabled.' );
}
}
});
}
});
};
})( jQuery, window );

The main issues with this code are that we're using Bootbox, mousewheel, and Bootstrap while not checking
whether they are included, thus possibly throwing errors and not handling them but also not providing
alternatives.

Listing 1-12: Good Example of Abstracting the Plugin, Allowing the User to Use
Whatever They Want

(function( $, bootbox, window ) {


var methods = {
init : function( options ) {
var defaults = {
'minLeft': -500,
'maxLeft': 500,
'leftArrowSlideScroll': 50,
'rightArrowSlideScroll': -50,
'minDelta': 50,// Minimum delta value
'slideSpeed': 'fast',
'onReach': $.noop,
Pro jQuery Plugins v1.0.0
page 31
'onMinReach': $.noop,
'onMaxReach': $.noop
};

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSideScroll' );

if ( ! data ) {
$this.data( 'sampleSideScroll', {
target : $this,
options: options
});

/*
This will check for mousewheel, if it's not installed, it'll create
an event listener for the left/right arrow keys and throw an error about it.
*/
methods.checkForMousewheel.call( this, options );

$this.on( 'mousewheel.sampleSideScroll', function( event, delta ) {


if ( delta > 0 ) {// Up/Left
methods.slideLeft.call( this, event, delta, options );
} else {
methods.slideRight.call( this, event, delta, options );
}
});
}
});
},

destroy : function() {
$(window).off( '.sampleSideScroll' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'sampleSideScroll' );

$this.removeData( 'sampleSideScroll' );
});
},
Pro jQuery Plugins v1.0.0
page 32
error : function( errorMessage ) {
if ( bootbox ) {
bootbox.alert( errorMessage );
} else {
$.error( errorMessage );
}
},

checkForMousewheel : function( options ) {


if ( ! $.fn.mousewheel ) {
$(this).on( 'keydown.sampleSideScroll', function( event ) {
event.preventDefault();

if ( event.keyCode === 37 ) {// Left arrow


$(this).trigger( 'mousewheel.sampleSideScroll', options.
leftArrowSlideScroll );
} else/* if (event.keyCode === 39) {// Right arrow*/ {// Commented
because it's really not necessary
$(this).trigger( 'mousewheel.sampleSideScroll', options.
rightArrowSlideScroll );
}
});

methods.error.call( this, 'jQuery Mousewheel was not found! Please


install it. Arrow keys will be used instead.' );
}
},

slideLeft : function( event, delta, options ) {


var widthChange = window.Math.abs( delta ),
currentLeftPosition = window.parseInt( $(this).css('left'), 10 );

if ( widthChange < options.minDelta ) {


widthChange = options.minDelta;
}

if ( (currentLeftPosition - widthChange) > options.minLeft ) {


$(this).stop().animate({
'left': ( currentLeftPosition - widthChange )
}, options.slideSpeed );
} else {
$(this).stop().animate({
Pro jQuery Plugins v1.0.0
page 33
'left': options.minLeft
}, options.slideSpeed );

$(this).off( 'mousewheel.sampleSideScroll' );

methods.error.call( this, 'No more scrolling to the left. Scrolling is


now disabled.' );

if ( $.isFunction(options.onMinReach) ) {
options.onMinReach.call( this, options );
}

if ( $.isFunction(options.onReach) ) {
options.onReach.call( this, options );
}
}
},

slideRight : function( event, delta, options ) {


var widthChange = window.Math.abs( delta ),
currentLeftPosition = window.parseInt( $(this).css('left'), 10 );

if ( widthChange < options.minDelta ) {


widthChange = options.minDelta;
}

if ( (currentLeftPosition + widthChange) < options.maxLeft ) {


$(this).stop().animate({
'left': ( currentLeftPosition + widthChange )
}, options.slideSpeed );
} else {
$(this).stop().animate({
'left': options.maxLeft
}, options.slideSpeed );

$(this).off( 'mousewheel.sampleSideScroll' );

methods.error.call( this, 'No more scrolling to the right. Scrolling


is now disabled.' );

if ( $.isFunction(options.onMaxReach) ) {
options.onMaxReach.call( this, options );
}
Pro jQuery Plugins v1.0.0
page 34
if ( $.isFunction(options.onReach) ) {
options.onReach.call( this, options );
}
}
}
};

$.fn.sampleSideScroll = 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.sampleSideScroll' );
}
};
})( jQuery, bootbox, window );

You're now allowing the developers to use whatever they want (Bootbox with Bootstrap or something else) with
the callbacks onReach, onMinReach, and onMaxReach. Also, you're now detecting mousewheel and falling
back to arrow key scrolling. You're also triggering an error that will check whether Bootbox exists, and if it
doesn't, you fallback using $.error.

Note that this example is not perfect (since mousewheel is constantly sending the delta, the scroll only "works"
when the last value is sent, for example) and has room for improvement, such as refactoring slideLeft and
slideRight, combining them into one method, or even considering scenarios where the left CSS property
could be auto or where you could need $.position() or $.offset(). Keep in mind that I'm only using
this to show an example of how to consider different environments. You can improve this on your own if you
want, it'll be a great learning experience.

4. Don't ignore dierent scenarios


You're probably coding for one scenario, but remember you can't predict every situation in the wild, where that
plugin will be executed.

If you're developing a slider, for example, consider that some people may want to include different sliders.
Different positions. If you offer great flexibility, every one will be able to use your plugin and tweak it easily to their
needs.
Pro jQuery Plugins v1.0.0
page 35
Also, avoid CSS the most you can on your plugin (see Listing 1-13 and Listing 1-14).

Listing 1-13: Bad Example of Considering Dierent Scenarios and Needs

// 1 slideshow with 3 slides, no more.


// Poor animation that won't even loop.
// Also, CSS in JavaScript like this? tsk tsk...

(function( $, window ) {
$.fn.sampleSlideshow = function() {
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideshow' );

if ( ! data ) {
$this.data( 'sampleSlideshow', {
target : $this
});

$('#slide-1, #slide-2, #slide-3').css( 'position', 'absolute' );

$('#slide-1').fadeIn( 'fast', function() {


window.setTimeout(function() {
$('#slide-1').fadeOut( 'fast' );
$('#slide-2').fadeIn( 'fast', function() {
window.setTimeout(function() {
$('#slide-2').fadeOut( 'fast' );
$('#slide-3').fadeIn( 'fast' );
}, 2000 );
});
}, 2000 );
});
}
});
};
})( jQuery, window );

This slider has several issues besides the constants not being configurable settings, such as having CSS inside
the JavaScript (reducing flexibility and customization options) and using constant element selectors.

Pro jQuery Plugins v1.0.0


page 36
This code wouldn't work if the user needed to use a class, for example, or those IDs were already taken for
other elements. Also, if the user didn't need the slides to be absolutely positioned, the user would have to edit
the plugin, not change a setting.

Listing 1-14: Good Example of Considering Dierent Scenarios and Needs

// The absolute positioning (or whatever the user wants/needs) goes in an


external CSS file.
(function( $, window ) {
var flags = {
'slideTimeout': null// This will be used to have the timeout "globally", so
it can be cancelled.
};

var methods = {
init : function( options ) {
var defaults = {
'fadeSpeed': 'fast',
'slideSelector': '.slide',
'slideTime': 2000,
'activeClass': 'active',
'loop': true,// If true, when the end or start is reached,
it'll continue looping, otherwise it'll just stop there.
'autoShowNext': true// If true, show next element on load and so on.
};

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideshow' );

if ( ! data ) {
$this.data( 'sampleSlideshow', {
target : $this,
options: options
});

/*

Pro jQuery Plugins v1.0.0


page 37
When an element is clicked, the slideshow will stop. This should
actually be another option so a different element could be set as the "stopper",
but if we do it like that this example will get too big to prove its point :)
*/

$this.find(options.slideSelector).on( 'click.sampleSlideshow',
function( event ) {
window.clearTimeout( flags.slideTimeout );
});

// Show first slide


methods.goToSlide.call( this, 0, options );
}
});
},

destroy : function() {
$(window).off( '.sampleSlideshow' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'sampleSlideshow' );

$this.removeData( 'sampleSlideshow' );
});
},

// 0-based index
goToSlide : function( slideIndex, options ) {
var $this = $(this),
slideCount = $this.find( options.slideSelector ).length,
activeSlide = $this.find( options.slideSelector + '.' +
options.activeClass ),
activeSlideIndex =
$this.find( options.slideSelector).index(activeSlide );

if ( slideIndex < 0 || slideIndex > (slideCount - 1) || slideIndex ===


activeSlideIndex ) {
if ( ! options.loop || slideIndex === activeSlideIndex ) {
$.error( 'Invalid slideIndex Requested' );
return false;
} else if ( slideIndex < 0 ) {
slideIndex = slideCount - 1;
Pro jQuery Plugins v1.0.0
page 38
} else if ( slideIndex > (slideCount - 1) ) {
slideIndex = 0;
}
}

// We could have callbacks here for the fadeOut and fadeIn also
$this.find( options.slideSelector )
.removeClass( options.activeClass )
.fadeOut( options.fadeSpeed )
.eq( slideIndex )
.addClass( options.activeClass )
.fadeIn( options.fadeSpeed );

// Set a Timeout to show the next slide


if ( options.autoShowNext ) {
flags.slideTimeout = window.setTimeout(function() {
methods.nextSlide.call( $this, options );
}, options.slideTime );
}
},

prevSlide : function( options ) {


var activeSlide = $(this).find( options.slideSelector + '.' +
options.activeClass ),
activeSlideIndex = $
(this).find(options.slideSelector).index( activeSlide );

methods.goToSlide.call( this, activeSlideIndex - 1, options );


},

nextSlide : function( options ) {


var activeSlide = $(this).find( options.slideSelector + '.' +
options.activeClass ),
activeSlideIndex = $
(this).find(options.slideSelector).index( activeSlide );

methods.goToSlide.call( this, activeSlideIndex + 1, options );


}
};

$.fn.sampleSlideshow = function( method ) {


if ( methods[method] ) {

Pro jQuery Plugins v1.0.0


page 39
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.sampleSlideshow' );
}
};
})( jQuery, window );

Now the slider will work for different situations, and all the user needs to do is change a setting, not change the
plugin's code.

Section 5: Identification of Elements on your Plugins

1. Use classes the most you can


IDs have to be unique. That's why they are IDs. A panda dies every time there is a repeated ID in a single web
page (that's why they're in risk of extinction).

Most times you'll want people to be able to use your plugin over and over, so use classes the most you can (see
Listing 1-15 and Listing 1-16).

Listing 1-15: Bad Example of Using IDs

(function( $, window ) {

// ...

$('#slide-1').fadeIn( 'fast', function() {


window.setTimeout(function() {
$('#slide-1').fadeOut( 'fast' );
$('#slide-2').fadeIn( 'fast', function() {
window.setTimeout(function() {
$('#slide-2').fadeOut( 'fast' );
$('#slide-3').fadeIn( 'fast' );
}, 2000 );

Pro jQuery Plugins v1.0.0


page 40
});
}, 2000 );
});

// ...

})( jQuery, window );

This example is using IDs to select several elements, and although it is faster than using classes, it's a lot less
flexible and customizable than using classes.

Listing 1-16: Good Example of Using Classes for the Same Objective as in Listing
1-15

(function( $, window ) {

// ...

$('.slide:eq(0)').fadeIn( 'fast', function() {


window.setTimeout(function() {
$('.slide:eq(0)').fadeOut( 'fast' );
$('.slide:eq(1)').fadeIn( 'fast', function() {
window.setTimeout(function() {
$('.slide:eq(1)').fadeOut( 'fast' );
$('.slide:eq(2)').fadeIn( 'fast' );
}, 2000 );
});
}, 2000 );
});

// ...

})( jQuery, window );

These classes should ideally be in a setting, like all the previous examples, but this example is just to make a
simple point.

Pro jQuery Plugins v1.0.0


page 41
2. IDs aren't so bad, if they're really unique
It's much quicker to refer to an element by ID than a class, so it's ok to use them, but in that case, always
generate IDs on-the-fly to avoid duplications and assure they are unique (see Figure 1-5).

Figure 1-5: Benchmark for fetching an element by ID and by class

As you can see, it's ridiculously faster to fetch an element by ID than by class ( http://jsperf.com/jquery-select-
id-vs-class ); Take a look at Listing 1-17, Listing 1-18, and Listing 1-19 for some bad, good, and better
examples.

Listing 1-17: Bad Example with Good Performance but Poor Flexibility

(function( $ ) {

// ...

var html = '<input type="text" id="search" name="search" value=""


placeholder="Search">';

$('#area').append( html );

// ...

})( jQuery );

Pro jQuery Plugins v1.0.0


page 42
Listing 1-18: Good but Not Optimal Example with Average Performance and
Flexibility

(function( $ ) {

// ...

var html = '<input type="text" class="search" name="search" value=""


placeholder="Search">';

$('.search-wrapper').append( html );

// ...

})( jQuery );

Listing 1-19: Good Example with Average Performance and Great Flexibility

(function( $ ) {

// ...

var generatedID = new Date().getTime();

var html = '<input type="text" class="search" id="search-' + generatedID + '"


name="search" value="" placeholder="Search">';

$('.search-wrapper').append( html );

// ...

})( jQuery );

Performance is better when you try to get the search input, though, with $('#search-' + generatedID).

Pro jQuery Plugins v1.0.0


page 43
Summary
In this chapter, you learned some essential ways of problem solving and best practices when developing a
jQuery plugin, including the topics of why to do it, the best skeleton of a plugin, proper event binding,
extensibility, and selector performance. You should now feel confident about how to tackle problems and justify
the usage of some methods over others because of performance or just flexibility.

In the next chapter, you'll start building a simple plugin from scratch that will validate input fields.

Pro jQuery Plugins v1.0.0


page 44
Chapter 2: A Simple Input Field
Validation Plugin

We'll start simple so we can focus on the small things first.

This plugin will validate an input field and show a warning inside it if something's wrong. It will also consider the
HTML5 specification and make use of the browser's built-in input validation before your script.

Let's start with the base for our plugin (see Listing 2-1).

Listing 2-1: HTML for the Plugin, a Form

<form action="#" method="post" class="sample-form">


<span>
<input type="text" data-type="alphanumeric-extended" name="name"
placeholder="Name" required>
</span>
<span>
<input type="text" data-type="alphanumeric" name="username"
placeholder="Username" required>
</span>
<span>
<input type="email" name="email" placeholder="Email" required>
</span>
<span>
<input type="password" name="password" placeholder="Password" required>
</span>
<span>
<input type="tel" name="phone" placeholder="Phone Number">
</span>
<span>
<input type="url" name="url" placeholder="Website">
</span>
<span>

Pro jQuery Plugins v1.0.0


page 45
<input type="number" name="children" placeholder="Number of Children">
</span>
<button type="submit" name="signup">Signup!</button>
</form>

NOTE: I'm wrapping the inputs around a span because you'll add an element next to the input, since it's
not possible to add it inside the input. It can be anything other than a span, as long as it's containing the
input.

Listing 2-2 shows the base CSS you need for this plugin.

Listing 2-2: CSS for the Form for the Plugin

.sample-form {
display: block;
padding: 10px;
background: #FFF;
}

.sample-form input, .sample-form button {


display: block;
margin: 5px 0;
padding: 5px 10px;
}

.sample-form input {
width: 200px;
font-size: 14px;
}

.sample-form button {
font-size: 16px;
}

Listing 2-3 shows the JavaScript code for the plugin.

Pro jQuery Plugins v1.0.0


page 46
Listing 2-3: JavaScript for the Plugin, with Initial Planning Commented

(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {
'dataType': 'type',// data-* property to check for the field/
validation type
'autoCheck': true,// If true, validate the field when the plugin is
initialized
'fadeSpeed': 'fast'
};

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'inputValidation' );

if ( ! data ) {
$this.data( 'inputValidation', {
target : $this
});

// TODO: Trigger field validation, if autoCheck is true

// TODO: Bind blur to validate the field

// TODO: Bind focusing on the field making the error go away


}
});
},

destroy : function() {
$(window).off( '.inputValidation' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'inputValidation' );

$this.removeData( 'inputValidation' );

Pro jQuery Plugins v1.0.0


page 47
});
},

validate: function( options ) {


// TODO: Check .data(options.dataType) OR .attr('type') for the type of
validation to check for

// TODO: Check .prop('required') to see if the field is required

// TODO: Check .attr('maxlength') for the characters limit

// TODO: Implement required, alphanumeric, alphanumeric-extended, email,


url, number, and slug validations
},

showError: function( options ) {


// TODO: Implement showing errors inside the input

// TODO: Bind clicking on error making the error go away


},

hideError: function( options ) {


// TODO: Implement hiding an error
},

hideAllErrors: function( options ) {


// TODO: Implement hiding all errors
}
};

$.fn.inputValidation = 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.inputValidation' );
}
};
})( jQuery, window );

Pro jQuery Plugins v1.0.0


page 48
Now let's implement those To-Dos (see Listing 2-4).

Listing 2-4: JavaScript for the Plugin, After Implementing the To-Dos

(function( $, window ) {
var helpers = {
// Helper function to generate a unique id, GUID-style. Idea from http://
guid.us/GUID/JavaScript
generateID : function() {
S4 = function() {
return ( ((1 + window.Math.random()) * 0x10000) |
0 ).toString( 16 ).substring( 1 );
};

return ( S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4()


+ "-" + S4() + S4() + S4() ).toLowerCase();
}
};

var methods = {
init : function( options ) {
var defaults = {
'dataType': 'type',// data-* property to check for the field/
validation type
'autoCheck': true,// If true, validate the field when the plugin is
initialized
'fadeSpeed': 'fast',
'errorClass': 'inputValidation-error',
'rightMargin': 3// Integer, the number of pixels to be "inside" the
input
};

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'inputValidation' );

if ( ! data ) {
$this.data( 'inputValidation', {
target : $this

Pro jQuery Plugins v1.0.0


page 49
});

// Trigger field validation, if autoCheck is true


if ( options.autoCheck ) {
methods.validate.call( this, options );
}

// Bind blur to validate the field


$(this).on( 'blur.inputValidation', function() {
methods.validate.call( this, options );
});

// Bind focusing on the field making the error go away


$(this).on( 'focus.inputValidation', function() {
methods.hideError.call( this, options );
});
}
});
},

destroy : function() {
$(window).off( '.inputValidation' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'inputValidation' );

$this.removeData( 'inputValidation' );
});
},

validate: function( options ) {


var $this = $(this),
validationType = '',
isRequired = false,
maxLength = 0,
fieldValue = $(this).val(),
validationRegularExpression = null;

// Check .data(options.dataType) OR .attr('type') for the type of


validation to check for
switch ( $this.attr('type') ) {
case 'email':
Pro jQuery Plugins v1.0.0
page 50
case 'url':
case 'number':
validationType = $this.attr( 'type' );
break;
case 'tel':
validationType = 'alphanumeric-extended';
break;
case 'text':
default:
switch ( $this.data(options.dataType) ) {
case 'alphanumeric':
case 'alphanumeric-extended':
case 'email':
case 'url':
case 'number':
case 'slug':
validationType = $this.data( options.dataType );
break;
}
break;
}

// Check .prop('required') to see if the field is required


if ( $this.prop('required') ) {
isRequired = true;
}

// Check .attr('maxlength') for the characters limit


if ( $this.attr('maxlength') ) {
maxLength = window.parseInt( $this.attr('maxlength'), 10 );
}

// If there's no validation type, the field isn't required, and no


maximum length to check for, there's nothing to validate.
if ( validationType === '' && ! isRequired && maxLength <= 0 ) {
return true;
}

// Make the actual validations for the type


switch( validationType ) {
case 'alphanumeric':
validationRegularExpression = /[^a-z0-9]/gi;

Pro jQuery Plugins v1.0.0


page 51
if ( validationRegularExpression.test(fieldValue) ) {
methods.showError.call( this, {
'title': 'Invalid',
'help': "This field's value isn't alphanumeric. It must consist
only of numbers and/or (non-special) letters."
}, options );

return false;
}
break;
case 'alphanumeric-extended':
/*
This regular expression will only match Basic Latin and Latin-1
Supplement special letters, but you can change it to support any kind of special
characters how you wish. Here's a nice website to help you get the range you
need: http://kourge.net/projects/regexp-unicode-block
*/
validationRegularExpression = /[^\u0000-\u00FFa-z0-9\-\._ ]/gi;

if ( validationRegularExpression.test(fieldValue) ) {
methods.showError.call( this, {
'title': 'Invalid',
'help': "This field's value isn't alphanumeric. It must consist
only of numbers, letters, dots, underscores, dashes and spaces."
}, options );

return false;
}
break;
case 'email':
/*
No need for a super complicated expression here. Note this is an
expression of what it should be, not what it shouldn't.
*/
validationRegularExpression = /^\S+@\S+\.\S+$/g;

if ( fieldValue.length > 0 && !


validationRegularExpression.test(fieldValue) ) {
methods.showError.call( this, {
'title': 'Invalid',
'help': "This field's value isn't an email. It must be a valid
email address."
}, options );
Pro jQuery Plugins v1.0.0
page 52
return false;
}
break;
case 'url':
validationRegularExpression = /^(http|ftp|https):\/\/\S+\.\S+$/gi;//
No need for a super complicated expression here. Note this is an expression of
what it should be, not what it shouldn't.

if ( fieldValue.length > 0 && !


validationRegularExpression.test(fieldValue) ) {
methods.showError.call( this, {
'title': 'Invalid',
'help': "This field's value isn't an URL. It must be a valid
URL and start with http://, for example."
}, options );

return false;
}
break;
case 'number':
validationRegularExpression = /[^\d]/g;

if ( validationRegularExpression.test(fieldValue) ) {
methods.showError.call( this, {
'title': 'Invalid',
'help': "This field's value isn't a number. It must be a valid
natural number."
}, options );

return false;
}
break;
case 'slug':
validationRegularExpression = /[^a-z0-9\-_]/g;

if ( validationRegularExpression.test(fieldValue) ) {
methods.showError.call( this, {
'title': 'Invalid',
'help': "This field's value isn't a valid slug. It must consist
only of numbers, lowercase (non-special) letters, underscores and dashes."
}, options );

Pro jQuery Plugins v1.0.0


page 53
return false;
}
break;
}

// Check if the field is required and empty


if ( isRequired && fieldValue.length === 0 ) {
methods.showError.call( this, {
'title': 'Required',
'help': 'This field is required.'
}, options );

return false;
}

// Check if the field has a maximum length that's being exceeded


if ( maxLength > 0 && fieldValue.length > maxLength ) {
methods.showError.call( this, {
'title': 'Too Big',
'help': "This field's value is too big. The maximum number of
characters for it is " + maxLength + "."
}, options );

return false;
}

return true;
},

showError: function( errorData, options ) {


var $this = $(this);

// Check if an error already exists for the element, if so, do nothing


if ( $this.siblings('.' + options.errorClass).length ) {
return true;
}

var generatedID = helpers.generateID.call( this ),


errorID = 'inputValidation-error-' + generatedID,
errorHTML = '<div id="' + errorID + '" class="' + options.errorClass +
'" title="' + errorData.help + '">' + errorData.title + '</div>';

// Add error inside input, position it inside, on the right, and show it
Pro jQuery Plugins v1.0.0
page 54
$this.after( errorHTML );

$('#' + errorID)
.css({
'margin-left': $this.outerWidth() - $('#' + errorID).outerWidth() -
options.rightMargin
})
.fadeIn( options.fadeSpeed );

// Bind clicking on error make the error go away


$('#' + errorID).on( 'click.inputValidation', function( event ) {
event.preventDefault();

methods.hideError.call( $this, options );


});
},

hideError: function( options ) {


$(this).siblings('.' + options.errorClass).fadeOut( options.fadeSpeed,
function() {
$(this).remove();
});
},

hideAllErrors: function( options ) {


$('.' + options.errorClass).fadeOut( options.fadeSpeed, function() {
$(this).remove();
});
}
};

$.fn.inputValidation = 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.inputValidation' );
}
};
})( jQuery, window );
Pro jQuery Plugins v1.0.0
page 55
There are a few things to note here. The idea for the GUID-style unique hash generation shown in this listing
came from http://guid.us/GUID/JavaScript. For errorHTML, I could've used a template, but since it's only a
small HTML string, there's no real benefit so far, and you'll get into that in later chapters.

The click bind that calls hideError could be done using the error class, on init. I just wanted to show you how
to use the generated ID for improved performance fetching of the element.

We also need to add the following CSS (see Listing 2-5):

Listing 2-5: Updates to the CSS

.sample-form .inputValidation-error {
background: #C00;
border-radius: 3px;
color: #FFF;
font-size: 11px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 16px;
padding: 4px 6px 5px;
position: absolute;
margin: -33px 0 0 0;
z-index: 1;
display: none;
cursor: pointer;
}

So, now you've built your basic plugin; Figure 2-1 shows how it looks before validation, and Figure 2-2 how it
looks after.

Pro jQuery Plugins v1.0.0


page 56
Figure 2-1: The form before the validation plugin is triggered

Figure 2-2: The form after the validation plugin is triggered

Pro jQuery Plugins v1.0.0


page 57
This is pretty good for some situations, but what if the elements are absolutely positioned or inside scrolling
wrappers? When testing this, you'll see it already works in those situations because you've abstracted most of
the CSS from the JavaScript, so users are responsible for it (you need only that right margin offset variable and
to set the left margin so the div can be placed "visually inside" the input).

Adding More Options to the Plugin


So, what if someone wants the error to show a different message? Or outside the box? Or with a different
animation? We need some more options, which we'll add now (see Listing 2-6).

Listing 2-6: JavaScript of the Plugin with More Options

(function( $, window ) {
// ...

var methods = {
init : function( options ) {
var defaults = {
'dataType': 'type',// data-* property to check for the
field/validation type
'dataErrorTitle': 'errorTitle',// data-* property to check for
the error title
'dataErrorHelp': 'errorHelp',// data-* property to check for the
error help
'errorRequiredTitle': 'Required',
'errorRequiredHelp': 'This field is required.',
'errorMaxLengthTitle': 'Too Big',
'errorMaxLengthHelp': "This field's value is too big. The maximum
number of characters for it is {maxLength}.",
'autoCheck': true,// If true, validate the field when the
plugin is initialized
'errorClass': 'inputValidation-error',
'position': 'inside',// Supports 'inside' and 'outside'
'animation': {
'type': 'fade',// Supports 'fade' and 'slide'
'speed': 'fast',
'easing': 'swing',
'onComplete': $.noop,
'extra': {
Pro jQuery Plugins v1.0.0
page 58
'margin': 3// Integer, the number of pixels to be "inside" or
"outside" the input
}
}
};

// ...
},

// ...

validate: function( options ) {

// ...

// Check if the field is required and empty


if ( isRequired && fieldValue.length === 0 ) {
methods.showError.call( this, {
'title': options.errorRequiredTitle,
'help': options.errorRequiredHelp
}, options );

return false;
}

// Check if the field has a maximum length that's being exceeded


if ( maxLength > 0 && fieldValue.length > maxLength ) {
methods.showError.call( this, {
'title': options.errorMaxLengthTitle,
'help': options.errorMaxLengthHelp.replace( '{maxLength}',
maxLength )
}, options );

return false;
}

return true;
},

showError: function( errorData, options ) {


var $this = $(this);

// Check if an error already exists for the element, if so, do nothing


Pro jQuery Plugins v1.0.0
page 59
if ( $this.siblings('.' + options.errorClass).length ) {
return true;
}

// Check if the input has a user-defined error title and help (and we're
not checking for the global required and maxlength errors)
if ( $this.data(options.dataErrorTitle) && errorData.title !=
options.errorRequiredTitle && errorData.title != options.errorMaxLengthTitle ) {
errorData.title = $this.data( options.dataErrorTitle );
}

if ( $this.data(options.dataErrorHelp) && errorData.help !=


options.errorRequiredHelp && errorData.help != options.errorMaxLengthHelp ) {
errorData.help = $this.data( options.dataErrorHelp );
}

var generatedID = helpers.generateID.call( this ),


errorID = 'inputValidation-error-' + generatedID,
errorHTML = '<div id="' + errorID + '" class="' + options.errorClass +
'" title="' + errorData.help + '">' + errorData.title + '</div>';

// Add error inside input


$this.after( errorHTML );

// Position error inside or outside


switch( options.position ) {
case 'outside':
$('#' + errorID).css({
'margin-left': $this.outerWidth() + options.animation.extra.margin
});
break;
case 'inside':
default:
$('#' + errorID).css({
'margin-left': $this.outerWidth() - $('#' + errorID).outerWidth()
- options.animation.extra.margin
});
break;
}

// Animate Error Showing


switch( options.animation.type ) {
case 'slide':
Pro jQuery Plugins v1.0.0
page 60
$('#' + errorID).slideDown( options.animation.speed,
options.animation.easing, options.animation.onComplete );
break;
case 'fade':
default:
$('#' + errorID).fadeIn( options.animation.speed,
options.animation.easing, options.animation.onComplete );
break;
}

// Bind clicking on error make the error go away


$('#' + errorID).on( 'click.inputValidation', function( event ) {
event.preventDefault();

methods.hideError.call( $this, options );


});
},

hideError: function( options ) {


// Animate Error Hiding
switch( options.animation.type ) {
case 'slide':
$(this).siblings('.' +
options.errorClass).slideUp( options.animation.speed, options.animation.easing,
function() {
$(this).remove();
});
break;
case 'fade':
default:
$(this).siblings('.' +
options.errorClass).fadeOut( options.animation.speed, options.animation.easing,
function() {
$(this).remove();
});
break;
}

},

hideAllErrors: function( options ) {


// Animate Error Hiding
switch( options.animation.type ) {
Pro jQuery Plugins v1.0.0
page 61
case 'slide':
$('.' + options.errorClass).slideUp( options.animation.speed,
options.animation.easing, function() {
$(this).remove();
});
break;
case 'fade':
default:
$('.' + options.errorClass).fadeOut( options.animation.speed,
options.animation.easing, function() {
$(this).remove();
});
break;
}
}
};

// ...

})( jQuery, window );

CAUTION: data-*-style attributes in jQuery separated with dashes (standard, because the attributes are
all lowercase and it would cripple readability) will be rendered as camelCase. So, an HTML attribute like
data-error-title is obtained with .data('errorTitle').

Using i18n
Lastly, you can't forget about i18n, right? Remember from the introduction that I'm considering you're using
https://github.com/recurser/jquery-i18n, so you would have something like Listing 2-7 somewhere before the
plugin.

Listing 2-7: JavaScript Sample of Existing i18n Dictionary for Translation

window.myApp = {};

window.myApp.ptDictionary = {
Pro jQuery Plugins v1.0.0
page 62
// ...
"Required" : "Obrigatrio",
"This field is required." : "Este campo obrigatrio.",
"Too Big" : "Grande",
"This field's value is too big. The maximum number of characters for it is
%s." : "O valor deste campo muito grande. O nmero mximo de caracteres para
ele de %s.",
"Invalid" : "Invlido",
"This field's value isn't alphanumeric. It must consist only of numbers and/
or (non-special) letters." : "O valor deste campo no alfanumrico. Tem de
consistir apenas em nmeros e/ou letras (no especiais).",
"This field's value isn't alphanumeric. It must consist only of numbers,
letters, dots, underscores, dashes and spaces." : "O valor deste campo no
alfanumrico. Tem de consistir apenas em nmeros, letras, pontos, underscores,
hfens e espaos.",
"This field's value isn't an email. It must be a valid email address." : "O
valor deste campo no um email. Tem de ser um endereo de email vlido.",
"This field's value isn't an URL. It must be a valid URL and start with
http://, for example." : "O valor deste campo no um URL. Tem de ser um URL
vlido e comear por http://, por exemplo.",
"This field's value isn't a number. It must be a valid natural number." : "O
valor deste campo no um nmero. Tem de ser um nmero natural vlido.",
"This field's value isn't a valid slug. It must consist only of numbers,
lowercase (non-special) letters, underscores and dashes." : "O valor deste campo
no um slug vlido. Tem de consistir apenas em nmeros, letras minsculas (no
especiais), underscores e hfens."
};

$.i18n.setDictionary( window.myApp.ptDictionary );

TIP: You may have noticed that I used window.myApp as a namespace for the app on which the plugin
is implemented. This code is not part of the plugin, but it would be part of the app. It's a great namespac-
ing technique so that global variables don't clash.

Now the plugin's code would have to be changed to what's shown in Listing 2-8.

Pro jQuery Plugins v1.0.0


page 63
Listing 2-8: JavaScript for the Plugin with i18n Support Now

(function( $, window ) {
// ...

// Check if the field is required and empty


if ( isRequired && fieldValue.length === 0 ) {
methods.showError.call( this, {
'title': $.i18n._( options.errorRequiredTitle ),
'help': $.i18n._( options.errorRequiredHelp )
}, options );

return false;
}

// Check if the field has a maximum length that's being exceeded


if ( maxLength > 0 && fieldValue.length > maxLength ) {
methods.showError.call( this, {
'title': $.i18n._( options.errorMaxLengthTitle ),
'help': $.i18n._( options.errorMaxLengthHelp, [maxLength] )
}, options );

return false;
}

// ...

var generatedID = helpers.generateID.call( this ),


errorID = 'inputValidation-error-' + generatedID,
errorHTML = '<div id="' + errorID + '" class="' + options.errorClass +
'" title="' + $.i18n._(errorData.help) + '">' + $.i18n._(errorData.title) + '</
div>';

// ...

})( jQuery, window );

You have replaced the text strings with variables and callings to the i18n plugin so the correct dictionary can be
retrieved, meaning that the result will now look like Figure 2-3.

Pro jQuery Plugins v1.0.0


page 64
Figure 2-3. The plugin now showing translated errors

A few things to notice are that I didn't implement the i18n on the default variables but instead when they are
used. This was mostly because one of them (the maxLength one) needed a variable later, not at the time it was
defined.

It also enables a user to send the variables just as text, not requiring the $.i18n._() when setting those
options. Also, the errorMaxLengthHelp default value has an %s instead of {maxLength} for $.i18n
compatibility. I also didn't implement i18n on the "method doesn't exist" error because it's something that will
show on the log, not for the end user but for people who are debugging.

I personally don't like translated errors because when you Google for them, they'll be different from the same
person with a different language who had the same problem, and you'll probably miss it.

Summary
In this chapter, you learned how to make a simple input field validation plugin that's flexible, translatable,
customizable, and easy to implement in various situations. You've made some assumptions, though, such as
allowing inside and outside positions only to be on the right of the input field because that's what makes more
sense UI-wise most of the time, but you can try to make the plugin even more flexible by allowing the inside and
outside positions to be anywhere.
Pro jQuery Plugins v1.0.0
page 65
In the next chapter, you'll continue with a simple plugin, but this time you'll add tooltips, which can have even
more different environments and scenarios; they will be a great addition to the input field validation plugin and
will show the error's help/description when the cursor hovers over the error.

Pro jQuery Plugins v1.0.0


page 66
Chapter 3:
A Simple Tooltip Plugin

Tooltips are very useful for UI and UX reasons, amongst others.

Browsers have built-in implementations of tooltips, commonly used to display the title attribute of elements.

For this reason (and to not mess with accessibility), I suggest the use of a data-tooltip attribute instead of
the title attribute, but customizable and with a fallback to the title attribute. It'll initially display a prettier
tooltip above or below the element in question, horizontally centered.

Let's start with some base HTML (see Listing 3-1), CSS (see Listing 3-2), and the plugin's JavaScript (see
Listing 3-3).

Listing 3-1: HTML for the Tooltip Plugin Sample

<h1 data-tooltip="This is a tooltip, yay!" data-tooltip-position="below">Title


with tool-tip</h1>
<p title="A title, yo!">This text has no tool-tip, but it has a title</p>
<p data-tooltip="All your base are belong to us!">This one does.</p>
<p title="This tool-tip is even bigger than everything you ever considered
existing in all of mankind's history! Not.">This one too, but a bigger one.</p>
<p data-tooltip="Looks Awesome, right?" data-tooltip-position="below">This one
has a positioned one, below this very descriptive text.</p>

Listing 3-2: CSS for the Tooltip Plugin Sample

h1, p {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
margin: 10px 5px;
}
Pro jQuery Plugins v1.0.0
page 67
h1 {
font-size: 22px;
}

p {
font-size: 14px;
}

.toolTip {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: #000;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
color: #FFF;
font-size: 10px;
line-height: 12px;
padding: 5px 10px;
position: absolute;
z-index: 9999;
min-width: 100px;
max-width: 160px;
display: none;
font-weight: normal;

zoom: 1;
filter:alpha(opacity=80);
opacity: 0.8;
}

NOTE: Please note the z-index value could vary according to user needs. That's one of the reasons why
keeping CSS out of the plugin JavaScript makes it easier for users to tweak it without changing the code.

Listing 3-3: JavaScript for the Tooltip Plugin Sample

(function( $, window ) {
// TODO: Helper to generate IDs
Pro jQuery Plugins v1.0.0
page 68
var methods = {
init : function( options ) {
var defaults = {};// TODO: Set the defaults for animation speed, data-*
attribute for the tool-tip position, etc.

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'toolTip' );

if ( ! data ) {
$this.data( 'toolTip', {
target : $this
});

// TODO: Create the tool-tip

// TODO: Bind the show on mouse in/hover

// TODO: Bind the hide on mouse out


}
});
},

destroy : function() {
$(window).off( '.toolTip' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'toolTip' );

$this.removeData( 'toolTip' );
});
}
// TODO: Method to show the tool-tip

// TODO: Method to hide the tool-tip

// TODO: Method to hide all the tool-tips


};

$.fn.toolTip = function( method ) {


Pro jQuery Plugins v1.0.0
page 69
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.toolTip' );
}
};
})( jQuery, window );

Now let's code those To-Dos (see Listing 3-4).

Listing 3-4: New JavaScript, with To-Dos Implemented

(function( $, window ) {
var helpers = {
// Helper function to generate a unique id, GUID-style. Idea from http://
guid.us/GUID/JavaScript
generateID : function() {
S4 = function() {
return ( ((1 + window.Math.random()) * 0x10000) |
0 ).toString( 16 ).substring( 1 );
};

return ( S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4()


+ "-" + S4() + S4() + S4() ).toLowerCase();
}
};

var methods = {
init : function( options ) {
var defaults = {
'dataToolTip': 'tooltip',// data-* property to check for the
tool-tip's content
'dataPosition': 'tooltipPosition',// data-* property to check
for the tool-tip's position
'toolTipClass': 'toolTip',
'position': 'above',// Supports 'above' and 'below'
'positionMargin': 10,

Pro jQuery Plugins v1.0.0


page 70
'animationSpeed': 'fast',
'animationEasing': 'swing',
'animationOnComplete': $.noop
};

options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'toolTip' );

if ( ! data ) {
$this.data( 'toolTip', {
target : $this
});

// Create the tool-tip


methods.create.call( this, options );

// Bind the show on mouse in/hover


$(this).on( 'mouseenter.toolTip', function() {
methods.show.call( this, options );
});

// Bind the hide on mouse out


$(this).on( 'mouseleave.toolTip', function() {
methods.hide.call( this, options );
});
}
});
},

destroy : function() {
// ...
},

create: function( options ) {


var $this = $(this),
data = $this.data( 'toolTip' ),
chosenPosition = options.position,
toolTipContent = '';

Pro jQuery Plugins v1.0.0


page 71
// Get the tool-tip content, and if it doesn't exist, default to the
title attribute
if ( ! $this.data(options.dataToolTip) ) {
toolTipContent = $this.attr( 'title' );
} else {
toolTipContent = $this.data( options.dataToolTip );
}

// We now need to empty the title attribute to avoid the default browser
behavior and showing it together with the tool-tip
$this.attr( 'title', '' );

var generatedID = helpers.generateID.call( this ),


toolTipID = 'toolTip-' + generatedID,
toolTipHTML = '<div id="' + toolTipID + '" class="' +
options.toolTipClass + '">' + toolTipContent + '</div>';

// Add the tool-tip inside the element


$(toolTipHTML).appendTo( $this );

// Store the toolTipID in the calling/parent element


data.toolTipID = toolTipID;

// Check if there is a position set in the element


if ( $this.data(options.dataPosition) ) {
chosenPosition = $this.data( options.dataPosition );
}

// Get the appropriate margin-left so we can position the tool-tip in the


center of the element, horizontally
var marginLeft = ( $this.outerWidth() - $('#' +
toolTipID).outerWidth() ) / 2;

// Position the tool-tip above or below the element


switch( chosenPosition ) {
case 'below':
$('#' + toolTipID).css({
'margin-top': options.positionMargin,
'margin-left': marginLeft
});
break;
case 'above':
default:
Pro jQuery Plugins v1.0.0
page 72
$('#' + toolTipID).css({
'margin-top': ( ($this.outerHeight() + $('#' +
toolTipID).outerHeight() + options.positionMargin) * -1 ),
'margin-left': marginLeft
});
break;
}
},

show: function( options ) {


var $this = $(this),
data = $this.data( 'toolTip' );

// Animate tool-tip showing


$('#' + data.toolTipID).stop().fadeIn( options.animationSpeed,
options.animationEasing, options.animationOnComplete );
},

hide: function( options ) {


var $this = $(this),
data = $this.data( 'toolTip' );

$('#' + data.toolTipID).stop().hide();
},

hideAllToolTips: function( options ) {


$('.' + options.toolTipClass).stop().hide();
}
};

// ...
})( jQuery, window );

If you take a look at the default settings, you'll note I didn't put the animation options as an object (as with the
previous chapter/plugin). While this way isn't as organized, it makes changing only a single property of the
animation easier. With the animation options as an object, you'd have to set up all of them, even if you just
wanted to change just one of the properties.

It is also worth noting that you are hiding the tooltip, instead of animating it hiding, to avoid issues with hovering
it while disappearing. It can be circumvented with a delay, for example, but that is a nasty choice, in my opinion,

Pro jQuery Plugins v1.0.0


page 73
because it doesn't bring much benefit, and it requires more code and resources. You're free to animate it,
though. Its a nice challenge.

Figure 3-1 and Figure 3-2 show how it looks now.

Figure 3-1: The tooltip plugin output, without any mouse hover

Figure 3-2: The tooltip plugin output with a mouse hover

Pro jQuery Plugins v1.0.0


page 74
Solving Issues
Now, this works well but has some problems:

The tooltips are horizontally aligned with the full width of the window since the p and h1 elements are
display: block.

With inline elements, the tooltips will show completely out of position in most situations.

And what if the tooltip is positioned outside the window? Do you show it cropped?

You need to consider all situations, so how can you solve these issues? Listing 3-5 shows some solutions, but I
encourage you to think a bit here before reading on.

This is how to tackle the problems:

To solve the "visually aligned" issue, you'll actually have to simulate the same content in an inline
element and calculate its width from there.

You need to consider a common starting point for all tooltips.

You need to check this starting point and re-position the tooltips in a visible place. This can result in
tooltips "out of position", but that's better than not showing their content in those situations, in my opinion.

NOTE: The first solution mentioned will fix that problem for most elements, but in the case of elements
that need the block-like width, you could create an option and decide how to calculate the width. It would
be a nice exercise, but I won't go over it because it'll just unnecessarily complicate this example.

Listing 3-5: Updated JavaScript for the Plugin

(function( $, window ) {
// ...

var methods = {

// ...

create: function( options ) {


Pro jQuery Plugins v1.0.0
page 75
var $this = $(this),
data = $this.data( 'toolTip' ),
chosenPosition = options.position,
toolTipContent = '';

// Get the tool-tip content, and if it doesn't exist, default to the


title attribute
if ( ! $this.data(options.dataToolTip) ) {
toolTipContent = $this.attr( 'title' );
} else {
toolTipContent = $this.data( options.dataToolTip );
}

// We now need to empty the title attribute to avoid the default browser
behavior and showing it together with the tool-tip
$this.attr( 'title', '' );

var generatedID = helpers.generateID.call( this ),


toolTipID = 'toolTip-' + generatedID,
toolTipHTML = '<div id="' + toolTipID + '" class="' +
options.toolTipClass + '">' + toolTipContent + '</div>';

// Add the tool-tip inside the body


$(toolTipHTML).appendTo( 'body' );

// Store the toolTipID in the calling/parent element


data.toolTipID = toolTipID;

// Check if there is a position set in the element


if ( $this.data(options.dataPosition) ) {
chosenPosition = $this.data( options.dataPosition );
}

// Get the real element's width & height, by creating a dummy clone
inline element with the same content and using it as reference
var dummyElement = $this.clone().css({
'display': 'inline',
'visibility': 'hidden'
}).appendTo( 'body' );

// Put the tool-tip in the same "starting position", so the positioning


is consistent and flexible: in the bottom left of the calling/parent element
$('#' + toolTipID).css({
Pro jQuery Plugins v1.0.0
page 76
'top': $this.offset().top + dummyElement.outerHeight(),
'left': $this.offset().left
});

// Get the appropriate margin-left so we can position the tool-tip in the


center of the element, horizontally
var marginLeft = ( dummyElement.outerWidth() - $('#' +
toolTipID).outerWidth() ) / 2;

var marginTop = ( (dummyElement.outerHeight() + $('#' +


toolTipID).outerHeight() + options.positionMargin) * -1 );

// Position the tool-tip above or below the element


switch( chosenPosition ) {
case 'below':
marginTop = options.positionMargin;
break;
case 'above':
default:
// We don't need to do anything here, as the default marginTop is
for this 'above' position already
break;
}

$('#' + toolTipID).css({
'margin-top': marginTop,
'margin-left': marginLeft
});

// Check if the tool-tip element is cropped on the top of the window view
if ( ($this.offset().top + marginTop) < 0 ) {
$('#' + toolTipID).css({ 'margin-top': 0 });
}

// Check if the tool-tip element is cropped on the bottom of the window


view
if ( ($this.offset().top + marginTop + $('#' + toolTipID).outerHeight())
> $(document).height() ) {
var heightDifference = $('#' + toolTipID).offset().top + $('#' +
toolTipID).outerHeight() - $(document).height();
$('#' + toolTipID).css({
'margin-top': ( $('#' + toolTipID).css('margin-top') -
heightDifference )
Pro jQuery Plugins v1.0.0
page 77
});
}

// Check if the tool-tip element is cropped on the left of the window


view
if ( ($this.offset().left + marginLeft) < 0 ) {
$('#' + toolTipID).css({ 'margin-left': 0 });
}

// Check if the tool-tip element is cropped on the right of the window


view
if ( $this.offset().left + marginLeft + $('#' + toolTipID).outerWidth() >
$(document).width() ) {
var widthDifference = $('#' + toolTipID).offset().left + $('#' +
toolTipID).outerWidth() - $(document).width();
$('#' + toolTipID).css({
'margin-left': ( $('#' + toolTipID).css('margin-left') -
widthDifference )
});
}

// Remove the dummy element


dummyElement.remove();
},

// ...

};

// ...

})( jQuery, window );

NOTE: To find out whether the tooltip was out of the document view, you actually had to calculate its
position from the calling/parent element, since hidden elements don't have .position() or .offset(). This in-
volved getting the margin-top property for the tooltip to be in a variable to be able to recalculate its posi-
tion without retyping a lot of code.

And now it looks much better (see Figure 3-3 and Figure 3-4).

Pro jQuery Plugins v1.0.0


page 78
Figure 3-3: The plugin's output, hovering one tooltip

Figure 3-4: The plugin's output hovering another tooltip

Following the mouse


Some people like to see the tooltips following the mouse, and sometimes that's actually better UX. Let's add
that as an option (see Listing 3-6). You'll continue to use above and below to consider the position of the tooltip
relative to the mouse cursor.

Listing 3-6: The Updated JavaScript, Allowing the Mouse to Follow the Tooltip

(function( $, window ) {

// ...

var methods = {
init : function( options ) {
Pro jQuery Plugins v1.0.0
page 79
var defaults = {
// ...
'followMouse': true
};

// ...

return this.each(function() {
// ...

if ( ! data ) {
// ...

if ( options.followMouse ) {
// Bind the chaseCursor in mouse move
$(this).on( 'mousemove.toolTip', function( event ) {
methods.chaseCursor.call( this, event, options );
});
}
}
});
},

// ...

create: function( options ) {


// ...

// Check if there is a position set in the element


if ( $this.data(options.dataPosition) ) {
chosenPosition = $this.data( options.dataPosition );
}

if ( ! options.followMouse ) {

// Get the real element's width & height, by creating a dummy clone
inline element with the same content and using it as reference
var dummyElement = $this.clone().css({
'display': 'inline',
'visibility': 'hidden'
}).appendTo( 'body' );

// ...
Pro jQuery Plugins v1.0.0
page 80
// Remove the dummy element
dummyElement.remove();
}
},

// ...

chaseCursor: function( event, options ) {


var $this = $(this),
data = $this.data( 'toolTip' ),
chosenPosition = options.position,
topPosition = 0,
leftPosition = 0;

// Check if there is a position set in the element


if ( $this.data(options.dataPosition) ) {
chosenPosition = $this.data( options.dataPosition );
}

var toolTipElement = $('#' + data.toolTipID);

leftPosition = event.pageX + options.positionMargin;

switch( chosenPosition ) {
case 'below':
topPosition = event.pageY + options.positionMargin;
break;
case 'above':
default:
topPosition = event.pageY - options.positionMargin -
toolTipElement.outerHeight();
break;
}

toolTipElement.css({
'top': topPosition,
'left': leftPosition
});
}
};

// ...
Pro jQuery Plugins v1.0.0
page 81
})( jQuery, window );

Now when you move over the element, the tooltip follows it (see Figure 3-5).

Figure 3-5: Now the tooltip is following the mouse!

You can make the appearing and disappearing animations of the tooltip look more interesting. Let's try to make
them slide from the left to the right when appearing and disappearing. Also, let's make it possible for the tooltip
to appear on the left and right of the element or mouse (see Listing 3-7).

Listing 3-7: Updated JavaScript for This New Animation and Position Options

(function( $, window ) {

// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'toolTipClass': 'toolTip',
'position': 'above',// Supports 'above', 'below', 'left' and
'right'
'positionMargin': 10,
'animationType': 'fade',// Supports 'fade' and 'slide'
// ...
Pro jQuery Plugins v1.0.0
page 82
};

// ...
},

// ...

create: function( options ) {


// ...

if ( ! options.followMouse ) {

// ...

// Position the tool-tip to the left, right, above, or below the


element
switch( chosenPosition ) {
case 'left':
marginTop = ( dummyElement.outerHeight() * -1 );
marginLeft = ( (dummyElement.outerWidth() +
options.positionMargin) * -1 );
break;
case 'right':
marginTop = ( dummyElement.outerHeight() * -1 );
marginLeft = dummyElement.outerWidth() + options.positionMargin;
break;
// ...
}

// ...

// Store these values, as we might need them


data.marginLeft = $('#' + toolTipID).css( 'margin-left' );
data.marginTop = $('#' + toolTipID).css( 'margin-top' );

// Remove the dummy element


dummyElement.remove();
}
},

show: function( options ) {


var $this = $(this),
data = $this.data( 'toolTip' );
Pro jQuery Plugins v1.0.0
page 83
var toolTipElement = $('#' + data.toolTipID);

// Animate tool-tip showing


switch( options.animationType ) {
case 'slide':
var finalMarginLeft = parseInt( data.marginLeft, 10 );

toolTipElement.stop().css({
'opacity': 0,
'display': 'block',
'margin-left': ( finalMarginLeft - (options.positionMargin * 2) )
}).animate({
'opacity': 1,
'margin-left': finalMarginLeft
}, options.animationSpeed, options.animationEasing,
options.animationOnComplete );
break;
case 'fade':
default:
toolTipElement.stop().fadeIn( options.animationSpeed,
options.animationEasing, options.animationOnComplete );
break;
}
},

// ...

};

// ...
})( jQuery, window );

Now, this can't be really caught in an image, but feel free to try it out and even add yourself a new animation
(maybe with the same sliding effect but from top to bottom or depending on the position) to see how it looks
like.

Pro jQuery Plugins v1.0.0


page 84
Working with Input Field validation
Lastly, let's see how this plugin works together with the Input Field validation one from the previous Chapter (see
Listing 3-8).

Listing 3-8: JavaScript to Call Both Plugins

$('.sample-form input').inputValidation({
'animation': {
'type': 'fade',
'speed': 'fast',
'easing': 'swing',
'onComplete': function() {
$('.inputValidation-error').toolTip({ 'followMouse': false, 'position':
'right' });
},
'extra': {
'margin': 3
}
}
});

This will give you the outcome shown in Figure 3-6.

Pro jQuery Plugins v1.0.0


page 85
Figure 3-6: Both plugins working together

Summary
In this chapter, you learned to make a simple tooltip plugin that's flexible, customizable, and easy to implement
in various situations.

This plugin won't support HTML in the tooltips, though, but feel free to try and add support for that; it'll be a
great challenge!

In the next chapter, you'll learn how to build a simple lightbox-style plugin that will support images, galleries,
iframes, and HTML.

Pro jQuery Plugins v1.0.0


page 86
Chapter 4:
A Simple Lightbox Plugin

Lightboxes have many uses and have liberated us from the dreaded pop-ups.

They can have many uses, but the most common one is to show a larger/regular-sized image when clicking on
a thumbnail without leaving the page. That's what we'll build first.

Building the Base


We know we're going to want a couple of things to be customizable upfront, like:

Is it a modal lightbox? (if so, the window will have to be closed properly clicking the close button ,
not by clicking outside of the lightbox)

Does pressing Escape close the lightbox?

So, taking that into consideration, let's get a backbone ready of the HTML (see Listing 4-1), CSS (see Listing
4-2) and JS (see Listing 4-3).

NOTE: I do not own copyright of these images and I'm only using them as samples because they look
pretty.

Listing 4-1: HTML for our plugin's demo

<div class="thumbnails">
<div class="thumbnail">

Pro jQuery Plugins v1.0.0


page 87
<a href="http://us.123rf.com/400wm/400/400/EnjoyLife25/EnjoyLife250608/
EnjoyLife25060800219/503216-beautiful-sunset-on-the-beach.jpg">
<img src="http://us.cdn2.123rf.com/168nwm/enjoylife25/enjoylife250608/
enjoylife25060800219/503216-beautiful-sunset-on-the-beach.jpg" alt="">
</a>
</div>
<div class="thumbnail">
<a href="http://us.123rf.com/400wm/400/400/enjoylife25/enjoylife250710/
enjoylife25071000026/1829472-beautiful-night-at-the-beach.jpg">
<img src="http://us.cdn2.123rf.com/168nwm/enjoylife25/enjoylife250710/
enjoylife25071000026/1829472-beautiful-night-at-the-beach.jpg" alt="">
</a>
</div>
<div class="thumbnail">
<a href="http://us.123rf.com/400wm/400/400/enjoylife25/enjoylife250710/
enjoylife25071000023/1829475-a-beautiful-night-at-the-beach--slow-
shutterspeed.jpg">
<img src="http://us.cdn1.123rf.com/168nwm/enjoylife25/enjoylife250710/
enjoylife25071000023/1829475-a-beautiful-night-at-the-beach--slow-
shutterspeed.jpg" alt="">
</a>
</div>
<div class="thumbnail">
<a href="http://us.123rf.com/400wm/400/400/EnjoyLife25/EnjoyLife250608/
EnjoyLife25060800020/482752-beautiful-sunset-landscape-photo.jpg">
<img src="http://us.cdn2.123rf.com/168nwm/enjoylife25/enjoylife250608/
enjoylife25060800020/482752-beautiful-sunset-landscape-photo.jpg" alt="">
</a>
</div>
</div>

Listing 4-2: CSS for our plugin's demo

h1, p {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
margin: 10px 5px;
}

h1 {

Pro jQuery Plugins v1.0.0


page 88
font-size: 22px;
}

p {
font-size: 14px;
}

.thumbnails {
display: block;
padding: 10px;
}

.thumbnails .thumbnail {
display: inline-block;
width: 48%;
margin: 0.8%;
text-align: center;
}

.thumbnails .thumbnail img {


max-width: 100%;
vertical-align: middle;
}

.lightbox-background {
display: none;
position: fixed;
z-index: 200;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, .5);
}

.lightbox-wrapper {
display: none;
position: fixed;
min-width: 200px;
min-height: 150px;
max-width: 90%;
max-height: 420px;
padding: 10px;
Pro jQuery Plugins v1.0.0
page 89
background: #FFF;
border-radius: 3px;
box-shadow: 0 0 1px #333;
z-index: 205;
top: 50%;
left: 50%;
margin-left: -100px;
margin-top: -75px;
transition: margin 200ms, width 200ms, height 200ms;
-webkit-transition: margin 200ms, width 200ms, height 200ms;
-moz-transition: margin 200ms, width 200ms, height 200ms;
-o-transition: margin 200ms, width 200ms, height 200ms;
}

.lightbox-wrapper .lightbox-close {
display: block;
position: absolute;
top: 2px;
left: 2px;
color: #333;
font-size: 14px;
font-weight: bold;
cursor: pointer;
padding: 2px 6px;
border-radius: 10px;
background: #FFF;
}

.lightbox-wrapper .lightbox-content {
display: block;
overflow: auto;
max-height: 400px;
}

.lightbox-wrapper .lightbox-content img {


max-width: 500px;
display: block;
}

Pro jQuery Plugins v1.0.0


page 90
Listing 4-3: JavaScript for our plugin

(function( $, window ) {

// TODO: Helper to generate IDs

var methods = {
init : function( options ) {
var defaults = {
'lightboxID': 'lightbox',
'isModal': false,
'escapeCloses': true
};

options = $.extend( defaults, options );

// Create the lightbox "holder" if it doesn't exist yet


methods.create.call( this, options );

if ( options.escapeCloses ) {
// TODO: Bind Escape Key to close the lightbox
}

return this.each(function() {
var $this = $(this),
data = $this.data( 'lightbox' );

if ( ! data ) {
$this.data( 'lightbox', {
target : $this
});

// TODO: Bind click to open the lightbox


}
});
},

destroy : function() {
$(window).off( '.lightbox' );

return this.each(function() {

Pro jQuery Plugins v1.0.0


page 91
var $this = $(this),
data = $this.data( 'lightbox' );

$this.removeData( 'lightbox' );
});
},

// TODO: Create the lightbox "holder" if it doesn't exist


create: function( options ) {
// TODO: Check if the lightbox "holder" exists, if not, create it

// TODO: Append the lightbox to the <body> tag.


}

// TODO: Method to open the lightbox

// TODO: Method to close the lightbox

// TODO: Method to position the lightbox on the center of the screen


};

$.fn.lightbox = 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.lightbox' );
}
};
})( jQuery, window );

We'd call it with:

$('.thumbnails .thumbnail a').lightbox();

Implementing our To-Dos


Let's go ahead and code those To-Dos (see Listing 4-4).
Pro jQuery Plugins v1.0.0
page 92
Listing 4-4: Our plugin's code, with the To-Dos implemented

(function( $, window ) {
var helpers = {
// Helper function to generate a unique id, GUID-style. Idea from http://
guid.us/GUID/JavaScript
generateID : function() {
S4 = function() {
return ( ((1 + window.Math.random()) * 0x10000) |
0 ).toString( 16 ).substring( 1 );
};

return ( S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4()


+ "-" + S4() + S4() + S4() ).toLowerCase();
}
};

var methods = {
init : function( options ) {
var defaults = {
'lightboxID': 'lightbox',
'backgroundClass': 'lightbox-background',
'wrapperClass': 'lightbox-wrapper',
'contentClass': 'lightbox-content',
'closeButtonClass': 'lightbox-close',
'isModal': false,
'escapeCloses': true
};

options = $.extend( defaults, options );

// Create the lightbox "holder" if it doesn't exist yet


methods.create.call( this, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'lightbox' );

if ( ! data ) {
$this.data( 'lightbox', {
target : $this

Pro jQuery Plugins v1.0.0


page 93
});

// Bind click to open the lightbox


$(this).on( 'click.lightbox', function( event ) {
event.preventDefault();

methods.open.call( this, options );


});
}
});
},

destroy : function() {
$(window).off( '.lightbox' );

return this.each(function() {
var $this = $(this),
data = $this.data( 'lightbox' );

$this.removeData( 'lightbox' );
});
},

// Create the lightbox "holder" if it doesn't exist


create: function( options ) {
// Check if the lightbox "holder" exists, if not, create it
if ( $('#' + options.lightboxID).length > 0 ) {
return false;
} else {
var lightboxHTML = '<div id="' + options.lightboxID + '-background"
class="' + options.backgroundClass + '"></div>' +
'<div id="' + options.lightboxID + '" class="' +
options.wrapperClass + '">' +
'<span class="' + options.closeButtonClass + '"></span>' +
'<div id="' + options.lightboxID + '-content" class="' +
options.contentClass + '">' +
'</div>' +
'</div>';

// Append the lightbox to the <body> tag.


$('body').append( lightboxHTML );

if ( options.escapeCloses ) {
Pro jQuery Plugins v1.0.0
page 94
// Bind Escape Key to close the lightbox
$(document).on( 'keydown.lightbox', function( event ) {
if ( event.keyCode === 27 ) {
event.preventDefault();

methods.close.call( this, options );


}
});
}

// If it's not modal, make sure you can close it clicking outside of
it
if ( ! options.isModal ) {
$('#' + options.lightboxID + '-background').on( 'click.lightbox',
function( event ) {
methods.close.call( this, options );
});
}

// Bind close button to close the lightbox


$('#' + options.lightboxID + ' .' +
options.closeButtonClass).on( 'click.lightbox', function( event ) {
event.preventDefault();

methods.close.call( this, options );


});

// Bind window resize to position the lightbox on the center of the


window
$(window).on( 'resize.lightbox', function( event ) {
methods.positionOnCenter.call( this, options );
});
}
},

// Method to open the lightbox


open: function( options ) {
var $this = $(this),
data = $this.data( 'lightbox' );

// If our plug-in wasn't initialized yet, do nothing


if ( ! data ) {
return false;
Pro jQuery Plugins v1.0.0
page 95
}

// Show the background/overlay


$('#' + options.lightboxID + '-background').fadeIn( 'fast' );

// Open the lightbox


$('#' + options.lightboxID).fadeIn( 'fast', function() {
// Image preload process
var objImagePreloader = new Image();
objImagePreloader.onload = function() {
// Load the image inside the lightbox
var generatedID = helpers.generateID.call( this ),
imageHTML = '<img id="' + options.lightboxID + '-img-' +
generatedID + '" src="' + $this.attr('href') + '" alt="">';

$('#' + options.lightboxID + '-content').html( imageHTML );

// Position the lightbox


methods.positionOnCenter.call( this, options );

// clear onLoad, IE behaves irratically with animated gifs otherwise


objImagePreloader.onload = function() {};
};

objImagePreloader.src = $this.attr( 'href' );


});
},

// Method to close the lightbox


close: function( options ) {
// If our plug-in wasn't initialized yet, do nothing
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

// Close the lightbox


$('#' + options.lightboxID).fadeOut( 'fast', function() {
// Remove content inside
$('#' + options.lightboxID + '-content').empty();
});

// Hide the background/overlay


$('#' + options.lightboxID + '-background').fadeOut( 'fast' );
Pro jQuery Plugins v1.0.0
page 96
},

// Method to position the lightbox on the center of the screen


positionOnCenter: function( options ) {
// If our plug-in wasn't initialized yet, do nothing
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

if ( $('#' + options.lightboxID).length > 0 ) {


var lightboxWidth = 0,
lightboxHeight = 0,
previousLightboxWidth = -5,
previousLightboxHeight = -5;

/*
Since we can "catch" the lightbox changing dimensions, we need to make
sure we keep aligning it until it's "still".
We don't care about variations of 1 pixel, though.
*/
while ( window.Math.abs(lightboxWidth - previousLightboxWidth) > 1 ||
window.Math.abs(lightboxHeight - previousLightboxHeight) > 1 ) {
previousLightboxWidth = lightboxWidth;
previousLightboxHeight = lightboxHeight;

lightboxWidth = $('#' + options.lightboxID).outerWidth();


lightboxHeight = $('#' + options.lightboxID).outerHeight();

$('#' + options.lightboxID).css({
'margin-left': (lightboxWidth / 2 * -1) + 'px',
'margin-top': (lightboxHeight / 2 * -1) + 'px'
});
}
}
}
};

// ...

})( jQuery, window );

And the result is something like this (see Figure 4-1 and Figure 4-2):
Pro jQuery Plugins v1.0.0
page 97
Figure 4-1: Our plugin!

Pro jQuery Plugins v1.0.0


page 98
Figure 4-2: Our plugin, after clicking the first image

NOTE: The position calculation could be greatly improved by getting the image width/height and calcu-
lating it with window boundaries and maximum width/height to achieve the final correct dimensions and
position, but I've got to leave something for you to get creative on. ;)

Notice we're missing quite a few things in there, like templating (even though it's only a little HTML, it will greatly
increase flexibility and that's always a good thing), image alt attributes (shouldn't be empty), callbacks,
animation speeds and animation types.

Pro jQuery Plugins v1.0.0


page 99
Also, sometimes you just want it to be possible to navigate between a gallery inside the lightbox, not having to
close and open it again and again.

Adding Gallery Support


Let's add this gallery support, tempting, and a couple of new animations: sliding down and zooming in (see
Listing 4-5).

We'll make use of the fact that data-* attributes support JSON to get more data into just one data-* attribute
(see Listing 4-6).

Listing 4-5: JavaScript for our plugin, with support for galleries

(function( $, _, window ) {

// ...

var globals = {
galleries: [],// This array will hold all galeries as objects
currentGallery: '',// This will hold the current gallery ID open, if
any
currentGalleryIndex: -1// This wil hold the current gallery index open, if
any
};

var methods = {
init : function( options ) {
var defaults = {
'lightboxID': 'lightbox',
'backgroundClass': 'lightbox-background',
'wrapperClass': 'lightbox-wrapper',
'contentClass': 'lightbox-content',
'closeButtonClass': 'lightbox-close',
'arrowButtonClass': 'lightbox-arrow',
'leftArrowButtonAddedClass': 'left',
'rightArrowButtonAddedClass': 'right',
'lightboxMainTemplateID': 'templates-lightbox',
'lightboxImageTemplateID': 'templates-lightbox-image',
'isModal': false,
Pro jQuery Plugins v1.0.0
page 100
'escapeCloses': true,
'arrowKeysNavigate': true,
'animationType': 'fade',// supports 'fade', 'slide' and
'zoom'
'animationSpeed': 'fast',
'openOnComplete': $.noop,
'dataLightbox': 'lightboxItemOptions'// data-* property
to check for the JSON object with item specific options (gallery, alt)
};

options = $.extend( defaults, options );

// Create the lightbox "holder" if it doesn't exist yet


methods.create.call( this, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'lightbox' );

if ( ! data ) {
var itemDefaults = {
'gallery': '',// Gallery identification, so that images with this
same value belong to the same gallery
'alt': ''// Alt attribute for the image in the href
};

var itemOptions = $.extend( itemDefaults,


$this.data(options.dataLightbox) );

var imageGalleryIndex = -1;

// We're going to add this item to the galleries variable if we're


supposed to
if ( itemOptions.gallery.length > 0 ) {
// Initialize array if it's not set yet
if ( ! $.isArray(globals.galleries[itemOptions.gallery]) ) {
globals.galleries[ itemOptions.gallery ] = [];
}

// Create the image object


var imageObject = {
'src': $this.attr( 'href' ),
'alt': itemOptions.alt
Pro jQuery Plugins v1.0.0
page 101
};

// Add the object to the array and get its index


imageGalleryIndex =
globals.galleries[itemOptions.gallery].push( imageObject ) - 1;
}

$this.data( 'lightbox', {
target : $this,
options: itemOptions,
galleryIndex: imageGalleryIndex
});

// Bind click to open the lightbox


$(this).on( 'click.lightbox', function( event ) {
event.preventDefault();

methods.open.call( this, options );


});
}
});
},

destroy : function() {
// ...
},

// Create the lightbox "holder" if it doesn't exist


create: function( options ) {
// Check if the lightbox "holder" exists, if not, create it
if ( $('#' + options.lightboxID).length > 0 ) {
return false;
} else {
var lightboxHTML = _.template( $('#' +
options.lightboxMainTemplateID).html(), {
'lightboxID': options.lightboxID,
'backgroundClass': options.backgroundClass,
'wrapperClass': options.wrapperClass,
'closeButtonClass': options.closeButtonClass,
'arrowButtonClass': options.arrowButtonClass,
'leftArrowButtonAddedClass': options.leftArrowButtonAddedClass,
'rightArrowButtonAddedClass': options.rightArrowButtonAddedClass,
'contentClass': options.contentClass
Pro jQuery Plugins v1.0.0
page 102
});

// Append the lightbox to the <body> tag.


$('body').append( lightboxHTML );

if ( options.escapeCloses ) {
// Bind Escape Key to close the lightbox
$(document).on( 'keydown.lightbox', function( event ) {
if ( event.keyCode === 27 ) {
event.preventDefault();

methods.close.call( this, options );


}
});
}

// If it's not modal, make sure you can close it clicking outside of
it
if ( ! options.isModal ) {
$('#' + options.lightboxID + '-background').on( 'click.lightbox',
function( event ) {
methods.close.call( this, options );
});
}

if ( options.arrowKeysNavigate ) {
// Bind Arrow Keys Navigation
$(document).on( 'keydown.lightbox', function( event ) {
if ( event.keyCode === 37 ) {// Left
event.preventDefault();

methods.navigateLeft.call( this, options );


} else if ( event.keyCode === 39 ) {// Right
event.preventDefault();

methods.navigateRight.call( this, options );


}
});
}

// Bind Arrow Buttons Navigation


$('#' + options.lightboxID + ' .' +
options.arrowButtonClass).on( 'click.lightbox', function( event ) {
Pro jQuery Plugins v1.0.0
page 103
event.preventDefault();

if ( $(this).hasClass(options.leftArrowButtonAddedClass) ) {
methods.navigateLeft.call( this, options );
} else if ( $(this).hasClass(options.rightArrowButtonAddedClass) ) {
methods.navigateRight.call( this, options );
}
});

// Bind close button to close the lightbox


$('#' + options.lightboxID + ' .' +
options.closeButtonClass).on( 'click.lightbox', function( event ) {
event.preventDefault();

methods.close.call( this, options );


});

// Bind window resize to position the lightbox on the center of the


window
$(window).on( 'resize.lightbox', function( event ) {
methods.positionOnCenter.call( this, options );
});
}
},

// Method to open the lightbox


open: function( options ) {
var $this = $(this),
data = $this.data( 'lightbox' ),
itemOptions = data.options,
itemGalleryIndex = data.galleryIndex;

// If our plug-in wasn't initialized yet, do nothing


if ( ! data ) {
return false;
}

// Show the background/overlay


$('#' + options.lightboxID + '-background').fadeIn( 'fast' );

// Function to call after the animation is done


var doAfterAnimationIsDone = function() {

Pro jQuery Plugins v1.0.0


page 104
methods.showImage.call( this, options, itemOptions.gallery,
itemGalleryIndex, $this.attr('href'), itemOptions.alt );
};

// Open the lightbox


switch ( options.animationType ) {
case 'slide':
var originalMarginTop = $('#' + options.lightboxID).css( 'margin-
top' );

$('#' + options.lightboxID).css({
'margin-top': ( (($('#' + options.lightboxID).outerHeight() * 2) +
parseInt($('#' + options.lightboxID).css('top'), 10)) * -1 ) + 'px',
'display': 'block',
'opacity': 0
}).animate({
'margin-top': originalMarginTop,
'opacity': 1
}, options.animationSpeed, doAfterAnimationIsDone );
break;
case 'zoom':
var originalZoom = $('#' + options.lightboxID).css( 'zoom' );

$('#' + options.lightboxID).css({
'zoom': ( originalZoom / 3 ),
'display': 'block',
'opacity': 0
}).animate({
'zoom': originalZoom,
'opacity': 1
}, options.animationSpeed, doAfterAnimationIsDone );
break;
case 'fade':
default:
$('#' + options.lightboxID).fadeIn( options.animationSpeed,
doAfterAnimationIsDone );
break;
}
},

// Method to close the lightbox


close: function( options ) {
// If our plug-in wasn't initialized yet, do nothing
Pro jQuery Plugins v1.0.0
page 105
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

// Close the lightbox


$('#' + options.lightboxID).fadeOut( 'fast', function() {
// Remove content inside
$('#' + options.lightboxID + '-content').empty();
});

// Hide the background/overlay


$('#' + options.lightboxID + '-background').fadeOut( 'fast' );

// Reset the current gallery and current gallery index


globals.currentGallery = '';
globals.currentGalleryIndex = -1;

// Hide arrows
$('#' + options.lightboxID + ' .' + options.arrowButtonClass).hide();
},

// Method to position the lightbox on the center of the screen


positionOnCenter: function( options ) {
// ...
},

// Show arrows if there is navigation


showOrHideArrows: function( options, itemGallery, itemGalleryIndex ) {
// If our plug-in wasn't initialized yet, do nothing
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

if ( itemGallery.length > 1 ) {
if ( itemGalleryIndex >= 0 ) {
// Show arrows
$('#' + options.lightboxID + ' .' +
options.arrowButtonClass).show();

// Is it the last image?


if ( itemGalleryIndex == (globals.galleries[itemGallery].length -
1) ) {
// Hide right arrow
Pro jQuery Plugins v1.0.0
page 106
$('#' + options.lightboxID + ' .' + options.arrowButtonClass + '.'
+ options.rightArrowButtonAddedClass).hide();
}

// Is it the first image?


if ( itemGalleryIndex === 0 ) {
// Hide left arrow
$('#' + options.lightboxID + ' .' + options.arrowButtonClass + '.'
+ options.leftArrowButtonAddedClass).hide();
}
}
}
},

// Method to show image


showImage: function( options, itemGallery, itemGalleryIndex, imageSrc,
imageAlt ) {
// If our plug-in wasn't initialized yet, do nothing
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

// Image preload process


var objImagePreloader = new Image();
objImagePreloader.onload = function() {
// Load the image inside the lightbox
var generatedID = helpers.generateID.call( this ),
imageHTML = _.template( $('#' +
options.lightboxImageTemplateID).html(), {
'lightboxID': options.lightboxID,
'generatedID': generatedID,
'imageSrc': imageSrc,
'imageAlt': imageAlt
});

$('#' + options.lightboxID + '-content').html( imageHTML );

// Position the lightbox


methods.positionOnCenter.call( this, options );

// Set the current gallery and current gallery index


globals.currentGallery = itemGallery;
globals.currentGalleryIndex = itemGalleryIndex;
Pro jQuery Plugins v1.0.0
page 107
// Show arrows if there is navigation
methods.showOrHideArrows.call( this, options, itemGallery,
itemGalleryIndex );

// clear onLoad, IE behaves irratically with animated gifs otherwise


objImagePreloader.onload = function() {};

if ( $.isFunction(options.openOnComplete) ) {
options.openOnComplete.call( this, options );
}
};

objImagePreloader.src = imageSrc;
},

// Method to navigate to an image


navigateTo: function( options, calledIndex ) {
// If our plug-in wasn't initialized yet, do nothing
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

if ( globals.galleries[globals.currentGallery][calledIndex] ) {
methods.showImage.call( this, options, globals.currentGallery,
calledIndex, globals.galleries[globals.currentGallery][calledIndex].src,
globals.galleries[globals.currentGallery][calledIndex].alt );
}
},

// Method to navigate "left"


navigateLeft: function( options ) {
// If our plug-in wasn't initialized yet, do nothing
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

if ( globals.currentGallery.length > 0 ) {
var newIndex = (globals.currentGalleryIndex - 1);

// If we're already on the first image, do nothing


if ( newIndex < 0 ) {
return false;
Pro jQuery Plugins v1.0.0
page 108
}

methods.navigateTo.call( this, options, newIndex );


}
},

// Method to navigate "right"


navigateRight: function( options ) {
// If our plug-in wasn't initialized yet, do nothing
if ( ! $('#' + options.lightboxID).length ) {
return false;
}

if ( globals.currentGallery.length > 0 ) {
var newIndex = ( globals.currentGalleryIndex + 1 );

// If we're already on the last image, do nothing


if ( newIndex > (globals.galleries[globals.currentGallery].length -
1) ) {
return false;
}

methods.navigateTo.call( this, options, newIndex );


}
}
};

// ...

})( jQuery, _, window );

NOTE: Again, we used data-* attributes instead of classes or IDs. Because jQuery supports it "natively",
the performance is better, and we don't need to bloat the `class` DOM attribute, which should be used for
styling anyway. Also, we added some meaningful alt attributes.

Pro jQuery Plugins v1.0.0


page 109
Listing 4-6: HTML for the new data-* attribute with JSON and the underscore
templates

<div class="thumbnails">
<div class="thumbnail">
<a href="http://us.123rf.com/400wm/400/400/EnjoyLife25/EnjoyLife250608/
EnjoyLife25060800219/503216-beautiful-sunset-on-the-beach.jpg" data-lightbox-
item-options='{"alt":"Beautiful sunset on the beach"}'>
<img src="http://us.cdn2.123rf.com/168nwm/enjoylife25/enjoylife250608/
enjoylife25060800219/503216-beautiful-sunset-on-the-beach.jpg" alt="Beautiful
sunset on the beach">
</a>
</div>
<div class="thumbnail">
<a href="http://us.123rf.com/400wm/400/400/enjoylife25/enjoylife250710/
enjoylife25071000026/1829472-beautiful-night-at-the-beach.jpg" data-lightbox-
item-options='{"gallery":"beach","alt":"Beautiful night at the beach"}'>
<img src="http://us.cdn2.123rf.com/168nwm/enjoylife25/enjoylife250710/
enjoylife25071000026/1829472-beautiful-night-at-the-beach.jpg" alt="Beautiful
night at the beach">
</a>
</div>
<div class="thumbnail">
<a href="http://us.123rf.com/400wm/400/400/enjoylife25/enjoylife250710/
enjoylife25071000023/1829475-a-beautiful-night-at-the-beach--slow-
shutterspeed.jpg" data-lightbox-item-
options='{"gallery":"beach","alt":"Beautiful night at the beach with slow
shutterspeed"}'>
<img src="http://us.cdn1.123rf.com/168nwm/enjoylife25/enjoylife250710/
enjoylife25071000023/1829475-a-beautiful-night-at-the-beach--slow-
shutterspeed.jpg" alt="Beautiful night at the beach with slow shutterspeed">
</a>
</div>
<div class="thumbnail">
<a href="http://us.123rf.com/400wm/400/400/EnjoyLife25/EnjoyLife250608/
EnjoyLife25060800020/482752-beautiful-sunset-landscape-photo.jpg" data-lightbox-
item-options='{"gallery":"beach","alt":"Beautiful sunset landscape"}'>
<img src="http://us.cdn2.123rf.com/168nwm/enjoylife25/enjoylife250608/
enjoylife25060800020/482752-beautiful-sunset-landscape-photo.jpg" alt="Beautiful
sunset landscape">
</a>
Pro jQuery Plugins v1.0.0
page 110
</div>
</div>

<script type="text/x-underscore-template" id="templates-lightbox">


<div id="<%= lightboxID %>-background" class="<%= backgroundClass %>"></div>
<div id="<%= lightboxID %>" class="<%= wrapperClass %>">
<span id="<%= lightboxID %>-close" class="<%= closeButtonClass %>"></span>
<span id="<%= lightboxID %>-arrow-left" class="<%= arrowButtonClass %> <%=
leftArrowButtonAddedClass %>"></span>
<span id="<%= lightboxID %>-arrow-right" class="<%= arrowButtonClass %> <%=
rightArrowButtonAddedClass %>"></span>
<div id="<%= lightboxID %>-content" class="<%= contentClass %>">
</div>
</div>
</script>

<script type="text/x-underscore-template" id="templates-lightbox-image">


<img id="<%= lightboxID %>-img-<%= generatedID %>" src="<%= imageSrc %>"
alt="<%= imageAlt %>">
</script>

We also had to add a bit of CSS because of the buttons (see Listing 4-7).

Listing 4-7: CSS with the styling for the buttons/arrows

.lightbox-wrapper .lightbox-arrow {
display: none;
position: absolute;
top: 50%;
color: #333;
font-size: 14px;
font-weight: bold;
cursor: pointer;
padding: 1px 6px 2px;
border-radius: 10px;
background: #FFF;
margin-top: -10px;
}

.lightbox-wrapper .lightbox-arrow.left {

Pro jQuery Plugins v1.0.0


page 111
left: 2px;
}

.lightbox-wrapper .lightbox-arrow.right {
right: 2px;
}

This is how it looks when opening a lightbox that has navigation, i.e. is part of a gallery (see Figure 4-3, Figure
4-4 and Figure 4-5):

Figure 4-3: First Photo of Gallery

Pro jQuery Plugins v1.0.0


page 112
Figure 4-4: Second Photo of Gallery

Figure 4-5: Last Photo of Gallery

Pro jQuery Plugins v1.0.0


page 113
Adding support for iFrames and HTML
Now, we want to make it a bit more versatile for anyone to do anything with this lightbox, so now we're going to
add support for iframes (URLs) and HTML from a DOM element, for example (see Listing 4-8).

Listing 4-8: JavaScript for our plugin, now supporting iframes (URLs) and HTML

(function( $, _, window ) {

// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'lightboxImageTemplateID': 'templates-lightbox-image',
'lightboxIFrameTemplateID': 'templates-lightbox-iframe',
'lightboxHTMLTemplateID': 'templates-lightbox-html',
'htmlClass': 'html',
// ...
'dataLightbox': 'lightboxItemOptions'// data-* property to
check for the JSON object with item specific options (gallery, alt, type)
};

// ...

return this.each(function() {
var $this = $(this),
data = $this.data( 'lightbox' );

if ( ! data ) {
var itemDefaults = {
'gallery': '',// Gallery identification, so that images with this
same value belong to the same gallery
'alt': '',// Alt attribute for the image in the href
'type': 'image'// Item type, can be 'image', 'iframe', or
'html'
};

// ...

Pro jQuery Plugins v1.0.0


page 114
}
});
},

// ...

// Method to open the lightbox


open: function( options ) {
// ...

// Function to call after the animation is done


var doAfterAnimationIsDone = function() {
// get itemOptions.type and show image, iframe or HTML
switch ( itemOptions.type ) {
case 'iframe':
methods.showIFrame.call( this, options, $this.attr('href') );
break;
case 'html':
methods.showHTML.call( this, options, $this.attr('href') );
break;
case 'image':
default:
methods.showImage.call( this, options, itemOptions.gallery,
itemGalleryIndex, $this.attr('href'), itemOptions.alt );
break;
}
};

// Open the lightbox


// ...
},

// ...

// Method to show iframe


showIFrame: function( options, iframeURL ) {
// Load the iframe inside the lightbox
var generatedID = helpers.generateID.call( this ),
iframeHTML = _.template( $('#' +
options.lightboxIFrameTemplateID).html(), {
'lightboxID': options.lightboxID,
'generatedID': generatedID,
'iframeURL': iframeURL
Pro jQuery Plugins v1.0.0
page 115
});

$('#' + options.lightboxID + '-content').html( iframeHTML );

// Position the lightbox


methods.positionOnCenter.call( this, options );

if ( $.isFunction(options.openOnComplete) ) {
options.openOnComplete.call( this, options );
}
},

// Method to show HTML


showHTML: function( options, elementSelector ) {
// Load the div inside the lightbox
var generatedID = helpers.generateID.call( this ),
contentHTML = _.template( $('#' +
options.lightboxHTMLTemplateID).html(), {
'lightboxID': options.lightboxID,
'generatedID': generatedID,
'htmlClass': options.htmlClass,
'htmlContent': $( elementSelector ).html()
});

$('#' + options.lightboxID + '-content').html( contentHTML );

// Position the lightbox


methods.positionOnCenter.call( this, options );

if ( $.isFunction(options.openOnComplete) ) {
options.openOnComplete.call( this, options );
}
}
};

// ...

})( jQuery, _, window );

We needed a couple of new templates and "thumbnails" to trigger the iframe and HTML new lightbox types (see
Listing 4-9).

Pro jQuery Plugins v1.0.0


page 116
Listing 4-9: New HTML markup, to make use of the iframe and HTML new lightbox
types

<div class="thumbnails">
<!-- ... -->
<div class="thumbnail">
<a href="http://www.youtube.com/embed/BcL---4xQYA" data-lightbox-item-
options='{"type":"iframe"}'>
<img src="http://upload.wikimedia.org/wikipedia/commons/thumb/9/98/
YouTube_Logo.svg/100px-YouTube_Logo.svg.png" alt="Youtube">
</a>
</div>
<div class="thumbnail">
<a href="#hidden-sample-div" data-lightbox-item-options='{"type":"html"}'>
<img src="http://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/HTML5-
logo.svg/100px-HTML5-logo.svg.png" alt="HTML5 Logo">
</a>
</div>
</div>

<div id="hidden-sample-div">
<h1>Sample</h1>
<p>This is a sample hidden div that will get added to the lightbox.</p>
</div>

<script type="text/x-underscore-template" id="templates-lightbox">


<!-- ... -->
</script>

<script type="text/x-underscore-template" id="templates-lightbox-image">


<!-- ... -->
</script>

<script type="text/x-underscore-template" id="templates-lightbox-iframe">


<iframe id="<%= lightboxID %>-iframe-<%= generatedID %>" src="<%= iframeURL
%>"></iframe>
</script>

<script type="text/x-underscore-template" id="templates-lightbox-html">


<div id="<%= lightboxID %>-html-<%= generatedID %>" class="<%= htmlClass
%>"><%= htmlContent %></div>
Pro jQuery Plugins v1.0.0
page 117
</script>

Also, a few lines of CSS were added to make sure the new lightboxes looked ok (see Listing 4-10).

Listing 4-10: CSS changes to make our new lightboxes look ok

.lightbox-wrapper .lightbox-content iframe {


display: block;
border: none;
overflow: auto;
min-width: 400px;
min-height: 300px;
}

.lightbox-wrapper .lightbox-content div.html {


display: block;
}

#hidden-sample-div {
display: none;
}

Considering these new additions and changes, here's how it looks like now for the iframe (see Figure 4-6) and
the HTML (see Figure 4-7).

Pro jQuery Plugins v1.0.0


page 118
Figure 4-6: Iframe lightbox, with a video of a great music!

Pro jQuery Plugins v1.0.0


page 119
Figure 4-7: HTML lightbox

Using it with our previous plugins


Now a great test to see how well our plugins work in different environments, is to call a form with the validation
plugin and tooltips in a lightbox (see Listing 4-11). Yes, the world might implode with all this plugin within a
plugin, but we'll do it anyway and hope for the best. It'll look like something similar to this (see Figure 4-8)

Listing 4-11: Sample code to call both plugins together.

// ... both plug-ins

Pro jQuery Plugins v1.0.0


page 120
$('.sample-trigger a').lightbox({
'openOnComplete': function() {
$('.sample-form').on( 'submit', function( event ) {
event.preventDefault();
});

//$('.sample-form input').inputValidation();
$('.sample-form input').inputValidation({
'animation': {
'type': 'fade',
'speed': 'fast',
'easing': 'linear',
'onComplete': function() {
$('.inputValidation-error').toolTip({ 'followMouse': false,
'position': 'right' });
},
'extra': {
'margin': 3
}
}
});
}
});

Pro jQuery Plugins v1.0.0


page 121
Figure 4-8: Yo dawg, I heard you like plugins...

Pro jQuery Plugins v1.0.0


page 122
Summary
In this chapter you've learned how to make a lightbox plugin that supports photos, photo galleries, iframes, and
HTML, that is flexible, customizable, and easy to implement in multiple scenarios. You've also learned how to
use underscore templates properly.

There are improvements that can be made, like making it possible for several lightboxes to exist (tip: you'd have
to calculate and change z-index and consider several other things, use a class and generated IDs, etc.). Go
ahead and try that. Add new functionality and improve the sizing and positioning of the lightboxes.

We've also tested it with the previous plugins, to prove how dynamic and flexible they all are.

In the next chapter, we'll learn how to make an amazing slider plugin.

Pro jQuery Plugins v1.0.0


page 123
Chapter 5: A Very Complete
Slider Plugin Part I

In this chapter, you're going to learn how to build, step-by-step, a very complete slider plugin. "Very complete,
why?", you ask. Well, because it will support multiple animations (including 3D and random animations per slide
transition), caption/description positioning, responsive/dynamic width, mobile-friendliness (supporting touch/
swipe), among many other things expected in today's sliders.

Since this plugin is very complex, this chapter will be the first of two for this plugin, in which we'll get the initial
base for the script, ending with a basic functionality slider.

Section 1: The Concept


Sliders nowadays are widely used. You see them on most websites, and their objective is to promote and/or
showcase something or some product.

While images and images with text are what's more commonly used for this purpose, sometimes we get a
much better experience by using more types of elements/media, like text with videos or images with videos, for
example.

Because we can't predict nor want to restrict what users want to use for their slides, we want to allow them to
use HTML, and that's why we want to build a very complete slider plugin, so that the users can decide to put
on their slides whatever they want, not having to worry if the plugin was built considering that option or not.

We will use everything you've learned so far, and understand how what you've learned can be used in a different
situation, considering we're now going to build a plugin that won't be adding any elements to the DOM, just
manipulating them, and thus making us work much more with events.

Pro jQuery Plugins v1.0.0


page 124
Section 2: The HTML & CSS
We know we want to give the slider basic functionality for this chapter, so let's start with three types of sliders
(the most common):

1. Just an image;

2. An image with caption on the right;

3. An image with caption on the left;

These captions will be on top of the images, with the CSS styling them with a transparent background, for eye
candy.

We want the slider to have an option to start cycling through slides automatically, but this should be canceled
when the user manually chooses a slide or navigates.

Standard left/right arrow navigation will be included.

So, like always, we start with getting a base plugin with To-Dos for what we want and need the plugin to do (see
Listing 5-1 for the HTML, Listing 5-2 for the CSS, and Listing 5-3 for the Javascript).

NOTE: I do not own copyright of these images and I'm only using them as samples because they look
pretty.

Listing 5-1: The HTML for the base of our plugin

<div class="slider">
<span class="slider-nav left"></span>
<span class="slider-nav right"></span>
<div class="slide-wrapper">
<div class="slide active">
<img src="http://thumbs.dreamstime.com/images/splash/4741169.jpg"
alt="Two flamingos on a beach">
</div>
<div class="slide">

Pro jQuery Plugins v1.0.0


page 125
<div class="caption right">
<h1>This is a lovely image</h1>
<p>Our caption supports HTML, as you can see.</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/9866332.jpg"
alt="Three firemen fighting fire">
</div>
<div class="slide">
<div class="caption left">
<h1>This is another lovely image</h1>
<p>Now this caption is on the left.</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/3012971.jpg" alt="A
frog inside a mug">
</div>
</div>
</div>

Listing 5-2: The CSS for the base of our plugin

h1, p {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #FFF;
margin: 10px 5px;
}

h1 {
font-size: 22px;
}

p {
font-size: 14px;
}

.slider {
display: block;
margin: 10px;
background: #333;
width: 520px;
height: 195px;

Pro jQuery Plugins v1.0.0


page 126
}

.slider .slider-nav {
display: block;
position: absolute;
margin-top: 76px;
color: #FFF;
font-size: 30px;
font-weight: bold;
cursor: pointer;
padding: 1px 10px 6px;
text-align: center;
background: #333;
opacity: 0;
transition: opacity 200ms;
-webkit-transition: opacity 200ms;
-moz-transition: opacity 200ms;
-o-transition: opacity 200ms;
z-index: 10;
}

.slider:hover .slider-nav {
opacity: 0.6;
}

.slider:hover .slider-nav:hover {
opacity: 1;
}

.slider .slider-nav.left {
margin-left: 0;
border-radius: 0 10px 10px 0;
display: none;
}

.slider .slider-nav.right {
margin-left: 485px;
border-radius: 10px 0 0 10px;
}

.slider .slide-wrapper {
display: block;
overflow: hidden;
Pro jQuery Plugins v1.0.0
page 127
width: 520px;
height: 195px;
position: relative;
}

.slider .slide-wrapper .slide {


display: none;
position: absolute;
width: 520px;
height: 195px;
}

.slider .slide-wrapper .slide.active {


display: block;
}

.slider .slide-wrapper .slide .caption {


display: block;
position: absolute;
margin: 10px;
width: 220px;
height: 175px;
background: rgba(0, 0, 0, .6);
z-index: 5;
}

.slider .slide-wrapper .slide .caption.left {


margin-left: 10px;
}

.slider .slide-wrapper .slide .caption.right {


margin-left: 290px;
}

.slider .slide-wrapper .slide img {


width: 520px;
height: 195px;
}

NOTE: We've hidden the left arrow by default because at the first slide, it makes no sense to show it.

Pro jQuery Plugins v1.0.0


page 128
Listing 5-3: The Javascript for the base of our plugin, with To-Dos

(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {
// TODO: We'll need navigation arrow selectors, slide wrapper class,
slide class, active class, if the arrow keys should navigate or not, etc.

// TODO: Thinking about the future, we should also set a default for
animation type, speed and a callback when the image loads

// TODO: Also get settings per slide using a data-* attribute. We may
not need that right away, but we know we will in the future
};

options = $.extend( defaults, options );

// TODO: Bind Arrow Keys Navigation

return this.each(function() {
var $this = $(this),
data = $this.data( 'slider' );

if ( ! data ) {
$this.data( 'slider', {
target : $this
});

// TODO: Bind Arrow Buttons Navigation


}
});
},

destroy : function() {
$(window).off( '.slider' );

return this.each(function(){
var $this = $(this),
data = $this.data( 'slider' );

Pro jQuery Plugins v1.0.0


page 129
$this.removeData( 'slider' );
});
},

// TODO: Method to show slide


showSlide: function( options, slideIndex ) {
},

// TODO: Method to navigate to a slide


navigateTo: function( options, calledIndex ) {
},

// TODO: Method to navigate "left"


navigateLeft: function( options ) {
},

// TODO: Method to navigate "right"


navigateRight: function( options ) {
}
};

$.fn.slider = 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.slider' );
}
};
})( jQuery, window );

Section 3: The Basic Functionality


Let's now implement the To-Dos, which results in the following Javascript code (see Listing 5-4):

Pro jQuery Plugins v1.0.0


page 130
Listing 5-4: The Javascript for the base of our plugin, now with the To-Dos
implemented

(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {
'leftArrowSelector': '.slider-nav.left',
'rightArrowSelector': '.slider-nav.right',
'slideWrapperSelector': '.slide-wrapper',
'slideClass': 'slide',
'activeSlideClass': 'active',
'arrowKeysNavigate': true,
'animationType': 'fade',// supports 'fade' for now
'animationSpeed': 'fast',
'onAnimationComplete': $.noop,
'autoStartSlideshow': true,
'slideTimeoutMilliseconds': 10000,
'dataSlide': 'slide'// data-* property to check for the
JSON object with slice specific options (animationType, animationSpeed)
};

options = $.extend( defaults, options );

// Bind Arrow Keys Navigation


if ( options.arrowKeysNavigate ) {
$(document).on( 'keydown.slider', function( event ) {
if ( event.keyCode === 37 ) {// Left
event.preventDefault();

methods.navigateLeft.call( this, options );


} else if ( event.keyCode === 39 ) {// Right
event.preventDefault();

methods.navigateRight.call( this, options );


}
});
}

return this.each(function() {
var $this = $(this),
Pro jQuery Plugins v1.0.0
page 131
data = $this.data( 'slider' );

if ( ! data ) {
$this.data( 'slider', {
target : $this
});

// Bind Arrow Buttons Navigation


$this.find( options.leftArrowSelector ).on( 'click.slider',
function( event ) {
event.preventDefault();

methods.navigateLeft.call( this, options );


});

$this.find( options.rightArrowSelector ).on( 'click.slider',


function( event ) {
event.preventDefault();

methods.navigateRight.call( this, options );


});
}
});
},

destroy : function() {
// ...
},

// Method to show slide


showSlide: function( options, slideIndex ) {
var slides = $(options.slideWrapperSelector).find( '.' +
options.slideClass ),
currentSlide = slides.siblings( '.' + options.activeSlideClass ),
currentIndex = currentSlide.index(),
$this = $( options.slideWrapperSelector ).find( '.' +
options.slideClass + ':eq(' + slideIndex + ')' );

var itemDefaults = {
'animationType': options.animationType,
'animationSpeed': options.animationSpeed
};

Pro jQuery Plugins v1.0.0


page 132
// Extend the default item options using data-* attribute
var itemOptions = $.extend(itemDefaults,
$this.data( options.dataSlide ));

switch ( itemOptions.animationType ) {
case 'fade':
default:
// Hide previous slide
currentSlide.fadeOut( itemOptions.animationSpeed, function() {
$(this).removeClass( options.activeSlideClass );
});

// Do animation
$this.fadeIn( itemOptions.animationSpeed, function() {
$(this).addClass( options.activeSlideClass );

// Trigger onAnimationComplete
if ( $.isFunction(options.onAnimationComplete) ) {
options.onAnimationComplete.call( this, options, itemOptions,
slideIndex );
}

// Show arrows if there is navigation


methods.showOrHideArrows.call( this, options );
});
break;
}
},

// Show arrows if there is navigation


showOrHideArrows: function( options ) {
var slides = $(options.slideWrapperSelector).find( '.' +
options.slideClass ),
currentIndex = slides.siblings( '.' +
options.activeSlideClass ).index();
if ( slides.length > 1 ) {
if ( currentIndex >= 0 ) {
// Show arrows
$
( options.slideWrapperSelector ).siblings( options.leftArrowSelector + ', ' +
options.rightArrowSelector ).show();

// Is it the last slide?


Pro jQuery Plugins v1.0.0
page 133
if ( currentIndex == (slides.length - 1) ) {
// Hide right arrow
$
( options.slideWrapperSelector ).siblings( options.rightArrowSelector ).hide();
}

// Is it the first slide?


if ( currentIndex === 0 ) {
// Hide left arrow
$
( options.slideWrapperSelector ).siblings( options.leftArrowSelector ).hide();
}
}
}
},

// Method to navigate to a slide


navigateTo: function( options, calledIndex ) {
var slides = $(options.slideWrapperSelector).find( '.' +
options.slideClass ),
currentIndex = slides.siblings( '.' +
options.activeSlideClass ).index();
if ( calledIndex != currentIndex ) {
methods.showSlide.call( this, options, calledIndex );
}
},

// Method to navigate "left"


navigateLeft: function( options ) {
var slides = $(options.slideWrapperSelector).find( '.' +
options.slideClass ),
currentIndex = slides.siblings( '.' +
options.activeSlideClass ).index();

if ( slides.length > 0 ) {
var newIndex = ( currentIndex - 1 );

// If we're already on the first slide, do nothing


if ( newIndex < 0 ) {
return false;
}

methods.navigateTo.call( this, options, newIndex );


Pro jQuery Plugins v1.0.0
page 134
}
},

// Method to navigate "right"


navigateRight: function( options ) {
var slides = $(options.slideWrapperSelector).find( '.' +
options.slideClass ),
currentIndex = slides.siblings( '.' +
options.activeSlideClass ).index();

if ( slides.length > 0 ) {
var newIndex = ( currentIndex + 1 );

// If we're already on the last slide, do nothing


if ( newIndex > (slides.length - 1) ) {
return false;
}

methods.navigateTo.call( this, options, newIndex );


}
}
};

$.fn.slider = 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.slider' );
}
};
})( jQuery, window );

There are some methods similar in logic to the lightbox gallery part, because we're basically doing the same
thing, but moving left and right through slides, not just images, and not adding/removing elements to the DOM,
just manipulating to hide/show/animate existing ones.

We now have a functional and flexible slider, albeit simple.

Here's how it looks (see Figure 5-1, Figure 5-2 and Figure 5-3):
Pro jQuery Plugins v1.0.0
page 135
Figure 5-1: First slide of our slider plugin

Figure 5-2: Second slide of our slider plugin

Pro jQuery Plugins v1.0.0


page 136
Figure 5-3: Third slide of our slider plugin

NOTE: The arrows are visible only because I was hovering the images with my mouse cursor. The arrows
go away when you're not hovering them, and while that's a CSS touch, it's a great usability enhance-
ment.

Making our slider automatic


Looks great, but aren't we missing something? Our slider is not doing an automatic slideshow (if you were
paying attention, you saw I already added two settings for the timeout)!

Yeah, that part's a bit important, and we'll need a "global" (in the closure scope) variable to handle the timer.
We'll use window.setTimeout() to slide and window.clearTimeout() to clear it when needed (see Listing 5-5).

Also, we can't forget we'll want the slideshow to "loop" (instead of navigating right and stopping), so we'll also
need a new method for that. Fun!

Listing 5-5: Javascript for the automatic slideshow part

(function( $, window ) {

Pro jQuery Plugins v1.0.0


page 137
var globals = {
'timeoutID': null,// This variable will hold the timeout ID
'timeoutCanceled': false// We'll use this variable to know if the timeout
was canceled or not (so we know if we have to reset it or not).
};

var methods = {
init : function( options ) {

// ...

return this.each(function() {
var $this = $(this),
data = $this.data( 'slider' );

if ( ! data ) {
$this.data( 'slider', {
target : $this
});

// ...

// Start automatically if the setting is set to do so


if ( options.autoStartSlideshow ) {
methods.navigateTo.call( this, options, 0 );
globals.timeoutID = window.setTimeout(function() {
methods.doLoop.call( this, options );
}, options.slideTimeoutMilliseconds );
} else {
globals.timeoutCanceled = true;
}
}
});
},

// ...

// Method to show slide


showSlide: function( options, slideIndex ) {
// ...

switch ( itemOptions.animationType ) {
case 'fade':
Pro jQuery Plugins v1.0.0
page 138
default:
// ...

// Do animation
$this.fadeIn( itemOptions.animationSpeed, function() {
// ...

// Keep sliding automatically if the setting is set to do so


if ( ! globals.timeoutCanceled ) {
globals.timeoutID = window.setTimeout(function() {
methods.doLoop.call( this, options );
}, options.slideTimeoutMilliseconds);
}
});
break;
}
},

// ...

// Method to navigate "left"


navigateLeft: function( options ) {
// ...

if ( slides.length > 0 ) {
// ...

// If the navigation was triggered, we need to cancel the slideshow


timeout
methods.cancelTimeout.call( this, options );

methods.navigateTo.call( this, options, newIndex );


}
},

// Method to navigate "right"


navigateRight: function( options ) {
// ...

if ( slides.length > 0 ) {
// ...

Pro jQuery Plugins v1.0.0


page 139
// If the navigation was triggered, we need to cancel the slideshow
timeout
methods.cancelTimeout.call( this, options );

methods.navigateTo.call( this, options, newIndex );


}
},

// Add slideshow method (looping)


doLoop: function( options ) {
var slides = $(options.slideWrapperSelector).find( '.' +
options.slideClass ),
currentIndex = slides.siblings( '.' +
options.activeSlideClass ).index();

// There's no need to loop if there's only 1 slide


if ( slides.length > 1 ) {
var newIndex = ( currentIndex + 1 );

// If we're already on the last slide, go to the first


if ( newIndex > (slides.length - 1) ) {
newIndex = 0;
}

methods.navigateTo.call( this, options, newIndex );


}
},

// Cancel the slideshow timeout


cancelTimeout: function( options ) {
if ( globals.timeoutID ) {
window.clearTimeout( globals.timeoutID );
}

globals.timeoutCanceled = true;
}
};

// ...

})( jQuery, window );

Pro jQuery Plugins v1.0.0


page 140
NOTE: We used window.setTimeout() and window.clearTimeout() instead of window.setInterval() and
window.clearInterval() so we can start the "timer" when the animation is done, and not have to worry
about the animation time itself, otherwise, after a while, we could have the interval running too closely
between animations and we don't want that.

Adding more to our slider


While this would work great for people who just wanted a slider for these options, there are still a few things
missing that can greatly enhance usability for our slider, like having a static, per item navigation (not a part of the
plugin itself, but a sample that works with it), that would make the slider go to the desired slide by clicking on it.

This is usually seen as dots below the slider, where each dot represents a slide.

We can consider adding the following to our sample HTML (see Listing 5-6):

Listing 5-6: New HTML for the slider static navigation

<div class="slider">
<!-- ... -->
<div class="slider-bottom-nav">
<span data-slide="0" class="active"></span>
<span data-slide="1"></span>
<span data-slide="2"></span>
</div>
</div>

Also, we will need to style this a bit, so add this CSS (see Listing 5-7):

Listing 5-7: New CSS for the slider static navigation

.slider .slider-bottom-nav {
display: block;
text-align: center;
Pro jQuery Plugins v1.0.0
page 141
width: 520px;
padding-top: 10px;
}

.slider .slider-bottom-nav span {


background: #CCC;
width: 10px;
height: 10px;
display: inline-block;
border-radius: 5px;
cursor: pointer;
margin: 0 2px;
transition: background 200ms;
-webkit-transition: background 200ms;
-moz-transition: background 200ms;
-o-transition: background 200ms;
}

.slider .slider-bottom-nav span.active {


background: #333;
}

Lastly, we need to add the Javascript code to our plugin that will make this navigation actually navigate to the
slider, don't we?

Well, sort of. Because since we already have the navigateTo() method, we can simply call it on our navigation,
not needing to change the plugin itself (see Listing 5-9), but we need to access the options from the plugin, and
for that we need to create a new global var and method for it (see Listing 5-8).

Listing 5-8: Javascript to set and get the plugin options

(function( $, window ) {
var globals = {
// ...
'options': {}
};

var methods = {

Pro jQuery Plugins v1.0.0


page 142
init : function( options ) {
// ...

options = $.extend( defaults, options );

globals.options = options;

// ...
},

destroy : function() {
// ...
},

getOptions : function() {
return globals.options;
},

// ...
};

// ...

})( jQuery, window );

NOTE: Don't forget we want to stop the automatic slideshow with this navigation, and activate/
deactivate the according navigation link.

Listing 5-9: Javascript to bind the correct actions to the slider static navigation

$('.slider').slider({
'onAnimationComplete': function( options, itemOptions, slideIndex ) {
$('.slider .slider-bottom-nav span').removeClass( 'active' );
$('.slider .slider-bottom-nav span:eq(' + slideIndex +
')').addClass( 'active' );
}
});

Pro jQuery Plugins v1.0.0


page 143
$('.slider .slider-bottom-nav span').on( 'click.app', function( event ) {
event.preventDefault();

var navIndex = window.parseInt( $(this).data('slide'), 10 );

$('.slider').slider( 'navigateTo', $('.slider').slider('getOptions'),


navIndex );
$('.slider').slider( 'cancelTimeout' );
});

This is a great example of how building our plugin in a flexible, customizable, and abstract way, can make
adding usability features and changing/improve use cases easy, by not requiring any actual code rewrite or
change in the plugin!

This is how it looks now (see Figure 5-4)!

Figure 5-4: Our slider with the static navigation below it

Summary
In this chapter you've built a simple but functional and flexible slider plugin, learning and seeing in action the
great advantages of flexibility and abstraction in a plugin.

For most website uses, this slider would be enough, though.


Pro jQuery Plugins v1.0.0
page 144
In the next chapter, we'll make our slider plugin much more feature-rich, because, after all, we're aiming for a
"very complete" slider plugin, here.

Pro jQuery Plugins v1.0.0


page 145
Chapter 6: A Very Complete
Slider Plugin Part II

In this chapter, we continue and finish building a very complete slider plugin. We will take what you've learned
and built in the previous chapter, adding more flexibility and making it mobile friendly (touch/swipe events and
responsive design).

Section 1: Adding Flexibility


We'll start by adding a simple animation, and then moving on to some more complex ones.

Since we already prepared the plugin in the previous chapter for animation support, we don't need to implement
that structure.

Slide up and down


We already have fade in and fade out, so let's add a slide up and down (see Listing 6-1).

Listing 6-1: Javascript to add slide up and down animation

(function( $, window ) {

// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide'
// ...
};

Pro jQuery Plugins v1.0.0


page 146
// ...
},

// ...

// Method to show slide


showSlide: function( options, slideIndex ) {
// ...

switch ( itemOptions.animationType ) {
case 'slide':
// Hide previous slide
currentSlide.slideUp( itemOptions.animationSpeed, function() {
$(this).removeClass( options.activeSlideClass );
});

// Do animation
$this.slideDown( itemOptions.animationSpeed, function() {
$(this).addClass( options.activeSlideClass );

// Trigger onAnimationComplete
if ( $.isFunction(options.onAnimationComplete) ) {
options.onAnimationComplete.call( this, options, itemOptions,
slideIndex );
}

// Show arrows if there is navigation


methods.showOrHideArrows.call( this, options );

// Keep sliding automatically if the setting is set to do so


if ( ! globals.timeoutCanceled ) {
globals.timeoutID = window.setTimeout(function() {
methods.doLoop.call( this, options );
}, options.slideTimeoutMilliseconds );
}
});
break;
case 'fade':
default:
// ...
break;
}
},
Pro jQuery Plugins v1.0.0
page 147
// ...
};

// ...

})( jQuery, window );

Side by side
Now, we've been using the images as a background to the captions, but what if we want them side-by-side in a
slide?

We should do this with CSS and HTML instead of JavaScript, just like we did for the positioning of the captions,
to allow greater flexibility (see Listing 6-2 and Listing 6-3).

Listing 6-2: HTML for new slides, with captions on the left and right, but side-by-
side with the images, not using them as a full width background

<div class="slider">
<!-- ... -->
<div class="slide-wrapper">
<!-- ... -->
<div class="slide split">
<div class="caption right">
<h1>One more image</h1>
<p>Split caption, on the right.</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/4343648.jpg" alt="A
man playing soccer">
</div>
<div class="slide split">
<div class="caption left">
<h1>This is another new image</h1>
<p>Now this caption is on the left.</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/1096215.jpg" alt="A
tropical forest">
</div>
</div>
Pro jQuery Plugins v1.0.0
page 148
<div class="slider-bottom-nav">
<!-- ... -->
<span data-slide="3"></span>
<span data-slide="4"></span>
</div>
</div>

Listing 6-3: CSS for the new slides

/* ... */

.slider .slide-wrapper .slide .caption.right {


/* ... */
}

.slider .slide-wrapper .slide.split .caption {


margin: 0;
width: 240px;
height: 195px;
}

.slider .slide-wrapper .slide.split .caption.left {


margin-left: 0;
}

.slider .slide-wrapper .slide.split .caption.right {


margin-left: 280px;
}

.slider .slide-wrapper .slide img {


/* ... */
}

.slider .slide-wrapper .slide.split img {


width: 280px;
height: 195px;
}

.slider .slide-wrapper .slide.split .caption.left + img {


margin-left: 240px;

Pro jQuery Plugins v1.0.0


page 149
}

.slider .slider-bottom-nav {
/* ... */
}

/* ... */

It looks pretty nice (see Figure 6-1 and Figure 6-2), and we didn't need to touch the JavaScript! See how
making the styling of our slides only in the CSS makes it very easy to change it, and add new styles?

Figure 6-1: One of our new slides

Pro jQuery Plugins v1.0.0


page 150
Figure 6-2: The other of our new slides

Horizontal slide
Let's add a new type of animation: horizontal slide (see Listing 6-4). We'll basically use .animate(), changing the
"left" style property.

Listing 6-4: New JavaScript for the horizontal slide animation

(function( $, window ) {

// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide', 'horizontal'
// ...
};

// ...
},

// ...

Pro jQuery Plugins v1.0.0


page 151
// Method to show slide
showSlide: function( options, slideIndex ) {
// ...

// For easier addition and management of animations, we're putting the


common actions after the animation in a function
var commonActionsAfterAnimation = function() {
// Trigger onAnimationComplete
if ( $.isFunction(options.onAnimationComplete) ) {
options.onAnimationComplete.call( this, options, itemOptions,
slideIndex );
}

// Show arrows if there is navigation


methods.showOrHideArrows.call( this, options );

// Keep sliding automatically if the setting is set to do so


if ( ! globals.timeoutCanceled ) {
globals.timeoutID = window.setTimeout(function() {
methods.doLoop.call( this, options );
}, options.slideTimeoutMilliseconds );
}
};

switch ( itemOptions.animationType ) {
case 'horizontal':
var currentNewCSS = {},// This will hold the current slide's
animation CSS
currentInitialCSS = {},// This will hold the current slide's
initial CSS
nextForceCSS = {},// This will hold the "next" slide's forced CSS
nextNewCSS = {};// This will hold the "next" slide's animation CSS

currentInitialCSS = {
'margin-left': $this.css('margin-left')
};

/*
Figure out if the new slide comes from the left or from the right
We need window.parseInt() to make sure the margin comes as a number.
We need the ,10 argument in it to make sure we're getting a 10-base/
decimal number
*/
Pro jQuery Plugins v1.0.0
page 152
if ( currentIndex > slideIndex ) {// New slide comes from the left
currentNewCSS = {// We calculate margin left + slide width
'margin-left': window.parseInt( currentSlide.css('margin-
left'), 10 ) + currentSlide.outerWidth()
};
nextForceCSS = {// We calculate margin left - slide width
'margin-left': window.parseInt( $this.css('margin-left'), 10 )
+ ( currentSlide.outerWidth() * -1 )
};
nextNewCSS = {
'margin-left': window.parseInt( $this.css('margin-left'), 10 )
};
} else {// New slide comes from the right
currentNewCSS = {// We calculate margin left + slide width
'margin-left': window.parseInt( currentSlide.css('margin-
left'), 10 ) + ( currentSlide.outerWidth() * -1 )
};
nextForceCSS = {
'margin-left': window.parseInt( $this.css('margin-left'), 10 )
+ currentSlide.outerWidth()
};
nextNewCSS = {
'margin-left': window.parseInt( $this.css('margin-left'), 10 )
};
}

// Animate previous slide


currentSlide.animate( currentNewCSS, itemOptions.animationSpeed,
function() {
$(this).removeClass( options.activeSlideClass );
$(this).hide();
$(this).css( currentInitialCSS );
});

// Do animation
$this.show().css( nextForceCSS ).animate( nextNewCSS,
itemOptions.animationSpeed, function() {
$(this).addClass( options.activeSlideClass );

commonActionsAfterAnimation.call( this );
});
break;
case 'slide':
Pro jQuery Plugins v1.0.0
page 153
// ...

// Do animation
$this.slideDown( itemOptions.animationSpeed, function() {
$(this).addClass( options.activeSlideClass );

commonActionsAfterAnimation.call( this );
});
break;
case 'fade':
default:
// ...

// Do animation
$this.fadeIn( itemOptions.animationSpeed, function() {
$(this).addClass( options.activeSlideClass );

commonActionsAfterAnimation.call( this );
});
break;
}
},

// ...
};

// ...

})( jQuery, window );

Because this type of animation requires the elements to be specifically aligned next to each other and visible
before the animation starts, we've applied that CSS rule in the JavaScript. We did that in order to be possible to
use different animations through slides (we've talked about "random" in the previous chapter and we're still
going to add it further down the line).

It's not limiting and increases flexibility.

This is how it looks the during a transition (see Figure 6-3).

Pro jQuery Plugins v1.0.0


page 154
Figure 6-3: Screenshot of the slider during a transition

Vertical slide
This animation looks great, but it would also look awesome if it was vertical! Let's do that (see Listing 6-5).

Listing 6-5: New JavaScript for the vertical slide animation

(function( $, window ) {

// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide', 'horizontal',
'vertical'
// ...
};

// ...
Pro jQuery Plugins v1.0.0
page 155
},

// ...

// Method to show slide


showSlide: function( options, slideIndex ) {

// ...

switch ( itemOptions.animationType ) {
case 'vertical':
var currentNewCSS = {},// This will hold the current slide's
animation CSS
currentInitialCSS = {},// This will hold the current slide's
initial CSS
nextForceCSS = {},// This will hold the "next" slide's forced CSS
nextNewCSS = {};// This will hold the "next" slide's animation CSS

currentInitialCSS = {
'margin-top': $this.css( 'margin-top' )
};

// Figure out if the new slide comes from the top or from the bottom
if ( currentIndex > slideIndex ) {// New slide comes from the top
currentNewCSS = {
'margin-top': window.parseInt( currentSlide.css('margin-top'),
10 ) + currentSlide.outerHeight()
};
nextForceCSS = {
'margin-top': window.parseInt( $this.css('margin-top'), 10 ) +
( currentSlide.outerHeight() * -1 )
};
nextNewCSS = {
'margin-top': window.parseInt( $this.css('margin-top'), 10 )
};
} else {// New slide comes from the bottom
currentNewCSS = {
'margin-top': window.parseInt( currentSlide.css('margin-top'),
10 ) + ( currentSlide.outerHeight() * -1 )
};
nextForceCSS = {
'margin-top': window.parseInt( $this.css('margin-top'), 10 ) +
currentSlide.outerHeight()
Pro jQuery Plugins v1.0.0
page 156
};
nextNewCSS = {
'margin-top': window.parseInt( $this.css('margin-top'), 10 )
};
}

// Animate previous slide


currentSlide.animate( currentNewCSS, itemOptions.animationSpeed,
function() {
$(this).removeClass( options.activeSlideClass );
$(this).hide();
$(this).css( currentInitialCSS );
});

// Do animation
$this.show().css( nextForceCSS ).animate( nextNewCSS,
itemOptions.animationSpeed, function() {
$(this).addClass( options.activeSlideClass );

commonActionsAfterAnimation.call( this );
});
break;

// ...
}
},

// ...
};

// ...

})( jQuery, window );

NOTE: Like the previous animation added, this one also requires the elements to be in a predefined posi-
tion (on top of each other), and we forced the CSS for the same reasons.

This is how it looks during a transition (Figure 6-4).

Pro jQuery Plugins v1.0.0


page 157
Figure 6-4: Screenshot of the slider during a transition

I really like the slide animation where different elements slide in and out at different times out of and into the
slide. Since this is a very complete slider plugin, it makes sense to add that, right? Let's go for it.

Because CSS3 is awesome, we don't actually need to do much about it, it's just a regular fade, but the
elements sliding in and out is done by CSS! Let's add a couple of slides using that (see Listing 6-6 and Listing
6-7).

Listing 6-6: HTML for our two new sliders

<div class="slider">
<!-- // ... -->
<div class="slide-wrapper">
<!-- // ... -->
<div class="slide partial">
<div class="caption right">
<h1>This title will slide in from the right</h1>
<p>This caption text will also slide in from the right.</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/22881046.jpg"
alt="Abstract humanoid head">
</div>

Pro jQuery Plugins v1.0.0


page 158
<div class="slide partial">
<div class="caption left">
<h1>This caption slides in from the left</h1>
<p>This caption text also slides in from the left.</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/6091404.jpg" alt="A
Road in a forest">
</div>
</div>
<div class="slider-bottom-nav">
<!-- // ... -->
<span data-slide="5"></span>
<span data-slide="6"></span>
</div>
</div>

Listing 6-7: CSS for our two new sliders to be specially animated

/* // ... */

.slider .slider-bottom-nav span.active {


/* // ... */
}

@-webkit-keyframes slideFromRight {
0% {
-webkit-transform-origin: center center;
-webkit-transform: translateX(200px);
}
100% {
-webkit-transform-origin: center center;
-webkit-transform: translateX(0);
}
}
@-moz-keyframes slideFromRight {
0% {
-moz-transform-origin: center center;
-moz-transform: translateX(200px);
}
100% {

Pro jQuery Plugins v1.0.0


page 159
-moz-transform-origin: center center;
-moz-transform: translateX(0);
}
}
@-o-keyframes slideFromRight {
0% {
-o-transform-origin: center center;
-o-transform: translateX(200px);
}
100% {
-o-transform-origin: center center;
-o-transform: translateX(0);
}
}
@keyframes slideFromRight {
0% {
transform-origin: center center;
transform: translateX(200px);
}
100% {
transform-origin: center center;
transform: translateX(0);
}
}

.slider .slide-wrapper .slide.partial .caption.right h1, .slider .slide-


wrapper .slide.partial .caption.right p {
-webkit-animation-duration: 400ms;
-moz-animation-duration: 400ms;
-o-animation-duration: 400ms;
animation-duration: 400ms;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: slideFromRight;
-moz-animation-name: slideFromRight;
-o-animation-name: slideFromRight;
animation-name: slideFromRight;
}
.slider .slide-wrapper .slide.partial .caption.right p {
-webkit-animation-duration: 800ms;
-moz-animation-duration: 800ms;
Pro jQuery Plugins v1.0.0
page 160
-o-animation-duration: 800ms;
animation-duration: 800ms;
}

@-webkit-keyframes slideFromLeft {
0% {
-webkit-transform-origin: center center;
-webkit-transform: translateX(-200px);
}
100% {
-webkit-transform-origin: center center;
-webkit-transform: translateX(0);
}
}
@-moz-keyframes slideFromLeft {
0% {
-moz-transform-origin: center center;
-moz-transform: translateX(-200px);
}
100% {
-moz-transform-origin: center center;
-moz-transform: translateX(0);
}
}
@-o-keyframes slideFromLeft {
0% {
-o-transform-origin: center center;
-o-transform: translateX(-200px);
}
100% {
-o-transform-origin: center center;
-o-transform: translateX(0);
}
}
@keyframes slideFromLeft {
0% {
transform-origin: center center;
transform: translateX(-200px);
}
100% {
transform-origin: center center;
transform: translateX(0);
}
Pro jQuery Plugins v1.0.0
page 161
}

.slider .slide-wrapper .slide.partial .caption.left h1, .slider .slide-


wrapper .slide.partial .caption.left p {
-webkit-animation-duration: 400ms;
-moz-animation-duration: 400ms;
-o-animation-duration: 400ms;
animation-duration: 400ms;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: slideFromLeft;
-moz-animation-name: slideFromLeft;
-o-animation-name: slideFromLeft;
animation-name: slideFromLeft;
}
.slider .slide-wrapper .slide.partial .caption.left p {
-webkit-animation-duration: 800ms;
-moz-animation-duration: 800ms;
-o-animation-duration: 800ms;
animation-duration: 800ms;
}

NOTE: This type of animation doesn't work on any version of Internet Explorer to date (unsurprisingly),
but it will degrade gracefully, simply not animating the items.

If you want to learn and/or understand more about CSS(3) animations, you can go to http://
www.w3schools.com/css3/css3_animations.asp, http://www.w3schools.com/cssref/css3_pr_keyframes.asp,
and https://developer.mozilla.org/en-US/docs/CSS/Tutorials/Using_CSS_animations.

Sadly, I can only show you statically how they look during their transitions (see Figure 6-5 and Figure 6-6).

Pro jQuery Plugins v1.0.0


page 162
Figure 6-5: One of our new slides during its animation

Figure 6-6: The other of our new slides during its animation

2D and 3D transitions
To continue with the power of CSS, we're now going to add 2D and 3D transitions that are possible through
CSS, supported across major browsers (who likes Internet Explorer anyway?). It's awesome because we can

Pro jQuery Plugins v1.0.0


page 163
just keep using the fade animation and leave the CSS (more easy for the user to manage and tweak) to trigger
these awesome transitions.

I'm just going to add two more slides with these transitions (see Listing 6-8 and Listing 6-9), but you can see
many more examples at http://www.apple.com/html5/showcase/transitions/.

Listing 6-8: HTML for our two new slides

<div class="slider">
<!-- // ... -->
<div class="slide-wrapper">
<!-- // ... -->
<div class="slide two-d">
<div class="caption right">
<h1>Haunted house</h1>
<p>This house sure looks haunted. Will you dare to go in?</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/18221876.jpg" alt="A
haunted house">
</div>
<div class="slide three-d">
<div class="caption left">
<h1>Gold texture</h1>
<p>This image has a beautiful scratched gold texture.</p>
</div>
<img src="http://thumbs.dreamstime.com/images/splash/34670.jpg" alt="Gold
texture">
</div>
</div>
<div class="slider-bottom-nav">
<!-- // ... -->
<span data-slide="7"></span>
<span data-slide="8"></span>
</div>
</div>

Listing 6-9: CSS for our two new sliders

/* // ... */
Pro jQuery Plugins v1.0.0
page 164
.slider .slide-wrapper .slide.partial .caption.left p {
/* // ... */
}

/* 2D */
@-webkit-keyframes flip {
0% {
-webkit-transform-origin: center center;
-webkit-transform: rotateX(180deg);
}
100% {
-webkit-transform-origin: center center;
-webkit-transform: rotateX(0deg);
}
}
@-moz-keyframes flip {
0% {
-moz-transform-origin: center center;
-moz-transform: rotateX(180deg);
}
100% {
-moz-transform-origin: center center;
-moz-transform: rotateX(0);
}
}
@-o-keyframes flip {
0% {
-o-transform-origin: center center;
-o-transform: rotateX(180deg);
}
100% {
-o-transform-origin: center center;
-o-transform: rotateX(0);
}
}
@keyframes flip {
0% {
transform-origin: center center;
transform: rotateX(180deg);
}
100% {
transform-origin: center center;
Pro jQuery Plugins v1.0.0
page 165
transform: rotateX(0);
}
}

.slider .slide-wrapper .slide.two-d {


-webkit-animation-duration: 1s;
-moz-animation-duration: 1s;
-o-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: flip;
-moz-animation-name: flip;
-o-animation-name: flip;
animation-name: flip;
}

/* 3D */
@-webkit-keyframes cube {
0% {
-webkit-transform-origin: center center;
-webkit-transform: scale3d(.6,.6,.6) rotateY(180deg) rotateX(-90deg)
translateZ(200px);
}
100% {
-webkit-transform-origin: center center;
-webkit-transform: scale3d(1,1,1) rotateY(0) rotateX(0) translateZ(0);
}
}
@-moz-keyframes cube {
0% {
-moz-transform-origin: center center;
-moz-transform: scale3d(.6,.6,.6) rotateY(180deg) rotateX(-90deg)
translateZ(200px);
}
100% {
-moz-transform-origin: center center;
-moz-transform: scale3d(1,1,1) rotateY(0) rotateX(0) translateZ(0);
}
}
@-o-keyframes cube {
Pro jQuery Plugins v1.0.0
page 166
0% {
-o-transform-origin: center center;
-o-transform: scale3d(.6,.6,.6) rotateY(180deg) rotateX(-90deg)
translateZ(200px);
}
100% {
-o-transform-origin: center center;
-o-transform: scale3d(1,1,1) rotateY(0) rotateX(0) translateZ(0);
}
}
@keyframes cube {
0% {
transform-origin: center center;
transform: scale3d(.6,.6,.6) rotateY(180deg) rotateX(-90deg)
translateZ(200px);
}
100% {
transform-origin: center center;
transform: scale3d(1,1,1) rotateY(0) rotateX(0) translateZ(0);
}
}

.slider .slide-wrapper .slide.three-d {


-webkit-animation-duration: 1s;
-moz-animation-duration: 1s;
-o-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: cube;
-moz-animation-name: cube;
-o-animation-name: cube;
animation-name: cube;
}

Again, I can only show you statically how they look during their transitions (see Figure 6-7 and Figure 6-8).

Pro jQuery Plugins v1.0.0


page 167
Figure 6-7: One of our new slides during its animation

Figure 6-8: The other of our new slides during its animation

It's possible to make the 3D animations look more "real", but it would require more markup and losing a bit of
flexibility, due to the fact that we would have to force previous/next images to match the current animation. But I
encourage you to still make your tests and experiments by looking at https://developer.apple.com/safaridemos/
showcase/transitions/, for example.

Pro jQuery Plugins v1.0.0


page 168
Finally, let's add the last of our animations: random.

Adding the random animation


Basically what this animation will do is pick a random animation from any of the available ones for each slide
animation (see Listing 6-10).

Listing 6-10: JavaScript for adding random animations between slides

(function( $, window ) {

// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide', 'horizontal',
'vertical', 'random'
// ...
};

// ...
},

// ...

// Method to show slide


showSlide: function( options, slideIndex ) {
// ...

var animationType = null;

// If the itemOptions.animationType is random, get a random animation


if ( itemOptions.animationType == 'random' ) {
var animationsList = [ 'fade', 'slide', 'horizontal', 'vertical' ],
randomIndex = window.Math.floor( window.Math.random() *
animationsList.length );

Pro jQuery Plugins v1.0.0


page 169
animationType = animationsList[ randomIndex ];
} else {
animationType = itemOptions.animationType;
}

switch ( animationType ) {
// ...
}
},

// ...
};

// ...

})( jQuery, window );

Now that we've made our plugin more flexible and added a lot of animation options, we're going to make it
mobile-friendly.

Section 2: Making it Mobile-Friendly


To make our slider mobile friendly, we have to consider the following:

1. The slider needs to be responsive (you can learn more about responsive web design at http://
en.wikipedia.org/wiki/Responsive_web_design). We will use CSS media queries for that;

2. As for the navigation, we have two options:

2.1. Either we make the arrows visible all the time (but to avoid being on top of the content, we
move them outside of the actual slide), which is achieved only using CSS;

2.2. Or we can make use of the mobile UX and trigger that navigation using swipe gestures on top
of the slide, which is done by JavaScript.

As for the options under 2., we'll do both, because the swipe may not be obvious for everyone.

To be able to detect such mobile-specific events, we're not going to build them from scratch, but use a great
library instead: jQuery Mobile ( http://jquerymobile.com/ ), which has `swipeleft` and `swiperight`.

Pro jQuery Plugins v1.0.0


page 170
Let's get on with the CSS (see Listing 6-11) and the JavaScript (Listing 6-12).

Listing 6-11: CSS for making the slider responsive and the arrows visible all the time
for tablets and smartphones

/* // ... */

.slider {
display: block;
margin: 10px auto;
background: #FFF;
max-width: 800px;
min-width: 300px;
height: 300px;
position: relative;
}

.slider .slider-nav {
display: block;
position: absolute;
color: #FFF;
font-size: 30px;
font-weight: bold;
cursor: pointer;
padding: 1px 10px 6px;
text-align: center;
background: #333;
opacity: 0;
transition: opacity 200ms;
-webkit-transition: opacity 200ms;
-moz-transition: opacity 200ms;
-o-transition: opacity 200ms;
z-index: 10;
top: 50%;
margin-top: -22px;/* Half the height */
}

/* // ... */

.slider .slider-nav.left {
left: 0;
Pro jQuery Plugins v1.0.0
page 171
border-radius: 0 10px 10px 0;
display: none;
}

.slider .slider-nav.right {
right: 0;
border-radius: 10px 0 0 10px;
}

.slider .slide-wrapper {
display: block;
overflow: hidden;
width: 100%;
height: 100%;
position: relative;
}

.slider .slide-wrapper .slide {


display: none;
position: absolute;
width: 100%;
}

.slider .slide-wrapper .slide.active {


display: block;
}

.slider .slide-wrapper .slide .caption {


display: block;
position: absolute;
margin: 10px;
width: 42.3%;
height: 175px;
background: rgba(0, 0, 0, .6);
}

.slider .slide-wrapper .slide .caption.left {


margin-left: 10px;
}

.slider .slide-wrapper .slide .caption.right {


margin-left: 55.769%;
}
Pro jQuery Plugins v1.0.0
page 172
.slider .slide-wrapper .slide.split .caption {
margin: 0;
width: 46.154%;
height: 100%;
}

.slider .slide-wrapper .slide.split .caption.left {


margin-left: 0;
}

.slider .slide-wrapper .slide.split .caption.right {


margin-left: 53.846%;
}

.slider .slide-wrapper .slide img {


width: 100%;
height: 100%;
}

.slider .slide-wrapper .slide.split img {


width: 53.846%;
height: 100%;
}

.slider .slide-wrapper .slide.split .caption.left + img {


margin-left: 46.154%;
}

.slider .slider-bottom-nav {
display: block;
text-align: center;
width: 100%;
padding-top: 10px;
}

/* // ... */

/* Custom portrait to fit our slider max width of 800px */


@media (max-width: 820px) {
.slider {
margin: 10px;
}
Pro jQuery Plugins v1.0.0
page 173
}

/* Landscape phone to portrait tablet */


@media (max-width: 767px) {
.slider {
height: 240px;
margin: 10px;
}
}

/* Landscape phones and down */


@media (max-width: 480px) {
.slider {
height: 180px;
margin: 10px;
}
}

As you can see, quite a bit has changed, mostly converting pixel dimensions into percentages, repositioning
some elements relatively to the slider itself and some other minor things that are good practice when doing
proper responsive design.

Listing 6-12: JavaScript for detecting swipe left and swipe right events, triggering
navigation

(function( $, window ) {
// ..

var methods = {
init : function( options ) {
var defaults = {
// ...
'autoStartSlideshow': true,
'usejQueryMobile': true,
// ...
};

// ...

// Check if jQuery Mobile is available


Pro jQuery Plugins v1.0.0
page 174
if ( options.usejQueryMobile && $.mobile ) {
// Bind swipe left
$(document).on( 'swipeleft.slider', function( event ) {
event.preventDefault();

methods.navigateLeft.call( this, options );


});

// Bind swipe right


$(document).on('swiperight.slider', function( event ) {
event.preventDefault();

methods.navigateRight.call( this, options );


});
}

return this.each(function() {

// ...

});
},

// ...
};

// ...

})( jQuery, window );

NOTE: We're checking if the jQuery mobile library is being included using $.mobile before using
the swipe events. Alternatively, you could simply bind swipeleft and swiperight events and if there
was being used any other framework that supported it, it would work. I just wanted to show you how to
check for the existence of jQuery mobile.

Pro jQuery Plugins v1.0.0


page 175
Summary
In this chapter we've enhanced our very complete slider plugin and finished it, adding more animations, flexibility
and making it mobile-friendly.

In the next chapter, we'll start building a Timeline plugin, similar to Facebook's own timeline.

Pro jQuery Plugins v1.0.0


page 176
Chapter 7:
A Timeline Plugin Part I

In this chapter, you'll learn how to prepare yourself to build a timeline plugin, similar to what Facebook uses.

You'll learn a few new concepts and methods, think about the structure and how our plugin will work, among
other planning bits.

We'll also code the HTML and CSS for the timeline plugin. The JavaScript will be done on the next Chapter.

Section 1: Introducing Deferred Objects


We've been using "simple" callback functions for all our other plugins, and that works well for the purposes
we've been giving them (simply call a function when a given action is completed), but in this case, since we
want to create our own process of adding posts into the timeline, we need to be able to have a more robust
implementation of callbacks, which is provided by Deferred Objects.

NOTE: Since Deferred Objects have a "do once" nature, we will not use them, but create a callback
structure based on their architecture. That's why it's important to learn about them.

The jQuery Documentation ( http://api.jquery.com/category/deferred-object/ ) explains what Deferred Objects


are very nicely:

jQuery.Deferred(), introduced in version 1.5, is a chainable utility object that can


register multiple callbacks into callback queues, invoke callback queues, and relay the
success or failure state of any synchronous or asynchronous function.

Pro jQuery Plugins v1.0.0


page 177
jQuery.Deferred() introduces several enhancements to the way callbacks are
managed and invoked. In particular, jQuery.Deferred() provides flexible ways to provide
multiple callbacks, and these callbacks can be invoked regardless of whether the original
callback dispatch has already occurred. jQuery Deferred is based on the CommonJS
Promises/A design.

The Deferred Object ( $.Deferred() ) has six important methods that can be separated into 3 categories for
easier understanding:

1. Actions

1.1. .resolve()

1.2. .reject()

2. Events

2.1. .done()

2.2. .fail()

2.3. .always()

3. Object

3.1. .promise()

I'm going to explain each of these methods so it's easier for you to understand how and why we're going to use
them in our plugin.

NOTE: For simplicity and easier exemplification, I won't be showing closures or a plugin skeleton in the
samples below, but consider these excerpts as part of a plugin, already inside a closure.

1.1. deferred.resolve( args ) will resolve the deferred object and call any callbacks added by
deferred.always() or deferred.done().

Pro jQuery Plugins v1.0.0


page 178
These callbacks are executed in the way they were added, and each will be passed the args from the .resolve()
call.

Here's a quick sample of how it would be used (see Listing 7-1):

Listing 7-1: Example of usage for deferred.resolve()

var deferred = $.Deferred();// We're creating a deferred object

// We'll get into .done() afterwards, but we're adding a callback here
deferred.done(function( value ) {
window.alert( value );
});

// This will trigger the window.alert() from the code above, with value =
'Success!'
deferred.resolve( 'Success!' );

1.2. deferred.reject( args ) will reject the deferred object and call any callbacks added by
deferred.always() or deferred.fail().

These callbacks are executed in the way they were added, and each will be passed the args from
the .reject() call.

Here's a quick sample of how it would be used (see Listing 7-2):

Listing 7-2: Example of usage for deferred.reject()

var deferred = $.Deferred();// We're creating a deferred object

// We'll get into .fail() afterwards, but we're adding a callback here
deferred.fail(function( value ) {
window.alert( value );
});

// This will trigger the window.alert() from the code above, with value = 'This
failed!'
deferred.reject( 'This failed!' );

Pro jQuery Plugins v1.0.0


page 179
2.1. deferred.done( doneCallbacks ) will add a callback to be executed when the deferred object is
successfully resolved.

The argument doneCallbacks can be a function, or an array of functions.

Here's a quick sample of how it would be used (see Listing 7-3):

Listing 7-3: Example of usage for deferred.done()

var deferred = $.Deferred();// We're creating a deferred object

// We're adding a callback here to be executed when the deferred object is


resolved
deferred.done(function( value ) {
window.alert( value );
});

2.2. deferred.fail( failCallbacks ) will add a callback to be executed when the deferred object is
rejected.

The argument failCallbacks can be a function, or an array of functions.

Here's a quick sample of how it would be used (see Listing 7-4):

Listing 7-4: Example of usage for deferred.fail()

var deferred = $.Deferred();// We're creating a deferred object

// We're adding a callback here to be executed when the deferred object is


rejected
deferred.fail(function( value ) {
window.alert( value );
});

2.3. deferred.always( alwaysCallbacks ) will add a callback to be executed when the deferred
object is either rejected or resolved, i.e. when the outcome of its execution doesn't matter.

The argument alwaysCallbacks can be a function, or an array of functions.

Pro jQuery Plugins v1.0.0


page 180
Here's a quick sample of how it would be used (see Listing 7-5):

Listing 7-5: Example of usage for deferred.always()

var deferred = $.Deferred();// We're creating a deferred object

// We're adding a callback here to be executed when the deferred object is


either rejected or resolved
deferred.always(function( value ) {
window.alert( value );
});

3.1. deferred.promise() will return an object with almost the same interface as the Deferred, but it only
has the methods to add callbacks and does not have the methods to resolve and reject the deferred object.

This is useful when you want to allow callbacks to be added to the deferred object, but not the ability to resolve
or reject it.

Here's a quick sample of how it would be used (see Listing 7-6):

Listing 7-6: Example of usage for deferred.promise()

var deferred = $.Deferred();// We're creating a deferred object


var promise = deferred.promise();// We're getting this deferred object's promise

// We're adding a callback here to be executed when the deferred object is


either rejected or resolved
deferred.always(function( value ) {
window.alert( value );
});

// We're adding a callback here through the promise, to be executed when the
object is resolved
promise.done(function( value ) {
window.alert( 'Success! ' + value );
});

Pro jQuery Plugins v1.0.0


page 181
try {
promise.resolve( 'Yes!' );// This code won't work
} catch ( errorCaught ) {
deferred.resolve( 'Yes!' );// This code will work, and the callback added
with promise.done() above will be executed
}

Now, we will need to output dates, and to keep the sample a bit simpler, we'll just output YYYY-mm-dd HH:ii
dates. You can try and use jQuery.localize by David Chambers ( https://github.com/davidchambers/
jQuery.localize ) or anything else to localize the dates, if you wish, since we'll be parsing the dates from
ISO-8601.

Section 2: The Concept


First of all, we're going to use JSON (an array of objects) to populate the timeline posts, as this will make it
easier to get and add new posts using synchronous or asynchronous methods. Our plugin will support text and
text+image posts, and have a neat animation for each post being added.

We will try to use CSS the most we can.

So, our "Post" object will have the following attributes:

Name: The post author's name;

Avatar: The post author's avatar image;


Date: The post's publish date;

Content: The post's text;


Image: A post's image (optional).

Our plugin will show read-only posts (no editing, liking, or adding comments), but we'll make it possible for you
to add and delete posts.

Also, we'll only show 10 posts initially, but we'll show more as we reach the bottom of the page. This will be
something supported by the plugin, but not part of the plugin itself (just a use-case and sample of how to use
our plugin's flexibility).
Pro jQuery Plugins v1.0.0
page 182
So, let's plan the methods our plugin will need to accomplish these:

addPost() will add and show a new post, receiving the post object as an argument;

deletePost() will delete a post, receiving the post id as an argument;


getNextPage() will get the next page of posts.

Apart from the plugin, but for the demo, we'll also need to have:

An event bind which will get the data from a form and parse it into a JSON object, calling the addPost()
method from our plugin;

An event bind which will get more posts (a simulated "next page") and append them to the timeline,
scrollWatch(), which will see if the user has scrolled into the last 200px of the current page, and trigger
getNextPage(), to fetch the next page.

Section 3: The HTML & CSS


Ok, now we're going to create a static page in HTML and CSS only, to mimic how our plugin will look like when
it's working, just so that we can get the HTML and CSS right before diving into the JavaScript.

First, let's start with the actual post markup (see Listing 7-7):

Listing 7-7: Post's base markup

<article class="post">
<header>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">
Pro jQuery Plugins v1.0.0
page 183
<time datetime="2013-03-01T12:14:53.316Z" pubdate>2013.03.01 14:53</
time>
</a>
</div>
</header>
<section class="content">
<p>Hey, this is a nice post markup, right? Very clean.</p>
<div class="media">
<img src="http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg" alt="Bruno Bernardino's Mojito recipe">
</div>
</section>
</article>

Now, we need posts on the left and on the right, with the actual "timeline" in the middle. We can use the
following markup for that (see Listing 7-8):

Listing 7-8: Left and right post's base markup, with the timeline separating them

<section id="posts">
<article class="post" data-side="left">
<header>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">
<time datetime="2013-03-02T09:01:27.591Z" pubdate>2013.03.02 09:01</
time>
</a>
</div>
</header>
<section class="content">
<p>A more recent post, without any image.</p>

Pro jQuery Plugins v1.0.0


page 184
</section>
</article>

<article class="post" data-side="right">


<header>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">
<time datetime="2013-03-01T12:14:53.316Z" pubdate>2013.03.01 12:14</
time>
</a>
</div>
</header>
<section class="content">
<p>Hey, this is a nice post markup, right? Very clean.</p>
<div class="media">
<img src="http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg" alt="Bruno Bernardino's Mojito recipe">
</div>
</section>
</article>
</section>

NOTE: We aren't actually going to interact with the timeline, so it can be a simple background, done by
CSS. If it was necessary for it to be an element, it should be added as an absolutely positioned element
before any post.

This looks good and simple, but we're missing our timeline pointer/arrow, right? Let's add it (see Listing 7-9).

Pro jQuery Plugins v1.0.0


page 185
Listing 7-9: New markup, updated with the pointer

<section id="posts">
<article class="post" data-side="left">
<header>
<i class="pointer"></i>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">
<time datetime="2013-03-02T09:01:27.591Z" pubdate>2013.03.02 09:01</
time>
</a>
</div>
</header>
<section class="content">
<p>A more recent post, without any image.</p>
</section>
</article>

<article class="post" data-side="right">


<header>
<i class="pointer"></i>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">

Pro jQuery Plugins v1.0.0


page 186
<time datetime="2013-03-01T12:14:53.316Z" pubdate>2013.03.01 12:14</
time>
</a>
</div>
</header>
<section class="content">
<p>Hey, this is a nice post markup, right? Very clean.</p>
<div class="media">
<img src="http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg" alt="Bruno Bernardino's Mojito recipe">
</div>
</section>
</article>
</section>

However, it may happen that two posts will start at the same top position, and thus having the pointers at the
same place. Since this is a timeline, they can't share the exact same time. We will need to create a new class for
the pointer to be positioned a bit below the standard position. This class will be added by JavaScript, since we'll
need to calculate the proximity of the top positions for posts side-by-side.

Next, we need the markup for the form to add a new post (see Listing 7-10). Don't forget the "loading" icon.

Listing 7-10: Markup for the "add a new post" form

<section id="add-post">
<form action="#" method="post" name="add-post-form">
<fieldset>
<textarea name="status-update" placeholder="What's up?" required></
textarea>
<input type="url" name="media" placeholder="Image URL">
</fieldset>
<button type="submit" name="add-post-submit">Post</button>
<div class="loading"><img src="https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/loading.gif"></div>
</form>
</section>

We'll also need a small loading icon on the bottom of the posts list, for when we reach the bottom, to let the
users know there are more posts being loaded (see Listing 7-11).

Pro jQuery Plugins v1.0.0


page 187
Listing 7-11: Markup for the loading icon on the bottom

<section id="posts">
<article class="post" data-side="left">
<header>
<i class="pointer"></i>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">
<time datetime="2013-03-02T09:01:27.591Z" pubdate>2013.03.02 09:01</
time>
</a>
</div>
</header>
<section class="content">
<p>A more recent post, without any image.</p>
</section>
</article>

<article class="post" data-side="right">


<header>
<i class="pointer"></i>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">

Pro jQuery Plugins v1.0.0


page 188
<time datetime="2013-03-01T12:14:53.316Z" pubdate>2013.03.01 12:14</
time>
</a>
</div>
</header>
<section class="content">
<p>Hey, this is a nice post markup, right? Very clean.</p>
<div class="media">
<img src="http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg" alt="Bruno Bernardino's Mojito recipe">
</div>
</section>
</article>

<div class="loading"><img src="https://raw.github.com/BrunoBernardino/


ProjQueryPlugins/master/assets/loading.gif"></div>
</section>

As for the CSS, we'll go with a style slightly more clean and modern than Facebook's, a bit similar to Google+'s.

Also, the animation for when a new post is added (fade in and slide from left/right) will be done by CSS.

For the HTML base, we'll use HTML5 Boilerplate ( http://html5boilerplate.com/ ) (see Listing 7-12).

Listing 7-12: HTML5 Boilerplate with our default markup

<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>jQuery Timeline Plugin</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">

<link rel="stylesheet" href="css/normalize.css">


<link rel="stylesheet" href="css/main.css">
<script src="js/vendor/modernizr-2.6.2.min.js"></script>
Pro jQuery Plugins v1.0.0
page 189
</head>
<body>
<div class="container">
<!--[if lt IE 7]>
<p class="chromeframe">You are using an <strong>outdated</strong>
browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a
href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome
Frame</a> to improve your experience.</p>
<![endif]-->

<section id="add-post">
<form action="#" method="post" name="add-post-form">
<fieldset>
<textarea name="status-update" placeholder="What's up?"
required></textarea>
<input type="url" name="media" placeholder="Image URL">
</fieldset>
<button type="submit" name="add-post-submit">Post</button>
<div class="loading"><img src="https://raw.github.com/
BrunoBernardino/ProjQueryPlugins/master/assets/loading.gif"></div>
</form>
</section>

<section id="posts">
<article class="post" data-side="left">
<header>
<i class="pointer"></i>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">
<time datetime="2013-03-02T09:01:27.591Z" pubdate>2013.03.02
09:01</time>
</a>
</div>
</header>
Pro jQuery Plugins v1.0.0
page 190
<section class="content">
<p>A more recent post, without any image.</p>
</section>
</article>

<article class="post too-close" data-side="right">


<header>
<i class="pointer"></i>
<div class="avatar">
<a href="#link-to-user">
<img src="https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/me.png" alt="Bruno Bernardino">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1>Bruno Bernardino</h1>
</a>
<a href="#link-to-post">
<time datetime="2013-03-01T12:14:53.316Z" pubdate>2013.03.01
12:14</time>
</a>
</div>
</header>
<section class="content">
<p>Hey, this is a nice post markup, right? Very clean.</p>
<div class="media">
<img src="http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg" alt="Bruno Bernardino's Mojito recipe">
</div>
</section>
</article>

<div class="loading"><img src="https://raw.github.com/BrunoBernardino/


ProjQueryPlugins/master/assets/loading.gif"></div>
</section>
</div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/
jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="js/vendor/
jquery-1.9.0.min.js"><\/script>')</script>
<script src="js/plugins.js"></script>
Pro jQuery Plugins v1.0.0
page 191
<script src="js/main.js"></script>
</body>
</html>

NOTE: We've added the "too-close" class to the second post to avoid the posts pointer to be in the ex-
act same place in the timeline. This is because we're still doing a static demo, because in the final version
the need to add this class or not will be calculated in the JavaScript

And for the CSS base, we'll use Normalize.css ( http://necolas.github.com/normalize.css/ ), already used on
HTML5 Boilerplate. There's a newer version, but it only supports IE8+. While on my personal projects I only
"support" (read "make it not be awful") the latest version of IE, the sad truth is there are still many people
(wrongly) using outdated versions of IE, and if you want to make a successful plugin, it has to at least be usable
in those outdated versions. The included main.css is where we'll put the "global" non-plugin-related changes,
making it look like this (see Listing 7-13):

Listing 7-13: main.css

/*
* HTML5 Boilerplate
*
* What follows is the result of much research on cross-browser styling.
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
* Kroc Camen, and the H5BP dev community and team.
*/

/* ==========================================================================
Base styles: opinionated defaults
========================================================================== */

html,
button,
input,
select,
textarea {
color: #333;
}

body {
Pro jQuery Plugins v1.0.0
page 192
font-size: 1em;
line-height: 1.4;
}

/*
* Remove text-shadow in selection highlight: h5bp.com/i
* These selection declarations have to be separate.
* Customize the background color to match your design.
*/

::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}

::selection {
background: #b3d4fc;
text-shadow: none;
}

/*
* A better looking default horizontal rule
*/

hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #ccc;
margin: 1em 0;
padding: 0;
}

/*
* Remove the gap between images and the bottom of their containers: h5bp.com/i/
440
*/

img {
vertical-align: middle;
}

/*
Pro jQuery Plugins v1.0.0
page 193
* Remove default fieldset styles.
*/

fieldset {
border: 0;
margin: 0;
padding: 0;
}

/*
* Allow only vertical resizing of textareas.
*/

textarea {
resize: vertical;
}

/* ==========================================================================
Chrome Frame prompt
========================================================================== */

.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}

/* ==========================================================================
Author's custom styles
========================================================================== */

.container {
max-width: 720px;
padding: 0 10px;
margin: 0 auto;
}

Pro jQuery Plugins v1.0.0


page 194
/* ==========================================================================
Helper classes
========================================================================== */

/*
* Image replacement
*/

.ir {
background-color: transparent;
border: 0;
overflow: hidden;
/* IE 6/7 fallback */
*text-indent: -9999px;
}

.ir:before {
content: "";
display: block;
width: 0;
height: 150%;
}

/*
* Hide from both screenreaders and browsers: h5bp.com/u
*/

.hidden {
display: none !important;
visibility: hidden;
}

/*
* Hide only visually, but have it available for screenreaders: h5bp.com/v
Pro jQuery Plugins v1.0.0
page 195
*/

.visuallyhidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}

/*
* Extends the .visuallyhidden class to allow the element to be focusable
* when navigated to via the keyboard: h5bp.com/p
*/

.visuallyhidden.focusable:active,
.visuallyhidden.focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
width: auto;
}

/*
* Hide visually and from screenreaders, but maintain layout
*/

.invisible {
visibility: hidden;
}

/*
* Clearfix: contain floats
*
* For modern browsers
* 1. The space content is one way to avoid an Opera bug when the
* `contenteditable` attribute is included anywhere else in the document.
* Otherwise it causes space to appear at the top and bottom of elements
Pro jQuery Plugins v1.0.0
page 196
* that receive the `clearfix` class.
* 2. The use of `table` rather than `block` is only necessary if using
* `:before` to contain the top-margins of child elements.
*/

.clearfix:before,
.clearfix:after {
content: " "; /* 1 */
display: table; /* 2 */
}

.clearfix:after {
clear: both;
}

/*
* For IE 6/7 only
* Include this rule to trigger hasLayout and contain floats.
*/

.clearfix {
*zoom: 1;
}

/* ==========================================================================
EXAMPLE Media Queries for Responsive Design.
Theses examples override the primary ('mobile first') styles.
Modify as content requires.
========================================================================== */

@media only screen and (min-width: 35em) {


/* Style adjustments for viewports that meet the condition */
}

@media print,
(-o-min-device-pixel-ratio: 5/4),
(-webkit-min-device-pixel-ratio: 1.25),
(min-resolution: 120dpi) {
/* Style adjustments for high resolution devices */
}

/* ==========================================================================
Print styles.
Pro jQuery Plugins v1.0.0
page 197
Inlined to avoid required HTTP connection: h5bp.com/r
========================================================================== */

@media print {
* {
background: transparent !important;
color: #000 !important; /* Black prints faster: h5bp.com/s */
box-shadow: none !important;
text-shadow: none !important;
}

a,
a:visited {
text-decoration: underline;
}

a[href]:after {
content: " (" attr(href) ")";
}

abbr[title]:after {
content: " (" attr(title) ")";
}

/*
* Don't show links for images, or javascript/internal links
*/

.ir a:after,
a[href^="javascript:"]:after,
a[href^="#"]:after {
content: "";
}

pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}

thead {
display: table-header-group; /* h5bp.com/t */
}
Pro jQuery Plugins v1.0.0
page 198
tr,
img {
page-break-inside: avoid;
}

img {
max-width: 100% !important;
}

@page {
margin: 0.5cm;
}

p,
h2,
h3 {
orphans: 3;
widows: 3;
}

h2,
h3 {
page-break-after: avoid;
}
}

We'll start now with the CSS for the posts and timeline, including our post animation (see Listing 7-14).

Listing 7-14: CSS for the posts and timeline

/*
* Timeline & Posts
*/
#posts {
margin: 20px 0;
background: transparent url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/timeline-bg.png') top center repeat-y;
background-image: url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/timeline-top-bg.png'), url('https://

Pro jQuery Plugins v1.0.0


page 199
raw.github.com/BrunoBernardino/ProjQueryPlugins/master/assets/timeline-bottom-
bg.png'), url('https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/timeline-bg.png');
background-position: top center, bottom center, top center;
background-repeat: no-repeat, no-repeat, repeat-y;
}

#posts::after {
clear: both;
display: block;
content: "";
}

/* Timeline loading */
#posts .loading {
display: none;
clear: both;
}

/* Individual posts */
#posts .post {
margin: 0 0 10px 0;
padding: 0;
background: #FFF;
border: 1px solid #D0D0D0;
width: 348px;
border-radius: 3px;
box-shadow: 0 1px 1px #EAEAEA;
}

#posts .post[data-side="left"] {
clear: left;
float: left;
}

#posts .post[data-side="right"] {
clear: right;
float: right;
}

/* Post header */
#posts .post header {
margin: 2px;
Pro jQuery Plugins v1.0.0
page 200
background: #EAEAEA;
padding: 10px 10px 0 10px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}

#posts .post header::after {


clear: both;
display: block;
content: "";
}

#posts .post header .pointer {


width: 15px;
height: 7px;
background: transparent url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/pointer-left.png') top left no-repeat;
position: absolute;
margin-left: 336px;
margin-top: 10px;
}

#posts .post[data-side="right"] header .pointer {


background-image: url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/pointer-right.png');
margin-left: -27px;
}

#posts .post.too-close header .pointer {


margin-top: 30px;
}

/* Post header avatar */


#posts .post .avatar {
margin: 0 10px 10px 0;
float: left;
}

#posts .post .avatar img {


max-width: 40px;
max-height: 40px;
}

Pro jQuery Plugins v1.0.0


page 201
/* Post header info */
#posts .post .info {
margin: 0 0 10px 0;
float: left;
}

#posts .post .info a {


display: block;
text-decoration: none;
}

#posts .post .info a h1 {


margin: 0;
font-size: 16px;
color: #222;
font-weight: 400;
}

#posts .post .info a time {


margin: 0 0 5px 0;
font-size: 12px;
color: #888;
display: block;
}

#posts .post .info a:hover h1, #posts .post .info a:hover time {
text-decoration: underline;
}

/* Post content */
#posts .post .content {
margin: 10px;
font-size: 15px;
font-weight: 300;
}

/* Post content media */


#posts .post .content .media {
margin: 10px 0;
padding-top: 10px;
border-top: 1px solid #E0E0E0;
text-align: center;
}
Pro jQuery Plugins v1.0.0
page 202
#posts .post .content .media img {
max-width: 328px;
max-height: 328px;
}

Now, we're just missing the CSS for the "Add Post" form (see Listing 7-15).

Listing 7-15: CSS for the "Add Post" form

/*
* Add new Post
*/
#add-post {
margin: 10px 0;
padding: 0;
background: #FFF;
border: 1px solid #D0D0D0;
display: block;
border-radius: 3px;
box-shadow: 0 1px 1px #EAEAEA;
}

#add-post form {
margin: 2px;
background: #FCFCFC;
background-image: -webkit-linear-gradient(top,#FCFCFC,#F6F6F6);
background-image: -moz-linear-gradient(top,#FCFCFC,#F6F6F6);
background-image: -ms-linear-gradient(top,#FCFCFC,#F6F6F6);
background-image: -o-linear-gradient(top,#FCFCFC,#F6F6F6);
background-image: linear-gradient(top,#FCFCFC,#F6F6F6);
padding: 10px 10px 0 10px;
border-radius: 3px;
}

#add-post form::after {
clear: both;
display: block;
content: "";
}

Pro jQuery Plugins v1.0.0


page 203
#add-post form fieldset textarea {
display: block;
margin-bottom: 10px;
padding: 10px;
width: 672px;
font-size: 14px;
resize: vertical;
border: 1px solid #D0D0D0;
box-shadow: inset 0 1px 2px #EAEAEA;
}

#add-post form fieldset input {


display: block;
margin-bottom: 2px;
padding: 5px 10px;
width: 672px;
font-size: 14px;
border: 1px solid #D0D0D0;
box-shadow: inset 0 1px 2px #EAEAEA;
}

#add-post form button {


margin: 10px 0;
padding: 5px 8px;
height: 32px;
line-height: 14px;
font-size: 14px;
min-width: 80px;
border-radius: 3px;
text-overflow: ellipsis;
white-space: nowrap;
background-color: #006DCC;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc),
to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(to bottom, #0088cc, #0044cc);
background-repeat: repeat-x;
border: 1px solid #DCDCDC;
border-color: #0044CC #0044CC #002A80;
border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);
Pro jQuery Plugins v1.0.0
page 204
color: #FFF;
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
}

#add-post form button:hover {


background-color: #0044CC;
background-position: 0 -15px;
-webkit-transition: background-position 0.1s linear;
-moz-transition: background-position 0.1s linear;
-o-transition: background-position 0.1s linear;
transition: background-position 0.1s linear;
box-shadow: 0 1px 1px #D0D0D0;
}

#add-post form .loading {


display: none;
float: left;
}

/*
* Timeline & Posts
*/
#posts {
margin: 20px 0;
background: transparent url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/timeline-bg.png') top center repeat-y;
background-image: url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/timeline-top-bg.png'), url('https://
raw.github.com/BrunoBernardino/ProjQueryPlugins/master/assets/timeline-bottom-
bg.png'), url('https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/timeline-bg.png');
background-position: top center, bottom center, top center;
background-repeat: no-repeat, no-repeat, repeat-y;
}

#posts::after {
clear: both;
display: block;
content: "";
}

/* Timeline loading */
#posts .loading {
Pro jQuery Plugins v1.0.0
page 205
display: none;
clear: both;
}

/* Individual posts */
#posts .post {
margin: 0 0 10px 0;
padding: 0;
background: #FFF;
border: 1px solid #D0D0D0;
width: 348px;
border-radius: 3px;
box-shadow: 0 1px 1px #EAEAEA;
}

#posts .post[data-side="left"] {
clear: left;
float: left;
}

#posts .post[data-side="right"] {
clear: right;
float: right;
}

/* Post header */
#posts .post header {
margin: 2px;
background: #EAEAEA;
padding: 10px 10px 0 10px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}

#posts .post header::after {


clear: both;
display: block;
content: "";
}

#posts .post header .pointer {


width: 15px;
height: 7px;
Pro jQuery Plugins v1.0.0
page 206
background: transparent url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/pointer-left.png') top left no-repeat;
position: absolute;
margin-left: 336px;
margin-top: 10px;
}

#posts .post[data-side="right"] header .pointer {


background-image: url('https://raw.github.com/BrunoBernardino/
ProjQueryPlugins/master/assets/pointer-right.png');
margin-left: -27px;
}

#posts .post.too-close header .pointer {


margin-top: 30px;
}

/* Post header avatar */


#posts .post .avatar {
margin: 0 10px 10px 0;
float: left;
}

#posts .post .avatar img {


max-width: 40px;
max-height: 40px;
}

/* Post header info */


#posts .post .info {
margin: 0 0 10px 0;
float: left;
}

#posts .post .info a {


display: block;
text-decoration: none;
}

#posts .post .info a h1 {


margin: 0;
font-size: 16px;
color: #222;
Pro jQuery Plugins v1.0.0
page 207
font-weight: 400;
}

#posts .post .info a time {


margin: 0 0 5px 0;
font-size: 12px;
color: #888;
display: block;
}

#posts .post .info a:hover h1, #posts .post .info a:hover time {
text-decoration: underline;
}

/* Post content */
#posts .post .content {
margin: 10px;
font-size: 15px;
font-weight: 300;
}

/* Post content media */


#posts .post .content .media {
margin: 10px 0;
padding-top: 10px;
border-top: 1px solid #E0E0E0;
text-align: center;
}

#posts .post .content .media img {


max-width: 328px;
max-height: 328px;
}

And this is how the "static" demo of how our plugin will work looks right now (see Figure 7-1):

Pro jQuery Plugins v1.0.0


page 208
Figure 7-1: Our "static" demo

Summary
In this chapter we've talked about Deferred Objects and how to plan a timeline plugin.

You've also tested and built a static page in HTML and CSS to get the HTML and CSS you'll need for our plugin
demonstration.

In the next chapter you'll code the JavaScript part of this timeline plugin.

Pro jQuery Plugins v1.0.0


page 209
Chapter 8:
A Timeline Plugin Part II

In this chapter, we'll finish developing the timeline plugin, by using the HTML and CSS we've already planned
and created in the previous chapter.

We'll use static JavaScript files with static post objects, so that we don't need to build the whole back-end,
database and all that, just to provide the post listing and management functionality.

We're going to build this "simulated back-end" like it was a RESTful API ( http://en.wikipedia.org/wiki/
Representational_state_transfer ), because of local restrictions we'll use "GET", but in the comments you'll see
the appropriate HTTP methods in our AJAX requests.

Section 1: The Basic Functionality


Before we get into the functionality itself, we need the basic skeleton for our plugin (see Listing 8-1).

Listing 8-1: Basic Plugin Skeleton

(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {
/*
NOTE: Usually we'd set a RESTful API base URL here, but since we're
simulating and thus not following standard conventions for its URL naming, we'll
have one URL per action (add, get, and delete)
*/

// TODO: URLs, Post's class, Post's side data-* attribute


};

Pro jQuery Plugins v1.0.0


page 210
options = $.extend( defaults, options );

return this.each(function() {
var $this = $(this),
data = $this.data( 'timeline' );

if ( ! data ) {
$this.data( 'timeline', {
target : $this
});
}
});
},

destroy : function() {
$(window).off( '.timeline' );

return this.each(function() {
var $this = $(this),
data = $this.data( 'timeline' );

$this.removeData( 'timeline' );
});
}

// TODO: Method to get posts

// TODO: Method to list posts

// TODO: Method to add a post

// TODO: Method to delete post


};

$.fn.timeline = 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.timeline' );
}
Pro jQuery Plugins v1.0.0
page 211
};
})( jQuery, window );

We'll start with the Post object. We already know the fields we'll need due to the planning on the previous
chapter, so our object will be something like the following (see Listing 8-2).

Listing 8-2: Our Post Object

{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T12:14:53.316Z",
"content": "Hey, this is a nice post markup, right? Very clean.",
"image": "http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg"
}

Now, we should build the post listing functionality, but we'll need an underscore template for that, so,
considering the Post object above, and the Post markup we planned in the previous chapter, we get something
like this (see Listing 8-3).

Listing 8-3: HTML for the Post underscore template

<script type="text/x-underscore-template" id="template-posts">


<% _.each( posts, function( post, index ) { %>
<article class="post" data-side="<%= (index % 2 === 0) ? 'left' : 'right'
%>">
<header>
<i class="pointer"></i>
<div class="avatar">
<a href="#link-to-user">
<img src="<%= post.avatar %>" alt="<%= post.name %>">
</a>
</div>
<div class="info">
<a href="#link-to-user">
<h1><%= post.name %></h1>
Pro jQuery Plugins v1.0.0
page 212
</a>
<a href="#link-to-post">
<time datetime="<%= post.date %>"><%= post.date %></time>
</a>
</div>
</header>
<section class="content">
<p><%= _.escape( post.content ) %></p>
<% if ( post.image ) { %>
<div class="media">
<img src="<%= _.escape( post.image ) %>" alt="">
</div>
<% } %>
</section>
</article>
<% }); %>
<% if ( posts.length == 0 ) { %>
<p>There are no posts to show.</p>
<% } %>
</script>

With that, we can now build the method to list the posts (see Listing 8-4). Don't forget to include the
underscore.js library.

Listing 8-4: Method to list posts (given the posts array as an argument)

(function( $, _, window ) {
var globals = {
'options' : {}
};

var methods = {
init : function( options ) {
var defaults = {
'templateID' : 'template-posts'// Posts Underscore.js template Id
};

options = $.extend( defaults, options );


globals.options = $.extend( {}, options );// Copy/clone options into
globals.options

Pro jQuery Plugins v1.0.0


page 213
// ...
},

destroy : function() {
// ...
},

// TODO: Method to get posts

// Method to list posts


list : function( postsList ) {
// If the plug-in hasn't been initialized yet, don't do anything
if ( globals.options.length === 0 ) {
return false;
}

var postsHTML = _.template( $('#' + globals.options.templateID).html(),


{ posts: postsList } );

$(this).append( postsHTML );
}

// TODO: Method to add a post

// TODO: Method to delete post


};

// ...
})( jQuery, _, window );

You'll notice we have a way to properly list the posts, but how do we get them? And where from?

Remember the static JavaScript files we mentioned in the beginning of this chapter? Here's the one we'll use
with the "default" posts listing (see Listing 8-5), that we'll have at ./api/postsList.json:

Listing 8-5: Posts List Static JavaScript (./api/postsList.json) File

[
{
"name": "Bruno Bernardino",
Pro jQuery Plugins v1.0.0
page 214
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-03T19:31:51.702Z",
"content": "This is just a newer post.\nHave you seen this source code?\n
\nWhat do you think?",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T12:01:27.591Z",
"content": "Just another post with not much to say.\nWhat was it? Lorem
ipsum dolor sit amet, consectetur adipiscing elit. Ut semper elit quis justo
facilisis interdum. Nullam sollicitudin ullamcorper ante ac consequat...",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T10:01:27.591Z",
"content": "Another post with an image.",
"image": "https://pbs.twimg.com/media/A4_2SzKCcAAEWbb.jpg:large"
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T09:01:27.591Z",
"content": "A more recent post, without any image.",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T12:14:53.316Z",
"content": "Hey, this is a nice post markup, right? Very clean.",
"image": "http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg"
},
{
Pro jQuery Plugins v1.0.0
page 215
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T11:44:10.018Z",
"content": "This is an older post.\nThere's nothing here to really show.",
"image": ""
}
]

Considering that, we can now build the method to actually get the posts, to send them over to the listing
method (see Listing 8-6).

Listing 8-6: Method to get posts

(function( $, _, window ) {
// ...

var methods = {
init : function( options ) {
var defaults = {
'dataSide' : 'side',// Post's side data-* attribute
'templateID' : 'template-posts',// Posts Underscore.js template Id
'loadingSelector' : '.loading',// Selector for the loading element
'getURL' : './api/postsList.json'// File with the API response
simulation
};

options = $.extend( defaults, options );


globals.options = $.extend( {}, options );// Copy/clone options into
globals.options

return this.each(function() {
var $this = $(this),
data = $this.data( 'timeline' );

if ( ! data ) {
$this.data( 'timeline', {
target : $this
});

Pro jQuery Plugins v1.0.0


page 216
// Do initial fetch
methods.get.call( this );
}
});
},

destroy : function() {
// ...
},

// Method to get posts


get : function() {
var $this = $(this),
that = this;

// If the plug-in hasn't been initialized yet, don't do anything


if ( globals.options.length === 0 ) {
return false;
}

// Start Loading
$this.find( globals.options.loadingSelector ).stop().fadeIn( 'fast' );

$.ajax({
url: globals.options.getURL,
type: 'GET',
data: {},
dataType: 'json',
success: function( response ) {
if ( response ) {
methods.list.call( that, response );
}
}
})
.always(function() {
// Stop Loading

$this.find( globals.options.loadingSelector ).stop().fadeOut( 'fast' );


});
},

// Method to list posts


list : function( postsList ) {
Pro jQuery Plugins v1.0.0
page 217
// ...
}

// TODO: Method to add a post

// TODO: Method to delete post


};

// ...
})( jQuery, _, window );

Let's check what we've got so far in the browser (see Figure 8-1).

Pro jQuery Plugins v1.0.0


page 218
Figure 8-1: Screenshot of what we've got so far

Pro jQuery Plugins v1.0.0


page 219
It's looking good! But notice how the pointers are both in the same place, remember we had a CSS class to fix
this in the previous chapter? Now we need to build a method that will add that class if the previous post (at the
left) starts at about the same top position (see Listing 8-7).

Listing 8-7: Method to fix the pointer position

(function( $, _, window ) {
// ..

var methods = {
init : function( options ) {
var defaults = {
// ..
'getURL' : './api/postsList.json',// File with the API response
simulation
'postSelector' : '.post',// Selector for the post elements
'pointerFixClass' : 'too-close',// Class to fix the pointer position
'pointerFixHeightDistance' : 25// Number of pixels in height where
we'll consider two posts "too close"
};

// ...
},

// ...

// Method to list posts


list : function( postsList ) {
// ...

methods.fixPointer.call( this );
},

fixPointer : function() {
// If the plug-in hasn't been initialized yet, don't do anything
if ( globals.options.length === 0 ) {
return false;
}

$(this).find( globals.options.postSelector ).each(function() {


// We only need to check the posts on the right
Pro jQuery Plugins v1.0.0
page 220
if ( $(this).data( globals.options.dataSide ) === 'right' ) {
if ( $(this).offset().top - globals.options.pointerFixHeightDistance
<= $(this).prev().offset().top ) {
$(this).addClass( globals.options.pointerFixClass );
}
}
});
}

// TODO: Method to add a post

// TODO: Method to delete post


};

// ...
})( jQuery, _, window );

NOTE: This method needs to be executed every time a post is added (including the initial listing)

Let's see how it looks now (see Figure 8-2).

Pro jQuery Plugins v1.0.0


page 221
Figure 8-2: Screenshot after the pointer fix method is implemented

Pro jQuery Plugins v1.0.0


page 222
It's better, but notice how the post with my lovely mojito should be on the right.

We've came across a problem here. In our planning, we didn't remember that the posts' height varies a lot, and
as such, we can't decide if a post goes on the left or right alternatively. It's ok, because we don't want to plan
too far ahead, this is the kind of problem that we can solve more adequately now, that we have a structure built.

The solution that we're going to implement is figuring out if a post goes on the left or right after we've listed
them, based on the end position (top + height) of the previous post for the same side, and that post's previous
post for the alternate side's end position.

Basically, if the previous post's end position is lower than that post's previous post (on the alternate side) end
position, we'll change the post's side to the same as the previous. Let's code that (see Listing 8-8).

Listing 8-8: Implementation of the method to fix position of posts

(function( $, _, window ) {
// ...

var methods = {
// ...

// Method to list posts


list : function( postsList ) {
// ...

methods.fixPosition.call( this );

methods.fixPointer.call( this );
},

fixPointer : function() {
// ...
},

fixPosition : function() {
// If the plug-in hasn't been initialized yet, don't do anything
if ( globals.options.length === 0 ) {
return false;
}

$(this).find( globals.options.postSelector ).each(function() {

Pro jQuery Plugins v1.0.0


page 223
var previousPosition, beforePreviousPosition, prevSide, prevAltSide,
newSide;

prevSide = $(this).prev().data( globals.options.dataSide );

// The previous post's previous post only interests us when it's for
the alternate side
if ( prevSide === 'left' ) {
prevAltSide = 'right';
} else {
prevAltSide = 'left';
}

// We only need to start checking at post #3, as the first and second
posts will always start at the same height
if ( $(this).prev().length === 1 && $(this).prev().prevAll('[data-' +
globals.options.dataSide + '="' + prevAltSide + '"]').length > 0 ) {
previousPosition = $(this).prev().offset().top + $
(this).prev().outerHeight();
beforePreviousPosition = $(this).prev().prevAll('[data-' +
globals.options.dataSide + '="' + prevAltSide + '"]').offset().top + $
(this).prev().prevAll('[data-' + globals.options.dataSide + '="' + prevAltSide +
'"]').outerHeight();

// If the end point of the previous post is smaller than that post's
previous post, we change this post's side to the same as the previous
if ( previousPosition < beforePreviousPosition ) {
newSide = $(this).prev().data( globals.options.dataSide );
} else {
// We need to enforce the alternating sides because if one change
happens, all the following would need to be fixed every time
if ( prevSide === 'left' ) {
newSide = 'right';
} else {
newSide = 'left';
}
}

$(this).data( globals.options.dataSide, newSide );


$(this).attr( 'data-' + globals.options.dataSide, newSide );// We
need to change the attribute, because of the CSS rule we use
}
});
Pro jQuery Plugins v1.0.0
page 224
}

// TODO: Method to add a post

// TODO: Method to delete post


};

// ...
})( jQuery, _, window );

NOTE: We need to run this before the fixPointer method, so the starting position considers being in the
right place.

Let's check out how it is now (see Figure 8-3).

Pro jQuery Plugins v1.0.0


page 225
Figure 8-3: Our plugin demo with the fixPosition() method implemented

Lovely. There's still one thing that's annoying me and is easily fixable, which is the dates. They're not readable.
Though you could use a plugin to make the date something similar to "a day ago", we're just going to show the
date formatted. Note that we will still get the date with the timezone, so it can be properly processed, and add
the pubdate boolean attribute on the <time> in the post's template (see Listing 8-9). This is just so we have a
nice semantic HTML5 markup, it's not required, but good practice.

Pro jQuery Plugins v1.0.0


page 226
Listing 8-9: Updated Post's template

<script type="text/x-underscore-template" id="template-posts">


<% _.each( posts, function( post, index ) { %>
<article class="post" data-side="<%= (index % 2 === 0) ? 'left' : 'right'
%>">
<header>
<!-- ... -->
<div class="info">
<!-- ... -->
<a href="#link-to-post">
<time datetime="<%= post.date %>" pubdate><%= post.date %></time>
</a>
</div>
</header>
<!-- ... -->
</article>
<% }); %>
<% if ( posts.length === 0 ) { %>
<p>There are no posts to show.</p>
<% } %>
</script>

Now let's change that visible date (see Listing 8-10).

Listing 8-10: Our plugin's method to parse dates

(function( $, _, window ) {
// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'timeSelector' : 'header .info time'// Selector for the post's time
};

// ...
},
Pro jQuery Plugins v1.0.0
page 227
// ...

// Method to list posts


list : function( postsList ) {
// ...

methods.parseDates.call( this );
},

// ...

parseDates : function() {
// If the plug-in hasn't been initialized yet, don't do anything
if ( globals.options.length === 0 ) {
return false;
}

$(this).find( globals.options.postSelector ).each(function() {


var timezoneDate = $
(this).find( globals.options.timeSelector ).attr( 'datetime' );

var dateObject = new Date( timezoneDate );

// This date format below should be an option, but for simplicity


sake, we'll keep it like this
// Year
var newDate = dateObject.getFullYear() + '.';

// Month
// Don't forget getMonth() starts at 0
if ( dateObject.getMonth() < 9 ) {
newDate += '0' + ( dateObject.getMonth() + 1 );
} else {
newDate += ( dateObject.getMonth() + 1 );
}

newDate += '.';

// Day
if ( dateObject.getDate() < 10 ) {
newDate += '0' + dateObject.getDate();
} else {
Pro jQuery Plugins v1.0.0
page 228
newDate += dateObject.getDate();
}

newDate += ' ';

// Hours
if ( dateObject.getHours() < 10 ) {
newDate += '0' + dateObject.getHours();
} else {
newDate += dateObject.getHours();
}

newDate += ':';

// Minutes
if ( dateObject.getMinutes() < 10 ) {
newDate += '0' + dateObject.getMinutes();
} else {
newDate += dateObject.getMinutes();
}

$(this).find( globals.options.timeSelector ).html( newDate );


});
}

// TODO: Method to add a post

// TODO: Method to delete post


};

// ...
})( jQuery, _, window );

Here's how it looks, now (see Figure 8-4).

Pro jQuery Plugins v1.0.0


page 229
Figure 8-4: Our plugin demo with the dates parsed now

Beautiful. The next step is to get pagination working.

This poses a bit of a problem in our sample, because we're not using a backend infrastructure, so we could
send a "page" parameter to the server to interpret and retrieve the appropriate page. We're instead going to
rename our ./api/postsList.json file to postsList1.json, add 4 posts (see Listing 8-11), and create two new files:
postsList2.json (see Listing 8-12) with 10 posts, and postsList3.json (see Listing 8-13) with an empty array.

Pro jQuery Plugins v1.0.0


page 230
Listing 8-11: Posts List Static JavaScript (./api/postsList1.json) File, Page 1

[
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-04T12:48:38.813Z",
"content": "This is the most recent post of all.\n\nShowing off my cooking
skills.",
"image": "http://distilleryimage10.s3.amazonaws.com/
27bc4c1245a411e1abb01231381b65e3_7.jpg"
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-04T11:16:01.003Z",
"content": "I find your lack of faith disturbing. No! Alderaan is peaceful.
\n\nWe have no weapons. You can't possibly...\n\nObi-Wan is here. The Force is
with him.",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-04T00:03:32.209Z",
"content": "What!? Don't act so surprised, Your Highness.\n\nYou weren't on
any mercy mission this time. Several transmissions were beamed to this ship by
Rebel spies. I want to know what happened to the plans they sent you. She must
have hidden the plans in the escape pod. Send a detachment down to retrieve
them, and see to it personally, Commander.\n\nThere'll be no one to stop us this
time!",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-03T21:19:11.341Z",

Pro jQuery Plugins v1.0.0


page 231
"content": "The Force is strong with this one.\n\nI have you now. He is
here. I want to come with you to Alderaan. There's nothing for me here now.",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-03T19:31:51.702Z",
"content": "This is just a newer post.\nHave you seen this source code?\n
\nWhat do you think?",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T12:07:27.513Z",
"content": "Just another post with not much to say.\nWhat was it? Lorem
ipsum dolor sit amet, consectetur adipiscing elit. Ut semper elit quis justo
facilisis interdum. Nullam sollicitudin ullamcorper ante ac consequat...",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T10:01:27.591Z",
"content": "Another post with an image.",
"image": "https://pbs.twimg.com/media/A4_2SzKCcAAEWbb.jpg:large"
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T09:03:27.961Z",
"content": "A more recent post, without any image.",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
Pro jQuery Plugins v1.0.0
page 232
"date": "2013-03-01T12:14:53.316Z",
"content": "Hey, this is a nice post markup, right? Very clean.",
"image": "http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg"
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T11:44:10.018Z",
"content": "This is an older post.\nThere's nothing here to really show.",
"image": ""
}
]

Listing 8-12: Posts List Static JavaScript (./api/postsList2.json) File, Page 2

[
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T10:31:17.128Z",
"content": "Some of my delicious pasta.",
"image": "http://distilleryimage11.s3.amazonaws.com/
5fd192c2194111e19896123138142014_7.jpg"
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-28T10:11:23.652Z",
"content": "I want to learn the ways of the Force and be a Jedi, like my
father before me.\n\nA tremor in the Force. The last time I felt it was in the
presence of my old master.",
"image": ""
},
{
"name": "Bruno Bernardino",

Pro jQuery Plugins v1.0.0


page 233
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-28T09:21:48.917Z",
"content": "Still, she's got a lot of spirit. I don't know, what do you
think?\n\nHokey religions and ancient weapons are no match for a good blaster at
your side, kid. But with the blast shield down, I can't even see! How am I
supposed to fight?",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-27T16:14:08.106Z",
"content": "A sexy mistake. Yeah. Give a little credit to our public
schools. Actually, that's still true. I'm sure those windmills will keep them
cool.",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-26T03:12:45.081Z",
"content": "An attempt at coffee art, though this one is with chocolate.",
"image": "http://distilleryimage6.s3.amazonaws.com/
65fda9a8377b11e180c9123138016265_7.jpg"
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-26T01:09:13.349Z",
"content": "No... It's a thing; it's like a plan, but with more greatness.
\n\nI'm nobody's taxi service; I'm not gonna be there to catch you every time
you feel like jumping out of a spaceship. All I've got to do is pass as an
ordinary human being.",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
Pro jQuery Plugins v1.0.0
page 234
"date": "2013-02-25T14:49:51.392Z",
"content": "Sorry, checking all the water in this area; there's an escaped
fish. It's art!",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-25T12:37:19.715Z",
"content": "A chinese recipe now!",
"image": "http://distilleryimage8.s3.amazonaws.com/
645b2f8e834411e1b10e123138105d6b_7.jpg"
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-25T10:43:15.612Z",
"content": "You've swallowed a planet! Heh-haa! No. No violence. I won't
stand for it. Not now, not ever, do you understand me?!",
"image": ""
},
{
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-24T19:03:29.260Z",
"content": "This is the last post, the most ancient one.",
"image": ""
}
]

Listing 8-13: Posts List Static JavaScript (./api/postsList3.json) File, Page 3

[]

Pro jQuery Plugins v1.0.0


page 235
NOTE: We need this file to mimic the "last" page, where no posts would be obtained (or less than the
limit per page, indicating it was the last page)

So, for our specific (static) demo, we'll use the page as part of the URL to get the posts, instead of a parameter
sent to that URL (see Listing 8-14).

Listing 8-14: Implementation of pagination on our plugin

(function( $, _, window ) {
// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'getURL' : './api/postsList{page}.json',// File with the API response
simulation
// ...
'postsPerPage' : 10,
'reachedLastPage' : false,
'currentPage' : 1
};

// ...

return this.each(function() {
var $this = $(this),
data = $this.data( 'timeline' );

if ( ! data ) {
// ...

// Start Loading

$this.find( globals.options.loadingSelector ).stop().fadeIn( 'fast' );


Pro jQuery Plugins v1.0.0
page 236
// Do initial fetch
methods.get.call( this );
}
});
},

destroy : function() {
// ...
},

// Method to get posts, with pagination


get : function() {
var $this = $(this),
that = this;

// If the plug-in hasn't been initialized yet, don't do anything


if ( globals.options.length === 0 ) {
return false;
}

// If we've already reached the last page, we don't need to make more
requests
if ( globals.options.reachedLastPage ) {
return false;
}

$.ajax({
url: globals.options.getURL.replace( '{page}',
globals.options.currentPage ),
type: 'GET',
data: {},
dataType: 'json',
success: function( response ) {
if ( response ) {
// If the number of posts is less than the number of posts per
page, we've reached the last page
if ( response.length < globals.options.postsPerPage ) {
globals.options.reachedLastPage = true;

// Stop Loading

$this.find( globals.options.loadingSelector ).stop().fadeOut( 'fast' );


Pro jQuery Plugins v1.0.0
page 237
}

methods.list.call( that, response );


}
}
});
},

// Method to list posts


list : function( postsList ) {
// If the plug-in hasn't been initialized yet, don't do anything
if ( globals.options.length === 0 ) {
return false;
}

// If we've reached the last page, there's nothing to list


if ( globals.options.reachedLastPage ) {
return false;
}

var postsHTML = _.template( $('#' + globals.options.templateID).html(),


{ posts: postsList } );

// We need to append, but before the loading, so it remains in the bottom


$(this).find( globals.options.loadingSelector ).before( postsHTML );

methods.fixPosition.call( this );

methods.fixPointer.call( this );

methods.parseDates.call( this );
},

// ...

getNextPage : function() {
// If the plug-in hasn't been initialized yet, don't do anything
if ( globals.options.length === 0 ) {
return false;
}

// If we've reached the last page, there's nothing to do


if ( globals.options.reachedLastPage ) {
Pro jQuery Plugins v1.0.0
page 238
return false;
}

++globals.options.currentPage;

methods.get.call( this );
}

// TODO: Method to add a post

// TODO: Method to delete post


};

// ...
})( jQuery, _, window );

Now that we have a way to get the "next page" of posts, we just need to implement that in our demo.

Since we want to get the next page when reaching the bottom of the page, we'll need a function to "listen" for
when the user reaches the bottom of the page (actually when the user is in the last 200px of the page, for
usability improvement), which we will call scrollWatch(), that will in its turn execute the plugin's getNextPage()
(see Listing 8-15).

Listing 8-15: Implementation of pagination on our plugin demo (./js/main.js)

(function( $, window, document ) {


$(document).ready(function() {
$('#posts').timeline();

$(window).on( 'scroll.timeline-demo', function( event ) {


scrollWatch();
});
});

var scrollWatch = function() {


// We only need to check while the loading is visible, since it's hidden
from the plug-in once it reaches the last page
if ( $('#posts .loading:visible').length === 1 ) {
if ( $(document).scrollTop() >= ($(document).height() - $
(window).height() - 200) ) {

Pro jQuery Plugins v1.0.0


page 239
$('#posts').timeline( 'getNextPage' );
}
}
};
})( jQuery, window, document );

We can now see the second page of posts. Test it out (see Figure 8-5).

Pro jQuery Plugins v1.0.0


page 240
Figure 8-5: Page 1 and 2 of our posts listing plugin demo

Pro jQuery Plugins v1.0.0


page 241
The next step is to make the new post form functional, for which we need a plugin method (see Listing 8-16),
and a demo function to call that method (see Listing 8-17) that will simulate the actual POST request to our
RESTful API, and call the plugin's add post method.

Listing 8-16: Our plugin's method to add a post

(function( $, _, window ) {
var globals = {
'options' : {}
};

var methods = {
init : function( options ) {
var defaults = {
// ...
'getURL' : './api/postsList{page}.json',// File with the API response
simulation
'createURL' : './api/addPost.json',// File with the API response
simulation
// ...
};

// ...
},

// ...

// Method to add a post


addPost : function( postObject ) {
var $this = $(this),
that = this;

// If the plug-in hasn't been initialized yet, don't do anything


if ( globals.options.length === 0 ) {
return false;
}

$.ajax({
url: globals.options.createURL,

Pro jQuery Plugins v1.0.0


page 242
type: 'GET',// NOTE: In a RESTful API, this would be 'POST', but since
we're doing a static demo, we need 'GET', otherwise we can get a "Not Allowed"
error
data: {
post: postObject
},
dataType: 'json',
success: function( response ) {
if ( response ) {
var postsHTML = _.template( $('#' +
globals.options.templateID).html(), { posts: [ postObject ] } );

// We need to prepend
$this.prepend( postsHTML );

// Switch side of the second post (previously first)

$this.find( globals.options.postSelector ).eq(1).data( globals.options.dataSide,


'right' );
$this.find( globals.options.postSelector ).eq(1).attr( 'data-' +
globals.options.dataSide, 'right' );// We need to change the attribute, because
of the CSS rule we use

methods.fixPosition.call( that );

methods.fixPointer.call( that );

methods.parseDates.call( that );
}
}
});
}

// TODO: Method to delete post


};

// ...
})( jQuery, _, window );

Pro jQuery Plugins v1.0.0


page 243
NOTE: The method only needs to switch the side of the second post (previously the first). The other
posts are adjusted with the fixPosition() method.

Listing 8-17: Our demo function to add a post to the timeline plugin

(function( $, window, document ) {


$(document).ready(function() {
$('#posts').timeline();

$(document).on( 'submit.timeline-demo', '#add-post form', function( event )


{
event.preventDefault();

var formElement = event.target;

if ( $(formElement).find('textarea[name="status-update"]').val().length
=== 0 ) {
window.alert( 'You need to write something!' );
}

var post = {
"name": "Bruno Bernardino",// This value is static, but should be
obtained by an hidden input or could be filled by the backend app
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/
master/assets/me.png",// This value is static, but should be obtained by an
hidden input or could be filled by the backend app
"date": new Date().toISOString(),
"content": $(formElement).find('textarea[name="status-
update"]').val(),
"image": $(formElement).find('input[name="media"]').val()
};

$('#posts').timeline( 'addPost', post );


});

Pro jQuery Plugins v1.0.0


page 244
$(window).on( 'scroll.timeline-demo', function( event ) {
scrollWatch();
});
});

// ...
})( jQuery, window, document );

The ./api/addPost.json file simulates the API's response, basically only with a message type and message (see
Listing 8-18).

Listing 8-18: The ./api/addPost.json file

{
"type": "success",
"message": "Post created successfully!"
}

Our final "basic functionality" to add is the option to delete a post. Like when adding a new post, we'll need to
switch the sides of the posts that follow the post we deleted (see Listing 8-19).

Listing 8-19: Our plugin's method to delete a post

(function( $, _, window ) {
// ...

var methods = {
init : function( options ) {
var defaults = {
// ...
'createURL' : './api/addPost.json',// File with the API response
simulation
'deleteURL' : './api/deletePost.json',// File with the API response
simulation
'postSelector' : '.post',// Selector for the post elements
'postIDPrefix' : 'post-',// Selector prefix for the post elements Id
// ...
};
Pro jQuery Plugins v1.0.0
page 245
// ...
},

// ...

fixPointer : function() {
// ...

$(this).find( globals.options.postSelector ).each(function() {


// Remove any previous pointerFixClass
$(this).removeClass( globals.options.pointerFixClass );

// ...
});
},

fixPosition : function() {
// ...
},

parseDates : function() {
// ...
},

getNextPage : function() {
// ...
},

// Method to add a post


addPost : function( postObject ) {
// ...

$.ajax({
// ...
dataType: 'json',
success: function( response ) {
if ( response ) {
postObject.id = new Date().getTime();// Ideally this would be a
value obtained from the server response.

// ...
}
Pro jQuery Plugins v1.0.0
page 246
}
});
},

// Method to delete a post


deletePost : function( postID ) {
var $this = $(this),
that = this;

// If the plug-in hasn't been initialized yet, don't do anything


if ( globals.options.length === 0 ) {
return false;
}

$.ajax({
url: globals.options.deleteURL,
type: 'GET',// NOTE: In a RESTful API, this would be 'DELETE', but
since we're doing a static demo, we need 'GET', otherwise we can get a "Not
Allowed" error
data: {
postID: postID
},
dataType: 'json',
success: function( response ) {
if ( response ) {
// Remove the post from the listing
$this.find( '#' + globals.options.postIDPrefix +
postID ).remove();

// Forcefully set sides of the first and second posts

$this.find( globals.options.postSelector ).eq(0).data( globals.options.dataSide,


'left' );
$this.find( globals.options.postSelector ).eq(0).attr( 'data-' +
globals.options.dataSide, 'left' );// We need to change the attribute, because
of the CSS rule we use

$this.find( globals.options.postSelector ).eq(1).data( globals.options.dataSide,


'right' );

Pro jQuery Plugins v1.0.0


page 247
$this.find( globals.options.postSelector ).eq(1).attr( 'data-' +
globals.options.dataSide, 'right' );// We need to change the attribute, because
of the CSS rule we use

methods.fixPosition.call( that );

methods.fixPointer.call( that );

methods.parseDates.call( that );
}
}
});
}
};

$.fn.timeline = 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.timeline' );
}
};
})( jQuery, _, window );

NOTE: Again, since this is a static demo, the post won't really get deleted, but we'll assume the server
told the plugin it was. Note how we had to bring in post IDs, otherwise we'd lose a lot of time and per-
formance just going through the DOM.

The ./api/deletePost.json file simulates the API's response, like ./api/addPost.json (see Listing 8-20)

Listing 8-20: The ./api/deletePost.json file

{
"type": "success",
"message": "Post deleted successfully!"
Pro jQuery Plugins v1.0.0
page 248
}

For this to work we need to add the delete action to the underscore template (see Listing 8-21), CSS (see
Listing 8-22), and demo JavaScript (see Listing 8-23). Also, the postsList1.json (see Listing 8-24) and
postsList2.json (see Listing 8-25) now bring post IDs.

Listing 8-21: Underscore template for the post, now with the delete action

<!DOCTYPE html>
<!-- ... -->
<body>
<!-- ... -->

<script type="text/x-underscore-template" id="template-posts">


<% _.each( posts, function( post, index ) { %>
<article class="post" data-side="<%= (index % 2 === 0) ? 'left' : 'right'
%>" data-id="<%= post.id %>" id="post-<%= post.id %>">
<header>
<!-- ... -->
<span class="actions">
<a href="#" class="delete" title="Delete this post">&times;</a>
</span>
</header>
<!-- ... -->
</article>
<% }); %>
<% if ( posts.length === 0 ) { %>
<p>There are no posts to show.</p>
<% } %>
</script>
</body>
</html>

Listing 8-22: CSS for the post's delete action

/* ... */
Pro jQuery Plugins v1.0.0
page 249
/* Individual posts */
#posts .post {
/* ... */
position: relative;
}

/* ... */

#posts .post .info a:hover h1, #posts .post .info a:hover time {
/* ... */
}

/* Post header actions */


#posts .post .actions {
display: none;
position: absolute;
top: 10px;
right: 15px;
}

#posts .post:hover .actions {


display: block;
}

#posts .post .actions a {


display: block;
color: #333;
text-decoration: none;
font-size: 12px;
}

/* Post content */
#posts .post .content {
/* ... */
}

/* ... */

Pro jQuery Plugins v1.0.0


page 250
Listing 8-23: JavaScript for the delete action in our demo

(function( $, window, document ) {


$(document).ready(function() {
$('#posts').timeline();

$(document).on( 'submit.timeline-demo', '#add-post form', function( event )


{
// ...
});

$(document).on( 'click.timeline-demo', '#posts .post .actions .delete',


function( event ) {
event.preventDefault();

var anchorElement = event.target;


var postElement = $(anchorElement).closest( '.post' );

var goOn = window.confirm( 'Are you sure you want to delete this
post?' );

if ( goOn ) {
$('#posts').timeline( 'deletePost', postElement.data('id') );
}
});

$(window).on( 'scroll.timeline-demo', function( event ) {


scrollWatch();
});
});

// ...
})( jQuery, window, document );

Listing 8-24: Updated postsList1.json with post IDs

[
{
"id": 20,
Pro jQuery Plugins v1.0.0
page 251
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-04T12:48:38.813Z",
"content": "This is the most recent post of all.\n\nShowing off my cooking
skills.",
"image": "http://distilleryimage10.s3.amazonaws.com/
27bc4c1245a411e1abb01231381b65e3_7.jpg"
},
{
"id": 19,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-04T11:16:01.003Z",
"content": "I find your lack of faith disturbing. No! Alderaan is peaceful.
\n\nWe have no weapons. You can't possibly...\n\nObi-Wan is here. The Force is
with him.",
"image": ""
},
{
"id": 18,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-04T00:03:32.209Z",
"content": "What!? Don't act so surprised, Your Highness.\n\nYou weren't on
any mercy mission this time. Several transmissions were beamed to this ship by
Rebel spies. I want to know what happened to the plans they sent you. She must
have hidden the plans in the escape pod. Send a detachment down to retrieve
them, and see to it personally, Commander.\n\nThere'll be no one to stop us this
time!",
"image": ""
},
{
"id": 17,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-03T21:19:11.341Z",
"content": "The Force is strong with this one.\n\nI have you now. He is
here. I want to come with you to Alderaan. There's nothing for me here now.",
"image": ""
Pro jQuery Plugins v1.0.0
page 252
},
{
"id": 16,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-03T19:31:51.702Z",
"content": "This is just a newer post.\nHave you seen this source code?\n
\nWhat do you think?",
"image": ""
},
{
"id": 15,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T12:07:27.513Z",
"content": "Just another post with not much to say.\nWhat was it? Lorem
ipsum dolor sit amet, consectetur adipiscing elit. Ut semper elit quis justo
facilisis interdum. Nullam sollicitudin ullamcorper ante ac consequat...",
"image": ""
},
{
"id": 14,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T10:01:27.591Z",
"content": "Another post with an image.",
"image": "https://pbs.twimg.com/media/A4_2SzKCcAAEWbb.jpg:large"
},
{
"id": 13,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-02T09:03:27.961Z",
"content": "A more recent post, without any image.",
"image": ""
},
{
"id": 12,
"name": "Bruno Bernardino",
Pro jQuery Plugins v1.0.0
page 253
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T12:14:53.316Z",
"content": "Hey, this is a nice post markup, right? Very clean.",
"image": "http://distilleryimage9.s3.amazonaws.com/
cd3540ae369411e2ada322000a1fbcdb_7.jpg"
},
{
"id": 11,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T11:44:10.018Z",
"content": "This is an older post.\nThere's nothing here to really show.",
"image": ""
}
]

Listing 8-25: Updated postsList2.json with post IDs

[
{
"id": 10,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-03-01T10:31:17.128Z",
"content": "Some of my delicious pasta.",
"image": "http://distilleryimage11.s3.amazonaws.com/
5fd192c2194111e19896123138142014_7.jpg"
},
{
"id": 9,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-28T10:11:23.652Z",
"content": "I want to learn the ways of the Force and be a Jedi, like my
father before me.\n\nA tremor in the Force. The last time I felt it was in the
presence of my old master.",

Pro jQuery Plugins v1.0.0


page 254
"image": ""
},
{
"id": 8,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-28T09:21:48.917Z",
"content": "Still, she's got a lot of spirit. I don't know, what do you
think?\n\nHokey religions and ancient weapons are no match for a good blaster at
your side, kid. But with the blast shield down, I can't even see! How am I
supposed to fight?",
"image": ""
},
{
"id": 7,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-27T16:14:08.106Z",
"content": "A sexy mistake. Yeah. Give a little credit to our public
schools. Actually, that's still true. I'm sure those windmills will keep them
cool.",
"image": ""
},
{
"id": 6,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-26T03:12:45.081Z",
"content": "An attempt at coffee art, though this one is with chocolate.",
"image": "http://distilleryimage6.s3.amazonaws.com/
65fda9a8377b11e180c9123138016265_7.jpg"
},
{
"id": 5,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-26T01:09:13.349Z",
"content": "No... It's a thing; it's like a plan, but with more greatness.
\n\nI'm nobody's taxi service; I'm not gonna be there to catch you every time
Pro jQuery Plugins v1.0.0
page 255
you feel like jumping out of a spaceship. All I've got to do is pass as an
ordinary human being.",
"image": ""
},
{
"id": 4,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-25T14:49:51.392Z",
"content": "Sorry, checking all the water in this area; there's an escaped
fish. It's art!",
"image": ""
},
{
"id": 3,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-25T12:37:19.715Z",
"content": "A chinese recipe now!",
"image": "http://distilleryimage8.s3.amazonaws.com/
645b2f8e834411e1b10e123138105d6b_7.jpg"
},
{
"id": 2,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-25T10:43:15.612Z",
"content": "You've swallowed a planet! Heh-haa! No. No violence. I won't
stand for it. Not now, not ever, do you understand me?!",
"image": ""
},
{
"id": 1,
"name": "Bruno Bernardino",
"avatar": "https://raw.github.com/BrunoBernardino/ProjQueryPlugins/master/
assets/me.png",
"date": "2013-02-24T19:03:29.260Z",
"content": "This is the last post, the most ancient one.",
"image": ""
}
Pro jQuery Plugins v1.0.0
page 256
]

Section 2: Adding Animations & Flexibility


Let's make our plugin a bit more good looking, with some fancy animations (all done by CSS, so it is as
dynamic and flexible as possible, keeping performance).

The first animation we'll do is for the loading of the posts, which will slide in from the bottom (see Listing 8-26).

Listing 8-26: CSS animation for the loading of posts

/* ... */

/* Individual Post Animation */


@-webkit-keyframes slideFromBottom {
0% {
-webkit-transform-origin: center center;
-webkit-transform: translateY(10px);
opacity: 0;
}
100% {
-webkit-transform-origin: center center;
-webkit-transform: translateY(0);
opacity: 1;
}
}
@-moz-keyframes slideFromBottom {
0% {
-moz-transform-origin: center center;
-moz-transform: translateY(10px);
opacity: 0;
}
100% {
-moz-transform-origin: center center;
-moz-transform: translateY(0);
opacity: 1;
}
}
@-o-keyframes slideFromBottom {

Pro jQuery Plugins v1.0.0


page 257
0% {
-o-transform-origin: center center;
-o-transform: translateY(10px);
opacity: 0;
}
100% {
-o-transform-origin: center center;
-o-transform: translateY(0);
opacity: 1;
}
}
@keyframes slideFromBottom {
0% {
transform-origin: center center;
transform: translateY(10px);
opacity: 0;
}
100% {
transform-origin: center center;
transform: translateY(0);
opacity: 1;
}
}

#posts .post {
-webkit-animation-duration: 400ms;
-moz-animation-duration: 400ms;
-o-animation-duration: 400ms;
animation-duration: 400ms;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: slideFromBottom;
-moz-animation-name: slideFromBottom;
-o-animation-name: slideFromBottom;
animation-name: slideFromBottom;
}

Test it out. Looks nice, right?

Pro jQuery Plugins v1.0.0


page 258
Now, adding a new post will have a different animation, as it's a new element entering the timeline, so we'll slide
it, from the top (see Listing 8-27).

Listing 8-27: CSS animation for the new posts being added

/* ... */

/* Individual New Post Animation */


@-webkit-keyframes slideFromTop {
0% {
-webkit-transform-origin: center center;
-webkit-transform: translateY(-10px);
opacity: 0;
}
100% {
-webkit-transform-origin: center center;
-webkit-transform: translateY(0);
opacity: 1;
}
}
@-moz-keyframes slideFromTop {
0% {
-moz-transform-origin: center center;
-moz-transform: translateY(-10px);
opacity: 0;
}
100% {
-moz-transform-origin: center center;
-moz-transform: translateY(0);
opacity: 1;
}
}
@-o-keyframes slideFromTop {
0% {
-o-transform-origin: center center;
-o-transform: translateY(-10px);
opacity: 0;
}
100% {
-o-transform-origin: center center;
-o-transform: translateY(0);

Pro jQuery Plugins v1.0.0


page 259
opacity: 1;
}
}
@keyframes slideFromTop {
0% {
transform-origin: center center;
transform: translateY(-10px);
opacity: 0;
}
100% {
transform-origin: center center;
transform: translateY(0);
opacity: 1;
}
}

#posts .post.new-post {
-webkit-animation-duration: 400ms;
-moz-animation-duration: 400ms;
-o-animation-duration: 400ms;
animation-duration: 400ms;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: slideFromTop;
-moz-animation-name: slideFromTop;
-o-animation-name: slideFromTop;
animation-name: slideFromTop;
}

We need to apply that "new-post" class to newly added posts, so our plugin's code will need a few small
changes (see Listing 8-28).

Listing 8-28: Our plugin code changed to use the "new-post" class

(function( $, _, window ) {
// ...

var methods = {

Pro jQuery Plugins v1.0.0


page 260
init : function( options ) {
var defaults = {
// ...
'currentPage' : 1,
'newPostClass' : 'new-post'
};

// ...
},

// ...

// Method to add a post


addPost : function( postObject ) {
// ...

$.ajax({
// ...
dataType: 'json',
success: function( response ) {
if ( response ) {
postObject.id = new Date().getTime();// Ideally this would be a
value obtained from the server response.

var postsHTML = _.template( $('#' +


globals.options.templateID).html(), { posts: [ postObject ] } );

// We need to prepend & Add the newPostClass to it


$this.prepend( postsHTML ).find( '#' +
globals.options.postIDPrefix +
postObject.id ).addClass( globals.options.newPostClass );

// ...
}
}
});
},

// Method to delete a post


deletePost : function( postID ) {
// ...
}
};
Pro jQuery Plugins v1.0.0
page 261
// ...
})( jQuery, _, window );

The next thing we'll tackle is flexibility. Our plugin is already very flexible, but there are a few places where we
can improve it, like, for example, a callback option for when posts are added, loaded, and deleted.

Also, we should be making better use of the Deferred objects we learned about in the previous chapter, not by
using them themselves, because their nature is "do once", but creating something based on that architecture
(see Listing 8-29).

Listing 8-29: Plugin code after improvements

(function( $, _, window ) {
var globals = {
'getPreProcessor': [],
'getPostProcessor': [],
'listPreProcessor': [],
'listPostProcessor': [],
'addPreProcessor': [],
'addPostProcessor': [],
'deletePreProcessor': [],
'deletePostProcessor': [],
'options' : {}
};

var methods = {
init : function( options ) {
var defaults = {
// ...
'getPreProcess' : $.noop,// Callback function(s), for processing
before post fetching
'getPostProcess' : $.noop,// Callback function(s), for processing
after post fetching
'listPreProcess' : $.noop,// Callback function(s), for processing
before post listing
'listPostProcess' : $.noop,// Callback function(s), for processing
after post listing
'addPreProcess' : $.noop,// Callback function(s), for processing
before post addition

Pro jQuery Plugins v1.0.0


page 262
'addPostProcess' : $.noop,// Callback function(s), for processing
after post addition
'deletePreProcess' : $.noop,// Callback function(s), for processing
before post deletion
'deletePostProcess' : $.noop// Callback function(s), for processing
after post deletion
};

options = $.extend( defaults, options );


globals.options = $.extend( {}, options );// Copy/clone options into
globals.options

// Create Deferred objects


globals.options.getPreProcessor = [];
globals.options.getPreProcessor.push(function( that ) {
globals.options.getPreProcess.call( that );
});

globals.options.getPostProcessor = [];
globals.options.getPostProcessor.push(function( that ) {
globals.options.getPostProcess.call( that );
});

globals.options.listPreProcessor = [];
globals.options.listPreProcessor.push(function( that ) {
globals.options.listPreProcess.call( that );
});

globals.options.listPostProcessor = [];
globals.options.listPostProcessor.push(function( that ) {
globals.options.listPostProcess.call( that );
});

globals.options.addPreProcessor = [];
globals.options.addPreProcessor.push(function( that ) {
globals.options.addPreProcess.call( that );
});

globals.options.addPostProcessor = [];
globals.options.addPostProcessor.push(function( that ) {
globals.options.addPostProcess.call( that );
});

Pro jQuery Plugins v1.0.0


page 263
globals.options.deletePreProcessor = [];
globals.options.deletePreProcessor.push(function( that ) {
globals.options.deletePreProcess.call( that );
});

globals.options.deletePostProcessor = [];
globals.options.deletePostProcessor.push(function( that ) {
globals.options.deletePostProcess.call( that );
});

// Add plug-in's default post processors


globals.options.listPostProcessor.push(function( that ) {
methods.postProcessing.call( this, that );
});

globals.options.addPostProcessor.push(function( that ) {
methods.postProcessing.call( this, that );
});

globals.options.deletePostProcessor.push(function( that ) {
methods.postProcessing.call( this, that );
});

// ...
},

// ...

// Method to get posts, with pagination


get : function() {
// ...

methods.executeCallbackStack.call( this, globals.options.getPreProcessor,


this );

$.ajax({
// ...
success: function( response ) {
if ( response ) {
// If the number of posts is less than the number of posts per
page, we've reached the last page
if ( response.length < globals.options.postsPerPage ) {
// ...
Pro jQuery Plugins v1.0.0
page 264
}

methods.executeCallbackStack.call( this,
globals.options.getPostProcessor, that );

methods.list.call( that, response );


}
}
});
},

getNextPage : function() {
// ...
},

// Method to add a post


addPost : function( postObject ) {
// ...

methods.executeCallbackStack.call( this, globals.options.addPreProcessor,


this );

$.ajax({
// ...
success: function( response ) {
if ( response ) {
postObject.id = new Date().getTime();// Ideally this would be a
value obtained from the server response.

var postsHTML = _.template( $('#' +


globals.options.templateID).html(), { posts: [ postObject ] } );

// We need to prepend & Add the newPostClass to it


$this.prepend( postsHTML ).find( '#' +
globals.options.postIDPrefix +
postObject.id ).addClass( globals.options.newPostClass );

// Switch side of the second post (previously first)

$this.find( globals.options.postSelector ).eq(1).data( globals.options.dataSide,


'right' );

Pro jQuery Plugins v1.0.0


page 265
$this.find( globals.options.postSelector ).eq(1).attr( 'data-' +
globals.options.dataSide, 'right' );// We need to change the attribute, because
of the CSS rule we use

methods.executeCallbackStack.call( this,
globals.options.addPostProcessor, that );
}
}
});
},

// Method to delete a post


deletePost : function( postID ) {
// ...

methods.executeCallbackStack.call( this,
globals.options.deletePreProcessor, this );

$.ajax({
// ...
success: function( response ) {
if ( response ) {
// Remove the post from the listing
$this.find( '#' + globals.options.postIDPrefix +
postID ).remove();

// Forcefully set sides of the first and second posts

$this.find( globals.options.postSelector ).eq(0).data( globals.options.dataSide,


'left' );
$this.find( globals.options.postSelector ).eq(0).attr( 'data-' +
globals.options.dataSide, 'left' );// We need to change the attribute, because
of the CSS rule we use

$this.find( globals.options.postSelector ).eq(1).data( globals.options.dataSide,


'right' );
$this.find( globals.options.postSelector ).eq(1).attr( 'data-' +
globals.options.dataSide, 'right' );// We need to change the attribute, because
of the CSS rule we use

Pro jQuery Plugins v1.0.0


page 266
methods.executeCallbackStack.call( this,
globals.options.deletePostProcessor, that );
}
}
});
},

// Method to list posts


list : function( postsList ) {
// ...

methods.executeCallbackStack.call( this,
globals.options.listPreProcessor, this );

var postsHTML = _.template( $('#' + globals.options.templateID).html(),


{ posts: postsList } );

// We need to append, but before the loading, so it remains in the bottom


$(this).find( globals.options.loadingSelector ).before( postsHTML );

methods.executeCallbackStack.call( this,
globals.options.listPostProcessor, this );
},

fixPointer : function() {
// ...
},

fixPosition : function() {
// ...
},

parseDates : function() {
// ...
},

postProcessing : function( postsElement ) {


methods.fixPosition.call( postsElement );

methods.fixPointer.call( postsElement );

methods.parseDates.call( postsElement );
},
Pro jQuery Plugins v1.0.0
page 267
executeCallbackStack : function ( callbackStack ) {
var iLimit = callbackStack.length;

for (var i = 0; i < iLimit; i++) {


callbackStack[i].apply( this, Array.prototype.slice.call(arguments,
1) );
}
}
};

// ...
})( jQuery, _, window );

If you noticed, some of our posts don't get aligned. This is because our demo has the post animation, so the
positions don't get calculated at the right place. Because of that, we need to make use of the callbacks we just
built, in our demo JavaScript.

Listing 8-30: Demo JavaScript

(function( $, window, document ) {


$(document).ready(function() {
$('#posts').timeline({
listPostProcess: function() {// We need this because of the animations
"timelapse"
window.setTimeout(function() {
$('#posts').timeline( 'postProcessing',
document.getElementById('posts') );
}, 450 );// 400 is the number of milliseconds it takes to finish the
animation, the extra 50 is just to be safe
}
});

// ...
});

// ...
})( jQuery, window, document );

Pro jQuery Plugins v1.0.0


page 268
Lastly, we'll use the callbacks again to improve the UI and UX of our form a bit, by showing and hiding our
loading icon accordingly, when adding a post (see Listing 8-31).

Listing 8-31: Final Demo JavaScript, now with the loading animation on the form

(function( $, window, document ) {


$(document).ready(function() {
$('#posts').timeline({
listPostProcess: function() {// We need this because of the animations
"timelapse"
// ...
},
addPostProcess: function() {
$('#add-post .loading').stop().fadeOut( 'fast' );
}
});

// ...
});

// ...
})( jQuery, window, document );

I had to tweak something at the CSS, though (see Listing 8-32).

Listing 8-32: Final Demo CSS

/* ... */

#add-post form .loading {


display: none;
float: right;
margin-top: 8px;
}

/* ... */

Pro jQuery Plugins v1.0.0


page 269
Summary
In this chapter you've finished building the timeline plugin complete with a working sample, which is very flexible
and extensible.

You've applied several concepts that you've learned in the previous chapters, all bundled together in a single
plugin and sample.

In the next chapter, I'm going to talk about existing jQuery plugins that extend jQuery's functionality, and
samples for you to build your own.

Pro jQuery Plugins v1.0.0


page 270
Chapter 9:
Extending functionality

In this chapter we'll talk about known (and less-known) ways of extending jQuery's functionality to our needs,
and how to appropriately apply them.

Please note the code samples here won't be inside closures for simplicity of presentation, but they should
always be in "real world" use cases.

Extending selector functionality


Few people know that you can actually extend jQuery's selector functionality to include custom selectors and
selector filters.

Selectors are the queries that match elements (tag names, for example), and filters complement these, filtering
elements according to their attributes or properties.

As an example of use case, imagine you wanted to select all paragraph (<p>) elements that had the CSS
attribute "display" with the value of "inline".

You could add the ":inline" selector filter by coding this (see Listing 9-1).

Listing 9-1: JavaScript code to extend jQuery's selector to select elements with
"inline"

$.extend( $.expr[':'], {
'inline': function( a ) {
return $(a).css( 'display' ) === 'inline';
}
});

Pro jQuery Plugins v1.0.0


page 271
NOTE: $.expr[':'] is just an alias for $.expr.filters

And to fetch the paragraph elements with display: inline, we'd simply do (see Listing 9-2):

Listing 9-2: Sample usage to fetch paragraphs with "display: inline"

$('p:inline');

So, looking at that small bit of code (Listing 9-1), you can see that, to extend the ':' selector, we use jQuery's
$.extend(), using the $.expr[':'] as the object we're extending, and adding to it the "inline" method.

This method receives 3 parameters, though we're only using the first:

1. The DOM element object that we're testing at the moment;

2. The index (iteration number) of the current element, in the matched elements list;

3. An array with 4 elements that are regular expression matches to filter parts.

Basically, you'll only need the fourth element (index = 3) if you want to build a more complex filter, that uses
parentheses (what's matched in the fourth element is the content of the parentheses). The first three array
elements were mainly used in previous jQuery versions and I've honestly never found use for them (let me know
if you find use for them!).

This function has to return a boolean value, which will indicate if the current element should be included in the
final elements list or not.

Pro jQuery Plugins v1.0.0


page 272
A RegEx :match() selector
We've used Regular Expressions (A.K.A. RegExp or RegEx) on Chapter 2, and so, if you're not familiar with
them, they provide a lot of power when searching for a pattern.

We're going to create a :match() selector filter.

It will receive two parameters:

1. The attribute of the element to search for (e.g. src, class, id);

2. The regular expression to match in the given attribute.

Remember that jQuery's selector engine is already very powerful and you can search for any attribute with
[attribute="value"] for an attribute with "value" as its value, [attribute^="value"] for an
attribute's value starting with "value", [attribute$="value"] for an attribute's value ending with "value"
and so on. A comprehensive list can be found at http://api.jquery.com/category/selectors/.

Because of that our selector filter should be used for more complex selections only (the simpler ones are doable
with jQuery's default selector engine).

We'll also take in consideration how the default jQuery's selector engine works and make it consistent and
coherent with it (by being able to use the quotes or single quotes to define the value for the RegEx).

Here's how our selector filter looks like (see Listing 9-3):

Listing 9-3: Our :match() selector filter's JavaScript

$.extend( $.expr[':'], {
'match': function( element, idx, matches ) {
var parametersRegEx = /^([^=]+)(?:=)(?:\"|\')?\/(.*)\/([^\"\']+)?(?:\"|\')?
$/;// This is the regular expression that will separate our attr=/regexp/ with
the variations including quotes, single quotes, and no quotes.

var parameters = matches[3].match( parametersRegEx );

if ( ! parameters || ! parameters[1] || ! parameters[2] ) {

Pro jQuery Plugins v1.0.0


page 273
$.error( 'The filter expression is invalid. Please use :match(attr="/
RegExp/")' );
return false;
}

var queryAttribute = parameters[1];


var queryRegExModifiers = parameters[3] ? parameters[3] : '';
/*
We need a way to get forward slashes in the RegExp, but it would be too
complicated to implement it in the parametersRegEx, so we make it possible for
the user to use [FS] in the RegExp, that will then be replaced with the forward
slashes.
*/
var queryRegEx = new RegExp( parameters[2].replace( /(\[FS\])/g, '\/' ),
queryRegExModifiers );

return queryRegEx.test( $(element).attr(queryAttribute) );


}
});

NOTE: The [FS] could be replaced in the code by whatever the user saw fit. The problem is that we're
using the slashes to delimit our RegExp, so we need a way to still be able to use them, without breaking
our parameters.

So, to find, for example, all paragraphs with classes that have "span-NUMBER", where NUMBER is any given
number (useful, for example, in grid systems like Twitter's Bootstrap uses), we'd use it like this (see Listing 9-4):

Listing 9-4: Matching paragraphs that have a class "span-NUMBER"

$('p:match(class="/span-([0-9]+)/gi")');

Another usage example would be to find all <img> elements whose images are external (see Listing 9-5):

Pro jQuery Plugins v1.0.0


page 274
Listing 9-5: Matching images that are external

$('img:match(src="/^(f|ht)tp(s)?:[FS][FS](.*)/gi")');// This becomes the same


as /^(f|ht)tp(s)?:\/\/(.*)/gi

NOTE: Above we're basically only checking if the image src starts with ftp://, ftps://, http://, or https://,
which may not mean the image is external, but it's only a simple use case

A :data() selector
The final example in this chapter is a :data() selector filter. You'll build this one with a bit more functionality.

1. It will be able to filter elements with a given data property or not;

2. It will be able to filter elements with a given property data that equals to a given number;

3. It will be able to filter elements with a given property data that equals to a given boolean value;

4. It will be able to filter elements with a given property data that equals to a given string;

We'll again take in consideration how the default jQuery's selector engine works and make it able to use the
quotes or single quotes to define the value for the data property.

This is how our selector filter looks like (see Listing 9-6):

Listing 9-6: Our :data() selector filter's JavaScript

$.extend( $.expr[':'], {
'data': function( element, idx, matches ) {
var parametersRegEx = /^([^=]+)(?:=)?(\"|\')?(.+?)?(?:\"|\')?$/;// This is
the regular expression that will separate our property=value with the variations
including quotes, single quotes, and no quotes.

Pro jQuery Plugins v1.0.0


page 275
var parameters = matches[3].match( parametersRegEx );

if ( ! parameters || ! parameters[1] ) {
$.error( 'The filter expression is invalid. Please
use :data(property), :data(property=BooleanORInteger)
OR :data(property="value")' );
return false;
}

var queryProperty = parameters[1];

if ( ! parameters[3] ) {
// We're just trying to see if the data property exists or not
return ( $(element).data(queryProperty) !== undefined );
} else {
// We're looking for a specific value
if ( ! parameters[2] ) {
// We're looking at a boolean or integer
if ( /^(true|false)$/.test(parameters[3]) ) {
// We're looking for a boolean
if ( parameters[3] === 'true' ) {
return ( $(element).data(queryProperty) === true );
} else {
return ( $(element).data(queryProperty) === false );
}
} else {
// We're looking for an integer
var queryInteger = window.parseInt( parameters[3], 10 );

if ( window.isNaN(queryInteger) ) {
$.error( 'The filter expression is invalid. Format used
is :data(property=BooleanORInteger), but the value is not a valid boolean nor
integer' );
return false;
}

return ( $(element).data(queryProperty) === queryInteger );


}
} else {
// We're looking for a string
return ( $(element).data(queryProperty) === parameters[3] );
}
}
Pro jQuery Plugins v1.0.0
page 276
}
});

One possible use case is to look for images already loaded. We'll add a "loaded" data property with value = true
when an image loads. Before loading, it won't have this data attribute, so we can check it easily just by verifying
if the data property exists or not (see Listing 9-7).

Listing 9-7: JavaScript example to look for loaded images

// This will disable cache on images so we can see the load action being
triggered, otherwise it would be too fast (and IE is known to behave erratically
firing the load event for cached images)
$('img').each( function() {
$(this).attr( 'src', $(this).attr('src') + '?' + new Date().getTime() );
});

// We're adding the data property "loaded" with the value of "true" when the
images finish loading
$('img').on( 'load', function() {
$(this).data( 'loaded', true );
});

// This timeout is just to mimic other code running before we got to our images
loaded verification
window.setTimeout(function() {
$('img:data(loaded)').each(function() {
// Do whatever you want here
});
}, 1000 );

Now, imagine you have, in a blog category listing, the number of posts each category has (see Listing 9-8). We
can select the categories that only have one post by doing this (see Listing 9-9):

Listing 9-8: Sample HTML with the number of posts a given category has, as a data
property (data-posts attribute)

<ol class="post-categories">
<li data-posts="12">General</li>
Pro jQuery Plugins v1.0.0
page 277
<li data-posts="4">News</li>
<li data-posts="1">Developers</li>
<li data-posts="1">Advertisers</li>
<li data-posts="2">Guest Posts</li>
</ol>

Listing 9-9: JavaScript to look for the categories with a single post

$('.post-categories li:data(posts=1)').each( function() {


// Do something here
});

Note how we took advantage of jQuery's data-* attribute auto-loading.

As you can see, this can be a very powerful and helpful selector filter.

Summary
In this chapter you've learned about jQuery's selector filters and how to extend them with basic and complex
functionality.

You've also been presented with a few use cases for these samples.

In the next chapter I'll tell you about a few jQuery plugins that are widely used and often repeatedly used in
many projects due to their broad functionality or usefulness.

Pro jQuery Plugins v1.0.0


page 278
Chapter 10:
Useful Repeating Plugins

In this chapter I'm going to talk about some useful plugins that are commonly used, and that I often find myself
using them in a variety of projects.

jQuery UI
jQuery UI needs little introduction. It's built by the same people that build jQuery.

It is a curated set of user interface interactions, effects, widgets, and themes built on top of jQuery.

The parts I personally find more useful are the following Interactions/Events:

Draggable

Enables dragging functionality on any DOM element.

Droppable

Enables any DOM element to be a target for draggable elements, where they can be dropped into.

Resizable

Enables any DOM element to be resizable.

As you can imagine, these can be very useful in many situations, and you won't need to "reinvent the wheel",
while having properly built functionalities.

Pro jQuery Plugins v1.0.0


page 279
It has a couple more Interactions/Events, and a lot of Widgets, Effects, and Utilities, which are great, although I
usually and personally prefer to build custom ones for these (I always get something more lightweight and
simple, because I don't need to build for so many use cases). jQuery UI is great for this because it allows you to
download a custom build with only the functionalities you want/need from it.

You can learn more about it at http://jqueryui.com

jQuery Mobile
This one is also built by The jQuery Foundation.

jQuery Mobile is a touch-optimized HTML5 UI framework designed to make sites and apps that are accessible
on all popular smartphone, tablet and desktop devices.

I personally only use it in projects that need/require touch events/actions, because the rest of the interface
interactivity is usually customized to the specific project's needs.

To learn how to properly use touch events with jQuery Mobile, check out their FAQ about it ( http://
view.jquerymobile.com/1.3.0/docs/faq/how-do-i-use-touch-mouse-events.php ).

You can learn more about it at http://jquerymobile.com

Chosen
Chosen is not a jQuery plugin but a JavaScript one that makes long, unwieldy select boxes much more user-
friendly. It does have a jQuery plugin for easier usage, though, and that's why I've included it.

It's built by Harvest, and it works really well in many scenarios. I've used it in quite a few projects and definitely
recommend it.

You can learn more about it at http://harvesthq.github.com/chosen/

Spin.js
Spin.js is also a plugin that does not require jQuery, but supports it for easier usage. It creates a "loading
spinning icon" without using images or CSS, while being reliable in cross-browser (even IE 6!), and its "design" is
easily customizable.

It's a great plugin I've been using for quite a while now as well.

You can learn more about it at http://fgnass.github.com/spin.js/

Pro jQuery Plugins v1.0.0


page 280
Fancybox
Fancybox is a lightbox-like plugin, that I mostly use to showcase galleries/images/videos. It has a lot of other
uses, like simulating popups, among other things.

It's very easy to use and customize.

You can learn more about it at http://fancyapps.com/fancybox/

Hammer.js
As some of the plugins above, this one doesn't require jQuery, but supports it (and it requires jQuery for older
browser support).

This is a very nice plugin that basically enables you to use multi-touch gestures on your web site or web app.
Yes, awesome.

You can learn more about it at http://eightmedia.github.com/hammer.js/

gridster.js
This plugin is one that I've only used a couple of times for customizable dashboards. You could accomplish
something similar with jQuery UI's draggable and droppable, but this does do so much more already, like
automatically reorganizing the elements while dragging.

You can learn more about it at http://gridster.net

Knob
This jQuery plugin is also a very nice one, that I've never got a chance to use, yet, but I've got plans for it. It
creates a very nice effect, and the fact you can use arrow keys, scrolling, drag & drop, among other things, is
marvelous.

You can learn more about it at http://anthonyterrien.com/knob/

Noty
Notifications. Most web apps will need them. If you need a simple notification, this will most likely be an overkill,
but if you want to interact with them in several ways, you definitely need to take a look at Noty. It's really good.

You can learn more about it at http://needim.github.com/noty/

Pro jQuery Plugins v1.0.0


page 281
jqPagination
Many websites and web apps will need pagination. This jQuery plugin makes it very easy to add interactive and
very easy to use pagination to any web project. It's design agnostic and degrades beautifully.

You can learn more about it at http://beneverard.github.com/jqPagination/

Joyride
If you're building a web project that may require initial instructions, this is a very nice and easy way to do so. I
haven't used it yet, personally, but I have a use case for it in the near future.

You can learn more about it at http://www.zurb.com/playground/jquery-joyride-feature-tour-plugin

timeago
Timeago is a jQuery plugin that makes it easy to have automatically-updated "human" timestamps (e.g. "4
minutes ago" or "about 1 day ago"). I love the fact they support HTML5's new "time" tag.

You can learn more about it at http://timeago.yarp.com

Summary
In this chapter I've shown you a few very nice jQuery (and JavaScript) plugins that are useful in many situations
and some of them you'll find yourself using more than once.

I hope you enjoyed this book as much as I enjoyed writing it.

The following appendices have some great information not directly related to jQuery plugin development, but
they're very important.

Pro jQuery Plugins v1.0.0


page 282
Appendix A:
Fast-paced evolution

You've probably heard of Moore's law (if you haven't, look it up). It can arguably be applied to software as well,
but you have to keep in mind that the web evolves very quickly, so it's a good practice to keep up to date with
jQuery's latest versions, update your plugins to use the latest version of jQuery, when possible, but learn and
understand the differences between versions, don't just update "blindly", because they can deprecate and
remove methods in a version update. Learn what the new/preferred methods are, and why should you use them
instead.

This obviously can and should be applied to other parts of the web as well, that are intrinsically connected with
jQuery, like JavaScript, HTML, and CSS.

There are a few websites I'd recommend you visit to learn more about jQuery and JavaScript, as well as keeping
updated.

JSPRO.com ( http://jspro.com ) has many useful articles with tutorials

CodeSchool ( http://www.codeschool.com/paths/javascript ) is a great way to learn by doing. It might


be a bit more for beginners, but covers some nice advanced topics and I just love their concept anyway (I've
learned a bit about Ruby and Rails there)

Tuts+ ( https://tutsplus.com ) is a nice place with many eBooks, tutorials and courses about many
topics (JavaScript and jQuery as well).

Pro jQuery Plugins v1.0.0


page 283
Appendix B: Complements

Here is a list of other components that will complement and add great value to your plugin development (if you
consider and support them) and most importantly, JavaScript development, but are out of this book's scope.

CoeeScript
From their website:

CoffeeScript is a little language that compiles into JavaScript. () JavaScript has always
had a gorgeous heart. CoffeeScript is an attempt to expose the good parts of JavaScript in a
simple way.

It is a great preprocessor of JavaScript, and I definitely recommend you to learn it and start using it in your
projects and plugins. The main advantage is it produces better code with less effort.

You can learn more about it at http://coffeescript.org

Backbone.js
From their website:

Backbone.js gives structure to web applications by providing models with key-value


binding and custom events, collections with a rich API of enumerable functions, views with
declarative event handling, and connects it all to your existing API over a RESTful JSON
interface.

Basically, it brings "MVC" to the front-end. And it is awesome.

You can learn more about it at http://backbonejs.org

Pro jQuery Plugins v1.0.0


page 284
AngularJS
AngularJS is a different approach from Backbone.js to bring structure to the front-end. Instead of abstracting
the HTML/CSS/JS, it "enhances" the HTML to be used to declare dynamic views. It basically allows you to
produce more "intuitive" code.

You can learn more about it at http://www.angularjs.org

RequireJS
From their website:

RequireJS is a JavaScript file and module loader. It is optimized for in-browser use ().
Using a modular script loader like RequireJS will improve the speed and quality of your code.

You should learn and start using RequireJS as well in your projects and plugins, definitely.

You can learn more about it at http://requirejs.org/docs/jquery.html

Modernizr
From their website:

Modernizr is a JavaScript library that detects HTML5 and CSS3 features in the user's
browser.

So simple, yet so useful.

You can learn more about it at http://modernizr.com

Pro jQuery Plugins v1.0.0


page 285
Conditionizr
From their website:

Conditionizr is a fast and lightweight (3KB) javascript utility that detects browser vendor,
touch features and retina displays - allowing you to serve conditional JavaScript and CSS
files.

Again, simple, but very useful.

You can learn more about it at http://conditionizr.com

Zepto.js
From their website:

Zepto is a minimalist JavaScript library for modern browsers with a largely jQuery-
compatible API. If you use jQuery, you already know how to use Zepto.

Basically, if you don't need to support IE, zepto can be a good choice for a lighter solution than jQuery (it's
much faster and lighter, because it doesn't aim to support as much).

You can learn more about it at http://zeptojs.com

Pro jQuery Plugins v1.0.0


page 286
Appendix C:
Interesting Experiments

Here are a few interesting experiments that you should take a look at to realize the potential of HTML5 + JS +
CSS3 and just learn by exploring their source codes.

Some of them are also very useful and usable.

Pep: http://pep.briangonzalez.org
Radar: http://lab.hakim.se/radar/

dynamoCanvas: http://iwhitcomb.github.com/dynamocanvas/
Spritespin: http://spritespin.ginie.eu/index.html

Gauge.js/coffee: http://bernii.github.com/gauge.js/

Reveal.js: http://lab.hakim.se/reveal-js/
Makisu: https://github.com/soulwire/Makisu

Chrome Experiments: http://www.chromeexperiments.com

Pro jQuery Plugins v1.0.0


page 287

You might also like