Files
ECAILLE Fabrice (externe) e2277667c5 Fix: add dependencies
2017-05-04 10:26:11 +02:00

1131 lines
42 KiB
JavaScript

/**
* Durandal 2.1.0 Copyright (c) 2012 Blue Spire Consulting, Inc. All Rights Reserved.
* Available via the MIT license.
* see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details.
*/
/**
* Connects the history module's url and history tracking support to Durandal's activation and composition engine allowing you to easily build navigation-style applications.
* @module router
* @requires system
* @requires app
* @requires activator
* @requires events
* @requires composition
* @requires history
* @requires knockout
* @requires jquery
*/
define(['durandal/system', 'durandal/app', 'durandal/activator', 'durandal/events', 'durandal/composition', 'plugins/history', 'knockout', 'jquery'], function(system, app, activator, events, composition, history, ko, $) {
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
var startDeferred, rootRouter;
var trailingSlash = /\/$/;
var routesAreCaseSensitive = false;
var lastUrl = '/', lastTryUrl = '/';
function routeStringToRegExp(routeString) {
routeString = routeString.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional) {
return optional ? match : '([^\/]+)';
})
.replace(splatParam, '(.*?)');
return new RegExp('^' + routeString + '$', routesAreCaseSensitive ? undefined : 'i');
}
function stripParametersFromRoute(route) {
var colonIndex = route.indexOf(':');
var length = colonIndex > 0 ? colonIndex - 1 : route.length;
return route.substring(0, length);
}
function endsWith(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
function compareArrays(first, second) {
if (!first || !second){
return false;
}
if (first.length != second.length) {
return false;
}
for (var i = 0, len = first.length; i < len; i++) {
if (first[i] != second[i]) {
return false;
}
}
return true;
}
function reconstructUrl(instruction){
if(!instruction.queryString){
return instruction.fragment;
}
return instruction.fragment + '?' + instruction.queryString;
}
/**
* @class Router
* @uses Events
*/
/**
* Triggered when the navigation logic has completed.
* @event router:navigation:complete
* @param {object} instance The activated instance.
* @param {object} instruction The routing instruction.
* @param {Router} router The router.
*/
/**
* Triggered when the navigation has been cancelled.
* @event router:navigation:cancelled
* @param {object} instance The activated instance.
* @param {object} instruction The routing instruction.
* @param {Router} router The router.
*/
/**
* Triggered when navigation begins.
* @event router:navigation:processing
* @param {object} instruction The routing instruction.
* @param {Router} router The router.
*/
/**
* Triggered right before a route is activated.
* @event router:route:activating
* @param {object} instance The activated instance.
* @param {object} instruction The routing instruction.
* @param {Router} router The router.
*/
/**
* Triggered right before a route is configured.
* @event router:route:before-config
* @param {object} config The route config.
* @param {Router} router The router.
*/
/**
* Triggered just after a route is configured.
* @event router:route:after-config
* @param {object} config The route config.
* @param {Router} router The router.
*/
/**
* Triggered when the view for the activated instance is attached.
* @event router:navigation:attached
* @param {object} instance The activated instance.
* @param {object} instruction The routing instruction.
* @param {Router} router The router.
*/
/**
* Triggered when the composition that the activated instance participates in is complete.
* @event router:navigation:composition-complete
* @param {object} instance The activated instance.
* @param {object} instruction The routing instruction.
* @param {Router} router The router.
*/
/**
* Triggered when the router does not find a matching route.
* @event router:route:not-found
* @param {string} fragment The url fragment.
* @param {Router} router The router.
*/
var createRouter = function() {
var queue = [],
isProcessing = ko.observable(false),
currentActivation,
currentInstruction,
activeItem = activator.create();
var router = {
/**
* The route handlers that are registered. Each handler consists of a `routePattern` and a `callback`.
* @property {object[]} handlers
*/
handlers: [],
/**
* The route configs that are registered.
* @property {object[]} routes
*/
routes: [],
/**
* The route configurations that have been designated as displayable in a nav ui (nav:true).
* @property {KnockoutObservableArray} navigationModel
*/
navigationModel: ko.observableArray([]),
/**
* The active item/screen based on the current navigation state.
* @property {Activator} activeItem
*/
activeItem: activeItem,
/**
* Indicates that the router (or a child router) is currently in the process of navigating.
* @property {KnockoutComputed} isNavigating
*/
isNavigating: ko.computed(function() {
var current = activeItem();
var processing = isProcessing();
var currentRouterIsProcesing = current
&& current.router
&& current.router != router
&& current.router.isNavigating() ? true : false;
return processing || currentRouterIsProcesing;
}),
/**
* An observable surfacing the active routing instruction that is currently being processed or has recently finished processing.
* The instruction object has `config`, `fragment`, `queryString`, `params` and `queryParams` properties.
* @property {KnockoutObservable} activeInstruction
*/
activeInstruction:ko.observable(null),
__router__:true
};
events.includeIn(router);
activeItem.settings.areSameItem = function (currentItem, newItem, currentActivationData, newActivationData) {
if (currentItem == newItem) {
return compareArrays(currentActivationData, newActivationData);
}
return false;
};
activeItem.settings.findChildActivator = function(item) {
if (item && item.router && item.router.parent == router) {
return item.router.activeItem;
}
return null;
};
function hasChildRouter(instance, parentRouter) {
return instance.router && instance.router.parent == parentRouter;
}
function setCurrentInstructionRouteIsActive(flag) {
if (currentInstruction && currentInstruction.config.isActive) {
currentInstruction.config.isActive(flag);
}
}
function completeNavigation(instance, instruction, mode) {
system.log('Navigation Complete', instance, instruction);
var fromModuleId = system.getModuleId(currentActivation);
if (fromModuleId) {
router.trigger('router:navigation:from:' + fromModuleId);
}
currentActivation = instance;
setCurrentInstructionRouteIsActive(false);
currentInstruction = instruction;
setCurrentInstructionRouteIsActive(true);
var toModuleId = system.getModuleId(currentActivation);
if (toModuleId) {
router.trigger('router:navigation:to:' + toModuleId);
}
if (!hasChildRouter(instance, router)) {
router.updateDocumentTitle(instance, instruction);
}
switch (mode) {
case 'rootRouter':
lastUrl = reconstructUrl(currentInstruction);
break;
case 'rootRouterWithChild':
lastTryUrl = reconstructUrl(currentInstruction);
break;
case 'lastChildRouter':
lastUrl = lastTryUrl;
break;
}
rootRouter.explicitNavigation = false;
rootRouter.navigatingBack = false;
router.trigger('router:navigation:complete', instance, instruction, router);
}
function cancelNavigation(instance, instruction) {
system.log('Navigation Cancelled');
router.activeInstruction(currentInstruction);
router.navigate(lastUrl, false);
isProcessing(false);
rootRouter.explicitNavigation = false;
rootRouter.navigatingBack = false;
router.trigger('router:navigation:cancelled', instance, instruction, router);
}
function redirect(url) {
system.log('Navigation Redirecting');
isProcessing(false);
rootRouter.explicitNavigation = false;
rootRouter.navigatingBack = false;
router.navigate(url, { trigger: true, replace: true });
}
function activateRoute(activator, instance, instruction) {
rootRouter.navigatingBack = !rootRouter.explicitNavigation && currentActivation != instruction.fragment;
router.trigger('router:route:activating', instance, instruction, router);
var options = {
canDeactivate: !router.parent
};
activator.activateItem(instance, instruction.params, options).then(function(succeeded) {
if (succeeded) {
var previousActivation = currentActivation;
var withChild = hasChildRouter(instance, router);
var mode = '';
if (router.parent) {
if(!withChild) {
mode = 'lastChildRouter';
}
} else {
if (withChild) {
mode = 'rootRouterWithChild';
} else {
mode = 'rootRouter';
}
}
completeNavigation(instance, instruction, mode);
if (withChild) {
instance.router.trigger('router:route:before-child-routes', instance, instruction, router);
var fullFragment = instruction.fragment;
if (instruction.queryString) {
fullFragment += "?" + instruction.queryString;
}
instance.router.loadUrl(fullFragment);
}
if (previousActivation == instance) {
router.attached();
router.compositionComplete();
}
} else if(activator.settings.lifecycleData && activator.settings.lifecycleData.redirect){
redirect(activator.settings.lifecycleData.redirect);
}else{
cancelNavigation(instance, instruction);
}
if (startDeferred) {
startDeferred.resolve();
startDeferred = null;
}
}).fail(function(err){
system.error(err);
});
}
/**
* Inspects routes and modules before activation. Can be used to protect access by cancelling navigation or redirecting.
* @method guardRoute
* @param {object} instance The module instance that is about to be activated by the router.
* @param {object} instruction The route instruction. The instruction object has config, fragment, queryString, params and queryParams properties.
* @return {Promise|Boolean|String} If a boolean, determines whether or not the route should activate or be cancelled. If a string, causes a redirect to the specified route. Can also be a promise for either of these value types.
*/
function handleGuardedRoute(activator, instance, instruction) {
var resultOrPromise = router.guardRoute(instance, instruction);
if (resultOrPromise || resultOrPromise === '') {
if (resultOrPromise.then) {
resultOrPromise.then(function(result) {
if (result) {
if (system.isString(result)) {
redirect(result);
} else {
activateRoute(activator, instance, instruction);
}
} else {
cancelNavigation(instance, instruction);
}
});
} else {
if (system.isString(resultOrPromise)) {
redirect(resultOrPromise);
} else {
activateRoute(activator, instance, instruction);
}
}
} else {
cancelNavigation(instance, instruction);
}
}
function ensureActivation(activator, instance, instruction) {
if (router.guardRoute) {
handleGuardedRoute(activator, instance, instruction);
} else {
activateRoute(activator, instance, instruction);
}
}
function canReuseCurrentActivation(instruction) {
return currentInstruction
&& currentInstruction.config.moduleId == instruction.config.moduleId
&& currentActivation
&& ((currentActivation.canReuseForRoute && currentActivation.canReuseForRoute.apply(currentActivation, instruction.params))
|| (!currentActivation.canReuseForRoute && currentActivation.router && currentActivation.router.loadUrl));
}
function dequeueInstruction() {
if (isProcessing()) {
return;
}
var instruction = queue.shift();
queue = [];
if (!instruction) {
return;
}
isProcessing(true);
router.activeInstruction(instruction);
router.trigger('router:navigation:processing', instruction, router);
if (canReuseCurrentActivation(instruction)) {
var tempActivator = activator.create();
tempActivator.forceActiveItem(currentActivation); //enforce lifecycle without re-compose
tempActivator.settings.areSameItem = activeItem.settings.areSameItem;
tempActivator.settings.findChildActivator = activeItem.settings.findChildActivator;
ensureActivation(tempActivator, currentActivation, instruction);
} else if(!instruction.config.moduleId) {
ensureActivation(activeItem, {
viewUrl:instruction.config.viewUrl,
canReuseForRoute:function() {
return true;
}
}, instruction);
} else {
system.acquire(instruction.config.moduleId).then(function(m) {
var instance = system.resolveObject(m);
if(instruction.config.viewUrl) {
instance.viewUrl = instruction.config.viewUrl;
}
ensureActivation(activeItem, instance, instruction);
}).fail(function(err) {
system.error('Failed to load routed module (' + instruction.config.moduleId + '). Details: ' + err.message, err);
});
}
}
function queueInstruction(instruction) {
queue.unshift(instruction);
dequeueInstruction();
}
// Given a route, and a URL fragment that it matches, return the array of
// extracted decoded parameters. Empty or unmatched parameters will be
// treated as `null` to normalize cross-browser behavior.
function createParams(routePattern, fragment, queryString) {
var params = routePattern.exec(fragment).slice(1);
for (var i = 0; i < params.length; i++) {
var current = params[i];
params[i] = current ? decodeURIComponent(current) : null;
}
var queryParams = router.parseQueryString(queryString);
if (queryParams) {
params.push(queryParams);
}
return {
params:params,
queryParams:queryParams
};
}
function configureRoute(config){
router.trigger('router:route:before-config', config, router);
if (!system.isRegExp(config.route)) {
config.title = config.title || router.convertRouteToTitle(config.route);
if (!config.viewUrl) {
config.moduleId = config.moduleId || router.convertRouteToModuleId(config.route);
}
config.hash = config.hash || router.convertRouteToHash(config.route);
if (config.hasChildRoutes) {
config.route = config.route + '*childRoutes';
}
config.routePattern = routeStringToRegExp(config.route);
}else{
config.routePattern = config.route;
}
config.isActive = config.isActive || ko.observable(false);
router.trigger('router:route:after-config', config, router);
router.routes.push(config);
router.route(config.routePattern, function(fragment, queryString) {
var paramInfo = createParams(config.routePattern, fragment, queryString);
queueInstruction({
fragment: fragment,
queryString:queryString,
config: config,
params: paramInfo.params,
queryParams:paramInfo.queryParams
});
});
};
function mapRoute(config) {
if(system.isArray(config.route)){
var isActive = config.isActive || ko.observable(false);
for(var i = 0, length = config.route.length; i < length; i++){
var current = system.extend({}, config);
current.route = config.route[i];
current.isActive = isActive;
if(i > 0){
delete current.nav;
}
configureRoute(current);
}
}else{
configureRoute(config);
}
return router;
}
/**
* Parses a query string into an object.
* @method parseQueryString
* @param {string} queryString The query string to parse.
* @return {object} An object keyed according to the query string parameters.
*/
router.parseQueryString = function (queryString) {
var queryObject, pairs;
if (!queryString) {
return null;
}
pairs = queryString.split('&');
if (pairs.length == 0) {
return null;
}
queryObject = {};
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i];
if (pair === '') {
continue;
}
var parts = pair.split(/=(.+)?/),
key = parts[0],
value = parts[1] && decodeURIComponent(parts[1].replace(/\+/g, ' '));
var existing = queryObject[key];
if (existing) {
if (system.isArray(existing)) {
existing.push(value);
} else {
queryObject[key] = [existing, value];
}
}
else {
queryObject[key] = value;
}
}
return queryObject;
};
/**
* Add a route to be tested when the url fragment changes.
* @method route
* @param {RegEx} routePattern The route pattern to test against.
* @param {function} callback The callback to execute when the route pattern is matched.
*/
router.route = function(routePattern, callback) {
router.handlers.push({ routePattern: routePattern, callback: callback });
};
/**
* Attempt to load the specified URL fragment. If a route succeeds with a match, returns `true`. If no defined routes matches the fragment, returns `false`.
* @method loadUrl
* @param {string} fragment The URL fragment to find a match for.
* @return {boolean} True if a match was found, false otherwise.
*/
router.loadUrl = function(fragment) {
var handlers = router.handlers,
queryString = null,
coreFragment = fragment,
queryIndex = fragment.indexOf('?');
if (queryIndex != -1) {
coreFragment = fragment.substring(0, queryIndex);
queryString = fragment.substr(queryIndex + 1);
}
if(router.relativeToParentRouter){
var instruction = this.parent.activeInstruction();
coreFragment = queryIndex == -1 ? instruction.params.join('/') : instruction.params.slice(0, -1).join('/');
if(coreFragment && coreFragment.charAt(0) == '/'){
coreFragment = coreFragment.substr(1);
}
if(!coreFragment){
coreFragment = '';
}
coreFragment = coreFragment.replace('//', '/').replace('//', '/');
}
coreFragment = coreFragment.replace(trailingSlash, '');
for (var i = 0; i < handlers.length; i++) {
var current = handlers[i];
if (current.routePattern.test(coreFragment)) {
current.callback(coreFragment, queryString);
return true;
}
}
system.log('Route Not Found', fragment, currentInstruction);
router.trigger('router:route:not-found', fragment, router);
if (router.parent) {
lastUrl = lastTryUrl;
}
history.navigate(lastUrl, { trigger:false, replace:true });
rootRouter.explicitNavigation = false;
rootRouter.navigatingBack = false;
return false;
};
var titleSubscription;
function setTitle(value) {
var appTitle = ko.unwrap(app.title);
if (appTitle) {
document.title = value + " | " + appTitle;
} else {
document.title = value;
}
}
// Allow observable to be used for app.title
if(ko.isObservable(app.title)) {
app.title.subscribe(function () {
var instruction = router.activeInstruction();
var title = instruction != null ? ko.unwrap(instruction.config.title) : '';
setTitle(title);
});
}
/**
* Updates the document title based on the activated module instance, the routing instruction and the app.title.
* @method updateDocumentTitle
* @param {object} instance The activated module.
* @param {object} instruction The routing instruction associated with the action. It has a `config` property that references the original route mapping config.
*/
router.updateDocumentTitle = function (instance, instruction) {
var appTitle = ko.unwrap(app.title),
title = instruction.config.title;
if (titleSubscription) {
titleSubscription.dispose();
}
if (title) {
if (ko.isObservable(title)) {
titleSubscription = title.subscribe(setTitle);
setTitle(title());
} else {
setTitle(title);
}
} else if (appTitle) {
document.title = appTitle;
}
};
/**
* 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: false` if you wish to not have the
* route callback be fired, or `replace: true`, if
* you wish to modify the current URL without adding an entry to the history.
* @method navigate
* @param {string} fragment The url fragment to navigate to.
* @param {object|boolean} options An options object with optional trigger and replace flags. You can also pass a boolean directly to set the trigger option. Trigger is `true` by default.
* @return {boolean} Returns true/false from loading the url.
*/
router.navigate = function(fragment, options) {
if(fragment && fragment.indexOf('://') != -1) {
window.location.href = fragment;
return true;
}
if(options === undefined || (system.isBoolean(options) && options) || (system.isObject(options) && options.trigger)) {
rootRouter.explicitNavigation = true;
}
if ((system.isBoolean(options) && !options) || (options && options.trigger != undefined && !options.trigger)) {
lastUrl = fragment;
}
return history.navigate(fragment, options);
};
/**
* Navigates back in the browser history.
* @method navigateBack
*/
router.navigateBack = function() {
history.navigateBack();
};
router.attached = function() {
router.trigger('router:navigation:attached', currentActivation, currentInstruction, router);
};
router.compositionComplete = function(){
isProcessing(false);
router.trigger('router:navigation:composition-complete', currentActivation, currentInstruction, router);
dequeueInstruction();
};
/**
* Converts a route to a hash suitable for binding to a link's href.
* @method convertRouteToHash
* @param {string} route
* @return {string} The hash.
*/
router.convertRouteToHash = function(route) {
route = route.replace(/\*.*$/, '');
if(router.relativeToParentRouter){
var instruction = router.parent.activeInstruction(),
hash = route ? instruction.config.hash + '/' + route : instruction.config.hash;
if(history._hasPushState){
hash = '/' + hash;
}
hash = hash.replace('//', '/').replace('//', '/');
return hash;
}
if(history._hasPushState){
return route;
}
return "#" + route;
};
/**
* Converts a route to a module id. This is only called if no module id is supplied as part of the route mapping.
* @method convertRouteToModuleId
* @param {string} route
* @return {string} The module id.
*/
router.convertRouteToModuleId = function(route) {
return stripParametersFromRoute(route);
};
/**
* Converts a route to a displayable title. This is only called if no title is specified as part of the route mapping.
* @method convertRouteToTitle
* @param {string} route
* @return {string} The title.
*/
router.convertRouteToTitle = function(route) {
var value = stripParametersFromRoute(route);
return value.substring(0, 1).toUpperCase() + value.substring(1);
};
/**
* Maps route patterns to modules.
* @method map
* @param {string|object|object[]} route A route, config or array of configs.
* @param {object} [config] The config for the specified route.
* @chainable
* @example
router.map([
{ route: '', title:'Home', moduleId: 'homeScreen', nav: true },
{ route: 'customer/:id', moduleId: 'customerDetails'}
]);
*/
router.map = function(route, config) {
if (system.isArray(route)) {
for (var i = 0; i < route.length; i++) {
router.map(route[i]);
}
return router;
}
if (system.isString(route) || system.isRegExp(route)) {
if (!config) {
config = {};
} else if (system.isString(config)) {
config = { moduleId: config };
}
config.route = route;
} else {
config = route;
}
return mapRoute(config);
};
/**
* Builds an observable array designed to bind a navigation UI to. The model will exist in the `navigationModel` property.
* @method buildNavigationModel
* @param {number} defaultOrder The default order to use for navigation visible routes that don't specify an order. The default is 100 and each successive route will be one more than that.
* @chainable
*/
router.buildNavigationModel = function(defaultOrder) {
var nav = [], routes = router.routes;
var fallbackOrder = defaultOrder || 100;
for (var i = 0; i < routes.length; i++) {
var current = routes[i];
if (current.nav) {
if (!system.isNumber(current.nav)) {
current.nav = ++fallbackOrder;
}
nav.push(current);
}
}
nav.sort(function(a, b) { return a.nav - b.nav; });
router.navigationModel(nav);
return router;
};
/**
* Configures how the router will handle unknown routes.
* @method mapUnknownRoutes
* @param {string|function} [config] If not supplied, then the router will map routes to modules with the same name.
* If a string is supplied, it represents the module id to route all unknown routes to.
* Finally, if config is a function, it will be called back with the route instruction containing the route info. The function can then modify the instruction by adding a moduleId and the router will take over from there.
* @param {string} [replaceRoute] If config is a module id, then you can optionally provide a route to replace the url with.
* @chainable
*/
router.mapUnknownRoutes = function(config, replaceRoute) {
var catchAllRoute = "*catchall";
var catchAllPattern = routeStringToRegExp(catchAllRoute);
router.route(catchAllPattern, function (fragment, queryString) {
var paramInfo = createParams(catchAllPattern, fragment, queryString);
var instruction = {
fragment: fragment,
queryString: queryString,
config: {
route: catchAllRoute,
routePattern: catchAllPattern
},
params: paramInfo.params,
queryParams: paramInfo.queryParams
};
if (!config) {
instruction.config.moduleId = fragment;
} else if (system.isString(config)) {
instruction.config.moduleId = config;
if(replaceRoute){
history.navigate(replaceRoute, { trigger:false, replace:true });
}
} else if (system.isFunction(config)) {
var result = config(instruction);
if (result && result.then) {
result.then(function() {
router.trigger('router:route:before-config', instruction.config, router);
router.trigger('router:route:after-config', instruction.config, router);
queueInstruction(instruction);
});
return;
}
} else {
instruction.config = config;
instruction.config.route = catchAllRoute;
instruction.config.routePattern = catchAllPattern;
}
router.trigger('router:route:before-config', instruction.config, router);
router.trigger('router:route:after-config', instruction.config, router);
queueInstruction(instruction);
});
return router;
};
/**
* Resets the router by removing handlers, routes, event handlers and previously configured options.
* @method reset
* @chainable
*/
router.reset = function() {
currentInstruction = currentActivation = undefined;
router.handlers = [];
router.routes = [];
router.off();
delete router.options;
return router;
};
/**
* Makes all configured routes and/or module ids relative to a certain base url.
* @method makeRelative
* @param {string|object} settings If string, the value is used as the base for routes and module ids. If an object, you can specify `route` and `moduleId` separately. In place of specifying route, you can set `fromParent:true` to make routes automatically relative to the parent router's active route.
* @chainable
*/
router.makeRelative = function(settings){
if(system.isString(settings)){
settings = {
moduleId:settings,
route:settings
};
}
if(settings.moduleId && !endsWith(settings.moduleId, '/')){
settings.moduleId += '/';
}
if(settings.route && !endsWith(settings.route, '/')){
settings.route += '/';
}
if(settings.fromParent){
router.relativeToParentRouter = true;
}
router.on('router:route:before-config').then(function(config){
if(settings.moduleId){
config.moduleId = settings.moduleId + config.moduleId;
}
if(settings.route){
if(config.route === ''){
config.route = settings.route.substring(0, settings.route.length - 1);
}else{
config.route = settings.route + config.route;
}
}
});
if (settings.dynamicHash) {
router.on('router:route:after-config').then(function (config) {
config.routePattern = routeStringToRegExp(config.route ? settings.dynamicHash + '/' + config.route : settings.dynamicHash);
config.dynamicHash = config.dynamicHash || ko.observable(config.hash);
});
router.on('router:route:before-child-routes').then(function(instance, instruction, parentRouter) {
var childRouter = instance.router;
for(var i = 0; i < childRouter.routes.length; i++) {
var route = childRouter.routes[i];
var params = instruction.params.slice(0);
route.hash = childRouter.convertRouteToHash(route.route)
.replace(namedParam, function(match) {
return params.length > 0 ? params.shift() : match;
});
route.dynamicHash(route.hash);
}
});
}
return router;
};
/**
* Creates a child router.
* @method createChildRouter
* @return {Router} The child router.
*/
router.createChildRouter = function() {
var childRouter = createRouter();
childRouter.parent = router;
return childRouter;
};
return router;
};
/**
* @class RouterModule
* @extends Router
* @static
*/
rootRouter = createRouter();
rootRouter.explicitNavigation = false;
rootRouter.navigatingBack = false;
/**
* Makes the RegExp generated for routes case sensitive, rather than the default of case insensitive.
* @method makeRoutesCaseSensitive
*/
rootRouter.makeRoutesCaseSensitive = function(){
routesAreCaseSensitive = true;
};
/**
* Verify that the target is the current window
* @method targetIsThisWindow
* @return {boolean} True if the event's target is the current window, false otherwise.
*/
rootRouter.targetIsThisWindow = function(event) {
var targetWindow = $(event.target).attr('target');
if (!targetWindow ||
targetWindow === window.name ||
targetWindow === '_self' ||
(targetWindow === 'top' && window === window.top)) { return true; }
return false;
};
/**
* Activates the router and the underlying history tracking mechanism.
* @method activate
* @return {Promise} A promise that resolves when the router is ready.
*/
rootRouter.activate = function(options) {
return system.defer(function(dfd) {
startDeferred = dfd;
rootRouter.options = system.extend({ routeHandler: rootRouter.loadUrl }, rootRouter.options, options);
history.activate(rootRouter.options);
if(history._hasPushState){
var routes = rootRouter.routes,
i = routes.length;
while(i--){
var current = routes[i];
current.hash = current.hash.replace('#', '/');
}
}
var rootStripper = rootRouter.options.root && new RegExp("^" + rootRouter.options.root + "/");
$(document).delegate("a", 'click', function(evt){
if(history._hasPushState){
if(!evt.altKey && !evt.ctrlKey && !evt.metaKey && !evt.shiftKey && rootRouter.targetIsThisWindow(evt)){
var href = $(this).attr("href");
// Ensure the protocol is not part of URL, meaning its relative.
// Stop the event bubbling to ensure the link will not cause a page refresh.
if (href != null && !(href.charAt(0) === "#" || /^[a-z]+:/i.test(href))) {
rootRouter.explicitNavigation = true;
evt.preventDefault();
if (rootStripper) {
href = href.replace(rootStripper, "");
}
history.navigate(href);
}
}
}else{
rootRouter.explicitNavigation = true;
}
});
if(history.options.silent && startDeferred){
startDeferred.resolve();
startDeferred = null;
}
}).promise();
};
/**
* Disable history, perhaps temporarily. Not useful in a real app, but possibly useful for unit testing Routers.
* @method deactivate
*/
rootRouter.deactivate = function() {
history.deactivate();
};
/**
* Installs the router's custom ko binding handler.
* @method install
*/
rootRouter.install = function(){
ko.bindingHandlers.router = {
init: function() {
return { controlsDescendantBindings: true };
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var settings = ko.utils.unwrapObservable(valueAccessor()) || {};
if (settings.__router__) {
settings = {
model:settings.activeItem(),
attached:settings.attached,
compositionComplete:settings.compositionComplete,
activate: false
};
} else {
var theRouter = ko.utils.unwrapObservable(settings.router || viewModel.router) || rootRouter;
settings.model = theRouter.activeItem();
settings.attached = theRouter.attached;
settings.compositionComplete = theRouter.compositionComplete;
settings.activate = false;
}
composition.compose(element, settings, bindingContext);
}
};
ko.virtualElements.allowedBindings.router = true;
};
return rootRouter;
});