/** * The Router is an ordered set of {@link Ext.route.Route} definitions that decode a * url into a controller function to execute. Each `route` defines a type of url to match, * along with the controller function to call if it is matched. The Router uses the * {@link Ext.util.History} singleton to find out when the browser's url has changed. * * Routes are almost always defined inside a {@link Ext.Controller Controller}, as * opposed to on the Router itself. End-developers should not usually need to interact * directly with the Router as the Controllers manage everything automatically. See the * {@link Ext.Controller Controller documentation} for more information on specifying * routes. */Ext.define('Ext.route.Router', { singleton: true, requires: [ 'Ext.route.Action', 'Ext.route.Route', 'Ext.util.History' ], /** * @event beforeroutes * @member Ext.GlobalEvents * * Fires when the hash has changed and before any routes are executed. This allows * pre-processing to add additional {@link Ext.route.Action#before before} or * {@link Ext.route.Action#action action} handlers when the * {@link Ext.route.Action Action} is run. * * Route execution can be prevented by returning `false` in the listener * or executing the {@link Ext.route.Action#stop stop} method on the action. * * @param {Ext.route.Action} action An action that will be executed * prior to any route execution. * @param {String[]} tokens An array of individual tokens in the hash. */ /** * @event routereject * @member Ext.GlobalEvents * * Fires when a route was rejected from either a before action, * {@link Ext.GlobalEvents#beforeroutes} event or {@link Ext.GlobalEvents#beforeroute} event. * * @param {Ext.route.Route} route The route which had it's execution rejected. */ config: { /** * @cfg {Boolean} hashBang Sets {@link Ext.util.History#hashbang} to enable/disable * hashbang support. */ hashbang: null, /** * @cfg {String} [multipleToken=|] The token to split the routes to support multiple routes. */ multipleToken: '|', /** * @cfg {Boolean} [queueRoutes=true] `true` to queue routes to be executed one after the * other, false to execute routes immediately. */ queueRoutes: true }, /** * @property {Object} routes The connected {@link Ext.route.Route} * instances. */ /** * @property {Boolean} isSuspended `true` if the router is currently suspended. */ constructor: function() { var History = Ext.util.History; if (!History.ready) { History.init(); } History.on('change', this.onStateChange, this); this.initConfig(); this.clear(); }, updateHashbang: function(hashbang) { Ext.util.History.hashbang = hashbang; }, /** * React to a token * * @private * @param {String} token The token to react to. */ onStateChange: function(token) { var me = this, tokens = token.split(me.getMultipleToken()), queue, i, length; if (me.isSuspended) { queue = me.suspendedQueue; i = 0; length = tokens.length; if (queue) { for (; i < length; i++) { token = tokens[i]; // shouldn't keep track of duplicates if (!Ext.Array.contains(queue, token)) { queue.push(token); } } } } else { me.handleBefore(tokens); } }, /** * Fires the {@link Ext.GlobalEvents#beforeroutes} event and if * `false` is returned can prevent any routes from executing. * * @private * @param {String[]} tokens The individual tokens that were split from the hash * using {@link #multipleToken}. */ handleBefore: function(tokens) { var me = this, action = new Ext.route.Action(); if (Ext.fireEvent('beforeroutes', action, tokens) === false) { action.destroy(); } else { action .run() .then(me.handleBeforeRoute.bind(me, tokens), Ext.emptyFn); } }, /** * If a wildcard route was connected, that route needs to execute prior * to any other route. * * @private * @param {String[]} tokens The individual tokens that were split from the hash * using {@link #multipleToken}. */ handleBeforeRoute: function(tokens) { var me = this, beforeRoute = me.getByName('*'); if (beforeRoute) { beforeRoute .execute() .then(me.doRun.bind(me, tokens), Ext.emptyFn); } else { // no befores, go ahead with route determination me.doRun(tokens); } }, /** * Find routes that recognize one of the tokens in the document fragment * and then exeucte the routes. * * @private * @param {String[]} tokens The individual tokens that were split from the hash * using {@link #multipleToken}. */ doRun: function(tokens) { var me = this, app = me.application, routes = me.routes, i = 0, length = tokens.length, matched = {}, unmatched = [], token, found, name, route, recognize; for (; i < length; i++) { token = tokens[i]; found = false; for (name in routes) { route = routes[name]; recognize = route.recognize(token); if (recognize) { found = true; if (recognize !== true) { // The document fragment may have changed but the token // part that the route recognized did not change. Therefore // is was matched but we should not execute the route again. route .execute(token, recognize) .then(null, Ext.bind(me.onRouteRejection, me, [route], 0)); } Ext.Array.remove(unmatched, route); if (!matched[name]) { matched[name] = 1; } } else if (!matched[name]) { unmatched.push(route); } } if (!found) { if (app) { // backwards compat app.fireEvent('unmatchedroute', token); } Ext.fireEvent('unmatchedroute', token); } } i = 0; length = unmatched.length; for (; i < length; i++) { unmatched[i].onExit(); } }, /** * @private * Called when a route was rejected. */ onRouteRejection: function(route, error) { Ext.fireEvent('routereject', route, error); if (error) { Ext.raise(error); } }, /** * Create the {@link Ext.route.Route} instance and connect to the * {@link Ext.route.Router} singleton. * * @param {String} url The url to recognize. * @param {String} config The config on the controller to execute when the url is * matched. * @param {Ext.Base} instance The class instance associated with the * {@link Ext.route.Route} * @return {Ext.route.Handler} The handler that was added. */ connect: function(url, config, instance) { var routes = this.routes, delimiter = this.getMultipleToken(), name = config.name || url, //<debug> version = Ext.getVersion().parts.slice(0, 3).join('.'), //</debug> handler, route; if (url[0] === '!') { //<debug> if (!Ext.util.History.hashbang) { Ext.log({ level: 'error', msg: 'Route found with "!" ("' + url + '"). Should use new hashbang functionality instead. ' + 'Please see the router guide for more: https://docs.sencha.com/extjs/' + version + '/guides/application_architecture/router.html' }); } //</debug> url = url.substr(1); this.setHashbang(true); } if (Ext.isString(config)) { config = { action: config }; } handler = Ext.route.Handler.fromRouteConfig(config, instance); route = routes[name]; if (!route) { config.name = name; config.url = url; route = routes[name] = new Ext.route.Route(config); } route.addHandler(handler); if (handler.lazy) { // eslint-disable-next-line vars-on-top var currentHash = Ext.util.History.getToken(), tokens = currentHash.split(delimiter), length = tokens.length, matched = [], i, token; for (i = 0; i < length; i++) { token = tokens[i]; if (Ext.Array.indexOf(matched, token) === -1 && route.recognize(token)) { matched.push(token); } } this.onStateChange(matched.join(delimiter)); } return handler; }, /** * Disconnects all route handlers for a class instance. * * @param {Ext.Base} instance The class instance to disconnect route handlers from. * @param {Object/Ext.route.Handler} [config] * An optional config object to match a handler for. This will check all route * handlers connected to the instance for match based on the action and before * configurations. This can also be the actual {@link Ext.route.Handler handler} * instance. */ disconnect: function(instance, config) { var routes = this.routes, route, name; if (config) { route = config.route || this.getByName(config.name || config.url); if (route) { route.removeHandler(instance, config); } } else { for (name in routes) { route = routes[name]; route.removeHandler(instance); } } }, /** * Recognizes a url string connected to the Router, return the controller/action pair * plus any additional config associated with it. * * @param {String} url The url to recognize. * @return {Object/Boolean} If the url was recognized, the controller and action to * call, else `false`. */ recognize: function(url) { var routes = this.routes, matches = [], name, arr, i, length, route, urlParams; for (name in routes) { arr = routes[name]; length = arr && arr.length; if (length) { i = 0; for (; i < length; i++) { route = arr[i]; urlParams = route.recognize(url); if (urlParams) { matches.push({ route: route, urlParams: urlParams }); } } } } return matches.length ? matches : false; }, /** * Convenience method which just calls the supplied function with the * {@link Ext.route.Router} singleton. Example usage: * * Ext.route.Router.draw(function(map) { * map.connect('activate/:token', {controller: 'users', action: 'activate'}); * map.connect('home', {controller: 'index', action: 'home'}); * }); * * @param {Function} fn The function to call */ draw: function(fn) { fn.call(this, this); }, /** * Clear all the recognized routes. */ clear: function() { this.routes = {}; }, /** * Resets the connected routes' last token they were executed on. * @param {String} [token] If passed, only clear matching routes. * @private */ clearLastTokens: function(token) { var routes = this.routes, name, route; for (name in routes) { route = routes[name]; if (!token || route.recognize(token)) { route.clearLastTokens(); } } }, /** * Gets all routes by {@link Ext.route.Route#name}. * * @return {Ext.route.Route[]} If no routes found, `undefined` will be returned otherwise * the array of {@link Ext.route.Route Routes} will be returned. */ getByName: function(name) { var routes = this.routes; if (routes) { return routes[name]; } }, /** * Suspends the handling of tokens (see {@link #resume}). * * @param {Boolean} [trackTokens] `false` to prevent any tokens to be * queued while being suspended. */ suspend: function(trackTokens) { this.isSuspended = true; if (!this.suspendedQueue && trackTokens !== false) { this.suspendedQueue = []; } }, /** * Resumes the execution of routes (see {@link #suspend}). * * @param {Boolean} [discardQueue] `true` to prevent any previously queued * tokens from being enacted on. */ resume: function(discardQueue) { var me = this, queue = me.suspendedQueue, token; if (me.isSuspended) { me.isSuspended = false; me.suspendedQueue = null; if (!discardQueue && queue) { token = queue.join(me.getMultipleToken()); me.onStateChange(token); } } }});