UNPKG

durandal

Version:

Durandal is a cross-device, cross-platform client framework written in JavaScript and designed to make Single Page Applications (SPAs) easy to create and maintain. We've used it to build apps for PC, Mac, Linux, iOS and Android...and now it's your turn...

1,139 lines (956 loc) 44.1 kB
/** * Durandal 2.2.0 Copyright (c) 2010-2016 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) { cancelNavigation(null, instruction); 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 sp = pair.indexOf("="), key = sp === -1 ? pair : pair.substr(0, sp), value = sp === -1 ? null : decodeURIComponent(pair.substr(sp + 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){ // ignore default prevented since these are not supposed to behave like links anyway if(evt.isDefaultPrevented()){ return; } 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(); }; /** * Deactivate current items and turn history listening off. * @method deactivate */ rootRouter.deactivate = function() { rootRouter.activeItem(null); 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; });