Source: jquery.cairn.js

/*
Copyright (C) 2013 Braydon Fuller <http://braydon.com/>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

(function($){
	var _requests = {};
	var _templates = {};
	var _images = {};
	var _callback = false;
	var __callback = false;
	var _requesting = {};
	var _placeholders = {};
	var _duration = 800;
	var _staticduration = 400;
	var _queryvars = {};
	var _minwidth = 320;
	var _minheight = 400;
	var _maxwidth = 4000;
	var _sprite_ids = [];
	var _timeout = 60; //seconds
	var _data = false;
	var _tags = {
		'%year%' : ['year', '([0-9]{4})'],
		'%monthnum%' : ['monthnum', '([0-9]{1,2})'],
		'%day%' : ['day', '([0-9]{1,2})'],
		'%paged%' : ['paged', '([0-9]+)'],
		'%hour%' : ['hour', '([0-9]{1,2})'],
		'%minute%' : ['minute', '([0-9]{1,2})'],
		'%second%' : ['second', '([0-9]{1,2})'],
		'%postname%' : ['name', '([^/]+)'],
		'%posts_set%' : ['posts_set', '([^/]+)'],
		'%posts_set_status%' : ['posts_set_status', '([^/]+)'],
		'%category%' : ['category_name', '(.+?)'],
		'%expertise%' : ['expertise', '(.+?)'],
		'%location%' : ['location', '(.+?)'],
		'%post_tag%' : ['tag', '(.+?)'],
		'%author%' : ['author_name', '([^/]+)'],
		'%pagename%' : ['pagename', '([^/]+)'],
		'%search%' : ['s', '(.+)'],
		'%feed%' : ['feed', '(feed|rdf|rss|rss2|atom)']
	}
	var _response = null;
	var _rules = [];

   /** 
    * Methods for the Cairn jQuery plugin for navigating to URI from within JavaScript for faster browsing.
	* 
    * @exports jQuery/cairn
    * @version 1.0
    */
	var methods = {
		homeurl : function(){
			return _data['homeurl'];
		},
		mediaurl : function(){
			return _data['mediaurl'];
		},
		get_excerpt : function( text, length ){
			return text.substring(0, length)+'...';
		},
		is : function( tag ) {
			if ( typeof(_queryvars[tag]) != 'undefined' ) {
				return true
			} else {
				return false 
			}
		},
		qv : function( tag ) {
			if ( typeof(_queryvars[tag]) != 'undefined' ) {
				return _queryvars[tag]
			} else {
				return false 
			}
		},
		/**
		 * Initialize the environment with our uri rules 
		 * @example 
		 * var data = {
		 *     "uri": [
		 *         { 
		 *             "path" : "^/$",
		 *             "view" : "main",
		 *             "title": "Welcome",
		 *             "callback" : "welcome_callback",
		 *             "class" : "welcome",
		 *             "template": "/templates/welcome.ejs"
		 *         },
		 *         { 
		 *             "path" : "^/news/%year%/%monthnum%/%postname%/?$",
		 *             "callback" : "news_callback",
		 *             "view" : "main",
		 *             "title": "News",
		 *             "request" : {
		 *                 "post_type" : "post", 
		 *                 "year" : "%year%",
		 *                 "monthnum" : "%monthnum%",
		 *                 "name" : "%postname%"
		 *             },
		 *             "class" : "single",
		 *             "template": "/templates/single.ejs"
		 *          }
		 *     ]
		 * }
		 * $().cairn('init', data, 'stage_loaded_callback', 'stage_leaving_callback');
		 * @param {array} d - An array of uri rules.
		 */
		init : function(d, endcallback, startcallback){
			_callback = endcallback
			__callback = startcallback
			_data = d

			var l = _data['uri'].length;
			for ( var i=0;i<l;i++) {
				// push an index of ids
				if ( typeof( _data['uri'][i]['template']) != 'undefined' ) {
					_data['uri'][i]['template'] += '?=v'+_data['version'];
					var sprite_id = _data['uri'][i]['template']
					_sprite_ids.push(sprite_id);
				}

			}

			for ( var placeholder in _tags ) {
				_placeholders[_tags[placeholder][0]] = placeholder
			}

		    $('a').live('click', function(event){
				var local = false;
				if ( $(this).attr('rel') != 'external' ) {
					if ( $(this).attr('rel') == 'cairn' ) {
						local = true;
					}
					if ( this.href.search( _data['homeurl'] ) >= 0 ) {
						local = true; 
					}
				}
				if ( local ) {
					methods['click'](this, event)					
				}
			})

			var body = $('body')
			body.append($('<div id="stage"></div>'));

			// initialize rules
			var l = _data['uri'].length;

			for (var i=0;i<l;i++) {
				var path = _data['uri'][i]['path'];
				var pathvars = [];
				for ( var t in _tags ) {
					var p = new RegExp( t );
					var m = p.test( path );
					if ( m ) {
						path = path.replace(t, _tags[t][1]);
						pathvars.push(_tags[t][0]);
					}
				}
				var rule = _data['uri'][i];
				rule['pathvars'] = pathvars;
				rule['path'] = path;
				_rules.push(rule);
			}

			// history change
			$(window).bind('popstate', function (event) {
		        var uri = window.location.href.replace( _data['homeurl'], '')
				methods['goto'](uri, false)
			});

			// initialize response
			_response = new Response();
		},
		uri_rule : function(uri){
			var l = _rules.length;

			// get query variable form query string
			var qv = function(key, uri){
				key = key.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
				var regex = new RegExp("[\\?&]"+key+"=([^&#]*)");
				var qs = regex.exec(uri);
				if ( qs == null ) {
						return false;
				} else {
					return qs[1];
				}
			}

			// find a matching url rule
			for ( var i=0;i<l;i++ ) {
				var p = new RegExp( _rules[i]['path'] );
				var hash_split = uri.split("#")
				var split_uri = hash_split[0].split("?")
				var m = p.test( split_uri[0] )
				if ( m ) {
					var _r = _rules[i];
					var match = split_uri[0].match(p)
					var pathvars = _rules[i]['pathvars']
					_queryvars = {}
					for ( var qv_tag in _placeholders ) {
						var qv_value = qv( qv_tag, uri )
						if ( qv_value ) {
							_queryvars[qv_tag] = qv_value;
						}
					}
					for ( var r_tag in _r['request'] ) {
						if ( typeof( _queryvars[r_tag] ) == 'undefined' ) {
							_queryvars[r_tag] = _r['request'][r_tag];
						}
					}
					for ( var r=0; r<pathvars.length; r++ ) {
						_queryvars[pathvars[r]] = match[ r+1 ]
					}
					return _r;
				}
			}
			return false;
		},
		/**
		 * Preload images in advance.
		 * @example 
		 * var images = ['a.png', 'b.png', 'c.png'];
		 * $().cairn('preload', images);
		 * @param {array} preload - An array of image sources.
		 */
		preload : function(preload) {
			for (i = 0; i < preload.length; i++) {
				if ( _images[preload[i]] == undefined ) {
					_images[preload[i]] = $('<img/>');
					_images[preload[i]].attr('src', preload[i])
				}
			}
		},
		/**
		 * Request a link in advance, so that when the link is clicked it loads instantly.
		 * @example 
		 * $().cairn('preclick', $('#previous'));
		 * @param {object} elm - An anchor tag HTML element.
		 */
		preclick : function(elm){
			var href = $(elm).attr('href')
			if ( href != undefined ) {
				if ( href.search( _data['homeurl'] ) >= 0 ) {
					var uri = href.replace( _data['homeurl'], '')
				} else {
					var uri = href
					href = _data['homeurl'] + href
				}
				var a = document.createElement('a')
				a.href = href
				$.ajax({
					url: a.href,
					data: {
						type: 'json'
					},
					success: function(data){
						var data_json = JSON.stringify(data);
						$(elm).attr('data-href', data_json);

						// gallery and window sizes
						var w = $(window).width();

						var gallery = $('#fineart-gallery');
						if ( gallery.size() > 0 ) {

							var gallery_height = $(window).height() - (gallery.position().top)*2;

							var sp = $('#fineart-gallery-info').position();

							if ( sp.left > 0 ) {
								var s = w-sp.left;
							} else {
								var s = 15; // padding
							}

							var gallery_width = w-s-gallery.position().left;

							// preload images at the correct size
							var images = [];

							var img_src = data['items'][0]['image'][0];
							var tw = data['items'][0]['image'][1];
							var th = data['items'][0]['image'][2];

							var image_height = th / tw * gallery_width;
							if ( image_height > gallery_height ) {
								var image_width = Math.round( tw / th * gallery_height );
								var image_height = Math.round( th / tw * image_width );
							} else {
								var image_width = Math.round( tw / th * image_height );
								var image_height = Math.round( image_height );
							}
							images.push( img_src+'?w='+image_width+'&h='+image_height );

							methods['preload'](images);
						} 
					},
					dataType: 'json'
				});
			}
		},
		/**
		 * Replace the default link behavior for faster navigation.
		 * @example 
		 * $('a').live('click', function(event){
		 *     $().cairn('click', this, event);
		 * })
		 * @param {object} elm - The anchor tag element.
		 * @param {event} event - A click event.
		 */
		click : function(elm, event){
	        event.preventDefault();

			var href = $(elm).attr('href')
			if ( href.search( _data['homeurl'] ) >= 0 ) {
				var uri = href.replace( _data['homeurl'], '')
			} else {
				var uri = href
				href = _data['homeurl'] + href
			}
	        var rule = methods['uri_rule']( uri )
			var a = document.createElement('a')
			a.href = href

			var data_json = $(elm).attr('data-href');

			if ( data_json ) {
				data = $.parseJSON( data_json );
				wp_response = data;
				methods['request']( uri );
				if ( typeof( history.pushState ) != 'undefined' ) {
					history.pushState( rule, rule['title'], a.pathname );
				}
			} else {

				$.ajax({
					url: a.href,
					type: 'GET',
					data: {
						type: 'json'
					},
					success: function(data){
						wp_response = data
						methods['request']( uri );
						if ( typeof( history.pushState ) != 'undefined' ) {
							history.pushState( rule, rule['title'], a.pathname );
						}
					},
					dataType: 'json'
				});

			}
		},
		/**
		 * Posts data and goes to the desired URI.
		 * @example 
		 * $().cairn('post', '/gallery/invoice/bitcoin/', false, data['request'] )
		 * @param {string} uri - The URI to navigate to.
		 * @param {boolean} pushstate - Weither or not to push the history state in the browser.
		 * @param {array} request - The associative array to post to the server.
		 */
		post : function(uri, pushstate, request){
			if ( typeof( pushstate ) == 'undefined' ) pushstate = true;
			if ( typeof( request ) == 'undefined' ) request = {};
	        var rule = methods['uri_rule']( uri )
			var a = document.createElement('a')
			a.href = _data['homeurl']+uri

			var data = {
				type: 'json',
				request: request
			}
			$.ajax({
				url: a.href,
				type: 'POST',
				data: data,
				success: function(data){
					wp_response = data
					methods['request']( uri );
					if ( pushstate && typeof( history.pushState ) != 'undefined' ) {
						history.pushState( rule, rule['title'], a.pathname );
					}
				},
				error: function(){
				},
				dataType: 'json'
			});

		},
		/**
		 * Prepare the environment and request the URI be rendered.
		 * @example 
		 * $().cairn('goto', '/gallery/');
		 * @param {string} uri - The URI to navigate to.
		 * @param {boolean} pushstate - If true it will push thet history state in the browser.
		 * @param {array} request - Any additional variables sent on the request query variable.
		 */
		goto : function(uri, pushstate, request){
			if ( typeof( pushstate ) == 'undefined' ) pushstate = true;
			if ( typeof( request ) == 'undefined' ) request = {};
	        var rule = methods['uri_rule']( uri )
			var a = document.createElement('a')
			a.href = _data['homeurl']+uri

			var data = {
				type: 'json',
				request: request
			}
			$.ajax({
				url: a.href,
				type: 'GET',
				data: data,
				success: function(data){
					wp_response = data
					methods['request']( uri );
					if ( pushstate && typeof( history.pushState ) != 'undefined' ) {
						history.pushState( rule, rule['title'], a.pathname );
					}
				},
				error: function(){
				},
				dataType: 'json'
			});

		},
		/**
		 * Request the URI be rendered by matching rules defined using init.
		 * @example 
		 * $().cairn('request', '/gallery/');
		 * @param {string} uri - The URI to navigate to.
		 */
		request : function(uri){

			if (!_requesting['status']) {

				//xxx:be able to skip this because of a preload

				_requesting['status'] = true;
				_requesting['uri'] = uri;

				var rule = methods['uri_rule'](uri);

				// only the sprites we need
				var requested_sprite_ids = [];
				requested_sprite_ids.push( rule['template'] )

				// remove sprites that are garbage
				var jl = _sprite_ids.length;
				for ( var j=0;j<jl;j++ ){
					var jjl = requested_sprite_ids.length;
					var jj = 0;
					var remove_id = _sprite_ids[j];
					while ( jj<jjl ){
						if ( _sprite_ids[j] == requested_sprite_ids[jj] ) {
							remove_id = false;
							break;
						}
						jj++;
					}
					if ( remove_id ) {
						$('#'+remove_id).remove();
					}

				}

				if ( rule ) {

					var loader = $('<div id="cairn-loader-icon"></div>')
					loader.css('top', $(window).height()/2 - 24)
					$('body').append(loader);
	
					var sprites = {};
					var templates = [];
					var requests = [];
					var mediaurl = _data['mediaurl'];

					var sprite_id = rule['template'];

					if ( sprite_id != undefined ) {
						templates.push(mediaurl+sprite_id);
					} else {
						console.log('Template "'+sprite_id+'" is not defined.');
					}


					var loaded = false
					var load_errors = [];

					var check_request_timeout = function(){
						if ( loaded == false ) {
							loaded = true
							console.log('Request Timeout')
							_requesting['status'] = false;
						}
					}

					var check_request = function(){
						
						if ( requests_checklist.length == 0 &&
							templates_checklist.length == 0 ) {
							loaded = true;
							clearTimeout(request_timeout)
							if ( load_errors.length == 0 ) {
								$('#cairn-loader-icon').remove();

								if ( rule['title'] ) {
									document.title = rule['title'] + ' | ' + _data['title'] + ' | ' + _data['description'];
								} else {
									document.title = _data['title'] + ' | ' + _data['description'];
								}
								_response.render( rule, rule['callback'] )
								_requesting['status'] = false;
							} else {
								console.log( 'Request had errors: '+load_errors.join(',') )
								_requesting['status'] = false;
							}
						}
					}

					var request_timeout = setTimeout( check_request_timeout, _timeout*1000 ) // ten seconds

					var requests_checklist = [];
					for ( var id in requests ) {
						requests_checklist.push(id)
					}
					for ( var id in requests ) {

						// fill in our variables
						var _send_data = requests[id];
						var send_data = {}
						for ( var sdi in _send_data ) {
							send_data[sdi] = _send_data[sdi]
							if ( _tags[ send_data[sdi] ] != undefined ) {
								var value = _queryvars[ _tags[ send_data[sdi] ][0] ];
								if ( value != undefined ) {
									send_data[sdi] = value
								}
							}
						}
						send_data['sprite'] = id
						send_data['action'] = 'query'

						// do our request
						$.ajax({
							url: ajaxurl,
							data: send_data,
							success: function(data){
								for (var ii = 0; ii < requests_checklist.length; ii++) {
									if (requests_checklist[ii] == data['sprite'] ) { 
										requests_checklist.splice(ii,1); 
									}
								}
								_requests[data['sprite']] = data['response'];
								check_request();
							},
							error: function(xhr, options, error){
								load_errors.push( error );
								for (var ii = 0; ii < requests_checklist.length; ii++) {
									if (requests_checklist[ii] == data['sprite'] ) { 
										requests_checklist.splice(ii,1); 
									}
								}
								_requests[data['sprite']] = false;
								check_request();
							},
							dataType: 'json'
						});
					}

					var templates_checklist = templates;
					var l = templates.length;
					for ( var i=0;i<l;i++ ) {
						var src = templates[i];
						$.ajax({
							url: src,
							success: function(data){
								for (var ii = 0; ii < templates_checklist.length; ii++) {
									if (templates_checklist[ii] == this.url) { 
										templates_checklist.splice(ii,1); 
									}
								}
								_templates[this.url] = data;
								check_request();
							},
							error: function(xhr, options, error){
								load_errors.push( error );
								for (var ii = 0; ii < templates_checklist.length; ii++) {
									if (templates_checklist[ii] == this.url) { 
										templates_checklist.splice(ii,1); 
									}
								}
								check_request();
							},
							dataType: 'html'
						});
					}

				} else {
					console.log( 'Request Not Found' )
					_requesting['status'] = false;
				}

			} else {
				// remove all sprites
				var l = _sprite_ids.length;
				for (var xx=0;xx<l;xx++){
					$('#'+_sprite_ids[xx]).remove();
				}
				// reset requesting state
				_requesting['status'] = false;	
				// start over
				methods['request'](uri)
			}
		}
	}

	var Response = function(){

		this.render = function( rule, callback ) { 

			$('#stage').empty();

			this.draw( rule )

			// initial request callback
			if ( typeof(_callback) != 'undefined' ) {
				var fn = eval(_callback)
				fn()
			}

			if ( typeof(callback) != 'undefined' ) {
				var fn = eval(callback)
				fn()
			}
		}

		this.draw = function( rule ){

			$('#stage').scrollTop(0)

			var stage = $( '#stage' );
			var wh = $(window).height();
			var ww = $(window).width();

			if ( ww < _minwidth ) {
				ww = _minwidth;
			} 

			if ( wh < _minheight ) {
				wh = _minheight;
			}

			stage.css( 'top', 0 )

			var sh = wh

			stage.css( 'position', 'absolute' );
			stage.css( 'overflow-x', 'hidden' );

			stage.height(sh);
			stage.width(ww);

			var mediaurl = _data['mediaurl'];

			var namespace = {};
			namespace = wp_response;
			namespace['homeurl'] = _data['homeurl'];
			var sid = mediaurl+rule['template'];
			var output = new EJS({'text' : _templates[sid]}).render(namespace);
			var elm = $( '<div class="template '+rule['class']+'">'+output+'</div>' );
			elm.width( ww )
			elm.height( sh )
			elm.css( 'position', 'absolute' )
			stage.append( elm );
		}
	}

	$.fn.cairn = function(method){
		if (methods[method]){
			return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
		} else if (typeof method === 'object' || ! method) {
			return methods.init.apply(this, arguments);
		} else {
			$.error('Method ' + method + ' does not exist on jQuery.cairn');
        } 
	}

})(jQuery);