UNPKG

lettuce

Version:

Lettuce JS, Mini Mobile Framework for Romantic with DSL.

223 lines (184 loc) 9.03 kB
var Events = require('backbone-events-standalone'); var _ = require('underscore'); // Handles cross-browser history management, based on either // [pushState](http://diveintohtml5.info/history.html) and real URLs, or // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) // and URL fragments. If the browser supports neither. var History = function () { this.handlers = []; this.checkUrl = _.bind(this.checkUrl, this); // Ensure that `History` can be used outside of the browser. if (typeof window !== 'undefined') { this.location = window.location; this.history = window.history; } }; // 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 stripping urls of hash. var pathStripper = /#.*$/; // Has the history handling already been started? History.started = false; // Set up all inheritable **Backbone.History** properties and methods. _.extend(History.prototype, Events, { // The default interval to poll for hash changes, if necessary, is // twenty times a second. interval: 50, // Are we at the app root? atRoot: function () { var path = this.location.pathname.replace(/[^\/]$/, '$&/'); return path === this.root && !this.location.search; }, // Gets the true hash value. Cannot use location.hash directly due to bug // in Firefox where location.hash will always be decoded. getHash: function (window) { var match = (window || this).location.href.match(/#(.*)$/); return match ? match[1] : ''; }, // Get the pathname and search params, without the root. getPath: function () { var path = decodeURI(this.location.pathname + this.location.search); var root = this.root.slice(0, -1); if (!path.indexOf(root)) path = path.slice(root.length); return path.slice(1); }, // Get the cross-browser normalized URL fragment from the path or hash. getFragment: function (fragment) { if (fragment == null) { if (this._hasPushState || !this._wantsHashChange) { fragment = this.getPath(); } else { fragment = this.getHash(); } } return fragment.replace(routeStripper, ''); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. start: function (options) { if (History.started) throw new Error("Backbone.history has already been started"); History.started = true; // Figure out the initial configuration. // Is pushState desired ... is it available? this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; this._hasHashChange = 'onhashchange' in window; this._wantsPushState = !!this.options.pushState; this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); this.fragment = this.getFragment(); // Add a cross-platform `addEventListener` shim for older browsers. var addEventListener = window.addEventListener; // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); // Depending on whether we're using pushState or hashes, and whether // 'onhashchange' is supported, determine how we check the URL state. if (this._hasPushState) { addEventListener('popstate', this.checkUrl, false); } else if (this._wantsHashChange && this._hasHashChange) { addEventListener('hashchange', this.checkUrl, false); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); } // Transition from hashChange to pushState or vice versa if both are // requested. if (this._wantsHashChange && this._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 (!this._hasPushState && !this.atRoot()) { this.location.replace(this.root + '#' + this.getPath()); // 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 (this._hasPushState && this.atRoot()) { this.navigate(this.getHash(), {replace: true}); } } if (!this.options.silent) return this.loadUrl(); }, // Disable Backbone.history, perhaps temporarily. Not useful in a real app, // but possibly useful for unit testing Routers. stop: function () { // Add a cross-platform `removeEventListener` shim for older browsers. var removeEventListener = window.removeEventListener; // Remove window listeners. if (this._hasPushState) { removeEventListener('popstate', this.checkUrl, false); } else if (this._wantsHashChange && this._hasHashChange) { removeEventListener('hashchange', this.checkUrl, false); } // Some environments will throw when clearing an undefined interval. if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); History.started = false; }, // Add a route to be tested when the fragment changes. Routes added later // may override previous routes. route: function (route, callback) { this.handlers.unshift({route: route, callback: callback}); }, // Checks the current URL to see if it has changed, and if it has, // calls `loadUrl`. checkUrl: function (e) { var current = this.getFragment(); if (current === this.fragment) return false; this.loadUrl(); }, // Attempt to load the current URL fragment. If a route succeeds with a // match, returns `true`. If no defined routes matches the fragment, // returns `false`. loadUrl: function (fragment) { fragment = this.fragment = this.getFragment(fragment); return this.handlers.some(function (handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } }); }, // 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: true` if you wish to have the // route callback be fired (not usually desirable), or `replace: true`, if // you wish to modify the current URL without adding an entry to the history. navigate: function (fragment, options) { if (!History.started) return false; if (!options || options === true) options = {trigger: !!options}; var url = this.root + (fragment = this.getFragment(fragment || '')); // Strip the hash and decode for matching. fragment = decodeURI(fragment.replace(pathStripper, '')); if (this.fragment === fragment) return; this.fragment = 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 (this._hasPushState) { this.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 (this._wantsHashChange) { this._updateHash(this.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 this.location.assign(url); } if (options.trigger) return this.loadUrl(fragment); }, // Update the hash location, either replacing the current entry, or adding // a new one to the browser history. _updateHash: function (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; } } }); module.exports = new History();