Skip to content

Instantly share code, notes, and snippets.

@staylor
Created April 16, 2012 17:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save staylor/2400270 to your computer and use it in GitHub Desktop.
Save staylor/2400270 to your computer and use it in GitHub Desktop.
a re-write of how eMusic handles site-wide AJAX
/*globals window, document, $, jQuery */
(function ($) {
"use strict";
var loaded = false,
hashbang = '#!',
anychar = '.*?',
the_hash,
lastPath,
thePath,
lastXhr,
master = {},
archive = {},
archives = {},
events = ['init', 'start', 'always-before', 'before', 'execute', 'always-after', 'after', 'end'],
wrapper,
title,
head,
re_head = new RegExp('<head[^>]+>(.+)</head>'),
re_body = new RegExp('<body[^>]+?>.+?</body>'),
re_title = new RegExp('<title>(.+)</title>'),
re_content = new RegExp('<div id="the-content">(.+)<!--the-content--></div>'),
re_body_classes = new RegExp('<body.*?class="([^"]+)".*?>'),
re_metas = new RegExp('<meta[^>]+/?>', 'g'),
re_linked = new RegExp('<link rel=[\'"](?:alternate|prev|next)[\'"][^>]+?>', 'g'),
re_hrefs,
re_srcs;
function quotify(name, value) {
return [name, '=', '[\'"]', value, '[\'"]'].join('');
}
function urlify(ext) {
return ['(https?://', window.location.host, '/.+?', ext, '.*?)'].join('');
}
re_hrefs = new RegExp(
['<link', quotify('rel', 'stylesheet'), quotify('href', urlify('.css')), '/?>'].join(anychar),
'g'
);
re_srcs = new RegExp(
['<script', quotify('type', 'text/javascript'), quotify('src', urlify('.js')), '></script>'].join(anychar),
'g'
);
/**
* Support AJAX-legacy URLs (e.g. /listen/#/browse/album/all/)
* Support poorly pasted URLs (e.g. /browse/album/all/#/browse/book/all/)
* Redirect hashbang URLs (e.g. /browse/album/all/#!/browse/book/all/)
*
* Redirect to their hash value
* (this can't be done on the server)
*
* Hashbang URLs sent to server as ?_escaped_fragment_=/browse/album/all/
* get 301 redirected to their value
*
*/
the_hash = window.location.hash;
if (the_hash && '#' !== the_hash && -1 < the_hash.indexOf('/')) {
window.location.href = the_hash.replace(hashbang, '').replace('#', '');
return;
}
function remove_excess_ws(data) {
return data.replace(/\t|\n|\r/g, '').replace(/\s+/g, ' ');
}
function extract_head(data) {
var parsed = re_head.exec(data);
return parsed[1];
}
function extract_body(data) {
var parsed = re_body.exec(data);
return parsed[0];
}
function parseURL(url) {
var a = document.createElement('a');
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':', ''),
host: a.hostname,
port: a.port,
query: a.search,
params: (function () {
var ret = {},
seg = a.search.replace(/^\?/, '').split('&'),
len = seg.length,
i = 0,
s;
for (i; i < len; i += 1) {
if (seg[i]) {
s = seg[i].split('=');
ret[s[0]] = s[1];
}
}
return ret;
}()),
file: (a.pathname.match(new RegExp('/([^/?#]+)$', 'i')) || ['', ''])[1],
hash: a.hash.replace('#', ''),
path: a.pathname.replace(new RegExp('^([^/])'), '/$1'),
relative: (a.href.match(new RegExp('tps?://[^/]+(.+)')) || ['', ''])[1],
segments: a.pathname.replace(new RegExp('^/'), '').split('/')
};
}
function add_action(action, callback) {
if (jQuery.isFunction(callback) && jQuery.inArray(action, events)) {
if (master.hasOwnProperty(action)) {
master[action].push(callback);
} else {
master[action] = [callback];
}
}
}
function do_action(value) {
if (jQuery.inArray(value, events) && master.hasOwnProperty(value)) {
$(master[value]).each(function () {
this(value, thePath);
});
}
}
function do_actions(actions) {
$(actions).each(function () {
do_action(this);
});
}
function flush_action(action) {
if (master.hasOwnProperty(action)) {
delete master[action];
}
}
function flush_actions(actions) {
$(actions).each(function () {
flush_action(this);
});
}
function empty_content() {
wrapper.html('');
}
function transition_content(body_classes, body) {
document.body.className = body_classes;
wrapper.html(body);
}
function transition_title(text) {
title.html(text);
}
function transition_meta(metas) {
$('meta').remove();
$(metas).each(function () {
head.append($(this));
});
}
function transition_links(links) {
$('link[rel!="stylesheet"]').remove();
$(links).each(function () {
head.append($(this));
});
}
/**
* Skip the minified global-styles file
* In Minify is turned off, wait until we hit style.css to start messing with files
* These files are hardcoded in a list, not dynamically joined
*
*/
function after_global_styles(href) {
return -1 < href.indexOf('style.css') || -1 < href.indexOf('minify-global-styles');
}
function transition_styles(stylesheets) {
var at_style = false, link;
$(archive.stylesheets).each(function () {
if (!at_style && after_global_styles(this)) {
at_style = true;
}
if (at_style) {
$('link[href="' + this + '"]').remove();
}
});
at_style = false;
$(stylesheets).each(function () {
if (!at_style && after_global_styles(this)) {
at_style = true;
}
if (at_style) {
link = $('<link />').attr({
'href' : this,
'rel' : 'stylesheet',
'added' : 'ajax'
});
head.append(link);
}
});
}
/**
* On page load, page-specific scripts will be loaded at the end of <div id="the-content">
* On the first AJAX request, those scripts will get wiped out
* When scripts are loaded after AFTER AJAX content has been replaced,
* they are appended to the end of the body, not #the-content
* After initial page load, scripts with added="ajax" are targeted for removal
*
*/
function transition_scripts(scripts) {
var body = $(document.body);
$('script[added="ajax"]').remove();
$(scripts).each(function () {
var script = $('<script />').attr({
'src' : this,
'text' : 'text/javascript',
'added' : 'ajax'
});
body.append(script);
});
}
/**
* Use RegEx to parse a giant text blob
*
*/
function extract_items(data, regx) {
var parsed = [], temp = true;
while (temp) {
temp = regx.exec(data);
if (temp) {
parsed.push(temp[1]);
}
}
return parsed;
}
function extract_item(data, regx) {
var temp = regx.exec(data);
if (null !== temp) {
return temp[1];
}
return temp;
}
function extract_raw(data, regx) {
var parsed = [], temp = true;
while (temp) {
temp = regx.exec(data);
if (temp) {
parsed.push(temp[0]);
}
}
return parsed;
}
/**
* Create a map of data
* - - - - - - - - - - - - -
* <title>
* <meta property=....
* <meta name=....
* <link rel=....
* <link rel="stylsheet"...
* <script src="....
* contents of <div id="the-content">
* <body> classes
*
*/
function get_archive(url, head, body) {
var archive = {};
archive.title = extract_item(head, re_title);
archive.metas = extract_raw(head, re_metas);
archive.links = extract_raw(head, re_linked);
archive.stylesheets = extract_items(head, re_hrefs);
archive.scripts = extract_items(body, re_srcs);
archive.body = extract_item(body, re_content).replace(re_srcs, '');
archive.body_classes = extract_item(body, re_body_classes);
/**
* Ignore inline scripts, we don't need them / they're bad
* If an inline script doesn't load, the bug is its inclusion at all
*
*/
if (!cache_blacklist(url)) {
archives[url] = archive;
}
console.log(archive);
return archive;
}
function parse_archive(new_archive) {
if (new_archive === archive) {
return;
}
empty_content();
transition_styles(new_archive.stylesheets);
transition_content(new_archive.body_classes, new_archive.body);
transition_title(new_archive.title);
transition_meta(new_archive.metas);
transition_links(new_archive.links);
transition_scripts(new_archive.scripts);
archive = new_archive;
}
function cache_blacklist(path) {
/**
* Add rules here for when NOT to cache
*
* 1. Homepage - recs and programmed records need to re-order on pageload
*/
return '/' === parseURL(path).path;
}
function load_page() {
if (archives.hasOwnProperty(thePath) && !cache_blacklist(thePath)) {
parse_archive(archives[thePath]);
do_actions(['always-after', 'after', 'end']);
} else {
lastXhr = $.ajax({
type : 'get',
dataType: 'text',
url : thePath,
cache : false,
success : function (data) {
var head, body;
data = remove_excess_ws(data);
head = extract_head(data);
body = extract_body(data);
parse_archive(get_archive(thePath, head, body));
do_actions(['always-after', 'after', 'end']);
},
error : function () {
window.console.log('error :(');
}
});
}
}
/**
* Cross-domain function from jQuery
*
*/
function is_cross_domain(url) {
// https://github.com/jquery/jquery/blob/master/src/ajax.js
var ajaxLocation, ajaxLocParts, parts, rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/;
try {
ajaxLocation = window.location.href;
} catch (e) {
ajaxLocation = window.document.createElement("a");
ajaxLocation.href = "";
ajaxLocation = ajaxLocation.href;
}
ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || [];
parts = rurl.exec(url.toLowerCase());
return !!(parts &&
(parts[1] !== ajaxLocParts[1] || parts[2] !== ajaxLocParts[2] ||
(parts[3] || (parts[1] === "http:" ? 80 : 443)) !==
(ajaxLocParts[3] || (ajaxLocParts[1] === "http:" ? 80 : 443))));
}
/**
* Ideally, we would loop through a list of registered RegEx patterns
*
*/
function parse_ignore_paths(href) {
return href.match(/\/(?:info|tools)\//) || href.match(/\.mp3|\.m3u|\.emx|\.emp/);
}
function pathchange_callback() {
var hash = window.location.hash;
/**
* Save the relative path of the current URL
*
*/
thePath = $.pathchange.detectHistorySupport() ?
parseURL(window.location.href).relative :
hash.replace(hashbang, '');
/**
* Don't load when the page is already loaded
*
*/
if (thePath !== lastPath) {
do_actions(['start', 'always-before', 'before']);
flush_actions(['before', 'after']);
lastPath = thePath;
if (lastXhr) {
lastXhr.abort();
}
load_page();
return false;
}
return true;
}
function bind_pathchange() {
$.pathchange.init({interceptLinks: false});
$('a[href]:not(.no-ajax)')
.live('click', function () {
var rel, url = this.href, ignore = parse_ignore_paths(this.href);
/**
* Ignore links that match:
* 1. Cross-domain
* 2. rel="external"
* 3. aren't ignored by our regex matcher
*
*/
if (!is_cross_domain(url) && 'external' !== this.rel && !ignore) {
rel = parseURL(url).relative;
/**
* Modern browsers
*
*/
if ($.pathchange.detectHistorySupport()) {
$.pathchange.changeTo(rel);
/**
* Old as dirt browsers
*
*/
} else {
window.location.hash = '!' + rel;
pathchange_callback();
}
return false;
}
return true;
});
$(window).bind('pathchange', pathchange_callback);
}
window.add_action = add_action;
window.onload = function () {
var the_head = '', the_body = '';
lastPath = thePath = parseURL(window.location.href).relative;
if (!loaded) {
loaded = true;
/**
* actions fired in the 'init' action
* only fire once per full page load
*
*/
do_action('init');
/**
* Cache frequently-accessed jQuery selectors
*
*/
wrapper = $('#the-content');
head = $('head');
title = head.find('title');
/**
* When the page loads fully, we can parse the loaded content
* to determine our initial archive of data
*
* Remove whitespace to improve RegEx performance
*
*/
the_head = remove_excess_ws($('head').html());
the_body = remove_excess_ws(['<body class="', document.body.className, '">', $('body').html(), '</body>'].join(''));
/**
* Set inital "archive" of page data
*
*/
archive = get_archive(thePath, the_head, the_body);
/**
* bind Pathchange whether we degrade or not
*
*/
bind_pathchange();
do_actions(['always-after', 'after', 'end']);
}
};
}(jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment