/*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:';
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: '?',
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);
},
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);
},
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);
},
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);
}
};
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();
}
}
}
}
}
},
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;
});