/** * 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; });