/**
* Script lazy loader 0.5
* Copyright (c) 2008 Bob Matsuoka
*
* 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.
*/
var LazyLoader = {}; //namespace
LazyLoader.timer = {}; // contains timers for scripts
LazyLoader.scripts = []; // contains called script references
LazyLoader.load = function(url, callback) {
// handle object or path
var classname = null;
var properties = null;
try {
// make sure we only load once
if (LazyLoader.scripts.indexOf(url) == -1) {
// note that we loaded already
LazyLoader.scripts.push(url);
var script = document.createElement("script");
script.src = url;
script.type = "text/javascript";
$(script).appendTo("head"); // add script tag to head element
// was a callback requested
if (callback) {
// test for onreadystatechange to trigger callback
script.onreadystatechange = function () {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
callback();
}
};
// test for onload to trigger callback
script.onload = function () {
callback();
return;
};
// safari doesn't support either onload or readystate, create a timer
// only way to do this in safari
if (($.browser.webkit && !navigator.userAgent.match(/Version\/3/)) || $.browser.opera) { // sniff
LazyLoader.timer[url] = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
clearInterval(LazyLoader.timer[url]);
callback(); // call the callback handler
}
}, 10);
}
}
} else {
if (callback) { callback(); }
}
} catch (e) {
alert(e);
}
}
/**
* AjaxAssets
*
* A class representing an Array of assets. Call with an instance of
* Array which will be extended with special methods.
*
* Example: self.javascripts = new AjaxAssets([]);
*
* Once an asset is loaded, it is not loaded again. Pass with the
* following values:
*
* Ajax-Info{}
* assets{}
* javascripts []
* stylesheets []
*
*/
var AjaxAssets = function(array, type) {
var DATA_URI_START = "";
var DATA_URI_END = "";
var MHTML_START = "";
return jQuery.extend(array, {
/**
* Add an asset, but don't load it.
*/
addAsset: function(path) {
this.push(this.sanitizePath(path));
},
/**
* Load and add an asset. The asset is loaded using the
* unsanitized path should you need to put something in the
* query string.
*/
loadAsset: function(path, callback) {
console.log('[ajax] loading', type, path);
this.push(this.sanitizePath(path));
if (type == 'css') {
this.appendScriptTag(path, callback);
} else if ($.browser.msie || $.browser.mozilla) {
this.appendScriptTag(path, callback);
} else {
LazyLoader.load(path, callback);
}
},
/**
* Return a boolean indicating whether an asset has
* already been loaded.
*/
loadedAsset: function(path) {
path = this.sanitizePath(path);
for (var i=0; i < this.length; i++) {
if (this[i] == path) {
return true;
}
}
return false;
},
/**
* Remove query strings and otherwise cleanup paths
* before adding them.
*/
sanitizePath: function(path) {
return path.replace(/\?.*/, '');
},
/**
* Supports debugging and references the script files as external resources
* rather than inline.
*
* @see http://stackoverflow.com/questions/690781/debugging-scripts-added-via-jquery-getscript-function
*/
appendScriptTag: function(url, callback) {
if (type == 'js') {
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.src = url;
script.type = 'text/javascript'
head.appendChild(script);
// Handle Script loading
if (callback) {
var done = false;
script.onload = script.onreadystatechange = function(){
if ( !done && (!this.readyState ||
this.readyState == "loaded" || this.readyState == "complete") ) {
done = true;
if (callback)
callback();
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
}
};
}
} else if (type == 'css') {
if (url.match(/datauri/)) {
$(DATA_URI_START + '' + DATA_URI_END).appendTo('head');
} else if (url.match(/mhtml/)) {
$(MHTML_START + '' + MHTML_END).appendTo('head');
} else {
$('').appendTo('head');
}
}
return undefined;
}
});
};
/**
* Class Ajax
*
* Options:
* enabled boolean indicating whether the plugin is enabled.
* This must be set if you are using Ajax callbacks in your code,
* and you want them to still fire if Ajax is not enabled.
*
* default_container string jQuery selector of the default
* container element to receive content.
*
* Callbacks:
*
* Callbacks can be specified using Ajax-Info{ callbacks: 'javascript to eval' },
* or by adding callbacks directly to the Ajax instance:
*
* window.ajax.onLoad(function() { doSomething(args); });
*
* Order of execution:
*
*
*/
var Ajax = function(options) {
var self = this;
self.enabled = true;
self.default_container = undefined;
self.loaded_by_framework = false;
self.loading_icon = $('#loading-icon-small');
self.javascripts = undefined;
self.stylesheets = new AjaxAssets([], 'css');
self.callbacks = [];
self.loaded = false;
self.lazy_load_assets = false;
// For initial position of the loading icon. Often the mouse does not
// move so position it by the link that was clicked.
self.last_click_coords = undefined;
// Parse options
self.options = options;
self.default_container = options.default_container;
if (options.enabled !== undefined) {
self.enabled = options.enabled;
}
if (options.lazy_load_assets !== undefined) {
self.lazy_load_assets = options.lazy_load_assets;
}
// Initialize on DOM ready
$(function() { self.init() });
/**
* Initializations run on DOM ready.
*
* Bind event handlers and setup jQuery Address.
*/
self.init = function() {
// Configure jQuery Address
$.address.history(true);
$.address.change = self.addressChanged;
// Insert loading image
var image = ''
$(image).hide().appendTo($('body'));
// Bind a live event to all ajax-enabled links
$('a[data-deep-link]').live('click', self.linkClicked);
// Initialize the list of javascript assets
if (self.javascripts === undefined) {
self.javascripts = new AjaxAssets([], 'js');
$(document).find('script[type=text/javascript][src!=]').each(function() {
var script = $(this);
var src = script.attr('src');
// Local scripts only
if (src.match(/^\//)) {
// Parse parameters passed to the script via the query string.
// TODO: Untested. It's difficult for us to use this with Jammit.
if (src.match(/\Wajax.js\?.+/)) {
var params = src.split('?')[1].split('&');
jQuery.each(params, function(idx, param) {
param = param.split('=');
if (param.length == 1) { return true; }
switch(param[0]) {
case 'enabled':
self.enabled = param[1] == 'false' ? false : true;
console.log('[ajax] set param enabled=', self.enabled);
break;
case 'default_container':
self.default_container = param[1];
console.log('[ajax] set param default_container=', self.default_container);
break;
}
});
}
self.javascripts.addAsset(script.attr('src'));
}
});
}
self.initialized = true;
// Run onInit() callbacks
};
/**
* jQuery Address callback triggered when the address changes.
*/
self.addressChanged = function() {
if (document.location.pathname != '/') { return false; }
if (window.ajax.disable_address_intercept == true) {return false;}
if (typeof(self.loaded_by_framework) == 'undefined' || self.loaded_by_framework != true) {
self.loaded_by_framework = true;
return false;
}
self.loadPage({
url: $.address.value().replace(/\/\//, '/')
});
return true;
};
/**
* loadPage
*
* Request new content and insert it into the document. If the response
* Ajax-Info header contains any of the following we take the associated
* action:
*
* [title] String, Set the page title
* [tab] jQuery selector, trigger the 'activate' event on the tab
* [container] The container to receive the content, or main by default.
* [assets] Assets to load
* [callback] Execute a callback after assets have loaded
*
* Cookies in the response are automatically set on the document.cookie.
*/
self.loadPage = function(options) {
if (!self.enabled) {
document.location = options.url;
return true;
}
self.loaded = false;
self.showLoadingImage();
jQuery.ajax({
url: options.url,
method: options.method || 'GET',
beforeSend: self.setRequestHeaders,
success: self.responseHandler,
complete: function(XMLHttpRequest, responseText) {
// Stop watching the mouse position and scroll to the top of the page.
$(document).unbind('mousemove', self.updateImagePosition).scrollTop(0);
$('#loading-icon-small').hide();
self.loaded = true;
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
var responseText = XMLHttpRequest.responseText;
self.responseHandler(responseText, textStatus, XMLHttpRequest);
}
});
};
/**
* setRequestHeaders
*
* Set the AJAX_INFO request header. This includes all the data
* defined on the main (or receiving) container, plus some other
* useful information like the:
*
* referer - the current document.location
*
*/
self.setRequestHeaders = function(XMLHttpRequest) {
var data = $(self.default_container).data('ajax-info');
if (data === undefined || data === null) { data = {}; }
data['referer'] = document.location.href;
XMLHttpRequest.setRequestHeader('AJAX_INFO', $.toJSON(data));
};
/**
* linkClicked
*
* Called when the an AJAX-enabled link is clicked.
* Redirect back to the root URL if we are not on it.
*
*/
self.linkClicked = function(event) {
if (document.location.pathname != '/') {
var url = $.address.baseURL().replace(new RegExp(document.location.pathname), '')
url += '/#/' + $(this).attr('data-deep-link');
url.replace(/\/\//, '/');
document.location = url;
} else {
self.last_click_coords = { pageX: event.pageX, pageY: event.pageY };
$.address.value($(this).attr('data-deep-link'));
}
return false;
};
/**
* responseHandler
*
* Process the response of an AJAX call and put the contents in
* the appropriate container, activate tabs etc.
*
*/
self.responseHandler = function(responseText, textStatus, XMLHttpRequest) {
var data = self.processResponseHeaders(XMLHttpRequest);
var container = data.container === undefined ? $(self.default_container) : $(data.container);
// Redirect? Let the JS execute. It will set the new window location.
if (responseText && responseText.match(/try\s{\swindow\.location\.href/)) { return true; }
/**
* Extract the body
*/
if (responseText.search(/<\s*body[^>]*>/) != -1) {
var start = responseText.search(/<\s*body[^>]*>/);
start += responseText.match(/<\s*body[^>]*>/)[0].length;
var end = responseText.search(/<\s*\/\s*body\s*\>/);
console.log('Extracting body ['+start+'..'+end+'] chars');
responseText = responseText.substr(start, end - start);
}
// Handle special header instructions
// title - set page title
// tab - activate a tab
// assets - load assets
// callback - execute a callback
if (data.title !== undefined) {
console.log('Using page title '+data.title);
$.address.title(data.title);
}
if (data.tab !== undefined) {
console.log('Activating tab '+data.tab);
$(data.tab).trigger('activate');
}
/**
* Load stylesheets
*/
if (self.lazy_load_assets && data.assets && data.assets.stylesheets !== undefined) {
jQuery.each(jQuery.makeArray(data.assets.stylesheets), function(idx, url) {
if (self.stylesheets.loadedAsset(url)) {
console.log('[ajax] skipping css', url);
return true;
} else {
self.stylesheets.loadAsset(url);
}
});
}
/**
* Insert response
*/
console.log('Using container ',container.selector);
console.log('Set data ',data);
container.data('ajax-info', data)
container.html(responseText);
/**
* Include callbacks from Ajax-Info
*/
if (data.callbacks) {
data.callbacks = jQuery.makeArray(data.callbacks);
self.callbacks.concat(data.callbacks);
}
/**
* Load javascipts
*/
if (self.lazy_load_assets && data.assets && data.assets.javascripts !== undefined) {
var count = data.assets.javascripts.length;
var callback;
jQuery.each(jQuery.makeArray(data.assets.javascripts), function(idx, url) {
if (self.javascripts.loadedAsset(url)) {
console.log('[ajax] skipping js', url);
return true;
}
// Execute callbacks once the last asset has loaded
callback = (idx == count - 1) ? undefined : self.executeCallbacks;
self.javascripts.loadAsset(url, callback);
});
} else {
// Execute callbacks immediately
self.executeCallbacks();
}
$(document).trigger('ajax.onload');
/**
* Set cookies - browsers don't seem to allow this
*/
try {
var cookie = XMLHttpRequest.getResponseHeader('Set-Cookie');
if (cookie !== null) {
console.log('Setting cookie');
document.cookie = cookie;
}
} catch(e) {
}
};
/**
* Process the response headers.
*
* Set the page title.
*/
self.processResponseHeaders = function(XMLHttpRequest) {
var data = XMLHttpRequest.getResponseHeader('Ajax-Info');
if (data !== null) {
try { data = jQuery.parseJSON(data); }
catch(e) {
console.log('Failed to parse Ajax-Info header as JSON!', data);
}
}
if (data === null || data === undefined) {
data = {};
}
return data;
};
/**
* Show the loading image.
*/
self.showLoadingImage = function() {
var icon = $('#loading-icon-small');
// Follow the mouse pointer
$(document).bind('mousemove', self.updateImagePosition);
// Display at last click coords initially
if (self.last_click_coords !== undefined) {
self.updateImagePosition(self.last_click_coords);
// Center it
} else {
var marginTop = parseInt(icon.css('marginTop'), 10);
var marginLeft = parseInt(icon.css('marginLeft'), 10);
marginTop = isNaN(marginTop) ? 0 : marginTop;
marginLeft = isNaN(marginLeft) ? 0 : marginLeft;
icon.css({
position: 'absolute',
left: '50%',
top: '50%',
zIndex: '99',
marginTop: marginTop + jQuery(window).scrollTop(),
marginLeft: marginLeft + jQuery(window).scrollLeft()
});
}
icon.show();
};
/**
* Update the position of the loading icon.
*/
self.updateImagePosition = function(e) {
$('#loading-icon-small').css({
zIndex: 99,
position: 'absolute',
top: e.pageY + 14,
left: e.pageX + 14
});
};
/**
* onLoad
*
* Register a callback to be executed in the global scope
* once all Ajax assets have been loaded. Callbacks are
* appended to the queue.
*
* If the plugin is disabled, callbacks are executed immediately
* on DOM ready.
*/
self.onLoad = function(callback) {
if (self.enabled && (self.lazy_load_assets && !self.loaded)) {
self.callbacks.push(callback);
console.log('[ajax] appending callback', self.teaser(callback));
} else {
self.executeCallback(callback, true);
}
};
/**
* prependOnLoad
*
* Add a callback to the start of the queue.
*
* @see onLoad
*/
self.prependOnLoad = function(callback) {
if (self.enabled && (self.lazy_load_assets && !self.loaded)) {
self.callbacks.unshift(callback);
console.log('[ajax] prepending callback', self.teaser(callback));
} else {
self.executeCallback(callback, true);
}
};
/**
* Execute callbacks
*/
self.executeCallbacks = function() {
var callbacks = jQuery.makeArray(self.callbacks);
if (callbacks.length > 0) {
jQuery.each(callbacks, function(idx, callback) {
self.executeCallback(callback);
});
self.callbacks = [];
}
};
/**
* Execute a callback given as a string or function reference.
*
* dom_ready (optional) boolean, if true, the callback
* is wrapped in a DOM-ready jQuery callback.
*/
self.executeCallback = function(callback, dom_ready) {
if (dom_ready !== undefined && dom_ready) {
$(function() {
self.executeCallback(callback);
})
} else {
console.log('[ajax] executing callback', self.teaser(callback));
try {
if (jQuery.isFunction(callback)) {
callback();
} else {
jQuery.globalEval(callback);
}
} catch(e) {
console.log('[ajax] callback failed with exception', e);
}
}
};
self.teaser = function(callback) {
return new String(callback).slice(0,50);
};
};