You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tt-rss/lib/dojo/router/RouterBase.js.uncompressed.js

351 lines
9.7 KiB
JavaScript

define("dojo/router/RouterBase", [
"dojo/_base/declare",
"dojo/hash",
"dojo/topic"
], function(declare, hash, topic){
// module:
// dojo/router/RouterBase
// Creating a basic trim to avoid needing the full dojo/string module
// similarly to dojo/_base/lang's trim
var trim;
if(String.prototype.trim){
trim = function(str){ return str.trim(); };
}else{
trim = function(str){ return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); };
}
// Firing of routes on the route object is always the same,
// no clean way to expose this on the prototype since it's for the
// internal router objects.
function fireRoute(params, currentPath, newPath){
var queue, isStopped, isPrevented, eventObj, i, l;
queue = this.callbackQueue;
isStopped = false;
isPrevented = false;
eventObj = {
stopImmediatePropagation: function(){ isStopped = true; },
preventDefault: function(){ isPrevented = true; },
oldPath: currentPath,
newPath: newPath,
params: params
};
for(i=0, l=queue.length; i<l; ++i){
if(!isStopped){
queue[i](eventObj);
}
}
return !isPrevented;
}
// Our actual class-like object
var RouterBase = declare(null, {
// summary:
// A module that allows one to easily map hash-based structures into
// callbacks. The router module is a singleton, offering one central
// point for all registrations of this type.
// example:
// | var router = new RouterBase({});
// | router.register("/widgets/:id", function(evt){
// | // If "/widgets/3" was matched,
// | // evt.params.id === "3"
// | xhr.get({
// | url: "/some/path/" + evt.params.id,
// | load: function(data){
// | // ...
// | }
// | });
// | });
_routes: null,
_routeIndex: null,
_started: false,
_currentPath: "",
idMatch: /:(\w[\w\d]*)/g,
idReplacement: "([^\\/]+)",
globMatch: /\*(\w[\w\d]*)/,
globReplacement: "(.+)",
constructor: function(kwArgs){
// A couple of safety initializations
this._routes = [];
this._routeIndex = {};
// Simple constructor-style "Decorate myself all over" for now
for(var i in kwArgs){
if(kwArgs.hasOwnProperty(i)){
this[i] = kwArgs[i];
}
}
},
register: function(/*String|RegExp*/ route, /*Function*/ callback){
// summary:
// Registers a route to a handling callback
// description:
// Given either a string or a regular expression, the router
// will monitor the page's hash and respond to changes that
// match the string or regex as provided.
//
// When provided a regex for the route:
//
// - Matching is performed, and the resulting capture groups
// are passed through to the callback as an array.
//
// When provided a string for the route:
//
// - The string is parsed as a URL-like structure, like
// "/foo/bar"
// - If any portions of that URL are prefixed with a colon
// (:), they will be parsed out and provided to the callback
// as properties of an object.
// - If the last piece of the URL-like structure is prefixed
// with a star (*) instead of a colon, it will be replaced in
// the resulting regex with a greedy (.+) match and
// anything remaining on the hash will be provided as a
// property on the object passed into the callback. Think of
// it like a basic means of globbing the end of a route.
// example:
// | router.register("/foo/:bar/*baz", function(object){
// | // If the hash was "/foo/abc/def/ghi",
// | // object.bar === "abc"
// | // object.baz === "def/ghi"
// | });
// returns: Object
// A plain JavaScript object to be used as a handle for
// either removing this specific callback's registration, as
// well as to add new callbacks with the same route initially
// used.
// route: String|RegExp
// A string or regular expression which will be used when
// monitoring hash changes.
// callback: Function
// When the hash matches a pattern as described in the route,
// this callback will be executed. It will receive an event
// object that will have several properties:
//
// - params: Either an array or object of properties pulled
// from the new hash
// - oldPath: The hash in its state before the change
// - newPath: The new hash being shifted to
// - preventDefault: A method that will stop hash changes
// from being actually applied to the active hash. This only
// works if the hash change was initiated using `router.go`,
// as changes initiated more directly to the location.hash
// property will already be in place
// - stopImmediatePropagation: When called, will stop any
// further bound callbacks on this particular route from
// being executed. If two distinct routes are bound that are
// different, but both happen to match the current hash in
// some way, this will *not* keep other routes from receiving
// notice of the change.
return this._registerRoute(route, callback);
},
registerBefore: function(/*String|RegExp*/ route, /*Function*/ callback){
// summary:
// Registers a route to a handling callback, except before
// any previously registered callbacks
// description:
// Much like the `register` method, `registerBefore` allows
// us to register route callbacks to happen before any
// previously registered callbacks. See the documentation for
// `register` for more details and examples.
return this._registerRoute(route, callback, true);
},
go: function(path, replace){
// summary:
// A simple pass-through to make changing the hash easy,
// without having to require dojo/hash directly. It also
// synchronously fires off any routes that match.
// example:
// | router.go("/foo/bar");
var applyChange;
path = trim(path);
applyChange = this._handlePathChange(path);
if(applyChange){
hash(path, replace);
}
return applyChange;
},
startup: function(){
// summary:
// This method must be called to activate the router. Until
// startup is called, no hash changes will trigger route
// callbacks.
if(this._started){ return; }
var self = this;
this._started = true;
this._handlePathChange(hash());
topic.subscribe("/dojo/hashchange", function(){
// No need to load all of lang for just this
self._handlePathChange.apply(self, arguments);
});
},
_handlePathChange: function(newPath){
var i, j, li, lj, routeObj, result,
allowChange, parameterNames, params,
routes = this._routes,
currentPath = this._currentPath;
if(!this._started || newPath === currentPath){ return allowChange; }
allowChange = true;
for(i=0, li=routes.length; i<li; ++i){
routeObj = routes[i];
result = routeObj.route.exec(newPath);
if(result){
if(routeObj.parameterNames){
parameterNames = routeObj.parameterNames;
params = {};
for(j=0, lj=parameterNames.length; j<lj; ++j){
params[parameterNames[j]] = result[j+1];
}
}else{
params = result.slice(1);
}
allowChange = routeObj.fire(params, currentPath, newPath);
}
}
if(allowChange){
this._currentPath = newPath;
}
return allowChange;
},
_convertRouteToRegExp: function(route){
// Sub in based on IDs and globs
route = route.replace(this.idMatch, this.idReplacement);
route = route.replace(this.globMatch, this.globReplacement);
// Make sure it's an exact match
route = "^" + route + "$";
return new RegExp(route);
},
_getParameterNames: function(route){
var idMatch = this.idMatch,
globMatch = this.globMatch,
parameterNames = [], match;
idMatch.lastIndex = 0;
while((match = idMatch.exec(route)) !== null){
parameterNames.push(match[1]);
}
if((match = globMatch.exec(route)) !== null){
parameterNames.push(match[1]);
}
return parameterNames.length > 0 ? parameterNames : null;
},
_indexRoutes: function(){
var i, l, route, routeIndex, routes = this._routes;
// Start a new route index
routeIndex = this._routeIndex = {};
// Set it up again
for(i=0, l=routes.length; i<l; ++i){
route = routes[i];
routeIndex[route.route] = i;
}
},
_registerRoute: function(/*String|RegExp*/route, /*Function*/callback, /*Boolean?*/isBefore){
var index, exists, routeObj, callbackQueue, removed,
self = this, routes = this._routes,
routeIndex = this._routeIndex;
// Try to fetch the route if it already exists.
// This works thanks to stringifying of regex
index = this._routeIndex[route];
exists = typeof index !== "undefined";
if(exists){
routeObj = routes[index];
}
// If we didn't get one, make a default start point
if(!routeObj){
routeObj = {
route: route,
callbackQueue: [],
fire: fireRoute
};
}
callbackQueue = routeObj.callbackQueue;
if(typeof route == "string"){
routeObj.parameterNames = this._getParameterNames(route);
routeObj.route = this._convertRouteToRegExp(route);
}
if(isBefore){
callbackQueue.unshift(callback);
}else{
callbackQueue.push(callback);
}
if(!exists){
index = routes.length;
routeIndex[route] = index;
routes.push(routeObj);
}
// Useful in a moment to keep from re-removing routes
removed = false;
return { // Object
remove: function(){
var i, l;
if(removed){ return; }
for(i=0, l=callbackQueue.length; i<l; ++i){
if(callbackQueue[i] === callback){
callbackQueue.splice(i, 1);
}
}
if(callbackQueue.length === 0){
routes.splice(index, 1);
self._indexRoutes();
}
removed = true;
},
register: function(callback, isBefore){
return self.register(route, callback, isBefore);
}
};
}
});
return RouterBase;
});