Академический Документы
Профессиональный Документы
Культура Документы
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.
The book, like the code, is licensed under CC BY-SA 3.0 ( http://creativecommons.org/licenses/by-sa/3.0/
deed.en_US ).
Introduction
6
Objectives
6
JavaScript first 7
Personal Preferences 8
$ vs jQuery 9
JSON vs XML 10
Overlooked novelties 10
Template engines 13
Internationalization (i18n) 13
Publishing 13
Summary 44
Summary 65
Summary 86
Summary 123
Summary 144
Summary 176
Summary 209
Summary 270
Summary 278
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.
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.
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 ).
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.
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.
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.
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.
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.
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.
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.
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.
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().
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/.
This chapter covers the basics of jQuery plugins, concepts, and best practices.
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.
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.
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).
(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {};
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!' );
}
};
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):
(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):
(function( $, window ) {
var methods = {
destroy : function() {
$(window).off( '.pluginSkeleton' );
return this.each(function(){
var $this = $(this),
data = $this.data( 'pluginSkeleton' );
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.
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.
// 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.
// Do something here
});
A good example is the one at jQuery Docs ( http://api.jquery.com/event.data/ ), also shown in Listing 1-6.
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 ).
Figure 1-2: Same benchmark in a bar graph (the bigger the bar, the better)
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
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.
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.
(function( $, window ) {
$.fn.sampleSlideDownAndAlert = function() {
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideDownAndAlert' );
if ( ! data ) {
$(this).data( 'sampleSlideDownAndAlert', {
target : $this
});
(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
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideDownAndAlert' );
if ( ! data ) {
$(this).data( 'sampleSlideDownAndAlert', {
target : $this
});
destroy : function() {
$(window).off( '.sampleSlideDownAndAlert' );
return this.each(function(){
var $this = $(this),
data = $this.data( 'sampleSlideDownAndAlert' );
$this.removeData( 'sampleSlideDownAndAlert' );
});
},
if ( $.isFunction(options.onComplete) ) {
options.onComplete.call( this, options );
}
});
}
};
}
};
})( 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.
(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
});
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.
(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'
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleLoadPage' );
if ( ! data ) {
$(this).data( 'sampleLoadPage', {
target : $this,
options: options
});
destroy : function() {
$(window).off( '.sampleLoadPage' );
return this.each(function(){
var $this = $(this),
data = $this.data( 'sampleLoadPage' );
$this.removeData( 'sampleLoadPage' );
});
},
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).
(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).off( 'mousewheel' );
$(this).off( 'mousewheel' );
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
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 );
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 );
}
},
$(this).off( 'mousewheel.sampleSideScroll' );
if ( $.isFunction(options.onMinReach) ) {
options.onMinReach.call( this, options );
}
if ( $.isFunction(options.onReach) ) {
options.onReach.call( this, options );
}
}
},
$(this).off( 'mousewheel.sampleSideScroll' );
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 );
}
}
}
};
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.
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).
(function( $, window ) {
$.fn.sampleSlideshow = function() {
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideshow' );
if ( ! data ) {
$this.data( 'sampleSlideshow', {
target : $this
});
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.
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.
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'sampleSlideshow' );
if ( ! data ) {
$this.data( 'sampleSlideshow', {
target : $this,
options: options
});
/*
$this.find(options.slideSelector).on( 'click.sampleSlideshow',
function( event ) {
window.clearTimeout( flags.slideTimeout );
});
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 );
// 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 );
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.
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).
(function( $, 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 ) {
// ...
// ...
These classes should ideally be in a setting, like all the previous examples, but this example is just to make a
simple point.
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( $ ) {
// ...
$('#area').append( html );
// ...
})( jQuery );
(function( $ ) {
// ...
$('.search-wrapper').append( html );
// ...
})( jQuery );
Listing 1-19: Good Example with Average Performance and Great Flexibility
(function( $ ) {
// ...
$('.search-wrapper').append( html );
// ...
})( jQuery );
Performance is better when you try to get the search input, though, with $('#search-' + generatedID).
In the next chapter, you'll start building a simple plugin from scratch that will validate input fields.
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).
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.
.sample-form {
display: block;
padding: 10px;
background: #FFF;
}
.sample-form input {
width: 200px;
font-size: 14px;
}
.sample-form button {
font-size: 16px;
}
(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'
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'inputValidation' );
if ( ! data ) {
$this.data( 'inputValidation', {
target : $this
});
destroy : function() {
$(window).off( '.inputValidation' );
return this.each(function(){
var $this = $(this),
data = $this.data( 'inputValidation' );
$this.removeData( 'inputValidation' );
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 );
};
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
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'inputValidation' );
if ( ! data ) {
$this.data( 'inputValidation', {
target : $this
destroy : function() {
$(window).off( '.inputValidation' );
return this.each(function(){
var $this = $(this),
data = $this.data( 'inputValidation' );
$this.removeData( 'inputValidation' );
});
},
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;
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 );
return false;
}
return false;
}
return true;
},
// 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 );
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.
.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.
(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
}
}
};
// ...
},
// ...
// ...
return false;
}
return false;
}
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 );
}
},
// ...
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.
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.
(function( $, window ) {
// ...
return false;
}
return false;
}
// ...
// ...
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.
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.
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).
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.
(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.
return this.each(function() {
var $this = $(this),
data = $this.data( 'toolTip' );
if ( ! data ) {
$this.data( 'toolTip', {
target : $this
});
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
(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 );
};
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,
return this.each(function() {
var $this = $(this),
data = $this.data( 'toolTip' );
if ( ! data ) {
$this.data( 'toolTip', {
target : $this
});
destroy : function() {
// ...
},
// 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', '' );
$('#' + data.toolTipID).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,
Figure 3-1: The tooltip plugin output, without any mouse hover
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.
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 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.
(function( $, window ) {
// ...
var methods = {
// ...
// 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', '' );
// 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' );
$('#' + 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 });
}
// ...
};
// ...
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).
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 );
});
}
}
});
},
// ...
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();
}
},
// ...
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).
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
};
// ...
},
// ...
if ( ! options.followMouse ) {
// ...
// ...
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.
$('.sample-form input').inputValidation({
'animation': {
'type': 'fade',
'speed': 'fast',
'easing': 'swing',
'onComplete': function() {
$('.inputValidation-error').toolTip({ 'followMouse': false, 'position':
'right' });
},
'extra': {
'margin': 3
}
}
});
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.
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.
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)
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.
<div class="thumbnails">
<div class="thumbnail">
h1, p {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
margin: 10px 5px;
}
h1 {
p {
font-size: 14px;
}
.thumbnails {
display: block;
padding: 10px;
}
.thumbnails .thumbnail {
display: inline-block;
width: 48%;
margin: 0.8%;
text-align: center;
}
.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;
}
(function( $, window ) {
var methods = {
init : function( options ) {
var defaults = {
'lightboxID': 'lightbox',
'isModal': false,
'escapeCloses': true
};
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
});
destroy : function() {
$(window).off( '.lightbox' );
return this.each(function() {
$this.removeData( 'lightbox' );
});
},
(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 );
};
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
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'lightbox' );
if ( ! data ) {
$this.data( 'lightbox', {
target : $this
destroy : function() {
$(window).off( '.lightbox' );
return this.each(function() {
var $this = $(this),
data = $this.data( 'lightbox' );
$this.removeData( 'lightbox' );
});
},
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();
// 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 );
});
}
/*
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;
$('#' + options.lightboxID).css({
'margin-left': (lightboxWidth / 2 * -1) + 'px',
'margin-top': (lightboxHeight / 2 * -1) + 'px'
});
}
}
}
};
// ...
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!
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.
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)
};
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
};
$this.data( 'lightbox', {
target : $this,
options: itemOptions,
galleryIndex: imageGalleryIndex
});
destroy : function() {
// ...
},
if ( options.escapeCloses ) {
// Bind Escape Key to close the lightbox
$(document).on( 'keydown.lightbox', function( event ) {
if ( event.keyCode === 27 ) {
event.preventDefault();
// 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();
if ( $(this).hasClass(options.leftArrowButtonAddedClass) ) {
methods.navigateLeft.call( this, options );
} else if ( $(this).hasClass(options.rightArrowButtonAddedClass) ) {
methods.navigateRight.call( this, options );
}
});
$('#' + 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;
}
},
// Hide arrows
$('#' + options.lightboxID + ' .' + options.arrowButtonClass).hide();
},
if ( itemGallery.length > 1 ) {
if ( itemGalleryIndex >= 0 ) {
// Show arrows
$('#' + options.lightboxID + ' .' +
options.arrowButtonClass).show();
if ( $.isFunction(options.openOnComplete) ) {
options.openOnComplete.call( this, options );
}
};
objImagePreloader.src = imageSrc;
},
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 );
}
},
if ( globals.currentGallery.length > 0 ) {
var newIndex = (globals.currentGalleryIndex - 1);
if ( globals.currentGallery.length > 0 ) {
var newIndex = ( globals.currentGalleryIndex + 1 );
// ...
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.
<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>
We also had to add a bit of CSS because of the buttons (see Listing 4-7).
.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 {
.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):
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'
};
// ...
// ...
// ...
if ( $.isFunction(options.openOnComplete) ) {
options.openOnComplete.call( this, options );
}
},
if ( $.isFunction(options.openOnComplete) ) {
options.openOnComplete.call( this, options );
}
}
};
// ...
We needed a couple of new templates and "thumbnails" to trigger the iframe and HTML new lightbox types (see
Listing 4-9).
<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>
Also, a few lines of CSS were added to make sure the new lightboxes looked ok (see Listing 4-10).
#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).
//$('.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
}
}
});
}
});
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.
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.
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.
1. Just an image;
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.
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.
<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">
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;
.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;
}
NOTE: We've hidden the left arrow by default because at the first slide, it makes no sense to show it.
(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
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'slider' );
if ( ! data ) {
$this.data( 'slider', {
target : $this
});
destroy : function() {
$(window).off( '.slider' );
return this.each(function(){
var $this = $(this),
data = $this.data( 'slider' );
(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)
};
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
});
destroy : function() {
// ...
},
var itemDefaults = {
'animationType': options.animationType,
'animationSpeed': options.animationSpeed
};
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 );
}
if ( slides.length > 0 ) {
var newIndex = ( currentIndex - 1 );
if ( slides.length > 0 ) {
var newIndex = ( currentIndex + 1 );
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.
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
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.
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!
(function( $, window ) {
var methods = {
init : function( options ) {
// ...
return this.each(function() {
var $this = $(this),
data = $this.data( 'slider' );
if ( ! data ) {
$this.data( 'slider', {
target : $this
});
// ...
// ...
switch ( itemOptions.animationType ) {
case 'fade':
Pro jQuery Plugins v1.0.0
page 138
default:
// ...
// Do animation
$this.fadeIn( itemOptions.animationSpeed, function() {
// ...
// ...
if ( slides.length > 0 ) {
// ...
if ( slides.length > 0 ) {
// ...
globals.timeoutCanceled = true;
}
};
// ...
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):
<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):
.slider .slider-bottom-nav {
display: block;
text-align: center;
Pro jQuery Plugins v1.0.0
page 141
width: 520px;
padding-top: 10px;
}
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).
(function( $, window ) {
var globals = {
// ...
'options': {}
};
var methods = {
globals.options = options;
// ...
},
destroy : function() {
// ...
},
getOptions : function() {
return globals.options;
},
// ...
};
// ...
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' );
}
});
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!
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.
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).
Since we already prepared the plugin in the previous chapter for animation support, we don't need to implement
that structure.
(function( $, window ) {
// ...
var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide'
// ...
};
// ...
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 );
}
// ...
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>
/* ... */
.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?
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.
(function( $, window ) {
// ...
var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide', 'horizontal'
// ...
};
// ...
},
// ...
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 )
};
}
// 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;
}
},
// ...
};
// ...
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).
Vertical slide
This animation looks great, but it would also look awesome if it was vertical! Let's do that (see Listing 6-5).
(function( $, window ) {
// ...
var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide', 'horizontal',
'vertical'
// ...
};
// ...
Pro jQuery Plugins v1.0.0
page 155
},
// ...
// ...
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 )
};
}
// Do animation
$this.show().css( nextForceCSS ).animate( nextNewCSS,
itemOptions.animationSpeed, function() {
$(this).addClass( options.activeSlideClass );
commonActionsAfterAnimation.call( this );
});
break;
// ...
}
},
// ...
};
// ...
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.
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).
<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>
Listing 6-7: CSS for our two new sliders to be specially animated
/* // ... */
@-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% {
@-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
}
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).
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
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/.
<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>
/* // ... */
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);
}
}
/* 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);
}
}
Again, I can only show you statically how they look during their transitions (see Figure 6-7 and Figure 6-8).
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.
(function( $, window ) {
// ...
var methods = {
init : function( options ) {
var defaults = {
// ...
'animationType': 'fade',// supports 'fade', 'slide', 'horizontal',
'vertical', 'random'
// ...
};
// ...
},
// ...
switch ( animationType ) {
// ...
}
},
// ...
};
// ...
Now that we've made our plugin more flexible and added a lot of animation options, we're going to make it
mobile-friendly.
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.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`.
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 .slider-bottom-nav {
display: block;
text-align: center;
width: 100%;
padding-top: 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,
// ...
};
// ...
return this.each(function() {
// ...
});
},
// ...
};
// ...
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.
In the next chapter, we'll start building a Timeline plugin, similar to Facebook's own timeline.
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.
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 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().
// 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.
// 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!' );
2.2. deferred.fail( failCallbacks ) will add a callback to be executed when the deferred object is
rejected.
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.
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.
// 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 );
});
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.
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;
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.
First, let's start with the actual post markup (see Listing 7-7):
<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>
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).
<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>
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.
<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).
<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>
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).
<!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">
<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>
<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):
/*
* 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;
}
/*
* 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 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).
/*
* 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://
#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 .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;
}
Now, we're just missing the CSS for the "Add Post" form (see Listing 7-15).
/*
* 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: "";
}
/*
* 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 .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;
}
And this is how the "static" demo of how our plugin will work looks right now (see Figure 7-1):
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.
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.
(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)
*/
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' );
});
}
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).
{
"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).
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
};
destroy : function() {
// ...
},
$(this).append( postsHTML );
}
// ...
})( 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:
[
{
"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).
(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
};
return this.each(function() {
var $this = $(this),
data = $this.data( 'timeline' );
if ( ! data ) {
$this.data( 'timeline', {
target : $this
});
destroy : function() {
// ...
},
// 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
// ...
})( jQuery, _, window );
Let's check what we've got so far in the browser (see Figure 8-1).
(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"
};
// ...
},
// ...
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;
}
// ...
})( jQuery, _, window );
NOTE: This method needs to be executed every time a post is added (including the initial listing)
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).
(function( $, _, window ) {
// ...
var methods = {
// ...
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;
}
// 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';
}
}
// ...
})( jQuery, _, window );
NOTE: We need to run this before the fixPointer method, so the starting position considers being in the
right place.
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.
(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
// ...
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;
}
// 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();
}
// 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();
}
// ...
})( jQuery, _, window );
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.
[
{
"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",
[
{
"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",
[]
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).
(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
destroy : function() {
// ...
},
// 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
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;
}
++globals.options.currentPage;
methods.get.call( this );
}
// ...
})( 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).
We can now see the second page of posts. Test it out (see Figure 8-5).
(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
// ...
};
// ...
},
// ...
$.ajax({
url: globals.options.createURL,
// We need to prepend
$this.prepend( postsHTML );
methods.fixPosition.call( that );
methods.fixPointer.call( that );
methods.parseDates.call( that );
}
}
});
}
// ...
})( jQuery, _, window );
Listing 8-17: Our demo function to add a post to the timeline plugin
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()
};
// ...
})( 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).
{
"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).
(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() {
// ...
// ...
});
},
fixPosition : function() {
// ...
},
parseDates : function() {
// ...
},
getNextPage : function() {
// ...
},
$.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
}
});
},
$.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();
methods.fixPosition.call( that );
methods.fixPointer.call( that );
methods.parseDates.call( that );
}
}
});
}
};
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)
{
"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>
<!-- ... -->
/* ... */
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 content */
#posts .post .content {
/* ... */
}
/* ... */
var goOn = window.confirm( 'Are you sure you want to delete this
post?' );
if ( goOn ) {
$('#posts').timeline( 'deletePost', postElement.data('id') );
}
});
// ...
})( jQuery, window, document );
[
{
"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": ""
}
]
[
{
"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.",
The first animation we'll do is for the loading of the posts, which will slide in from the bottom (see Listing 8-26).
/* ... */
#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;
}
Listing 8-27: CSS animation for the new posts being added
/* ... */
#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 = {
// ...
},
// ...
$.ajax({
// ...
dataType: 'json',
success: function( response ) {
if ( response ) {
postObject.id = new Date().getTime();// Ideally this would be a
value obtained from the server response.
// ...
}
}
});
},
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).
(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
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 );
});
globals.options.deletePostProcessor = [];
globals.options.deletePostProcessor.push(function( that ) {
globals.options.deletePostProcess.call( that );
});
globals.options.addPostProcessor.push(function( that ) {
methods.postProcessing.call( this, that );
});
globals.options.deletePostProcessor.push(function( that ) {
methods.postProcessing.call( this, that );
});
// ...
},
// ...
$.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 );
getNextPage : function() {
// ...
},
$.ajax({
// ...
success: function( response ) {
if ( response ) {
postObject.id = new Date().getTime();// Ideally this would be a
value obtained from the server response.
methods.executeCallbackStack.call( this,
globals.options.addPostProcessor, that );
}
}
});
},
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();
methods.executeCallbackStack.call( this,
globals.options.listPreProcessor, this );
methods.executeCallbackStack.call( this,
globals.options.listPostProcessor, this );
},
fixPointer : function() {
// ...
},
fixPosition : function() {
// ...
},
parseDates : function() {
// ...
},
methods.fixPointer.call( postsElement );
methods.parseDates.call( postsElement );
},
Pro jQuery Plugins v1.0.0
page 267
executeCallbackStack : function ( callbackStack ) {
var iLimit = callbackStack.length;
// ...
})( 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.
// ...
});
// ...
})( jQuery, window, document );
Listing 8-31: Final Demo JavaScript, now with the loading animation on the form
// ...
});
// ...
})( jQuery, window, document );
/* ... */
/* ... */
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.
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.
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';
}
});
And to fetch the paragraph elements with display: inline, we'd simply do (see Listing 9-2):
$('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:
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.
1. The attribute of the element to search for (e.g. src, class, id);
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):
$.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.
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):
$('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):
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.
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):
$.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.
if ( ! parameters || ! parameters[1] ) {
$.error( 'The filter expression is invalid. Please
use :data(property), :data(property=BooleanORInteger)
OR :data(property="value")' );
return false;
}
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;
}
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).
// 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
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.
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
Droppable
Enables any DOM element to be a target for draggable elements, where they can be dropped into.
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.
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 ).
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.
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.
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.
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.
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.
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.
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.
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.
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.
The following appendices have some great information not directly related to jQuery plugin development, but
they're very important.
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.
Tuts+ ( https://tutsplus.com ) is a nice place with many eBooks, tutorials and courses about many
topics (JavaScript and jQuery as well).
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.
Backbone.js
From their website:
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.
Modernizr
From their website:
Modernizr is a JavaScript library that detects HTML5 and CSS3 features in the user's
browser.
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.
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).
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.
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