UNPKG

grunt-durandal

Version:

Grunt Durandal Builder - Build durandal project using a custom require config and a custom almond

322 lines (275 loc) 12.3 kB
/** * Durandal 2.0.0 Copyright (c) 2012 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:|#).*$/, ''); 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; 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(); } }; /** * 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; // 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; });