durandal
Version:
Durandal is a cross-device, cross-platform client framework written in JavaScript and designed to make Single Page Applications (SPAs) easy to create and maintain. We've used it to build apps for PC, Mac, Linux, iOS and Android...and now it's your turn...
333 lines (283 loc) • 12.8 kB
JavaScript
/**
* Durandal 2.2.0 Copyright (c) 2010-2016 Blue Spire Consulting, Inc. All Rights Reserved.
* Available via the MIT license.
* see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details.
*/
/**
* This module is based on Backbone's core history support. It abstracts away the low level details of working with browser history and url changes in order to provide a solid foundation for a router.
* @module history
* @requires system
* @requires jquery
*/
define(['durandal/system', 'jquery'], function (system, $) {
// Cached regex for stripping a leading hash/slash and trailing space.
var routeStripper = /^[#\/]|\s+$/g;
// Cached regex for stripping leading and trailing slashes.
var rootStripper = /^\/+|\/+$/g;
// Cached regex for detecting MSIE.
var isExplorer = /msie [\w.]+/;
// Cached regex for removing a trailing slash.
var trailingSlash = /\/$/;
// Update the hash location, either replacing the current entry, or adding
// a new one to the browser history.
function updateHash(location, fragment, replace) {
if (replace) {
var href = location.href.replace(/(javascript:|#).*$/, '');
if (history.history.replaceState) {
history.history.replaceState({}, document.title, href + '#' + fragment); // using history.replaceState instead of location.replace to work around chrom bug
} else {
location.replace(href + '#' + fragment);
}
} else {
// Some browsers require that `hash` contains a leading #.
location.hash = '#' + fragment;
}
};
/**
* @class HistoryModule
* @static
*/
var history = {
/**
* The setTimeout interval used when the browser does not support hash change events.
* @property {string} interval
* @default 50
*/
interval: 50,
/**
* Indicates whether or not the history module is actively tracking history.
* @property {string} active
*/
active: false
};
// Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') {
history.location = window.location;
history.history = window.history;
}
/**
* Gets the true hash value. Cannot use location.hash directly due to a bug in Firefox where location.hash will always be decoded.
* @method getHash
* @param {string} [window] The optional window instance
* @return {string} The hash.
*/
history.getHash = function(window) {
var match = (window || history).location.href.match(/#(.*)$/);
return match ? match[1] : '';
};
/**
* Get the cross-browser normalized URL fragment, either from the URL, the hash, or the override.
* @method getFragment
* @param {string} fragment The fragment.
* @param {boolean} forcePushState Should we force push state?
* @return {string} he fragment.
*/
history.getFragment = function(fragment, forcePushState) {
if (fragment == null) {
if (history._hasPushState || !history._wantsHashChange || forcePushState) {
fragment = history.location.pathname + history.location.search;
var root = history.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) {
fragment = fragment.substr(root.length);
}
} else {
fragment = history.getHash();
}
}
return fragment.replace(routeStripper, '');
};
/**
* Activate the hash change handling, returning `true` if the current URL matches an existing route, and `false` otherwise.
* @method activate
* @param {HistoryOptions} options.
* @return {boolean|undefined} Returns true/false from loading the url unless the silent option was selected.
*/
history.activate = function(options) {
if (history.active) {
system.error("History has already been activated.");
}
history.active = true;
// Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available?
history.options = system.extend({}, { root: '/' }, history.options, options);
history.root = history.options.root;
history._wantsHashChange = history.options.hashChange !== false;
history._wantsPushState = !!history.options.pushState;
history._hasPushState = !!(history.options.pushState && history.history && history.history.pushState);
var fragment = history.getFragment();
var docMode = document.documentMode;
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
// Normalize root to always include a leading and trailing slash.
history.root = ('/' + history.root + '/').replace(rootStripper, '/');
if (oldIE && history._wantsHashChange) {
history.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
history.navigate(fragment, false);
}
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (history._hasPushState) {
$(window).on('popstate', history.checkUrl);
} else if (history._wantsHashChange && ('onhashchange' in window) && !oldIE) {
$(window).on('hashchange', history.checkUrl);
} else if (history._wantsHashChange) {
history._checkUrlInterval = setInterval(history.checkUrl, history.interval);
}
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
history.fragment = fragment;
var loc = history.location;
var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === history.root;
// Transition from hashChange to pushState or vice versa if both are requested.
if (history._wantsHashChange && history._wantsPushState) {
// If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it...
if (!history._hasPushState && !atRoot) {
history.fragment = history.getFragment(null, true);
history.location.replace(history.root + history.location.search + '#' + history.fragment);
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead...
} else if (history._hasPushState && atRoot && loc.hash) {
this.fragment = history.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, history.root + history.fragment + loc.search);
}
}
if (!history.options.silent) {
return history.loadUrl(options.startRoute);
}
};
/**
* Disable history, perhaps temporarily. Not useful in a real app, but possibly useful for unit testing Routers.
* @method deactivate
*/
history.deactivate = function() {
$(window).off('popstate', history.checkUrl).off('hashchange', history.checkUrl);
clearInterval(history._checkUrlInterval);
history.active = false;
};
/**
* Checks the current URL to see if it has changed, and if it has, calls `loadUrl`, normalizing across the hidden iframe.
* @method checkUrl
* @return {boolean} Returns true/false from loading the url.
*/
history.checkUrl = function() {
var current = history.getFragment();
if (current === history.fragment && history.iframe) {
current = history.getFragment(history.getHash(history.iframe));
}
if (current === history.fragment) {
return false;
}
if (history.iframe) {
history.navigate(current, false);
}
history.loadUrl();
};
/**
* Attempts to load the current URL fragment. A pass-through to options.routeHandler.
* @method loadUrl
* @return {boolean} Returns true/false from the route handler.
*/
history.loadUrl = function(fragmentOverride) {
var fragment = history.fragment = history.getFragment(fragmentOverride);
return history.options.routeHandler ?
history.options.routeHandler(fragment) :
false;
};
/**
* Save a fragment into the hash history, or replace the URL state if the
* 'replace' option is passed. You are responsible for properly URL-encoding
* the fragment in advance.
* The options object can contain `trigger: false` if you wish to not have the
* route callback be fired, or `replace: true`, if
* you wish to modify the current URL without adding an entry to the history.
* @method navigate
* @param {string} fragment The url fragment to navigate to.
* @param {object|boolean} options An options object with optional trigger and replace flags. You can also pass a boolean directly to set the trigger option. Trigger is `true` by default.
* @return {boolean} Returns true/false from loading the url.
*/
history.navigate = function(fragment, options) {
if (!history.active) {
return false;
}
if(options === undefined) {
options = {
trigger: true
};
}else if(system.isBoolean(options)) {
options = {
trigger: options
};
}
fragment = history.getFragment(fragment || '');
if (history.fragment === fragment) {
return;
}
history.fragment = fragment;
var url = history.root + fragment;
// Don't include a trailing slash on the root.
if(fragment === '' && url !== '/') {
url = url.slice(0, -1);
}
// If pushState is available, we use it to set the fragment as a real URL.
if (history._hasPushState) {
history.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (history._wantsHashChange) {
updateHash(history.location, fragment, options.replace);
if (history.iframe && (fragment !== history.getFragment(history.getHash(history.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want history.
if (!options.replace) {
history.iframe.document.open().close();
}
updateHash(history.iframe.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return history.location.assign(url);
}
if (options.trigger) {
return history.loadUrl(fragment);
}
};
/**
* Navigates back in the browser history.
* @method navigateBack
*/
history.navigateBack = function() {
history.history.back();
};
/**
* @class HistoryOptions
* @static
*/
/**
* The function that will be called back when the fragment changes.
* @property {function} routeHandler
*/
/**
* The url root used to extract the fragment when using push state.
* @property {string} root
*/
/**
* Use hash change when present.
* @property {boolean} hashChange
* @default true
*/
/**
* Use push state when present.
* @property {boolean} pushState
* @default false
*/
/**
* Prevents loading of the current url when activating history.
* @property {boolean} silent
* @default false
*/
return history;
});