/**
* Enables reactive actions to handle changes in the hash by using the
* {@link Ext.route.Mixin#routes routes} configuration in a controller.
* An example configuration would be:
*
* Ext.define('MyApp.view.main.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.app-main',
*
* routes: {
* 'user/:{id}': 'onUser'
* },
*
* onUser: function (values) {
* var id = values.id;
* // ...
* }
* });
*
* The `routes` object can also receive an object to further configure
* the route, for example you can configure a `before` action that will
* be executed before the `action` or can cancel the route execution:
*
* Ext.define('MyApp.view.main.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.app-main',
*
* routes: {
* 'user/:{id}': {
* action: 'onUser',
* before: 'onBeforeUser',
* name: 'user'
* }
* },
*
* onBeforeUser: function (values) {
* return Ext.Ajax
* .request({
* url: '/check/permission',
* params: {
* route: 'user',
* meta: {
* id: values.id
* }
* }
* });
* },
*
* onUser: function (values) {
* var id = values.id;
* // ...
* }
* });
*
* URL Parameters in a route can also define a type that will be used
* when matching hashes when finding routes that recognize a hash and
* also parses the value into numbers:
*
* Ext.define('MyApp.view.main.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.app-main',
*
* routes: {
* 'user/:{id:num}': {
* action: 'onUser',
* before: 'onBeforeUser',
* name: 'user'
* }
* },
*
* onBeforeUser: function (values) {
* return Ext.Ajax
* .request({
* url: '/check/permission',
* params: {
* route: 'user',
* meta: {
* id: values.id
* }
* }
* });
* },
*
* onUser: function (values) {
* var id = values.id;
* // ...
* }
* });
*
* In this example, the id parameter added `:num` to the parameter which
* will now mean the route will only recognize a value for the id parameter
* that is a number such as `#user/123` and will not recognize `#user/abc`.
* The id passed to the action and before handlers will also get cast into
* a number instead of a string. If a type is not provided, it will use
* the {@link #defaultMatcher default matcher}.
*
* For more on types, see the {@link #cfg!types} config.
*
* For backwards compatibility, there is `positional` mode which is like
* `named` mode but how you define the url parameters and how they are passed
* to the action and before handlers is slightly different:
*
* Ext.define('MyApp.view.main.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.app-main',
*
* routes: {
* 'user/:id:action': {
* action: 'onUser',
* before: 'onBeforeUser',
* name: 'user',
* conditions: {
* ':action': '(edit|delete)?'
* }
* }
* },
*
* onBeforeUser: function (id, action) {
* return Ext.Ajax
* .request({
* url: '/check/permission',
* params: {
* route: 'user',
* meta: {
* action: action,
* id: id
* }
* }
* });
* },
*
* onUser: function (id) {
* // ...
* }
* });
*
* The parameters are defined without curly braces (`:id`, `:action`) and
* they are passed as individual arguments to the action and before handlers.
*
* It's important to note you cannot mix positional and named parameter formats
* in the same route since how they are passed to the handlers is different.
*
* Routes can define sections of a route pattern that are optional by surrounding
* the section that is to be optional with parenthesis. For example, if a route
* should match both `#user` and `#user/1234` to either show a grid of all users
* or details or a single user, you can define the route such as:
*
* Ext.define('MyApp.view.main.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.app-main',
*
* routes: {
* 'user(/:{id:num})': {
* action: 'onUser',
* name: 'user'
* }
* },
*
* onUser: function (params) {
* if (params.id) {
* // load user details
* } else {
* // load grid of users
* }
* }
* });
*/
Ext.define('Ext.route.Route', {
requires: [
'Ext.route.Action',
'Ext.route.Handler'
],
/**
* @event beforeroute
* @member Ext.GlobalEvents
*
* Fires when a route is about to be 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.
*
* The route can be prevented from executing by returning `false` in a listener
* or executing the {@link Ext.route.Action#stop stop} method on the action.
*
* @param {Ext.route.Route} route The route being executed.
* @param {Ext.route.Action} action The action that will be run.
*/
/**
* @event beforerouteexit
* @member Ext.GlobalEvents
*
* Fires when a route is being exited meaning when a route
* was executed but no longer matches a token in the current hash.
*
* The exit handlers can be prevented from executing by returning `false` in a listener
* or executing the {@link Ext.route.Action#stop stop} method on the action.
*
* @param {Ext.route.Action} action The action with defined exit actions. Each
* action will execute with the last token this route was connected with.
* @param {Ext.route.Route} route
*/
config: {
/**
* @cfg {String} name The name of this route. The name can be used when using
* {@link Ext.route.Mixin#redirectTo}.
*/
name: null,
/**
* @cfg {String} url (required) The url regex to match against.
*/
url: null,
/**
* @cfg {Boolean} [allowInactive=false] `true` to allow this route to be triggered on
* a controller that is not active.
*/
allowInactive: false,
/**
* @cfg {Object} conditions
* Optional set of conditions for each token in the url string. Each key should
* be one of the tokens, each value should be a regex that the token should accept.
*
* For `positional` mode, if you have a route with a url like `'files/:fileName'` and
* you want it to match urls like `files/someImage.jpg` then you can set these
* conditions to allow the :fileName token to accept strings containing a period:
*
* conditions: {
* ':fileName': '([0-9a-zA-Z\.]+)'
* }
*
* For `named` mode, if you have a route with a url like `'files/:{fileName}'`
* and you want it to match urls like `files/someImage.jpg` then you can set these
* conditions to allow the :{fileName} token to accept strings containing a period:
*
* conditions: {
* 'fileName': '([0-9a-zA-Z\.]+)'
* }
*
* You can also define a condition to parse the value or even split it on a character:
*
* conditions: {
* 'fileName': {
* re: '([0-9a-zA-Z\.]+)',
* split: '.', // split the value so you get an array ['someImage', 'jpg']
* parse: function (values) {
* return values[0]; // return a string without the extension
* }
* }
* }
*/
conditions: {},
/**
* @cfg {Boolean} [caseInsensitive=false] `true` to allow the tokens to be matched with
* case-insensitive.
*/
caseInsensitive: false,
/**
* @cfg {Object[]} [handlers=[]]
* The array of connected handlers to this route. Each handler must defined a
* `scope` and can define an `action`, `before` and/or `exit` handler:
*
* handlers: [{
* action: function() {
* //...
* },
* scope: {}
* }, {
* action: function() {
* //...
* },
* before: function() {
* //...
* },
* scope: {}
* }, {
* exit: function() {
* //...
* },
* scope: {}
* }]
*
* The `action`, `before` and `exit` handlers can be a string that will be resolved
* from the `scope`:
*
* handlers: [{
* action: 'onAction',
* before: 'onBefore',
* exit: 'onExit',
* scope: {
* onAction: function () {
* //...
* },
* onBefore: function () {
* //...
* },
* onExit: function () {
* //...
* }
* }
* }]
*/
handlers: [],
/* eslint-disable max-len */
/**
* @since 6.6.0
* @property {Object} types
* An object of types that will be used to match and parse values from a matched
* url. There are four default types:
*
* - `alpha` This will only match values that have only alpha characters using
* the regex `([a-zA-Z]+)`.
* - `alphanum` This will only match values that have alpha and numeric characters
* using the regex `([a-zA-Z0-9]+|[0-9]*(?:\\.[0-9]*)?)`. If a value is a number,
* which a number can have a period (`10.4`), the value will be case into a float
* using `parseFloat`.
* - `num` This will only match values that have numeric characters using the regex
* `([0-9]*(?:\\.[0-9]*)?)`. The value, which can have a period (`10.4`), will be
* case into a float using `parseFloat`.
* - `...` This is meant to be the last argument in the url and will match all
* characters using the regex `(.+)?`. If a value is matched, this is an optional
* type, the value will be split by `/` and an array will be sent to the handler
* methods. If no value was matched, the value will be `undefined`.
*
* When defining routes, a type is optional and will use the
* {@link #defaultMatcher default matcher} but the url parameter must be enclosed
* in curly braces which will send a single object to the route handlers:
*
* Ext.define('MyApp.view.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.myapp-main',
*
* routes: {
* 'view/:{view}/:{child:alphanum}:{args...}': {
* action: 'onView',
* before: 'onBeforeView',
* name: 'view'
* }
* },
*
* onBeforeView: function (values) {
* return Ext.Ajax.request({
* url: 'check/permission',
* params: {
* view: values.view,
* info: { childView: values.child }
* }
* });
* },
*
* onView: function (values) {}
* });
*
* In this example, there are 3 parameters defined. The `:{view}` parameter has no
* type which will match characters using the {@link #defaultMatcher default matcher}
* but is required to be in the matched url. The `:{child:alphanum}` will only match
* characters that are alpha or numeric but is required to be in the matched url. The
* `:{args...}` is the only optional parameter in this route but can match any
* character and will be an array of values split by `/` unless there are no values
* in which case `undefined` will be sent in the object.
*
* If the hash is `#view/user/edit`, the `values` argument sent to the handlers would be:
*
* {
* view: 'user',
* child: 'edit',
* args: undefined
* }
*
* Since there were no more values for the `args` parameter, it's value is `undefined`.
*
* If the hash is `#view/user/1234`, the `values` argument sent to the handlers would be:
*
* {
* view: 'user',
* child: 1234,
* args: undefined
* }
*
* Notice the `child` value is a number instead of a string.
*
* If the hash is `#view/user/1234/edit/settings`, the `values` argument sent to the
* handlers would be:
*
* {
* view: 'user',
* child: 1234,
* args: ['edit', 'settings']
* }
*
* The `args` parameter matched the `edit/settings` and split it by the `/` producing
* the array.
*
* To add custom types, you can override `Ext.route.Route`:
*
* Ext.define('Override.route.Route', {
* override: 'Ext.route.Route',
*
* config: {
* types: {
* uuid: {
* re: '([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})'
* }
* }
* }
* });
*
* You can now use the `uuid` type in your routes:
*
* Ext.define('MyApp.view.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.myapp-main',
*
* routes: {
* 'user/:{userid:uuid}': {
* action: 'onUser',
* caseInsensitive: true,
* name: 'user'
* }
* },
*
* onUser: function (values) {}
* });
*
* This would match if the hash was like `#user/C56A4180-65AA-42EC-A945-5FD21DEC0538`
* and the `values` object would then be:
*
* {
* user: 'C56A4180-65AA-42EC-A945-5FD21DEC0538'
* }
*/
/* eslint-enable max-len */
types: {
cached: true,
$value: {
alpha: {
re: '([a-zA-Z]+)'
},
alphanum: {
re: '([a-zA-Z0-9]+|[0-9]+(?:\\.[0-9]+)?|[0-9]*(?:\\.[0-9]+){1})',
parse: function(value) {
var test;
if (value && this.numRe.test(value)) {
test = parseFloat(value);
if (!isNaN(test)) {
value = test;
}
}
return value;
}
},
num: {
// allow `1`, `10`, 10.1`, `.1`
re: '([0-9]+(?:\\.[0-9]+)?|[0-9]*(?:\\.[0-9]+){1})',
parse: function(value) {
if (value) {
value = parseFloat(value);
}
return value;
}
},
'...': {
re: '(.+)?',
split: '/',
parse: function(values) {
var length, i, value;
if (values) {
length = values.length;
for (i = 0; i < length; i++) {
value = parseFloat(values[i]);
if (!isNaN(value)) {
values[i] = value;
}
}
}
return values;
}
}
}
}
},
/**
* @property {String} [defaultMatcher='([%a-zA-Z0-9\\-\\_\\s,]+)'] The default RegExp string
* to use to match parameters with.
*/
defaultMatcher: '([%a-zA-Z0-9\\-\\_\\s,]+)',
/**
* @private
* @property {RegExp} matcherRegex A regular expression to match the token to the
* configured {@link #url}.
*/
/**
* @since 6.6.0
* @property {RegExp} numRe A regular expression to match against float numbers for
* `alphanum`, `num` and `...` {@link #cfg!types} in order to cast into floats.
*/
numRe: /^[0-9]*(?:\.[0-9]*)?$/,
/**
* @private
* @since 6.6.0
* @property {RegExp} typeParamRegex
* A regular expression to determine if the parameter may contain type information.
* If a parameter does have type information, the url parameters sent to the
* {@link Ext.route.Handler#before} and {@link Ext.route.Handler#after} will
* be in an object instead of separate arguments.
*/
typeParamRegex: /:{([0-9A-Za-z_]+)(?::?([0-9A-Za-z_]+|.{3})?)}/g,
/**
* @private
* @since 6.6.0
* @property {RegExp} optionalParamRegex
* A regular expression to find groups intended to be optional values within the
* hash. This means that if they are in the hash they will match and return the
* values present. But, if they are not and the rest of the hash matches, the route
* will still execute passing `undefined` as the values of any parameters
* within an optional group.
*
* routes: {
* 'user(\/:{id:num})': {
* action: 'onUser',
* name: 'user'
* }
* }
*
* In this example, the `id` parameter and the slash will be optional since they
* are wrapped in the parentheses. This route would execute if the hash is `#user`
* or `#user/1234`.
*/
optionalGroupRegex: /\((.+?)\)/g,
/**
* @private
* @property {RegExp} paramMatchingRegex
* A regular expression to check if there are parameters in the configured
* {@link #url}.
*/
paramMatchingRegex: /:([0-9A-Za-z_]+)/g,
/**
* @private
* @property {Array/Object} paramsInMatchString
* An array or object of parameters in the configured {@link #url}.
*/
/**
* @private
* @since 6.6.0
* @property {String} mode
* The mode based on the {@link #cfg!url} pattern this route is configured with.
* Valid values are:
*
* - `positional` The {@link #cfg!url} was configured with the parameter format
* as `:param`. The values in the handler functions will be individual arguments.
* Example:
*
* Ext.define('MyApp.view.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.myapp-main',
*
* routes: {
* 'view/:view/:child': {
* action: 'onView',
* before: 'onBeforeView',
* name: 'view'
* }
* },
*
* onBeforeView: function (view, child) {
* return Ext.Ajax.request({
* url: 'check/permission',
* params: {
* view: view,
* info: { childView: child }
* }
* });
* },
*
* onView: function (view, child) {}
* });
*
* The values from the matched url that the `view` route would execute with are
* separate arguments in the before and action handlers.
* - `named` The {@link #cfg!url} was configured with the parameter format as
* `:{param:type}` where the `:type` is optional. Example:
*
* Ext.define('MyApp.view.MainController', {
* extend: 'Ext.app.ViewController',
* alias: 'controller.myapp-main',
*
* routes: {
* 'view/:{view}/:{child:alphanum}': {
* action: 'onView',
* before: 'onBeforeView',
* name: 'view'
* }
* },
*
* onBeforeView: function (values) {
* return Ext.Ajax.request({
* url: 'check/permission',
* params: {
* view: values.view,
* info: { childView: values.child }
* }
* });
* },
*
* onView: function (values) {}
* });
*
* The values from the matched url the `view` route would execute with are collected
* into an object with the parameter name as the key and the associated value as
* the value. See {@link #cfg!types} for more about this named mode.
*/
/**
* @protected
* @property {Boolean} isRoute
*/
isRoute: true,
constructor: function(config) {
var me = this,
url;
this.initConfig(config);
url = me.getUrl().replace(me.optionalGroupRegex, function(match, middle) {
return '(?:' + middle + ')?';
});
if (url.match(me.typeParamRegex)) {
me.handleNamedPattern(url);
}
else {
me.handlePositionalPattern(url);
}
},
/**
* @private
* @since 6.6.0
* Handles a pattern that will enable positional {@link #property!mode}.
*
* @param {String} url The url pattern.
*/
handlePositionalPattern: function(url) {
var me = this;
me.paramsInMatchString = url.match(me.paramMatchingRegex) || [];
me.matcherRegex = me.createMatcherRegex(url);
me.mode = 'positional';
},
/**
* @private
* @since 6.6.0
* Handles a pattern that will enable named {@link #property!mode}.
*
* @param {String} url The url pattern.
*/
handleNamedPattern: function(url) {
var me = this,
typeParamRegex = me.typeParamRegex,
conditions = me.getConditions(),
types = me.getTypes(),
defaultMatcher = me.defaultMatcher,
params = {},
re = url.replace(typeParamRegex, function(match, param, typeMatch) {
var type = typeMatch && types[typeMatch],
matcher = conditions[param] || type || defaultMatcher;
//<debug>
if (params[param]) {
Ext.raise('"' + param + '" already defined in route "' + url + '"');
}
if (typeMatch && !type) {
Ext.raise('Unknown parameter type "' + typeMatch + '" in route "' + url + '"');
}
//</debug>
if (Ext.isObject(matcher)) {
matcher = matcher.re;
}
params[param] = {
matcher: matcher,
type: typeMatch
};
return matcher;
});
//<debug>
if (re.search(me.paramMatchingRegex) !== -1) {
Ext.raise('URL parameter mismatch. Positional url parameter found ' +
'while in named mode.');
}
//</debug>
me.paramsInMatchString = params;
me.matcherRegex = new RegExp('^' + re + '$', me.getCaseInsensitive() ? 'i' : '');
me.mode = 'named';
},
/**
* Attempts to recognize a given url string and return a meta data object including
* any URL parameter matches.
*
* @param {String} url The url to recognize.
* @return {Object/Boolean} The matched data, or `false` if no match.
*/
recognize: function(url) {
var me = this,
recognized = me.recognizes(url),
handlers, length, hasHandler, handler, matches, urlParams, i;
if (recognized) {
handlers = me.getHandlers();
length = handlers.length;
for (i = 0; i < length; i++) {
handler = handlers[i];
if (handler.lastToken !== url) {
// there is a handler that can execute
hasHandler = true;
break;
}
}
if (!hasHandler && url === me.lastToken) {
// url matched the lastToken
return true;
}
// backwards compat
matches = me.matchesFor(url);
urlParams = me.getUrlParams(url);
return Ext.applyIf(matches, {
historyUrl: url,
urlParams: urlParams
});
}
return false;
},
/**
* @private
* @since 6.6.0
* Returns the url parameters matched in the given url.
*
* @param {String} url The url this route is executing on.
* @return {Array/Object} If {@link #property!mode} is `named`,
* an object from {@link #method!getNamedUrlParams} will be returned.
* If is `positional`, an array from {@link #method!getPositionalUrlParams}
* will be returned.
*/
getUrlParams: function(url) {
if (this.mode === 'named') {
return this.getNamedUrlParams(url);
}
else {
return this.getPositionalUrlParams(url);
}
},
/**
* @private
* @since 6.6.0
* Returns an array of url parameters values in order they appear in the url.
*
* @param {String} url The url the route is executing on.
* @return {Array}
*/
getPositionalUrlParams: function(url) {
var params = [],
conditions = this.getConditions(),
keys = this.paramsInMatchString,
values = url.match(this.matcherRegex),
length = keys.length,
i, key, type, value;
// remove the full match
values.shift();
for (i = 0; i < length; i++) {
key = keys[i];
value = values[i];
if (conditions[key]) {
type = conditions[key];
}
else if (key[0] === ':') {
key = key.substr(1);
if (conditions[key]) {
type = conditions[key];
}
}
value = this.parseValue(value, type);
if (Ext.isDefined(value) && value !== '') {
if (Ext.isArray(value)) {
params.push.apply(params, value);
}
else {
params.push(value);
}
}
}
return params;
},
/**
* @private
* @since 6.6.0
* Returns an object of url parameters with parameter name as the
* object key and the value.
*
* @param {String} url The url the route is executing on.
* @return {Array}
*/
getNamedUrlParams: function(url) {
var conditions = this.getConditions(),
types = this.getTypes(),
params = {},
keys = this.paramsInMatchString,
values = url.match(this.matcherRegex),
name, obj, value, type, condition;
// remove the full match
values.shift();
for (name in keys) {
obj = keys[name];
value = values.shift();
condition = conditions[name];
type = types[obj.type];
if (condition || type) {
type = Ext.merge({}, condition, types[obj.type]);
}
params[name] = this.parseValue(value, type);
}
return params;
},
/**
* @private
* @since 6.6.0
* Parses the value from the url with a {@link #cfg!types type}
* or a matching {@link #cfg!conditions condition}.
*
* @param {String} value The value from the url.
* @param {Object} [type] The type object that will be used to parse the value.
* @return {String/Number/Array}
*/
parseValue: function(value, type) {
if (type) {
if (value && type.split) {
value = value.split(type.split);
// If first is empty string, remove.
// This could be because the value prior
// was `/foo/bar` which would lead to
// `['', 'foo', 'bar']`.
if (!value[0]) {
value.shift();
}
// If last is empty string, remove.
// This could be because the value prior
// was `foo/bar/` which would lead to
// `['foo', 'bar', '']`.
if (!value[value.length - 1]) {
value.pop();
}
}
if (type.parse) {
value = type.parse.call(this, value);
}
}
if (!value && Ext.isString(value)) {
// IE8 may have values as an empty string
// if there was no value that was matched
value = undefined;
}
return value;
},
/**
* Returns `true` if this {@link Ext.route.Route} matches the given url string.
*
* @param {String} url The url to test.
* @return {Boolean} `true` if this {@link Ext.route.Route} recognizes the url.
*/
recognizes: function(url) {
return this.matcherRegex.test(url);
},
/**
* The method to execute the action using the configured before function which will
* kick off the actual {@link #actions} on the {@link #controller}.
*
* @param {String} token The token this route is being executed with.
* @param {Object} argConfig The object from the {@link Ext.route.Route}'s
* recognize method call.
* @return {Ext.promise.Promise}
*/
execute: function(token, argConfig) {
var me = this,
allowInactive = me.getAllowInactive(),
handlers = me.getHandlers(),
queue = Ext.route.Router.getQueueRoutes(),
length = handlers.length,
urlParams = (argConfig && argConfig.urlParams) || [],
i, handler, scope, action, promises, single, remover;
me.lastToken = token;
if (!queue) {
promises = [];
}
return new Ext.Promise(function(resolve, reject) {
if (argConfig === false) {
reject();
}
else {
if (queue) {
action = new Ext.route.Action({
urlParams: urlParams
});
}
for (i = 0; i < length; i++) {
handler = handlers[i];
if (token != null && handler.lastToken === token) {
// no change on this handler
continue;
}
scope = handler.scope;
handler.lastToken = token;
if (!allowInactive && scope.isActive && !scope.isActive()) {
continue;
}
if (!queue) {
action = new Ext.route.Action({
urlParams: urlParams
});
}
single = handler.single;
if (handler.before) {
action.before(handler.before, scope);
}
if (handler.action) {
action.action(handler.action, scope);
}
if (single) {
remover = Ext.bind(me.removeHandler, me, [null, handler]);
if (single === true) {
if (handler.action) {
action.action(remover, me);
}
else {
action.before(function() {
remover();
return Ext.Promise.resolve();
}, me);
}
}
else {
// all before actions have to resolve,
// resolve a promise to allow the action
// chain to continue
action.before(single === 'before', function() {
remover();
return Ext.Promise.resolve();
}, me);
}
}
if (!queue) {
if (Ext.fireEvent('beforeroute', action, me) === false) {
action.destroy();
}
else {
promises.push(action.run());
}
}
}
if (queue) {
if (Ext.fireEvent('beforeroute', action, me) === false) {
action.destroy();
reject();
}
else {
action.run().then(resolve, reject);
}
}
else {
Ext.Promise.all(promises).then(resolve, reject);
}
}
});
},
/**
* Returns a hash of matching url segments for the given url.
*
* @param {String} url The url to extract matches for
* @return {Object} matching url segments
*/
matchesFor: function(url) {
var params = {},
keys = this.mode === 'named'
? Ext.Object.getKeys(this.paramsInMatchString)
: this.paramsInMatchString,
values = url.match(this.matcherRegex),
length = keys.length,
i;
// first value is the entire match so reject
values.shift();
for (i = 0; i < length; i++) {
params[keys[i].replace(':', '')] = values[i];
}
return params;
},
/**
* Takes the configured url string including wildcards and returns a regex that can be
* used to match against a url.
*
* This is only used in `positional` {@link #property!mode}.
*
* @param {String} url The url string.
* @return {RegExp} The matcher regex.
*/
createMatcherRegex: function(url) {
// Converts a route string into an array of symbols starting with a colon. e.g.
// ":controller/:action/:id" => [':controller', ':action', ':id']
var me = this,
paramsInMatchString = me.paramsInMatchString,
conditions = me.getConditions(),
defaultMatcher = me.defaultMatcher,
length = paramsInMatchString.length,
modifiers = me.getCaseInsensitive() ? 'i' : '',
i, param, matcher;
if (url === '*') {
// handle wildcard routes, won't have conditions
url = url.replace('*', '\\*');
}
else {
for (i = 0; i < length; i++) {
param = paramsInMatchString[i];
// Even if the param is a named param, we need to
// allow "local" overriding.
if (conditions[param]) {
matcher = conditions[param];
// without colon
}
else if (param[0] === ':' && conditions[param.substr(1)]) {
matcher = conditions[param.substr(1)];
}
else {
matcher = defaultMatcher;
}
if (Ext.isObject(matcher)) {
matcher = matcher.re;
}
url = url.replace(new RegExp(param), matcher || defaultMatcher);
}
}
// we want to match the whole string, so include the anchors
return new RegExp('^' + url + '$', modifiers);
},
/**
* Adds a handler to the {@link #cfg!handlers} stack.
*
* @param {Object} handler
* An object to describe the handler. A handler should define a `fn` and `scope`.
* If the `fn` is a String, the function will be resolved from the `scope`.
* @return {Ext.route.Route} this
*/
addHandler: function(handler) {
var handlers = this.getHandlers();
if (!handler.isInstance) {
handler = new Ext.route.Handler(handler);
}
handlers.push(handler);
return handler.route = this;
},
/**
* Removes a handler from the {@link #cfg!handlers} stack. This normally happens when
* destroying a class instance.
*
* @param {Object/Ext.Base} scope The class instance to match handlers with.
* @param {Ext.route.Handler} [handler] An optional {@link Ext.route.Handler Handler}
* to only remove from the array of handlers. If no handler is passed, all handlers
* will be removed.
* @return {Ext.route.Route} this
*/
removeHandler: function(scope, handler) {
var handlers = this.getHandlers(),
length = handlers.length,
newHandlers = [],
i, item;
for (i = 0; i < length; i++) {
item = handlers[i];
if (handler) {
if (item !== handler) {
newHandlers.push(item);
}
}
else if (item.scope !== scope) {
newHandlers.push(item);
}
}
this.setHandlers(newHandlers);
return this;
},
/**
* Clears the last token properties of this route and all handlers.
*/
clearLastTokens: function() {
var handlers = this.getHandlers(),
length = handlers.length,
i;
for (i = 0; i < length; i++) {
handlers[i].lastToken = null;
}
this.lastToken = null;
},
/**
* @private
* @since 6.6.0
*
* When a route is exited (no longer recognizes a token in the current hash)
* we need to clear all last tokens and execute any exit handlers.
*/
onExit: function() {
var me = this,
handlers = me.getHandlers(),
allowInactive = me.getAllowInactive(),
length = handlers.length,
action = new Ext.route.Action({
urlParams: [me.lastToken]
}),
i, handler, scope;
// Need to reset handlers' `lastToken` so that when a token
// is added to the document fragment it will not be falsely
// matched.
me.clearLastTokens();
for (i = 0; i < length; i++) {
handler = handlers[i];
if (handler.exit) {
scope = handler.scope;
if (!allowInactive && scope.isActive && !scope.isActive()) {
continue;
}
action.action(handler.exit, scope);
}
}
if (Ext.fireEvent('beforerouteexit', action, me) === false) {
action.destroy();
}
else {
action.run();
}
}
});