/**
* @ignore
* simple router to get path parameter and query parameter from hash(old ie) or url(html5)
* @author yiminghe@gmail.com
*/
KISSY.add('mvc/router', function (S, Node, Base, undefined) {
var each = S.each,
// take a breath to avoid duplicate hashchange
BREATH_INTERVAL = 100,
grammar = /(:([\w\d]+))|(\\\*([\w\d]+))/g,
// all registered route instance
allRoutes = [],
win = S.Env.host,
$ = Node.all,
$win = $(win),
ie = win.document.documentMode || S.UA.ie,
history = win.history ,
supportNativeHistory = !!(history && history['pushState']),
ROUTER_MAP = "__routerMap";
function findFirstCaptureGroupIndex(regStr) {
var r, i;
for (i = 0; i < regStr.length; i++) {
r = regStr.charAt(i);
// skip escaped reg meta char
if (r == "\\") {
i++;
} else if (r == "(") {
return i;
}
}
throw new Error("impossible to not to get capture group in kissy mvc route");
}
function getHash(url) {
// 不能 location.hash
// 1.
// http://xx.com/#yy?z=1
// ie6 => location.hash = #yy
// 其他浏览器 => location.hash = #yy?z=1
// 2.
// #!/home/q={%22thedate%22:%2220121010~20121010%22}
// firefox 15 => #!/home/q={"thedate":"20121010~20121010"}
// !! :(
return new S.Uri(url).getFragment().replace(/^!/, "");
}
// get url fragment and dispatch
function getFragment(url) {
url = url || location.href;
if (Router.nativeHistory && supportNativeHistory) {
url = new S.Uri(url);
var query = url.getQuery().toString();
return url.getPath().substr(Router.urlRoot.length) + (query ? ('?' + query) : '');
} else {
return getHash(url);
}
}
function endWithSlash(str) {
return S.endsWith(str, "/");
}
function startWithSlash(str) {
return S.startsWith(str, "/");
}
function removeEndSlash(str) {
if (endWithSlash(str)) {
str = str.substring(0, str.length - 1);
}
return str;
}
function removeStartSlash(str) {
if (startWithSlash(str)) {
str = str.substring(1);
}
return str;
}
function addEndSlash(str) {
return removeEndSlash(str) + "/";
}
function addStartSlash(str) {
if (str) {
return "/" + removeStartSlash(str);
} else {
return str;
}
}
function equalsIgnoreSlash(str1, str2) {
str1 = removeEndSlash(str1);
str2 = removeEndSlash(str2);
return str1 == str2;
}
// get full path from fragment for html history
function getFullPath(fragment) {
return location.protocol + "//" + location.host +
removeEndSlash(Router.urlRoot) + addStartSlash(fragment)
}
// match url with route intelligently (always get optimal result)
function dispatch() {
var query,
path,
arg,
finalRoute = 0,
finalMatchLength = -1,
finalRegStr = "",
finalFirstCaptureGroupIndex = -1,
finalCallback = 0,
finalRouteName = "",
pathUri = new S.Uri(getFragment()),
finalParam = 0;
path = pathUri.clone();
path.query.reset();
path = path.toString();
// user input : /xx/yy/zz
each(allRoutes, function (route) {
var routeRegs = route[ROUTER_MAP],
// match exactly
exactlyMatch = 0;
each(routeRegs, function (desc) {
var reg = desc.reg,
regStr = desc.regStr,
paramNames = desc.paramNames,
firstCaptureGroupIndex = -1,
m,
name = desc.name,
callback = desc.callback;
if (m = path.match(reg)) {
// match all result item shift out
m.shift();
function genParam() {
if (paramNames) {
var params = {};
each(m, function (sm, i) {
params[paramNames[i]] = sm;
});
return params;
} else {
// if user gave directly reg
// then call callback with match result array
return [].concat(m);
}
}
function upToFinal() {
finalRegStr = regStr;
finalFirstCaptureGroupIndex = firstCaptureGroupIndex;
finalCallback = callback;
finalParam = genParam();
finalRoute = route;
finalRouteName = name;
finalMatchLength = m.length;
}
// route: /xx/yy/zz
if (!m.length) {
upToFinal();
exactlyMatch = 1;
return false;
}
else if (regStr) {
firstCaptureGroupIndex = findFirstCaptureGroupIndex(regStr);
// final route : /*
// now route : /xx/*
if (firstCaptureGroupIndex > finalFirstCaptureGroupIndex) {
upToFinal();
}
else if (
firstCaptureGroupIndex == finalFirstCaptureGroupIndex &&
finalMatchLength >= m.length
) {
// final route : /xx/:id/:id
// now route : /xx/:id/zz
if (m.length < finalMatchLength) {
upToFinal()
} else if (regStr.length > finalRegStr.length) {
upToFinal();
}
}
// first route has priority
else if (!finalRoute) {
upToFinal();
}
}
// if exists user-given reg router rule then update value directly
// first route has priority
// 用户设置的正则表达式具备高优先级
else {
upToFinal();
exactlyMatch = 1;
return false;
}
}
return undefined;
}
);
if (exactlyMatch) {
return false;
}
return undefined;
});
if (finalParam) {
query = pathUri.query.get();
finalCallback.apply(finalRoute, [finalParam, query, {
path: path,
url: location.href
}]);
arg = {
name: finalRouteName,
"paths": finalParam,
path: path,
url: location.href,
query: query
};
finalRoute.fire('route:' + finalRouteName, arg);
finalRoute.fire('route', arg);
}
}
/*
transform route declaration to router reg
@param str
/search/:q
/user/*path
*/
function transformRouterReg(self, str, callback) {
var name = str,
paramNames = [];
if (typeof callback === 'function') {
// escape keyword from regexp
str = S.escapeRegExp(str);
str = str.replace(grammar, function (m, g1, g2, g3, g4) {
paramNames.push(g2 || g4);
// :name
if (g2) {
return "([^/]+)";
}
// *name
else if (g4) {
return "(.*)";
}
return undefined;
});
return {
name: name,
paramNames: paramNames,
reg: new RegExp("^" + str + "$"),
regStr: str,
callback: callback
};
} else {
return {
name: name,
reg: callback.reg,
callback: normFn(self, callback.callback)
};
}
}
// normalize function by self
function normFn(self, callback) {
if (typeof callback === 'function') {
return callback;
} else if (typeof callback == 'string') {
return self[callback];
}
return callback;
}
function _afterRoutesChange(e) {
var self = this;
self[ROUTER_MAP] = {};
self.addRoutes(e.newVal);
}
var Router;
/**
* Router used to route url to responding action callbacks.
* @class KISSY.MVC.Router
* @extends KISSY.Base
*/
return Router = Base.extend({
initializer: function () {
var self = this;
self.on("afterRoutesChange", _afterRoutesChange, self);
_afterRoutesChange.call(self, {newVal: self.get("routes")});
allRoutes.push(self);
},
/**
* Add config to current router.
* @param {Object} routes Route config.
*
*
* {
* "/search/:param":"callback"
* // or
* "search":{
* reg:/xx/,
* callback:fn
* }
* }
*/
addRoutes: function (routes) {
var self = this;
each(routes, function (callback, name) {
self[ROUTER_MAP][name] = transformRouterReg(self, name, normFn(self, callback));
});
}
}, {
ATTRS: {
/**
* Route and action config.
* @cfg {Object} routes
*
*
* {
* "/search/:param":"callback"
* // or
* "search":{
* reg:/xx/,
* callback:fn
* }
* }
*/
/**
* @ignore
*/
routes: {}
},
/**
* whether Router can process path
* @param {String} path path for route
* @return {Boolean}
* @static
* @member KISSY.MVC.Router
*/
hasRoute: function (path) {
var match = 0;
// user input : /xx/yy/zz
each(allRoutes, function (route) {
var routeRegs = route[ROUTER_MAP];
each(routeRegs, function (desc) {
var reg = desc.reg;
if (path.match(reg)) {
match = 1;
return false;
}
return undefined;
});
if (match) {
return false;
}
return undefined;
});
return !!match;
},
/**
* get the route path
* @param {String} url full location href
* @return {String} route path
* @static
* @member KISSY.MVC.Router
*/
removeRoot: function (url) {
var u = new S.Uri(url);
return u.getPath().substr(Router.urlRoot.length);
},
/**
* Navigate to specified path.
* @static
* @member KISSY.MVC.Router
* @param {String} path Destination path.
* @param {Object} [opts] Config for current navigation.
* @param {Boolean} opts.triggerRoute Whether to trigger responding action
* even current path is same as parameter
*/
navigate: function (path, opts) {
opts = opts || {};
var replaceHistory = opts.replaceHistory, normalizedPath;
if (getFragment() !== path) {
if (Router.nativeHistory && supportNativeHistory) {
history[replaceHistory ? 'replaceState' : 'pushState']({},
"", getFullPath(path));
// pushState does not fire popstate event (unlike hashchange)
// so popstate is not statechange
// fire manually
dispatch();
} else {
normalizedPath = '#!' + path;
if (replaceHistory) {
// add history hack
location.replace(normalizedPath +
(ie && ie < 8 ? Node.REPLACE_HISTORY : ''));
} else {
location.hash = normalizedPath;
}
}
} else if (opts && opts.triggerRoute) {
dispatch();
}
},
/**
* Start all routers (url monitor).
* @static
* @member KISSY.MVC.Router
* @param {Object} opts
* @param {Function} opts.success Callback function to be called after router is started.
* @param {String} opts.urlRoot Specify url root for html5 history management.
* @param {Boolean} opts.nativeHistory Whether enable html5 history management.
*/
start: function (opts) {
opts = opts || {};
if (Router.__started) {
return opts.success && opts.success();
}
// remove backslash
opts.urlRoot = (opts.urlRoot || "").replace(/\/$/, '');
var urlRoot,
nativeHistory = opts.nativeHistory,
locPath = location.pathname,
hash = getFragment(),
hashIsValid = location.hash.match(/#!.+/);
urlRoot = Router.urlRoot = opts.urlRoot;
Router.nativeHistory = nativeHistory;
if (nativeHistory) {
if (supportNativeHistory) {
// http://x.com/#!/x/y
// =>
// http://x.com/x/y
// =>
// process without refresh page and add history entry
if (hashIsValid) {
if (equalsIgnoreSlash(locPath, urlRoot)) {
// put hash to path
history['replaceState']({}, "", getFullPath(hash));
opts.triggerRoute = 1;
} else {
S.error("location path must be same with urlRoot!");
}
}
}
// http://x.com/x/y
// =>
// http://x.com/#!/x/y
// =>
// refresh page without add history entry
else if (!equalsIgnoreSlash(locPath, urlRoot)) {
location.replace(addEndSlash(urlRoot) + "#!" + hash);
return undefined;
}
}
// prevent hashChange trigger on start
setTimeout(function () {
if (nativeHistory && supportNativeHistory) {
$win.on('popstate', dispatch);
// html5 triggerRoute is leaved to user decision
// if provide no #! hash
} else {
$win.on("hashchange", dispatch);
// hash-based browser is forced to trigger route
opts.triggerRoute = 1;
}
// check initial hash on start
// in case server does not render initial state correctly
// when monitor hashchange ,client must be responsible for dispatching and rendering.
if (opts.triggerRoute) {
dispatch();
}
opts.success && opts.success();
}, BREATH_INTERVAL);
Router.__started = 1;
return undefined;
},
/**
* stop all routers
* @static
* @member KISSY.MVC.Router
*/
stop: function () {
Router.__started = 0;
$win.detach('popstate', dispatch);
$win.detach("hashchange", dispatch);
allRoutes = [];
}
});
}, {
requires: ['node', 'base']
});
/**
* @ignore
* 2011-11-30
* - support user-given native regexp for router rule
*
* refer :
* http://www.w3.org/TR/html5/history.html
* http://documentcloud.github.com/backbone/
**/