/** * A mixin to allow any class to configure and listen to routes and also change the hash. */Ext.define('Ext.route.Mixin', { extend: 'Ext.Mixin', requires: [ 'Ext.route.Handler', 'Ext.route.Router' ], mixinConfig: { id: 'routerable', before: { destroy: 'destroyRouterable' } }, config: { /** * @cfg {Object} routes * @accessor * * An object of routes to handle hash changes. A route can be defined in a simple way: * * routes: { * 'foo/bar': 'handleFoo', * 'user/:id': 'showUser' * } * * Where the property is the hash (which can accept a parameter defined by a colon) * and the value is the method on the controller to execute. The parameters will get sent * in the action method. * * If no routes match a given hash, an {@link Ext.GlobalEvents#unmatchedroute} event * will be fired. This can be listened to in four ways: * * Ext.on('unmatchedroute', function(token) {}); * * Ext.define('MyApp.controller.Foo', { * extend: 'Ext.app.Controller', * * listen: { * global: { * unmatchedroute: 'onUnmatchedRoute' * } * }, * * onUnmatchedRoute: function(token) {} * }); * * Ext.application({ * name: 'MyApp', * * listen: { * global: { * unmatchedroute: 'onUnmatchedRoute' * } * }, * * onUnmatchedRoute: function(token) {} * }); * * Ext.application({ * name: 'MyApp', * * listeners: { * unmatchedroute: 'onUnmatchedRoute' * }, * * onUnmatchedRoute: function(token) {} * }); * * There is also a complex means of defining a route where you can use a before action * and even specify your own RegEx for the parameter: * * routes: { * 'foo/bar': { * action: 'handleFoo', * before: 'beforeHandleFoo' * }, * 'user/:id': { * action: 'showUser', * before: 'beforeShowUser', * conditions: { * ':id': '([0-9]+)' * } * } * } * * This will only match if the `id` parameter is a number. * * The before action allows you to cancel an action. Every before action will get passed * an `action` argument with a `resume` and `stop` methods as the last argument of the * method and you *MUST* execute either method: * * beforeHandleFoo: function (action) { * // some logic here * * // this will allow the handleFoo action to be executed * action.resume(); * }, * handleFoo: function () { * // will get executed due to true being passed in callback in beforeHandleFoo * }, * beforeShowUser: function (id, action) { * // allows for async process like an Ajax * Ext.Ajax.request({ * url: 'foo.php', * success: function () { * // will not allow the showUser method to be executed * // but will continue other queued actions. * action.stop(); * }, * failure: function () { * // will not allow the showUser method to be executed * // and will not allow other queued actions to be executed. * action.stop(true); * } * }); * }, * showUser: function (id) { * // will not get executed due to false being passed in callback in beforeShowUser * } * * You **MUST** execute the `{@link Ext.route.Action#resume resume}` or * `{@link Ext.route.Action#stop stop}` method on the `action` argument. Executing * `action.resume();` will continue the action, `action.stop();` will prevent * further execution. * * The default RegEx that will be used is `([%a-zA-Z0-9\\-\\_\\s,]+)` but you can specify * any that may suit what you need to accomplish. An example of an advanced condition * may be to make a parameter optional and case-insensitive: * * routes: { * 'user:id': { * action: 'showUser', * before: 'beforeShowUser', * conditions: { * ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?' * } * } * } * * Each route can be named; this allows for the route to be looked up by name instead of * url. By default, the route's name will be the url you configure but you can provide * the `{@link Ext.route.Route#name name}` config to override the default: * * routes: { * 'user:id': { * action: 'showUser', * before: 'beforeShowUser', * name: 'user', * conditions: { * ':id': '(?:(?:\/){1}([%a-z0-9_,\s\-]+))?' * } * } * } * * The `user:id` route can not be looked up via the `user` name which is useful when using * `{@link #redirectTo}`. * * A wildcard route can also be defined which works exactly like any other route but will * always execute before any other route. To specify a wildcard route, use the `*` * as the url: * * routes: { * '*': 'onToken' * } * * Since a wildcard route will execute before any other route, it can delay the execution * of other routes allowing for such things like a user session to be retrieved: * * routes: { * '*': { * before: 'onBeforeToken' * } * }, * * onBeforeToken: function () { * return Ext.Ajax.request({ * url: '/user/session' * }); * } * * In the above example, no other route will execute unless that * {@link Ext.Ajax#request request} returns successfully. * * You can also use a wildcard route if you need to defer routes until a store has been * loaded when an application first starts up: * * routes: { * '*': { * before: 'onBeforeToken' * } * }, * * onBeforeToken: function (action) { * var store = Ext.getStore('Settings'); * * if (store.loaded) { * action.resume(); * } else { * store.on('load', action.resume, action, { single: true }); * } * } * * The valid options are configurations from {@link Ext.route.Handler} and * {@link Ext.route.Route}. */ routes: null }, destroyRouterable: function() { Ext.route.Router.disconnect(this); }, applyRoutes: function(routes, oldRoutes) { var Router = Ext.route.Router, url; if (routes) { for (url in routes) { routes[url] = Router.connect(url, routes[url], this); } } if (oldRoutes) { for (url in oldRoutes) { Router.disconnect(this, oldRoutes[url]); } } return routes; }, /** * Update the hash. By default, it will not execute the routes if the current token and the * token passed are the same. * * @param {String/Number/Object/Ext.data.Model} hash The hash to redirect to. The hash can be * of several values: * - **String** The hash to exactly be set to. * - **Number** If `1` is passed, {@link Ext.util.History#forward forward} function will be * executed. If `-1` is passed, {@link Ext.util.History#bck back} function will be executed. * - **Ext.data.Model** If a model instance is passed, the Model's * {@link Ext.data.Model#toUrl toUrl} function will be executed to convert it into a String * value to set the hash to. * - **Object** An Object can be passed to control individual tokens in the full hash. * The key should be an associated {@link Ext.route.Route Route}'s * {@link Ext.route.Route#name name} and the value should be the value of that token * in the complete hash. For example, if you have two routes configured, each token in the * hash that can be matched for each route can be individually controlled: * * routes: { * 'foo/bar': 'onFooBar', * 'baz/:id': { * action: 'onBaz', * name: 'baz' * } * } * * If you pass in a hash of `#foo/bar|baz/1`, each route will execute in response. If you want * to change only the `baz` route but leave the `foo/bar` route in the hash, you can pass only * the `baz` key in an object: * * this.redirectTo({ * baz : 'baz/5' * }); * * and the resulting hash will be `#foo/bar/|baz/5` and only the `baz` route will execute * in reaction but the `foo/bar` will not react since it's associated token in the hash * remained the same. If you wanted to update the `baz` route and remove `foo/bar` * from the hash, you can set the value to `null`: * * this.redirectTo({ * 'foo/bar': null, * baz: 'baz/3' * }); * * and the resulting hash will be `#baz/3`. Like before, the `baz` route will execute * in reaction. * * @param {Object} opt An optional `Object` describing how to enact the hash being passed in. * Valid options are: * * - `force` Even if the hash will not change, setting this to `true` will force the * {@link Ext.route.Router Router} to react. * - `replace` When set to `true`, this will replace the current resource in the history stack * with the hash being set. * * For backwards compatibility, if `true` is passed instead of an `Object`, this will set * the `force` option to `true`. * * @return {Boolean} Will return `true` if the token was updated. */ redirectTo: function(hash, opt) { var currentHash = Ext.util.History.getToken(), Router = Ext.route.Router, delimiter = Router.getMultipleToken(), tokens = currentHash ? currentHash.split(delimiter) : [], length = tokens.length, force, i, name, obj, route, token, match; if (hash === -1) { return Ext.util.History.back(); } else if (hash === 1) { return Ext.util.History.forward(); } else if (hash.isModel) { hash = hash.toUrl(); } else if (Ext.isObject(hash)) { // Passing an object attempts to replace a token in the hash. for (name in hash) { obj = hash[name]; if (!Ext.isObject(obj)) { obj = { token: obj }; } if (length) { route = Router.getByName(name); if (route) { match = false; for (i = 0; i < length; i++) { token = tokens[i]; if (route.matcherRegex.test(token)) { match = true; if (obj.token) { // a token was found in the hash, replace it if (obj.fn && obj.fn.call(this, token, tokens, obj) === false) { // if the fn returned false, skip update continue; } tokens[i] = obj.token; if (obj.force) { // clear lastToken to force recognition route.lastToken = null; } } else { // remove token tokens.splice(i, 1); i--; length--; // reset lastToken route.lastToken = null; } } } if (obj && obj.token && !match) { // a token was not found in the hash, push to the end tokens.push(obj.token); } } } else if (obj && obj.token) { // there is no current hash, push to the end tokens.push(obj.token); } } hash = tokens.join(delimiter); } if (opt === true) { // for backwards compatibility force = opt; opt = null; } else if (opt) { force = opt.force; } length = tokens.length; if (force && length) { for (i = 0; i < length; i++) { token = tokens[i]; Router.clearLastTokens(token); } } if (currentHash === hash) { if (force) { // hash won't change, trigger handling anyway Router.onStateChange(hash); } // hash isn't going to change, return false return false; } if (opt && opt.replace) { Ext.util.History.replace(hash); } else { Ext.util.History.add(hash); } return true; }, privates: { afterClassMixedIn: function(targetClass) { var proto = targetClass.prototype, routes = proto.routes; if (routes) { delete proto.routes; targetClass.getConfigurator().add({ routes: routes }); } } }});