UNPKG

grunt-durandal

Version:

Grunt Durandal Builder - Build durandal project using a custom require config and a custom almond

942 lines (803 loc) 36.4 kB
/** * Durandal 2.0.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 = /\/$/; function routeStringToRegExp(routeString) { routeString = routeString.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') .replace(namedParam, function(match, optional) { return optional ? match : '([^\/]+)'; }) .replace(splatParam, '(.*?)'); return new RegExp('^' + routeString + '$'); } function stripParametersFromRoute(route) { var colonIndex = route.indexOf(':'); var length = colonIndex > 0 ? colonIndex - 1 : route.length; return route.substring(0, length); } function hasChildRouter(instance) { return instance.router && instance.router.loadUrl; } 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; } /** * @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 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; }; function completeNavigation(instance, instruction) { system.log('Navigation Complete', instance, instruction); var fromModuleId = system.getModuleId(currentActivation); if (fromModuleId) { router.trigger('router:navigation:from:' + fromModuleId); } currentActivation = instance; currentInstruction = instruction; var toModuleId = system.getModuleId(currentActivation); if (toModuleId) { router.trigger('router:navigation:to:' + toModuleId); } if (!hasChildRouter(instance)) { router.updateDocumentTitle(instance, instruction); } 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); if (currentInstruction) { router.navigate(currentInstruction.fragment, 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); activator.activateItem(instance, instruction.params).then(function(succeeded) { if (succeeded) { var previousActivation = currentActivation; completeNavigation(instance, instruction); if (hasChildRouter(instance)) { queueInstruction({ router: instance.router, fragment: instruction.fragment, queryString: instruction.queryString }); } if (previousActivation == instance) { router.attached(); } } else if(activator.settings.lifecycleData && activator.settings.lifecycleData.redirect){ redirect(activator.settings.lifecycleData.redirect); }else{ cancelNavigation(instance, instruction); } if (startDeferred) { startDeferred.resolve(); startDeferred = null; } }); } /** * 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) { 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.router && currentActivation.router.loadUrl)); } function dequeueInstruction() { if (isProcessing()) { return; } var instruction = queue.shift(); queue = []; if (!instruction) { return; } if (instruction.router) { var fullFragment = instruction.fragment; if (instruction.queryString) { fullFragment += "?" + instruction.queryString; } instruction.router.loadUrl(fullFragment); return; } isProcessing(true); router.activeInstruction(instruction); if (canReuseCurrentActivation(instruction)) { ensureActivation(activator.create(), currentActivation, instruction); } else { system.acquire(instruction.config.moduleId).then(function(module) { var instance = system.resolveObject(module); ensureActivation(activeItem, instance, instruction); }).fail(function(err){ system.error('Failed to load routed module (' + instruction.config.moduleId + '). Details: ' + err.message); }); } } 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)) { config.title = config.title || router.convertRouteToTitle(config.route); config.moduleId = config.moduleId || router.convertRouteToModuleId(config.route); config.hash = config.hash || router.convertRouteToHash(config.route); config.routePattern = routeStringToRegExp(config.route); }else{ config.routePattern = config.route; } 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)){ for(var i = 0, length = config.route.length; i < length; i++){ var current = system.extend({}, config); current.route = config.route[i]; if(i > 0){ delete current.nav; } configureRoute(current); } }else{ configureRoute(config); } return router; } function addActiveFlag(config) { if(config.isActive){ return; } config.isActive = ko.computed(function() { var theItem = activeItem(); return theItem && theItem.__moduleId__ == config.moduleId; }); } /** * 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('='); queryObject[parts[0]] = parts[1] && decodeURIComponent(parts[1].replace(/\+/g, ' ')); } 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 = instruction.params.join('/'); if(coreFragment && coreFragment[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'); router.trigger('router:route:not-found', fragment, router); if (currentInstruction) { history.navigate(currentInstruction.fragment, { trigger:false, replace:true }); } rootRouter.explicitNavigation = false; rootRouter.navigatingBack = false; return false; }; /** * 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) { if (instruction.config.title) { if (app.title) { document.title = instruction.config.title + " | " + app.title; } else { document.title = instruction.config.title; } } else if (app.title) { document.title = app.title; } }; /** * 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; } rootRouter.explicitNavigation = true; return history.navigate(fragment, options); }; /** * Navigates back in the browser history. * @method navigateBack */ router.navigateBack = function() { history.navigateBack(); }; router.attached = function() { setTimeout(function() { isProcessing(false); router.trigger('router:navigation:attached', currentActivation, currentInstruction, router); dequeueInstruction(); }, 10); }; router.compositionComplete = function(){ router.trigger('router:navigation:composition-complete', currentActivation, currentInstruction, router); }; /** * 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) { if(router.relativeToParentRouter){ var instruction = router.parent.activeInstruction(), hash = instruction.config.hash + '/' + route; 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 defualt is 100. * @chainable */ router.buildNavigationModel = function(defaultOrder) { var nav = [], routes = router.routes; defaultOrder = defaultOrder || 100; for (var i = 0; i < routes.length; i++) { var current = routes[i]; if (current.nav) { if (!system.isNumber(current.nav)) { current.nav = defaultOrder; } addActiveFlag(current); 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; } } }); 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; /** * 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('#', ''); } } $(document).delegate("a", 'click', function(evt){ rootRouter.explicitNavigation = true; if(history._hasPushState){ if(!evt.altKey && !evt.ctrlKey && !evt.metaKey && !evt.shiftKey){ // Get the anchor href and protcol var href = $(this).attr("href"); var protocol = this.protocol + "//"; // 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 || (href.charAt(0) !== "#" && href.slice(protocol.length) !== protocol)) { evt.preventDefault(); history.navigate(href); } } } }); }).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; });