/** * 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. They 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 me = this, 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 }); } } }});