UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

240 lines (208 loc) 7.57 kB
// # can/route/pushstate/pushstate.js // // Plugin for `can.route` which uses browser `history.pushState` support // to update window's pathname instead of the `hash`. // // It registers itself as binding on `can.route`, intercepts `click` events // on `<a>` elements across document and accordingly updates `can.route` state // and window's pathname. /*jshint maxdepth:6, scripturl:true*/ steal('can/util', 'can/route', function (can) { "use strict"; var hasPushstate = window.history && window.history.pushState; var isFileProtocol = window.location && window.location.protocol === 'file:'; // Initialize plugin only if browser supports pushstate. if (!isFileProtocol && hasPushstate) { // Registers itself within `can.route.bindings`. can.route.bindings.pushstate = { /** * @property {String} can.route.pushstate.root * @parent can.route.pushstate * * @description Configure the base url that will not be modified. * * @option {String} Represents the base url that pushstate will prepend to all * routes. `root` defaults to: `"/"`. * * @body * * ## Use * * By default, a route like: * * can.route(":type/:id") * * Matches urls like: * * http://domain.com/contact/5 * * But sometimes, you only want to match pages within a certain directory. For * example, an application that is a filemanager. You might want to * specify root and routes like: * * can.route.pushstate.root = "/filemanager/" * can.route("file-:fileId"); * can.route("folder-:fileId") * * Which matches urls like: * * http://domain.com/filemanager/file-34234 * */ // Start of `location.pathname` is the root. // (Can be configured via `can.route.bindings.pushstate.root`) root: "/", // don't greedily match slashes in routing rules matchSlashes: false, paramsMatcher: /^\?(?:[^=]+=[^&]*&)*[^=]+=[^&]*/, querySeparator: '?', // ## bind // Intercepts clicks on `<a>` elements and rewrites original `history` methods. bind: function () { if(can.isNode) { return; } // Intercept routable links. can.delegate.call(can.$(document.documentElement), 'a', 'click', anchorClickHandler); // Rewrites original `pushState`/`replaceState` methods on `history` and keeps pointer to original methods can.each(methodsToOverwrite, function (method) { originalMethods[method] = window.history[method]; window.history[method] = function (state, title, url) { // Avoid doubled history states (with pushState). var absolute = url.indexOf("http") === 0; var searchHash = window.location.search + window.location.hash; // If url differs from current call original histoy method and update `can.route` state. if ((!absolute && url !== window.location.pathname + searchHash) || (absolute && url !== window.location.href + searchHash)) { originalMethods[method].apply(window.history, arguments); can.route.setState(); } }; }); // Bind to `popstate` event, fires on back/forward. can.bind.call(window, 'popstate', can.route.setState); }, // ## unbind // Unbinds and restores original `history` methods unbind: function () { can.undelegate.call(can.$(document.documentElement), 'click', 'a', anchorClickHandler); can.each(methodsToOverwrite, function (method) { window.history[method] = originalMethods[method]; }); can.unbind.call(window, 'popstate', can.route.setState); }, // ## matchingPartOfURL // Returns matching part of url without root. matchingPartOfURL: function () { var root = cleanRoot(), loc = (location.pathname + location.search), index = loc.indexOf(root); return loc.substr(index + root.length); }, // ## setURL // Updates URL by calling `pushState`. setURL: function (path, changed) { var method = "pushState"; // Keeps hash if not in path. if (includeHash && path.indexOf("#") === -1 && window.location.hash) { path += window.location.hash; } if(replaceStateAttrs.length > 0) { var toRemove = []; for(var i = 0, l = changed.length; i < l; i++) { if(can.inArray(changed[i], replaceStateAttrs) !== -1) { method = "replaceState"; } if(can.inArray(changed[i], replaceStateAttrs.once) !== -1) { toRemove.push(changed[i]); } } if(toRemove.length > 0) { removeAttrs(replaceStateAttrs, toRemove); removeAttrs(replaceStateAttrs.once, toRemove); } } window.history[method](null, null, can.route._call("root") + path); } }; // ## anchorClickHandler // Handler function for `click` events. var anchorClickHandler = function (e) { if (!(e.isDefaultPrevented ? e.isDefaultPrevented() : e.defaultPrevented === true)) { // YUI calls back events triggered with this as a wrapped object. var node = this._node || this; // Fix for IE showing blank host, but blank host means current host. var linksHost = node.host || window.location.host; if(node.href === "javascript://") { return; } // If link is within the same domain and descendant of `root` if (window.location.host === linksHost) { var root = cleanRoot(); if (node.pathname.indexOf(root) === 0) { // Removes root from url. var url = (node.pathname + node.search).substr(root.length); // If a route matches update the data. var curParams = can.route.deparam(url); if (curParams.hasOwnProperty('route')) { // Makes it possible to have a link with a hash. includeHash = true; window.history.pushState(null, null, node.href); // Test if you can preventDefault // our tests can't call .click() b/c this // freezes phantom. if (e.preventDefault) { e.preventDefault(); } } } } } }, // ## cleanRoot // Always returns clean root, without domain. cleanRoot = function () { var domain = location.protocol + "//" + location.host, root = can.route._call("root"), index = root.indexOf(domain); if (index === 0) { return root.substr(domain.length); } return root; }, removeAttrs = function(arr, attrs) { var index; for(var i = attrs.length - 1; i >= 0; i--) { if( (index = can.inArray(attrs[i], arr)) !== -1) { arr.splice(index, 1); } } }, // Original methods on `history` that will be overwritten methodsToOverwrite = ['pushState', 'replaceState'], // A place to store pointers to original `history` methods. originalMethods = {}, // Used to tell setURL to include the hash because we clicked on a link. includeHash = false, // Attributes that will cause replaceState to be called replaceStateAttrs = []; // Enables plugin, by default `hashchange` binding is used. can.route.defaultBinding = "pushstate"; can.extend(can.route, { replaceStateOn: function() { var attrs = can.makeArray(arguments); Array.prototype.push.apply(replaceStateAttrs, attrs); }, replaceStateOnce: function() { var attrs = can.makeArray(arguments); replaceStateAttrs.once = can.makeArray(replaceStateAttrs.once); Array.prototype.push.apply(replaceStateAttrs.once, attrs); can.route.replaceStateOn.apply(this, arguments); }, replaceStateOff: function() { var attrs = can.makeArray(arguments); removeAttrs(replaceStateAttrs, attrs); } }); } return can; });