| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- /**
- * Durandal 2.1.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:|#).*$/, '');
- 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;
- });
|