Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
Cannot retrieve contributors at this time.
Cannot retrieve contributors at this time
| import Ember from "ember-metal/core"; // FEATURES, Logger, assert | |
| import EmberError from "ember-metal/error"; | |
| import { get } from "ember-metal/property_get"; | |
| import { set } from "ember-metal/property_set"; | |
| import { defineProperty } from "ember-metal/properties"; | |
| import { computed } from "ember-metal/computed"; | |
| import merge from "ember-metal/merge"; | |
| import run from "ember-metal/run_loop"; | |
| import { fmt } from "ember-runtime/system/string"; | |
| import EmberObject from "ember-runtime/system/object"; | |
| import Evented from "ember-runtime/mixins/evented"; | |
| import EmberRouterDSL from "ember-routing/system/dsl"; | |
| import EmberLocation from "ember-routing/location/api"; | |
| import { | |
| routeArgs, | |
| getActiveTargetName, | |
| stashParamNames | |
| } from "ember-routing/utils"; | |
| import create from 'ember-metal/platform/create'; | |
| import RouterState from "./router_state"; | |
| /** | |
| @module ember | |
| @submodule ember-routing | |
| */ | |
| import Router from 'router'; | |
| import 'router/transition'; | |
| function K() { return this; } | |
| var slice = [].slice; | |
| /** | |
| The `Ember.Router` class manages the application state and URLs. Refer to | |
| the [routing guide](http://emberjs.com/guides/routing/) for documentation. | |
| @class Router | |
| @namespace Ember | |
| @extends Ember.Object | |
| @uses Ember.Evented | |
| @public | |
| */ | |
| var EmberRouter = EmberObject.extend(Evented, { | |
| /** | |
| The `location` property determines the type of URL's that your | |
| application will use. | |
| The following location types are currently available: | |
| * `auto` | |
| * `hash` | |
| * `history` | |
| * `none` | |
| @property location | |
| @default 'hash' | |
| @see {Ember.Location} | |
| @public | |
| */ | |
| location: 'hash', | |
| /** | |
| Represents the URL of the root of the application, often '/'. This prefix is | |
| assumed on all routes defined on this router. | |
| @property rootURL | |
| @default '/' | |
| @public | |
| */ | |
| rootURL: '/', | |
| _initRouterJs(moduleBasedResolver) { | |
| var router = this.router = new Router(); | |
| router.triggerEvent = triggerEvent; | |
| router._triggerWillChangeContext = K; | |
| router._triggerWillLeave = K; | |
| var dslCallbacks = this.constructor.dslCallbacks || [K]; | |
| var dsl = new EmberRouterDSL(null, { | |
| enableLoadingSubstates: !!moduleBasedResolver | |
| }); | |
| function generateDSL() { | |
| this.resource('application', { path: "/", overrideNameAssertion: true }, function() { | |
| for (var i=0; i < dslCallbacks.length; i++) { | |
| dslCallbacks[i].call(this); | |
| } | |
| }); | |
| } | |
| generateDSL.call(dsl); | |
| if (get(this, 'namespace.LOG_TRANSITIONS_INTERNAL')) { | |
| router.log = Ember.Logger.debug; | |
| } | |
| router.map(dsl.generate()); | |
| }, | |
| init() { | |
| this._activeViews = {}; | |
| this._qpCache = {}; | |
| this._queuedQPChanges = {}; | |
| }, | |
| /** | |
| Represents the current URL. | |
| @method url | |
| @return {String} The current URL. | |
| @private | |
| */ | |
| url: computed(function() { | |
| return get(this, 'location').getURL(); | |
| }), | |
| /** | |
| Initializes the current router instance and sets up the change handling | |
| event listeners used by the instances `location` implementation. | |
| A property named `initialURL` will be used to determine the initial URL. | |
| If no value is found `/` will be used. | |
| @method startRouting | |
| @private | |
| */ | |
| startRouting(moduleBasedResolver) { | |
| var initialURL = get(this, 'initialURL'); | |
| if (this.setupRouter(moduleBasedResolver)) { | |
| if (typeof initialURL === "undefined") { | |
| initialURL = get(this, 'location').getURL(); | |
| } | |
| var initialTransition = this.handleURL(initialURL); | |
| if (initialTransition && initialTransition.error) { | |
| throw initialTransition.error; | |
| } | |
| } | |
| }, | |
| setupRouter(moduleBasedResolver) { | |
| this._initRouterJs(moduleBasedResolver); | |
| this._setupLocation(); | |
| var router = this.router; | |
| var location = get(this, 'location'); | |
| // Allow the Location class to cancel the router setup while it refreshes | |
| // the page | |
| if (get(location, 'cancelRouterSetup')) { | |
| return false; | |
| } | |
| this._setupRouter(router, location); | |
| location.onUpdateURL((url) => { | |
| this.handleURL(url); | |
| }); | |
| return true; | |
| }, | |
| /** | |
| Handles updating the paths and notifying any listeners of the URL | |
| change. | |
| Triggers the router level `didTransition` hook. | |
| @method didTransition | |
| @private | |
| @since 1.2.0 | |
| */ | |
| didTransition(infos) { | |
| updatePaths(this); | |
| this._cancelSlowTransitionTimer(); | |
| this.notifyPropertyChange('url'); | |
| this.set('currentState', this.targetState); | |
| // Put this in the runloop so url will be accurate. Seems | |
| // less surprising than didTransition being out of sync. | |
| run.once(this, this.trigger, 'didTransition'); | |
| if (get(this, 'namespace').LOG_TRANSITIONS) { | |
| Ember.Logger.log(`Transitioned into '${EmberRouter._routePath(infos)}'`); | |
| } | |
| }, | |
| _setOutlets() { | |
| var handlerInfos = this.router.currentHandlerInfos; | |
| var route; | |
| var defaultParentState; | |
| var liveRoutes = null; | |
| if (!handlerInfos) { | |
| return; | |
| } | |
| for (var i = 0; i < handlerInfos.length; i++) { | |
| route = handlerInfos[i].handler; | |
| var connections = route.connections; | |
| var ownState; | |
| for (var j = 0; j < connections.length; j++) { | |
| var appended = appendLiveRoute(liveRoutes, defaultParentState, connections[j]); | |
| liveRoutes = appended.liveRoutes; | |
| if (appended.ownState.render.name === route.routeName || appended.ownState.render.outlet === 'main') { | |
| ownState = appended.ownState; | |
| } | |
| } | |
| if (connections.length === 0) { | |
| ownState = representEmptyRoute(liveRoutes, defaultParentState, route); | |
| } | |
| defaultParentState = ownState; | |
| } | |
| if (!this._toplevelView) { | |
| var OutletView = this.container.lookupFactory('view:-outlet'); | |
| this._toplevelView = OutletView.create(); | |
| var instance = this.container.lookup('-application-instance:main'); | |
| instance.didCreateRootView(this._toplevelView); | |
| } | |
| this._toplevelView.setOutletState(liveRoutes); | |
| }, | |
| /** | |
| Handles notifying any listeners of an impending URL | |
| change. | |
| Triggers the router level `willTransition` hook. | |
| @method willTransition | |
| @private | |
| @since 1.11.0 | |
| */ | |
| willTransition(oldInfos, newInfos, transition) { | |
| run.once(this, this.trigger, 'willTransition', transition); | |
| if (get(this, 'namespace').LOG_TRANSITIONS) { | |
| Ember.Logger.log(`Preparing to transition from '${EmberRouter._routePath(oldInfos)}' to ' ${EmberRouter._routePath(newInfos)}'`); | |
| } | |
| }, | |
| handleURL(url) { | |
| // Until we have an ember-idiomatic way of accessing #hashes, we need to | |
| // remove it because router.js doesn't know how to handle it. | |
| url = url.split(/#(.+)?/)[0]; | |
| return this._doURLTransition('handleURL', url); | |
| }, | |
| _doURLTransition(routerJsMethod, url) { | |
| var transition = this.router[routerJsMethod](url || '/'); | |
| didBeginTransition(transition, this); | |
| return transition; | |
| }, | |
| transitionTo(...args) { | |
| var queryParams; | |
| if (resemblesURL(args[0])) { | |
| return this._doURLTransition('transitionTo', args[0]); | |
| } | |
| var possibleQueryParams = args[args.length-1]; | |
| if (possibleQueryParams && possibleQueryParams.hasOwnProperty('queryParams')) { | |
| queryParams = args.pop().queryParams; | |
| } else { | |
| queryParams = {}; | |
| } | |
| var targetRouteName = args.shift(); | |
| return this._doTransition(targetRouteName, args, queryParams); | |
| }, | |
| intermediateTransitionTo() { | |
| this.router.intermediateTransitionTo(...arguments); | |
| updatePaths(this); | |
| var infos = this.router.currentHandlerInfos; | |
| if (get(this, 'namespace').LOG_TRANSITIONS) { | |
| Ember.Logger.log(`Intermediate-transitioned into '${EmberRouter._routePath(infos)}'`); | |
| } | |
| }, | |
| replaceWith() { | |
| return this.transitionTo(...arguments).method('replace'); | |
| }, | |
| generate() { | |
| var url = this.router.generate(...arguments); | |
| return this.location.formatURL(url); | |
| }, | |
| /** | |
| Determines if the supplied route is currently active. | |
| @method isActive | |
| @param routeName | |
| @return {Boolean} | |
| @private | |
| */ | |
| isActive(routeName) { | |
| var router = this.router; | |
| return router.isActive(...arguments); | |
| }, | |
| /** | |
| An alternative form of `isActive` that doesn't require | |
| manual concatenation of the arguments into a single | |
| array. | |
| @method isActiveIntent | |
| @param routeName | |
| @param models | |
| @param queryParams | |
| @return {Boolean} | |
| @private | |
| @since 1.7.0 | |
| */ | |
| isActiveIntent(routeName, models, queryParams) { | |
| return this.currentState.isActiveIntent(routeName, models, queryParams); | |
| }, | |
| send(name, context) { | |
| this.router.trigger(...arguments); | |
| }, | |
| /** | |
| Does this router instance have the given route. | |
| @method hasRoute | |
| @return {Boolean} | |
| @private | |
| */ | |
| hasRoute(route) { | |
| return this.router.hasRoute(route); | |
| }, | |
| /** | |
| Resets the state of the router by clearing the current route | |
| handlers and deactivating them. | |
| @private | |
| @method reset | |
| */ | |
| reset() { | |
| if (this.router) { | |
| this.router.reset(); | |
| } | |
| }, | |
| willDestroy() { | |
| if (this._toplevelView) { | |
| this._toplevelView.destroy(); | |
| this._toplevelView = null; | |
| } | |
| this._super(...arguments); | |
| this.reset(); | |
| }, | |
| _lookupActiveComponentNode(templateName) { | |
| return this._activeViews[templateName]; | |
| }, | |
| _connectActiveComponentNode(templateName, componentNode) { | |
| Ember.assert('cannot connect an activeView that already exists', !this._activeViews[templateName]); | |
| var _activeViews = this._activeViews; | |
| function disconnectActiveView() { | |
| delete _activeViews[templateName]; | |
| } | |
| this._activeViews[templateName] = componentNode; | |
| componentNode.renderNode.addDestruction({ destroy: disconnectActiveView }); | |
| }, | |
| _setupLocation() { | |
| var location = get(this, 'location'); | |
| var rootURL = get(this, 'rootURL'); | |
| if ('string' === typeof location && this.container) { | |
| var resolvedLocation = this.container.lookup(`location:${location}`); | |
| if ('undefined' !== typeof resolvedLocation) { | |
| location = set(this, 'location', resolvedLocation); | |
| } else { | |
| // Allow for deprecated registration of custom location API's | |
| var options = { | |
| implementation: location | |
| }; | |
| location = set(this, 'location', EmberLocation.create(options)); | |
| } | |
| } | |
| if (location !== null && typeof location === 'object') { | |
| if (rootURL) { | |
| set(location, 'rootURL', rootURL); | |
| } | |
| // Allow the location to do any feature detection, such as AutoLocation | |
| // detecting history support. This gives it a chance to set its | |
| // `cancelRouterSetup` property which aborts routing. | |
| if (typeof location.detect === 'function') { | |
| location.detect(); | |
| } | |
| // ensure that initState is called AFTER the rootURL is set on | |
| // the location instance | |
| if (typeof location.initState === 'function') { | |
| location.initState(); | |
| } | |
| } | |
| }, | |
| _getHandlerFunction() { | |
| var seen = create(null); | |
| var container = this.container; | |
| var DefaultRoute = container.lookupFactory('route:basic'); | |
| return (name) => { | |
| var routeName = 'route:' + name; | |
| var handler = container.lookup(routeName); | |
| if (seen[name]) { | |
| return handler; | |
| } | |
| seen[name] = true; | |
| if (!handler) { | |
| container._registry.register(routeName, DefaultRoute.extend()); | |
| handler = container.lookup(routeName); | |
| if (get(this, 'namespace.LOG_ACTIVE_GENERATION')) { | |
| Ember.Logger.info(`generated -> ${routeName}`, { fullName: routeName }); | |
| } | |
| } | |
| handler.routeName = name; | |
| return handler; | |
| }; | |
| }, | |
| _setupRouter(router, location) { | |
| var lastURL; | |
| var emberRouter = this; | |
| router.getHandler = this._getHandlerFunction(); | |
| var doUpdateURL = function() { | |
| location.setURL(lastURL); | |
| }; | |
| router.updateURL = function(path) { | |
| lastURL = path; | |
| run.once(doUpdateURL); | |
| }; | |
| if (location.replaceURL) { | |
| var doReplaceURL = function() { | |
| location.replaceURL(lastURL); | |
| }; | |
| router.replaceURL = function(path) { | |
| lastURL = path; | |
| run.once(doReplaceURL); | |
| }; | |
| } | |
| router.didTransition = function(infos) { | |
| emberRouter.didTransition(infos); | |
| }; | |
| if (Ember.FEATURES.isEnabled('ember-router-willtransition')) { | |
| router.willTransition = function(oldInfos, newInfos, transition) { | |
| emberRouter.willTransition(oldInfos, newInfos, transition); | |
| }; | |
| } | |
| }, | |
| _serializeQueryParams(targetRouteName, queryParams) { | |
| var groupedByUrlKey = {}; | |
| forEachQueryParam(this, targetRouteName, queryParams, function(key, value, qp) { | |
| var urlKey = qp.urlKey; | |
| if (!groupedByUrlKey[urlKey]) { | |
| groupedByUrlKey[urlKey] = []; | |
| } | |
| groupedByUrlKey[urlKey].push({ | |
| qp: qp, | |
| value: value | |
| }); | |
| delete queryParams[key]; | |
| }); | |
| for (var key in groupedByUrlKey) { | |
| var qps = groupedByUrlKey[key]; | |
| Ember.assert(fmt("You're not allowed to have more than one controller " + | |
| "property map to the same query param key, but both " + | |
| "`%@` and `%@` map to `%@`. You can fix this by mapping " + | |
| "one of the controller properties to a different query " + | |
| "param key via the `as` config option, e.g. `%@: { as: 'other-%@' }`", | |
| [qps[0].qp.fprop, qps[1] ? qps[1].qp.fprop : "", qps[0].qp.urlKey, qps[0].qp.prop, qps[0].qp.prop]), qps.length <= 1); | |
| var qp = qps[0].qp; | |
| queryParams[qp.urlKey] = qp.route.serializeQueryParam(qps[0].value, qp.urlKey, qp.type); | |
| } | |
| }, | |
| _deserializeQueryParams(targetRouteName, queryParams) { | |
| forEachQueryParam(this, targetRouteName, queryParams, function(key, value, qp) { | |
| delete queryParams[key]; | |
| queryParams[qp.prop] = qp.route.deserializeQueryParam(value, qp.urlKey, qp.type); | |
| }); | |
| }, | |
| _pruneDefaultQueryParamValues(targetRouteName, queryParams) { | |
| var qps = this._queryParamsFor(targetRouteName); | |
| for (var key in queryParams) { | |
| var qp = qps.map[key]; | |
| if (qp && qp.sdef === queryParams[key]) { | |
| delete queryParams[key]; | |
| } | |
| } | |
| }, | |
| _doTransition(_targetRouteName, models, _queryParams) { | |
| var targetRouteName = _targetRouteName || getActiveTargetName(this.router); | |
| Ember.assert(`The route ${targetRouteName} was not found`, targetRouteName && this.router.hasRoute(targetRouteName)); | |
| var queryParams = {}; | |
| merge(queryParams, _queryParams); | |
| this._prepareQueryParams(targetRouteName, models, queryParams); | |
| var transitionArgs = routeArgs(targetRouteName, models, queryParams); | |
| var transitionPromise = this.router.transitionTo.apply(this.router, transitionArgs); | |
| didBeginTransition(transitionPromise, this); | |
| return transitionPromise; | |
| }, | |
| _prepareQueryParams(targetRouteName, models, queryParams) { | |
| this._hydrateUnsuppliedQueryParams(targetRouteName, models, queryParams); | |
| this._serializeQueryParams(targetRouteName, queryParams); | |
| this._pruneDefaultQueryParamValues(targetRouteName, queryParams); | |
| }, | |
| /** | |
| Returns a merged query params meta object for a given route. | |
| Useful for asking a route what its known query params are. | |
| @private | |
| */ | |
| _queryParamsFor(leafRouteName) { | |
| if (this._qpCache[leafRouteName]) { | |
| return this._qpCache[leafRouteName]; | |
| } | |
| var map = {}; | |
| var qps = []; | |
| this._qpCache[leafRouteName] = { | |
| map: map, | |
| qps: qps | |
| }; | |
| var routerjs = this.router; | |
| var recogHandlerInfos = routerjs.recognizer.handlersFor(leafRouteName); | |
| for (var i = 0, len = recogHandlerInfos.length; i < len; ++i) { | |
| var recogHandler = recogHandlerInfos[i]; | |
| var route = routerjs.getHandler(recogHandler.handler); | |
| var qpMeta = get(route, '_qp'); | |
| if (!qpMeta) { continue; } | |
| merge(map, qpMeta.map); | |
| qps.push.apply(qps, qpMeta.qps); | |
| } | |
| return { | |
| qps: qps, | |
| map: map | |
| }; | |
| }, | |
| /* | |
| becomeResolved: function(payload, resolvedContext) { | |
| var params = this.serialize(resolvedContext); | |
| if (payload) { | |
| this.stashResolvedModel(payload, resolvedContext); | |
| payload.params = payload.params || {}; | |
| payload.params[this.name] = params; | |
| } | |
| return this.factory('resolved', { | |
| context: resolvedContext, | |
| name: this.name, | |
| handler: this.handler, | |
| params: params | |
| }); | |
| }, | |
| */ | |
| _hydrateUnsuppliedQueryParams(leafRouteName, contexts, queryParams) { | |
| var state = calculatePostTransitionState(this, leafRouteName, contexts); | |
| var handlerInfos = state.handlerInfos; | |
| var appCache = this._bucketCache; | |
| stashParamNames(this, handlerInfos); | |
| for (var i = 0, len = handlerInfos.length; i < len; ++i) { | |
| var route = handlerInfos[i].handler; | |
| var qpMeta = get(route, '_qp'); | |
| for (var j = 0, qpLen = qpMeta.qps.length; j < qpLen; ++j) { | |
| var qp = qpMeta.qps[j]; | |
| var presentProp = qp.prop in queryParams && qp.prop || | |
| qp.fprop in queryParams && qp.fprop; | |
| if (presentProp) { | |
| if (presentProp !== qp.fprop) { | |
| queryParams[qp.fprop] = queryParams[presentProp]; | |
| delete queryParams[presentProp]; | |
| } | |
| } else { | |
| var controllerProto = qp.cProto; | |
| var cacheMeta = get(controllerProto, '_cacheMeta'); | |
| var cacheKey = controllerProto._calculateCacheKey(qp.ctrl, cacheMeta[qp.prop].parts, state.params); | |
| queryParams[qp.fprop] = appCache.lookup(cacheKey, qp.prop, qp.def); | |
| } | |
| } | |
| } | |
| }, | |
| _scheduleLoadingEvent(transition, originRoute) { | |
| this._cancelSlowTransitionTimer(); | |
| this._slowTransitionTimer = run.scheduleOnce('routerTransitions', this, '_handleSlowTransition', transition, originRoute); | |
| }, | |
| currentState: null, | |
| targetState: null, | |
| _handleSlowTransition(transition, originRoute) { | |
| if (!this.router.activeTransition) { | |
| // Don't fire an event if we've since moved on from | |
| // the transition that put us in a loading state. | |
| return; | |
| } | |
| this.set('targetState', RouterState.create({ | |
| emberRouter: this, | |
| routerJs: this.router, | |
| routerJsState: this.router.activeTransition.state | |
| })); | |
| transition.trigger(true, 'loading', transition, originRoute); | |
| }, | |
| _cancelSlowTransitionTimer() { | |
| if (this._slowTransitionTimer) { | |
| run.cancel(this._slowTransitionTimer); | |
| } | |
| this._slowTransitionTimer = null; | |
| } | |
| }); | |
| /* | |
| Helper function for iterating root-ward, starting | |
| from (but not including) the provided `originRoute`. | |
| Returns true if the last callback fired requested | |
| to bubble upward. | |
| @private | |
| */ | |
| function forEachRouteAbove(originRoute, transition, callback) { | |
| var handlerInfos = transition.state.handlerInfos; | |
| var originRouteFound = false; | |
| var handlerInfo, route; | |
| for (var i = handlerInfos.length - 1; i >= 0; --i) { | |
| handlerInfo = handlerInfos[i]; | |
| route = handlerInfo.handler; | |
| if (!originRouteFound) { | |
| if (originRoute === route) { | |
| originRouteFound = true; | |
| } | |
| continue; | |
| } | |
| if (callback(route, handlerInfos[i + 1].handler) !== true) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // These get invoked when an action bubbles above ApplicationRoute | |
| // and are not meant to be overridable. | |
| var defaultActionHandlers = { | |
| willResolveModel(transition, originRoute) { | |
| originRoute.router._scheduleLoadingEvent(transition, originRoute); | |
| }, | |
| error(error, transition, originRoute) { | |
| // Attempt to find an appropriate error substate to enter. | |
| var router = originRoute.router; | |
| var tryTopLevel = forEachRouteAbove(originRoute, transition, function(route, childRoute) { | |
| var childErrorRouteName = findChildRouteName(route, childRoute, 'error'); | |
| if (childErrorRouteName) { | |
| router.intermediateTransitionTo(childErrorRouteName, error); | |
| return; | |
| } | |
| return true; | |
| }); | |
| if (tryTopLevel) { | |
| // Check for top-level error state to enter. | |
| if (routeHasBeenDefined(originRoute.router, 'application_error')) { | |
| router.intermediateTransitionTo('application_error', error); | |
| return; | |
| } | |
| } | |
| logError(error, 'Error while processing route: ' + transition.targetName); | |
| }, | |
| loading(transition, originRoute) { | |
| // Attempt to find an appropriate loading substate to enter. | |
| var router = originRoute.router; | |
| var tryTopLevel = forEachRouteAbove(originRoute, transition, function(route, childRoute) { | |
| var childLoadingRouteName = findChildRouteName(route, childRoute, 'loading'); | |
| if (childLoadingRouteName) { | |
| router.intermediateTransitionTo(childLoadingRouteName); | |
| return; | |
| } | |
| // Don't bubble above pivot route. | |
| if (transition.pivotHandler !== route) { | |
| return true; | |
| } | |
| }); | |
| if (tryTopLevel) { | |
| // Check for top-level loading state to enter. | |
| if (routeHasBeenDefined(originRoute.router, 'application_loading')) { | |
| router.intermediateTransitionTo('application_loading'); | |
| return; | |
| } | |
| } | |
| } | |
| }; | |
| function logError(_error, initialMessage) { | |
| var errorArgs = []; | |
| var error; | |
| if (_error && typeof _error === 'object' && typeof _error.errorThrown === 'object') { | |
| error = _error.errorThrown; | |
| } else { | |
| error = _error; | |
| } | |
| if (initialMessage) { errorArgs.push(initialMessage); } | |
| if (error) { | |
| if (error.message) { errorArgs.push(error.message); } | |
| if (error.stack) { errorArgs.push(error.stack); } | |
| if (typeof error === "string") { errorArgs.push(error); } | |
| } | |
| Ember.Logger.error.apply(this, errorArgs); | |
| } | |
| function findChildRouteName(parentRoute, originatingChildRoute, name) { | |
| var router = parentRoute.router; | |
| var childName; | |
| var targetChildRouteName = originatingChildRoute.routeName.split('.').pop(); | |
| var namespace = parentRoute.routeName === 'application' ? '' : parentRoute.routeName + '.'; | |
| if (Ember.FEATURES.isEnabled("ember-routing-named-substates")) { | |
| // First, try a named loading state, e.g. 'foo_loading' | |
| childName = namespace + targetChildRouteName + '_' + name; | |
| if (routeHasBeenDefined(router, childName)) { | |
| return childName; | |
| } | |
| } | |
| // Second, try general loading state, e.g. 'loading' | |
| childName = namespace + name; | |
| if (routeHasBeenDefined(router, childName)) { | |
| return childName; | |
| } | |
| } | |
| function routeHasBeenDefined(router, name) { | |
| var container = router.container; | |
| return router.hasRoute(name) && | |
| (container._registry.has(`template:${name}`) || container._registry.has(`route:${name}`)); | |
| } | |
| function triggerEvent(handlerInfos, ignoreFailure, args) { | |
| var name = args.shift(); | |
| if (!handlerInfos) { | |
| if (ignoreFailure) { return; } | |
| throw new EmberError(`Can't trigger action '${name}' because your app hasn't finished transitioning into its first route. To trigger an action on destination routes during a transition, you can call \`.send()\` on the \`Transition\` object passed to the \`model/beforeModel/afterModel\` hooks.`); | |
| } | |
| var eventWasHandled = false; | |
| var handlerInfo, handler; | |
| for (var i = handlerInfos.length - 1; i >= 0; i--) { | |
| handlerInfo = handlerInfos[i]; | |
| handler = handlerInfo.handler; | |
| if (handler._actions && handler._actions[name]) { | |
| if (handler._actions[name].apply(handler, args) === true) { | |
| eventWasHandled = true; | |
| } else { | |
| return; | |
| } | |
| } | |
| } | |
| if (defaultActionHandlers[name]) { | |
| defaultActionHandlers[name].apply(null, args); | |
| return; | |
| } | |
| if (!eventWasHandled && !ignoreFailure) { | |
| throw new EmberError(`Nothing handled the action '${name}'. If you did handle the action, this error can be caused by returning true from an action handler in a controller, causing the action to bubble.`); | |
| } | |
| } | |
| function calculatePostTransitionState(emberRouter, leafRouteName, contexts) { | |
| var routerjs = emberRouter.router; | |
| var state = routerjs.applyIntent(leafRouteName, contexts); | |
| var handlerInfos = state.handlerInfos; | |
| var params = state.params; | |
| for (var i = 0, len = handlerInfos.length; i < len; ++i) { | |
| var handlerInfo = handlerInfos[i]; | |
| if (!handlerInfo.isResolved) { | |
| handlerInfo = handlerInfo.becomeResolved(null, handlerInfo.context); | |
| } | |
| params[handlerInfo.name] = handlerInfo.params; | |
| } | |
| return state; | |
| } | |
| function updatePaths(router) { | |
| var appController = router.container.lookup('controller:application'); | |
| if (!appController) { | |
| // appController might not exist when top-level loading/error | |
| // substates have been entered since ApplicationRoute hasn't | |
| // actually been entered at that point. | |
| return; | |
| } | |
| var infos = router.router.currentHandlerInfos; | |
| var path = EmberRouter._routePath(infos); | |
| if (!('currentPath' in appController)) { | |
| defineProperty(appController, 'currentPath'); | |
| } | |
| set(appController, 'currentPath', path); | |
| set(router, 'currentPath', path); | |
| if (!('currentRouteName' in appController)) { | |
| defineProperty(appController, 'currentRouteName'); | |
| } | |
| set(appController, 'currentRouteName', infos[infos.length - 1].name); | |
| set(router, 'currentRouteName', infos[infos.length - 1].name); | |
| } | |
| EmberRouter.reopenClass({ | |
| router: null, | |
| /** | |
| The `Router.map` function allows you to define mappings from URLs to routes | |
| and resources in your application. These mappings are defined within the | |
| supplied callback function using `this.resource` and `this.route`. | |
| ```javascript | |
| App.Router.map(function(){ | |
| this.route('about'); | |
| this.resource('article'); | |
| }); | |
| ``` | |
| For more detailed examples please see | |
| [the guides](http://emberjs.com/guides/routing/defining-your-routes/). | |
| @method map | |
| @param callback | |
| @public | |
| */ | |
| map(callback) { | |
| if (!this.dslCallbacks) { | |
| this.dslCallbacks = []; | |
| this.reopenClass({ dslCallbacks: this.dslCallbacks }); | |
| } | |
| this.dslCallbacks.push(callback); | |
| return this; | |
| }, | |
| _routePath(handlerInfos) { | |
| var path = []; | |
| // We have to handle coalescing resource names that | |
| // are prefixed with their parent's names, e.g. | |
| // ['foo', 'foo.bar.baz'] => 'foo.bar.baz', not 'foo.foo.bar.baz' | |
| function intersectionMatches(a1, a2) { | |
| for (var i = 0, len = a1.length; i < len; ++i) { | |
| if (a1[i] !== a2[i]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| var name, nameParts, oldNameParts; | |
| for (var i=1, l=handlerInfos.length; i<l; i++) { | |
| name = handlerInfos[i].name; | |
| nameParts = name.split("."); | |
| oldNameParts = slice.call(path); | |
| while (oldNameParts.length) { | |
| if (intersectionMatches(oldNameParts, nameParts)) { | |
| break; | |
| } | |
| oldNameParts.shift(); | |
| } | |
| path.push.apply(path, nameParts.slice(oldNameParts.length)); | |
| } | |
| return path.join("."); | |
| } | |
| }); | |
| function didBeginTransition(transition, router) { | |
| var routerState = RouterState.create({ | |
| emberRouter: router, | |
| routerJs: router.router, | |
| routerJsState: transition.state | |
| }); | |
| if (!router.currentState) { | |
| router.set('currentState', routerState); | |
| } | |
| router.set('targetState', routerState); | |
| transition.then(null, function(error) { | |
| if (!error || !error.name) { return; } | |
| Ember.assert(`The URL '${error.message}' did not match any routes in your application`, error.name !== "UnrecognizedURLError"); | |
| return error; | |
| }, 'Ember: Process errors from Router'); | |
| } | |
| function resemblesURL(str) { | |
| return typeof str === 'string' && ( str === '' || str.charAt(0) === '/'); | |
| } | |
| function forEachQueryParam(router, targetRouteName, queryParams, callback) { | |
| var qpCache = router._queryParamsFor(targetRouteName); | |
| for (var key in queryParams) { | |
| if (!queryParams.hasOwnProperty(key)) { continue; } | |
| var value = queryParams[key]; | |
| var qp = qpCache.map[key]; | |
| if (qp) { | |
| callback(key, value, qp); | |
| } | |
| } | |
| } | |
| function findLiveRoute(liveRoutes, name) { | |
| if (!liveRoutes) { return; } | |
| var stack = [liveRoutes]; | |
| while (stack.length > 0) { | |
| var test = stack.shift(); | |
| if (test.render.name === name) { | |
| return test; | |
| } | |
| var outlets = test.outlets; | |
| for (var outletName in outlets) { | |
| stack.push(outlets[outletName]); | |
| } | |
| } | |
| } | |
| function appendLiveRoute(liveRoutes, defaultParentState, renderOptions) { | |
| var target; | |
| var myState = { | |
| render: renderOptions, | |
| outlets: create(null) | |
| }; | |
| if (renderOptions.into) { | |
| target = findLiveRoute(liveRoutes, renderOptions.into); | |
| } else { | |
| target = defaultParentState; | |
| } | |
| if (target) { | |
| set(target.outlets, renderOptions.outlet, myState); | |
| } else { | |
| if (renderOptions.into) { | |
| // Megahax time. Post-2.0-breaking-changes, we will just assert | |
| // right here that the user tried to target a nonexistent | |
| // thing. But for now we still need to support the `render` | |
| // helper, and people are allowed to target templates rendered | |
| // by the render helper. So instead we defer doing anyting with | |
| // these orphan renders until afterRender. | |
| appendOrphan(liveRoutes, renderOptions.into, myState); | |
| } else { | |
| liveRoutes = myState; | |
| } | |
| } | |
| return { | |
| liveRoutes: liveRoutes, | |
| ownState: myState | |
| }; | |
| } | |
| function appendOrphan(liveRoutes, into, myState) { | |
| if (!liveRoutes.outlets.__ember_orphans__) { | |
| liveRoutes.outlets.__ember_orphans__ = { | |
| render: { | |
| name: '__ember_orphans__' | |
| }, | |
| outlets: create(null) | |
| }; | |
| } | |
| liveRoutes.outlets.__ember_orphans__.outlets[into] = myState; | |
| Ember.run.schedule('afterRender', function() { | |
| // `wasUsed` gets set by the render helper. See the function | |
| // `impersonateAnOutlet`. | |
| Ember.assert("You attempted to render into '" + into + "' but it was not found", | |
| liveRoutes.outlets.__ember_orphans__.outlets[into].wasUsed); | |
| }); | |
| } | |
| function representEmptyRoute(liveRoutes, defaultParentState, route) { | |
| // the route didn't render anything | |
| var alreadyAppended = findLiveRoute(liveRoutes, route.routeName); | |
| if (alreadyAppended) { | |
| // But some other route has already rendered our default | |
| // template, so that becomes the default target for any | |
| // children we may have. | |
| return alreadyAppended; | |
| } else { | |
| // Create an entry to represent our default template name, | |
| // just so other routes can target it and inherit its place | |
| // in the outlet hierarchy. | |
| defaultParentState.outlets.main = { | |
| render: { | |
| name: route.routeName, | |
| outlet: 'main' | |
| }, | |
| outlets: {} | |
| }; | |
| return defaultParentState; | |
| } | |
| } | |
| export default EmberRouter; |