Duck Punched Filter
Something that has always bothered me is the inability to pass in arguments to custom filter functions. With jQuery 1.4.x, this problem has been semi-fixed, in that you can pass a function into the filter method, and base selections on the current scope. But what about when you have a filter that is used across multiple scripts and scopes, you still have to copy and paste the function between the scopes. Before we can solve this problem, lets take a look at what goes on when using the filter method.
Background
// http://github.com/jquery/jquery/blob/1.4.2/src/traversing.js#L75
filter: function( selector ) {
return this.pushStack( winnow(this, selector, true), "filter", selector );
},
This doesn't tell us much other than it utilizes the winnow function to create a new stack.
// http://github.com/jquery/jquery/blob/1.4.2/src/traversing.js#L8
var winnow = function( elements, qualifier, keep ) {
if ( jQuery.isFunction( qualifier ) ) {
return jQuery.grep(elements, function( elem, i ) {
return !!qualifier.call( elem, i, elem ) === keep;
});
} else if ( qualifier.nodeType ) {
return jQuery.grep(elements, function( elem, i ) {
return (elem === qualifier) === keep;
});
} else if ( typeof qualifier === "string" ) {
var filtered = jQuery.grep(elements, function( elem ) {
return elem.nodeType === 1;
});
if ( isSimple.test( qualifier ) ) {
return jQuery.filter(qualifier, filtered, !keep);
} else {
qualifier = jQuery.filter( qualifier, filtered );
}
}
return jQuery.grep(elements, function( elem, i ) {
return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
});
};
This is part of what we need. The winnow function is a proxy for both the filter and not methods. It runs the qualifier on the current stack, and then returns a new list of elements that passed the qualifier(or failed for not method). Lets break it down for a better understanding.
- If a function is passed, we loop through each of the current elements, and add those that pass the function into a new array and return it
- If and element is passed, we search through the current stack for the element that matches the one passed, and return an array containing only that element.
- If a string is passed, then Sizzle is used to filter based on the query and returns that array.
- The last catch-all handles instances where an array of elements is passed into the not method to check against and remove from the current stack. You can pass an array of elements for the filter method as well, but it would be faster to just use jQuery( [ elems ] ), since that's exactly what you need.
// http://github.com/jquery/jquery/blob/1.4.2/src/core.js#L198
pushStack: function( elems, name, selector ) {
// Build a new jQuery matched element set
var ret = jQuery();
if ( jQuery.isArray( elems ) ) {
push.apply( ret, elems );
} else {
jQuery.merge( ret, elems );
}
// Add the old object onto the stack (as a reference)
ret.prevObject = this;
ret.context = this.context;
if ( name === "find" ) {
ret.selector = this.selector + (this.selector ? " " : "") + selector;
} else if ( name ) {
ret.selector = this.selector + "." + name + "(" + selector + ")";
}
// Return the newly-formed element set
return ret;
},
Alright, so after a new set of elements are configured, we move onto the pushStack method, which creates a blank jQuery instance, merge the new set into it, store a reference to the pre-filter instance for use in the end method, and set the context and selector properly.
The Duck Punched Filter
So how do we hack this to allow for dynamic arguments passed into the qualifier? We duck punch it.
/*
* Duck Punched Filter
* May 1, 2010
* Corey Hart @ http://www.codenothing.com
*/
(function( $, undefined ) {
// Keep a copy of the old filter for reference
var old = $.fn.filter;
// Create space for holding filter custom filter functions
$.filters = {};
// Overload the filter method
$.fn.filter = function( pass, args, fn ) {
// Make sure the qualifier was passed, otherwise use old filter
if ( args === undefined ) {
return old.apply( this, arguments );
}
// Reorganize
if ( $.isArray( pass ) ) {
fn = args;
args = pass;
pass = false;
}
else if ( $.isFunction( args ) ) {
fn = args;
if ( typeof pass == 'boolean' ) {
args = [];
} else {
args = pass;
pass = false;
}
}
// Setup qualifier to allow a stored reference or anonymous function
fn = typeof fn == 'string' ? $.filters[ fn ] : fn;
var self = this, elems = [], i = -1, l = self.length;
// Allow for full control of elems stack creation
if ( pass === true ) {
// Convert the current stack into array of elements, and spot
// it at the first position
args.unshift( self.toArray() );
elems = fn.apply( self, args ) || [];
}
else {
// Configure new set of elements based on qualifier and the 2 first
// spots in the arguments for index and current element
args.unshift( null );
args.unshift( null );
for ( ; ++i < l; ) {
args[ 0 ] = i;
args[ 1 ] = self[ i ];
if ( fn.apply( self[ i ], args ) === true ) {
elems.push( self[ i ] );
}
}
}
// Make the new jquery object
return self.pushStack( elems, 'filter', fn );
};
})( jQuery );
With the above patch, we can setup a basic filter in a different scope.
jQuery.filters.MyFilter = function( i, elem, key, value ) {
// Check the elements data cache based on the dynamic arguments
// Return true to push the element onto the new stack
return jQuery.data( elem, key ) === value;
};
And then we can call that new filter from anywhere within our applications. Here's a basic call that will chop the stack into elements that only have a 'coolname' data key of 'kaeden'.
// Using the function reference directly
jQuery('div').filter( [ 'coolname', 'kaeden' ], jQuery.filters.MyFilter );
// Using the key mapping to jQuery.filters space created by this patch
jQuery('div').filter( [ 'coolname', 'kaeden' ], 'MyFilter' );
Loop Reduction
Even better, we can reduce the function calls on every iteration, and control the full loop from within the filter. Here's the filter setup.
jQuery.filters.MyBetterFilter = function( elems, key, value ) {
var newStack = [], i = -1, l = elems.length;
for ( ; ++i < l; ) {
if ( jQuery.data( elems[ i ], key ) === value ) {
newStack.push( elems[ i ] );
}
}
return newStack;
};
And to call the better filter, we pass true as the first argument to force full control.
// Using the function reference directly
jQuery('div').filter( true, [ 'coolname', 'kaeden' ], jQuery.filters.MyBetterFilter );
// Using the key mapping to jQuery.filters space created by this patch
jQuery('div').filter( true, [ 'coolname', 'kaeden' ], 'MyBetterFilter' );
I must point out that this hacked filter has only been tested with jQuery-1.4.2, and is in no way future proof. In any case, you can both play with the demo, and download it below. Please, let me know if you find a bug.
8 jQuery Micro Optimization Tips
Warning: Please do not go back to old code and start rewriting based on the following optimizations. While they provide a performance boost, it is minimal at best, and not worth all the extra work of rewriting your applications. These tips are meant for improving future work only.
Warning 2: Most of these tips break the 'Write Less, Do More' jQuery motto, so use with caution and balance elegance with performance correctly.
Background
To start, let's review the jQuery function and it's init constructor.
// Define a local copy of jQuery
var jQuery = function( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init( selector, context );
},
Above is taken directly from the jQuery-1.4.2 source file. So every time you call the jQuery function, a new jQuery instance is created. It's important to keep this in mind, to limit the number of selections you do make.
init: function( selector, context ) {
var match, elem, ret, doc;
// Handle $(""), $(null), or $(undefined)
if ( !selector ) {
return this;
}
// Handle $(DOMElement)
if ( selector.nodeType ) {
this.context = this[0] = selector;
this.length = 1;
return this;
}
// The body element only exists once, optimize finding it
if ( selector === "body" && !context ) {
this.context = document;
this[0] = document.body;
this.selector = "body";
this.length = 1;
return this;
}
// Handle HTML strings
if ( typeof selector === "string" ) {
// Are we dealing with HTML string or an ID?
match = quickExpr.exec( selector );
// Verify a match, and that no context was specified for #id
if ( match && (match[1] || !context) ) {
// HANDLE: $(html) -> $(array)
if ( match[1] ) {
doc = (context ? context.ownerDocument || context : document);
// If a single string is passed in and it's a single tag
// just do a createElement and skip the rest
ret = rsingleTag.exec( selector );
if ( ret ) {
if ( jQuery.isPlainObject( context ) ) {
selector = [ document.createElement( ret[1] ) ];
jQuery.fn.attr.call( selector, context, true );
} else {
selector = [ doc.createElement( ret[1] ) ];
}
} else {
ret = buildFragment( [ match[1] ], [ doc ] );
selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes;
}
return jQuery.merge( this, selector );
// HANDLE: $("#id")
} else {
elem = document.getElementById( match[2] );
if ( elem ) {
// Handle the case where IE and Opera return items
// by name instead of ID
if ( elem.id !== match[2] ) {
return rootjQuery.find( selector );
}
// Otherwise, we inject the element directly into the jQuery object
this.length = 1;
this[0] = elem;
}
this.context = document;
this.selector = selector;
return this;
}
// HANDLE: $("TAG")
} else if ( !context && /^\w+$/.test( selector ) ) {
this.selector = selector;
this.context = document;
selector = document.getElementsByTagName( selector );
return jQuery.merge( this, selector );
// HANDLE: $(expr, $(...))
} else if ( !context || context.jquery ) {
return (context || rootjQuery).find( selector );
// HANDLE: $(expr, context)
// (which is just equivalent to: $(context).find(expr)
} else {
return jQuery( context ).find( selector );
}
// HANDLE: $(function)
// Shortcut for document ready
} else if ( jQuery.isFunction( selector ) ) {
return rootjQuery.ready( selector );
}
if (selector.selector !== undefined) {
this.selector = selector.selector;
this.context = selector.context;
}
return jQuery.makeArray( selector, this );
},
There is a bit of magic inside the init constructor, most of which is outside the scope of this article. Here's a brief rundown of what's happening
- If an empty string, null, or undefined is passed in as the selector, an empty jQuery object is returned.
- If a DOM Element is passed, then it is stored inside the jQuery object wrapper and returned.
- If 'body' is passed, then a quick reference to the body element is wrapped into the jQuery object and returned.
- If a string is passed, jQuery/Sizzle traverses the DOM off the document/context element and returns the set wrapped inside a jQuery object
- If a function is passed, it gets added to the document ready handler set, and the root document element wrapper is returned
- Lastly, jQuery assumes another object is passed in, and injects that object into the jQuery wrapper
1. jQuery.root
Internally, all selectors that don't provide a context use jQuery( document ).find( selector ). Save yourself some ms, store the document root onto jQuery itself, and then run all global selectors off of that element. Take a look:
// Super Slow (not really, but still slower than it needs to be)
jQuery('ul.special-selector').doSomething();
// Faster (assuming you do more than 1-2 global selections)
jQuery.root = jQuery( document );
jQuery.root.find('div.special-selector').doSomething()
2. Context sucks, use find
Don't get me wrong, you should always run selections based on a context if possible. But passing in a context to the jQuery constructor creates an extra unneeded function call. Internally jQuery runs context.find( selector ) anyway, so skip that step:
// Extra Function Call
jQuery( 'div', context ).doSomething();
// Same functionality, minus the extra instance and function call
context.find('div').doSomething();
3. Live is terrible, delegate is awesome
The best part: delegate is live with a context. So why is live a bad idea? To use live, you first have to run a selection on the page, and then bind the live handler. Something like:
// Example of live binding
jQuery('div').live('click', fn);
This creates a new jQuery instance and adds all div elements on the page to the collection. Then, after all the traversing and processing, those elements get ignored. Live simply takes the selector string ('div'), binds a click element to the document, and tracks all click events on the page that match the 'div' selector.
So if you load a page with 400+ div elements, the above code example will find all 400 elements, and then ignore them. So how do we make this better?
// A Faster Live Event
jQuery.root.delegate('div', 'click', fn);
Using the cached document element from the first example on this page, we bind the same live function (through delegate) to the document. It's the same functionality as live, without the DOM traversal.
4. jQuery.data > jQuery.fn.data
If you are getting/setting single key values, always use jQuery.data over the jQuery.fn.data method. Here's a common example and the fix for it.
// Wasted instance and two extra function calls
elem.click(function(){
if ( jQuery(this).data('flag') == true ) {
// do something...
}
});
// Faster data process
elem.click(function(){
if ( jQuery.data( this, 'flag' ) == true ) {
// do something...
}
});
The second example works the same as the first, minus two function calls and a new jQuery object. Here's the speed culprit:
jQuery(this)
This part not only adds a useless function call, it creates a new instance of the jQuery object, both of which can be avoided by simply going directly to the base data method. To add to this, the data plugin uses the base data method internally. Here's a simplified example of how the data plugin works:
jQuery.fn.data = function( key, value ) {
if ( value === undefined ) {
return jQuery.data( this[0], key );
}
else {
return this.each(function(){
jQuery.data( this, key, value );
});
}
};
As you can see, in this very simplified version, jQuery uses the base data method internally. Skip the extra function calls, and go directly to the source. Here's one last example showing the setter method.
// Wasted time
elem.click(function(){
jQuery(this).data( 'flag', true );
});
// Faster
elem.click(function(){
jQuery.data( this, 'flag', true );
});
I should note, that skipping the jQuery.fn.data step will not trigger the getData / setData events. If you happen to use these, then you will need to keep using the data plugin, as it's the only one that triggers those events
5. Bind and Trigger, get used to it
jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup error").split(" "), function( i, name ) {
// Handle event binding
jQuery.fn[ name ] = function( fn ) {
return fn ? this.bind( name, fn ) : this.trigger( name );
};
if ( jQuery.attrFn ) {
jQuery.attrFn[ name ] = true;
}
});
Nothing better than going directly to the source. All the event methods, like click, focus, blur, submit, etc., are just short-hand methods mapping to bind and trigger, only with an extra function call. Getting used to using bind and trigger directly will not only reduce the number of function calls, but will also help you when working with larger applications that require namespacing your events.
6. Each is evil
each: function( object, callback, args ) {
var name, i = 0,
length = object.length,
isObj = length === undefined || jQuery.isFunction(object);
if ( args ) {
if ( isObj ) {
for ( name in object ) {
if ( callback.apply( object[ name ], args ) === false ) {
break;
}
}
} else {
for ( ; i < length; ) {
if ( callback.apply( object[ i++ ], args ) === false ) {
break;
}
}
}
// A special, fast, case for the most common use of each
} else {
if ( isObj ) {
for ( name in object ) {
if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
break;
}
}
} else {
for ( var value = object[0];
i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
}
}
return object;
},
Actually, each is a pretty awesome utility function. The problem is that there is only one true reason to use each, and that is when a closure is needed for each item. If you are just looping through an array, then the callback function gets triggered on every iteration. So using an array of 25 items, the callback gets triggered 25 times. That can really add up depending on the size of your array.
7. Classes over styles
Anytime you are adding/changing more than one style on an element(s), you should be using a class. Passing an object of changes to the css method runs a loop over the object, with a minimum of 3 function calls on each iteration through jQuery internals. This can all be skipped by using addCass / removeClass that holds the required styles. I would go so far as to say that using a class for a single style change is better than running through jQuery's style module.
// Slow and maintenance unfriendly
elem.css({ color: 'red', font-weight: 'bold' });
// Simple and elegant
elem.addClass('error');
8. Object.length, use it.
Every jQuery selection comes with a length property that defines how many elements were found. Always check to make sure that there is a set of elements in your object before running a chain of methods. Take the following:
jQuery('NoElementsSelector').trigger('click').addClass('blue');
In this sample, even though the selector is obviously not going to return any elements, both the trigger and addClass methods will still be called. On top of that, the trigger method calls the jQuery.fn.each function internally, which in turn calls the jQuery.each function to iterate over an empty list. That's a total of 4 function calls that should have never been run in the first place.
var $elem = jQuery('NoElementsSelector');
if ( $elem.length ) {
$elem.trigger('click').addClass('blue');
}
With a simple if check, we have removed a total of 4 useless function calls. Don't be lazy, if check your objects.
jQuery is a crutch, not the solution
Never forget about plain old javascript, which is all that jQuery is. Always explore native options before falling back onto jQuery, not the other way around. Taking the above css example, reworked as native javascript.
// Slow and maintenance unfriendly
elem.css({ color: 'red', font-weight: 'bold' });
// Still maintenance unfriendly, but faster and more direct
for ( var i = 0; i < elem.length; i++ ) {
elem[i].style.color = 'red';
elem[i].style.fontWeight = 'bold';
}
Now I am in no way advocating that you lose the abstraction that jQuery provides, but rather, providing a faster alternative that doesn't require the use of jQuery. Always keep in mind that while not as elegant, native javascript can do what you want faster.
Other Resources
jQuery Source - Git it, watch it, learn it.Firequery - Firebug extension for jQuery development
Paul Irish - Great slide show on jQuery performance
Selector Performance - Unit testing results of jQuery selectors
Nettus - Range of performance increases
Snippet: Simple PDO Wrapper
This simple wrapper stores a single instance of a PDO connection statically, so it can be recalled anywhere in an application without passing around a special variable. To use it, just call the getInstance method whenever a database connection is required.
/**
* Simple PDO Wrapper
* January 10, 2010
* Corey Hart @ http://www.codenothing.com
*
*
* The DB class provides a method to retrieve a singleton instance
* of a database connection statically.
*
* Usage -
* $conn = DB::getInstance();
*/
Class DB
{
// Database Credentials
const host = 'localhost';
const user = 'someuser';
const pass = 'somepass';
const db = 'dbname';
// Singleton Instance Storage
private static $connection;
// Returns singleton instance of a database connection
public static function getInstance(){
// Create a connection if not already done so
if (!self::$connection){
$config = "mysql:host=" . self::host . ";dbname=" . self::db;
self::$connection = new PDO($config, self::user, self::pass);
}
// Return the connection instance
return self::$connection;
}
};
Javascript Random Color Generator
The Random Color Generator not only randomly creates colors, it also stores those colors so they are not repeated in the next request.
UsageThe generator takes up a single namespace, Color, with a defaults object extension attached to it. Those defaults are defined below.
- predef: An array of hex values (prefixed with the '#' symbol) that cannot be used by the generator.
- randMax: Highest number that the rand method can obtain. Defaults to 255.
- randMin: Lowest number the the rand method can obtain. Defaults to 0.
- levelUp: Upper level that one of the rgb values must pass to be valid. Defaults to -1 (to include all values).
- levelDown: Lower level that one of the rgb values must pass to be valid. Defaults to 256 (to include all values).
- recursionLimit: Number of loops the generator can call upon itself before failing (to prevent recursion errors). Defaults to 15.
- recursion: Callback function for when the recursionLimit is reached. Default function throws an error.
The generator uses a few methods internally that have been exposed as they may be useful.
- random: Returns a non-duplicate hex value.
- rand: Returns a random integer between the randMin and randMax default values.
- reset: Clears the stack cache, and resets the generator to start from scratch.
- rgb2hex: Converts rgb values into hex code form. Takes three parameters, (red, green blue).
- hex2rgb: Converts a hex code string into rgb values. Returns in array form [red, green, blue].
| Color | Hex | RGB |
|---|
The source package includes a demo page that uses jQuery as a helper. The generator itself is framework independent.
/*!
* Random Color Generator
* December 28, 2009
* Corey Hart @ http://www.codenothing.com
*/
var Color = {
defaults: {
// Predefined hex codes that cant be used as random colors
// All must be prefixed with the '#' indicator
predef: [],
// Maximum & Minimum random range values
rangeMax: 255,
rangeMin: 0,
// Upper and lower level values that must be
// passed for random color acceptance
//
// By setting levelUp: 200, levelDown: 100; Neutral
// colors like White, Gray, and Black can be somewhat weeded
// out and your random colors will be full spectrum based.
// Note*: Doing so increases likely hood of recursion
levelUp: -1,
levelDown: 256,
// Recursion handlers
recursionLimit: 15,
recursion: function(){
throw 'Recursion Error in Random Color Generator, ' +
'too many tries on finding random color, ' +
'[Limit ' + this.recursionLimit + ']';
}
},
// Caching of random colors
stack: {},
// Returns a random color in hex code form, and caches
// find in the stack.
random: function(i){
var self = this,
defaults = self.defaults,
r = self.rand(),
g = self.rand(),
b = self.rand(),
hex = self.rgb2hex(r, g, b),
levels = true;
// Check for recursion
if (i === undefined || typeof i !== 'number') i = 0;
else if (i++ > defaults.recursionLimit) return defaults.recursion();
// Color already used, try another one
if (self.stack[hex]) hex = self.random(i);
// Ensure one of the vals is above levelUp and another is below levelDown
// Check defaults comments for better understanding
levels = !!(
(r > defaults.levelUp || g > defaults.levelUp || b > defaults.levelUp) &&
(r < defaults.levelDown || g < defaults.levelDown || b < defaults.levelDown)
);
if (! levels) hex = self.random(i);
// Store on stack to help prevent repeat
self.stack[hex] = [r,g,b];
// Return hex code in #
return hex;
},
// Returns random number within range
rand: function(){
var defaults = this.defaults;
return defaults.rangeMin + Math.floor(Math.random()*(defaults.rangeMax+1));
},
// Clears the stack
reset: function(){
var self = this,
predef = self.defaults.predef,
i = -1, l = predef.length;
self.stack = {};
if (l > 0)
for ( ; ++i < l; )
self.stack[ predef[i] ] = true;
},
// Returns hex code
rgb2hex: function(r, g, b){
var str = '0123456789ABCDEF';
return '#' + [
str.charAt((r-r%16)/16) + str.charAt(r%16),
str.charAt((g-g%16)/16) + str.charAt(g%16),
str.charAt((b-b%16)/16) + str.charAt(b%16)
].join('');
},
// Returns in array form [red, green, blue]
hex2rgb: function(hex){
if (hex.substr(0, 1) === '#')
hex = hex.substr(1);
// Use the stack if possible to reduce processing
return this.stack['#'+hex] ? this.stack['#'+hex] :
hex.length === 6 ? [
parseInt(hex.substr(0, 2), 16),
parseInt(hex.substr(2, 2), 16),
parseInt(hex.substr(4, 2), 16)
] : hex.length === 3 ? [
parseInt(hex.substr(0, 1), 16),
parseInt(hex.substr(1, 1), 16),
parseInt(hex.substr(2, 1), 16)
] : [];
}
};
jQuery Plugin: Activity
The activity plugin takes care of all the underlying work in tracking user's (in)activity with the smallest amount of interference as possible. The plugin utilizes jQuery's one method on specified intervals to track key strokes and mouse movement in relation to the current window.
UsageTo start tracking user's activity, call the init method described later in this page. Simplicity is key with this hundred line script, with only four possible options to manipulate.
- inactive: Time in milliseconds of user inactivity allowed (defaults to 30 mins)
- inactiveFn: Callback function when above inactive time is reached
- interval: Time in milliseconds to check user activity in intervals (defaults to 5 mins)
- intervalFn: Callback function when above interval time is reached
The activity object comes with a few methods that may be of use. Remember that the init function needs to be called before the plugin will start tracking the users activity.
- init: Initializes the activity tracker. Takes object of settings as it's single parameter.
- update: Updates the users last active time to the current time stamp, or one that is provided.
- isActive: Returns a boolean value of the user's current active state.
- getActivity: Returns an object containing the users activity information.
- reActivate: Re-activates the activity plugin after a user has gone inactive.
- now: Returns the current time stamp.
jQuery(function($){
// Initialize the activity check
$.activity.init({
// Set interval check to every 5 seconds
interval: 1000*5,
intervalFn: function(info){
console.log('Interval Check - Last Active:', info.lastActive, ', Difference in milliseconds to current time:', info.diff);
},
// Set inactive check to every 15 seconds
inactive: 1000*5*3,
inactiveFn: function(info){
console.warn('Inactive Triggered - Last Active:', info.lastActive, ', Difference in milliseconds to current time:', info.diff);
}
});
// Either reactivate, or update the current timestamp when user clicks on the page
$(document).click(function(){
if ( $.activity.isActive() )
$.activity.update();
else
$.activity.reActivate();
});
});
If you pop open firebug, There will be log statements indicating either intervals or inactivity on the current window (depending on how long it took to get to this point). If there is a warning log, this means that the inactive flag was triggered, and the plugin has stopped tracking. To reactivate the tracker, simply click on the screen.
As always the source code is provided below, along with a zip file containing an example page to test on locally.
/*!
* jQuery Activity
* December 20, 2009
* Corey Hart @ http://www.codenothing.com
*/
(function($, undefined){
// Current Timestamp
function now(){
return (new Date).getTime();
}
// Attach activity object to jQuery base object
$.activity = {
// Variable Storage
defaults: {
// Users last active timestamp
lastActive: now(),
// Interval times in milliseconds
inactive: 1000*60*30, // Default inactive at 30 minutes
interval: 1000*60*5, // Default interval check every 5 minutes
// Callback functions when intervals are reached
inactiveFn: function(){},
intervalFn: function(){}
},
// Timerid
timer: undefined,
// Expose the current time fn
now: now,
// Starts the interval
init: function(options){
// Update vars with options
var self = this;
self.defaults = $.extend(self.defaults, options||{});
self.defaults.lastActive = now();
// Check activity on a continuous interval
self.bind();
return self.timer = setInterval(function(){ self.check(); }, self.defaults.interval);
},
// Updates lastActive to current/provided timestamp
update: function(time){
return (this.defaults.lastActive = time === undefined ? now() : time);
},
// Returns boolean indicator of activity
isActive: function(){
return (this.timer !== undefined);
},
// Getter for current activity
getActivity: function(){
var self = this, time = now();
return {diff: time-self.defaults.lastActive, time: time, lastActive: self.defaults.lastActive};
},
// Re-activates the interval (options only required if change to defaults wanted)
reActivate: function(options){
var self = this;
// Clear interval if still running
if (self.timer)
self.timer = clearInterval(self.timer);
// Reactivation
return self.init(options||{});
},
// Interval check function
check: function(){
var self = this,
time = now(),
diff = time - self.defaults.lastActive;
// Trigger appropriate callback
if (diff > self.defaults.inactive){
self.defaults.inactiveFn.call(self, {diff: diff, time: time, lastActive: self.defaults.lastActive});
return self.timer = clearInterval(self.timer);
}
else if (diff > self.defaults.interval){
self.defaults.intervalFn.call(self, {diff: diff, time: time, lastActive: self.defaults.lastActive});
}
// Rebind mouse tracking
return self.bind();
},
// Rebinds activity events
bind: function(){
var self = this;
$(window).unbind('.activity').one('mousemove.activity keyup.activity', function(event){
self.defaults.lastActive = event.timeStamp;
});
return self;
}
};
})(jQuery);
RSS