| var $$UMFP; // reference to $UrlMatcherFactoryProvider |
| |
| /** |
| * @ngdoc object |
| * @name ui.router.util.type:UrlMatcher |
| * |
| * @description |
| * Matches URLs against patterns and extracts named parameters from the path or the search |
| * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list |
| * of search parameters. Multiple search parameter names are separated by '&'. Search parameters |
| * do not influence whether or not a URL is matched, but their values are passed through into |
| * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. |
| * |
| * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace |
| * syntax, which optionally allows a regular expression for the parameter to be specified: |
| * |
| * * `':'` name - colon placeholder |
| * * `'*'` name - catch-all placeholder |
| * * `'{' name '}'` - curly placeholder |
| * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the |
| * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. |
| * |
| * Parameter names may contain only word characters (latin letters, digits, and underscore) and |
| * must be unique within the pattern (across both path and search parameters). For colon |
| * placeholders or curly placeholders without an explicit regexp, a path parameter matches any |
| * number of characters other than '/'. For catch-all placeholders the path parameter matches |
| * any number of characters. |
| * |
| * Examples: |
| * |
| * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for |
| * trailing slashes, and patterns have to match the entire path, not just a prefix. |
| * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or |
| * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. |
| * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. |
| * * `'/user/{id:[^/]*}'` - Same as the previous example. |
| * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id |
| * parameter consists of 1 to 8 hex digits. |
| * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the |
| * path into the parameter 'path'. |
| * * `'/files/*path'` - ditto. |
| * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined |
| * in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start |
| * |
| * @param {string} pattern The pattern to compile into a matcher. |
| * @param {Object} config A configuration object hash: |
| * @param {Object=} parentMatcher Used to concatenate the pattern/config onto |
| * an existing UrlMatcher |
| * |
| * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. |
| * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. |
| * |
| * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any |
| * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns |
| * non-null) will start with this prefix. |
| * |
| * @property {string} source The pattern that was passed into the constructor |
| * |
| * @property {string} sourcePath The path portion of the source property |
| * |
| * @property {string} sourceSearch The search portion of the source property |
| * |
| * @property {string} regex The constructed regex that will be used to match against the url when |
| * it is time to determine which url will match. |
| * |
| * @returns {Object} New `UrlMatcher` object |
| */ |
| function UrlMatcher(pattern, config, parentMatcher) { |
| config = extend({ params: {} }, isObject(config) ? config : {}); |
| |
| // Find all placeholders and create a compiled pattern, using either classic or curly syntax: |
| // '*' name |
| // ':' name |
| // '{' name '}' |
| // '{' name ':' regexp '}' |
| // The regular expression is somewhat complicated due to the need to allow curly braces |
| // inside the regular expression. The placeholder regexp breaks down as follows: |
| // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case) |
| // \{([\w\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case |
| // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either |
| // [^{}\\]+ - anything other than curly braces or backslash |
| // \\. - a backslash escape |
| // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms |
| var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, |
| searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, |
| compiled = '^', last = 0, m, |
| segments = this.segments = [], |
| parentParams = parentMatcher ? parentMatcher.params : {}, |
| params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(), |
| paramNames = []; |
| |
| function addParameter(id, type, config, location) { |
| paramNames.push(id); |
| if (parentParams[id]) return parentParams[id]; |
| if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); |
| if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); |
| params[id] = new $$UMFP.Param(id, type, config, location); |
| return params[id]; |
| } |
| |
| function quoteRegExp(string, pattern, squash, optional) { |
| var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); |
| if (!pattern) return result; |
| switch(squash) { |
| case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; |
| case true: surroundPattern = ['?(', ')?']; break; |
| default: surroundPattern = ['(' + squash + "|", ')?']; break; |
| } |
| return result + surroundPattern[0] + pattern + surroundPattern[1]; |
| } |
| |
| this.source = pattern; |
| |
| // Split into static segments separated by path parameter placeholders. |
| // The number of segments is always 1 more than the number of parameters. |
| function matchDetails(m, isSearch) { |
| var id, regexp, segment, type, cfg, arrayMode; |
| id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null |
| cfg = config.params[id]; |
| segment = pattern.substring(last, m.index); |
| regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); |
| type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); |
| return { |
| id: id, regexp: regexp, segment: segment, type: type, cfg: cfg |
| }; |
| } |
| |
| var p, param, segment; |
| while ((m = placeholder.exec(pattern))) { |
| p = matchDetails(m, false); |
| if (p.segment.indexOf('?') >= 0) break; // we're into the search part |
| |
| param = addParameter(p.id, p.type, p.cfg, "path"); |
| compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional); |
| segments.push(p.segment); |
| last = placeholder.lastIndex; |
| } |
| segment = pattern.substring(last); |
| |
| // Find any search parameter names and remove them from the last segment |
| var i = segment.indexOf('?'); |
| |
| if (i >= 0) { |
| var search = this.sourceSearch = segment.substring(i); |
| segment = segment.substring(0, i); |
| this.sourcePath = pattern.substring(0, last + i); |
| |
| if (search.length > 0) { |
| last = 0; |
| while ((m = searchPlaceholder.exec(search))) { |
| p = matchDetails(m, true); |
| param = addParameter(p.id, p.type, p.cfg, "search"); |
| last = placeholder.lastIndex; |
| // check if ?& |
| } |
| } |
| } else { |
| this.sourcePath = pattern; |
| this.sourceSearch = ''; |
| } |
| |
| compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; |
| segments.push(segment); |
| |
| this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); |
| this.prefix = segments[0]; |
| this.$$paramNames = paramNames; |
| } |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:UrlMatcher#concat |
| * @methodOf ui.router.util.type:UrlMatcher |
| * |
| * @description |
| * Returns a new matcher for a pattern constructed by appending the path part and adding the |
| * search parameters of the specified pattern to this pattern. The current pattern is not |
| * modified. This can be understood as creating a pattern for URLs that are relative to (or |
| * suffixes of) the current pattern. |
| * |
| * @example |
| * The following two matchers are equivalent: |
| * <pre> |
| * new UrlMatcher('/user/{id}?q').concat('/details?date'); |
| * new UrlMatcher('/user/{id}/details?q&date'); |
| * </pre> |
| * |
| * @param {string} pattern The pattern to append. |
| * @param {Object} config An object hash of the configuration for the matcher. |
| * @returns {UrlMatcher} A matcher for the concatenated pattern. |
| */ |
| UrlMatcher.prototype.concat = function (pattern, config) { |
| // Because order of search parameters is irrelevant, we can add our own search |
| // parameters to the end of the new pattern. Parse the new pattern by itself |
| // and then join the bits together, but it's much easier to do this on a string level. |
| var defaultConfig = { |
| caseInsensitive: $$UMFP.caseInsensitive(), |
| strict: $$UMFP.strictMode(), |
| squash: $$UMFP.defaultSquashPolicy() |
| }; |
| return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); |
| }; |
| |
| UrlMatcher.prototype.toString = function () { |
| return this.source; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:UrlMatcher#exec |
| * @methodOf ui.router.util.type:UrlMatcher |
| * |
| * @description |
| * Tests the specified path against this matcher, and returns an object containing the captured |
| * parameter values, or null if the path does not match. The returned object contains the values |
| * of any search parameters that are mentioned in the pattern, but their value may be null if |
| * they are not present in `searchParams`. This means that search parameters are always treated |
| * as optional. |
| * |
| * @example |
| * <pre> |
| * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { |
| * x: '1', q: 'hello' |
| * }); |
| * // returns { id: 'bob', q: 'hello', r: null } |
| * </pre> |
| * |
| * @param {string} path The URL path to match, e.g. `$location.path()`. |
| * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. |
| * @returns {Object} The captured parameter values. |
| */ |
| UrlMatcher.prototype.exec = function (path, searchParams) { |
| var m = this.regexp.exec(path); |
| if (!m) return null; |
| searchParams = searchParams || {}; |
| |
| var paramNames = this.parameters(), nTotal = paramNames.length, |
| nPath = this.segments.length - 1, |
| values = {}, i, j, cfg, paramName; |
| |
| if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); |
| |
| function decodePathArray(string) { |
| function reverseString(str) { return str.split("").reverse().join(""); } |
| function unquoteDashes(str) { return str.replace(/\\-/g, "-"); } |
| |
| var split = reverseString(string).split(/-(?!\\)/); |
| var allReversed = map(split, reverseString); |
| return map(allReversed, unquoteDashes).reverse(); |
| } |
| |
| for (i = 0; i < nPath; i++) { |
| paramName = paramNames[i]; |
| var param = this.params[paramName]; |
| var paramVal = m[i+1]; |
| // if the param value matches a pre-replace pair, replace the value before decoding. |
| for (j = 0; j < param.replace; j++) { |
| if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; |
| } |
| if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); |
| values[paramName] = param.value(paramVal); |
| } |
| for (/**/; i < nTotal; i++) { |
| paramName = paramNames[i]; |
| values[paramName] = this.params[paramName].value(searchParams[paramName]); |
| } |
| |
| return values; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:UrlMatcher#parameters |
| * @methodOf ui.router.util.type:UrlMatcher |
| * |
| * @description |
| * Returns the names of all path and search parameters of this pattern in an unspecified order. |
| * |
| * @returns {Array.<string>} An array of parameter names. Must be treated as read-only. If the |
| * pattern has no parameters, an empty array is returned. |
| */ |
| UrlMatcher.prototype.parameters = function (param) { |
| if (!isDefined(param)) return this.$$paramNames; |
| return this.params[param] || null; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:UrlMatcher#validate |
| * @methodOf ui.router.util.type:UrlMatcher |
| * |
| * @description |
| * Checks an object hash of parameters to validate their correctness according to the parameter |
| * types of this `UrlMatcher`. |
| * |
| * @param {Object} params The object hash of parameters to validate. |
| * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. |
| */ |
| UrlMatcher.prototype.validates = function (params) { |
| return this.params.$$validates(params); |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:UrlMatcher#format |
| * @methodOf ui.router.util.type:UrlMatcher |
| * |
| * @description |
| * Creates a URL that matches this pattern by substituting the specified values |
| * for the path and search parameters. Null values for path parameters are |
| * treated as empty strings. |
| * |
| * @example |
| * <pre> |
| * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' }); |
| * // returns '/user/bob?q=yes' |
| * </pre> |
| * |
| * @param {Object} values the values to substitute for the parameters in this pattern. |
| * @returns {string} the formatted URL (path and optionally search part). |
| */ |
| UrlMatcher.prototype.format = function (values) { |
| values = values || {}; |
| var segments = this.segments, params = this.parameters(), paramset = this.params; |
| if (!this.validates(values)) return null; |
| |
| var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; |
| |
| function encodeDashes(str) { // Replace dashes with encoded "\-" |
| return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); |
| } |
| |
| for (i = 0; i < nTotal; i++) { |
| var isPathParam = i < nPath; |
| var name = params[i], param = paramset[name], value = param.value(values[name]); |
| var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); |
| var squash = isDefaultValue ? param.squash : false; |
| var encoded = param.type.encode(value); |
| |
| if (isPathParam) { |
| var nextSegment = segments[i + 1]; |
| if (squash === false) { |
| if (encoded != null) { |
| if (isArray(encoded)) { |
| result += map(encoded, encodeDashes).join("-"); |
| } else { |
| result += encodeURIComponent(encoded); |
| } |
| } |
| result += nextSegment; |
| } else if (squash === true) { |
| var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/; |
| result += nextSegment.match(capture)[1]; |
| } else if (isString(squash)) { |
| result += squash + nextSegment; |
| } |
| } else { |
| if (encoded == null || (isDefaultValue && squash !== false)) continue; |
| if (!isArray(encoded)) encoded = [ encoded ]; |
| encoded = map(encoded, encodeURIComponent).join('&' + name + '='); |
| result += (search ? '&' : '?') + (name + '=' + encoded); |
| search = true; |
| } |
| } |
| |
| return result; |
| }; |
| |
| /** |
| * @ngdoc object |
| * @name ui.router.util.type:Type |
| * |
| * @description |
| * Implements an interface to define custom parameter types that can be decoded from and encoded to |
| * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} |
| * objects when matching or formatting URLs, or comparing or validating parameter values. |
| * |
| * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more |
| * information on registering custom types. |
| * |
| * @param {Object} config A configuration object which contains the custom type definition. The object's |
| * properties will override the default methods and/or pattern in `Type`'s public interface. |
| * @example |
| * <pre> |
| * { |
| * decode: function(val) { return parseInt(val, 10); }, |
| * encode: function(val) { return val && val.toString(); }, |
| * equals: function(a, b) { return this.is(a) && a === b; }, |
| * is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; }, |
| * pattern: /\d+/ |
| * } |
| * </pre> |
| * |
| * @property {RegExp} pattern The regular expression pattern used to match values of this type when |
| * coming from a substring of a URL. |
| * |
| * @returns {Object} Returns a new `Type` object. |
| */ |
| function Type(config) { |
| extend(this, config); |
| } |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:Type#is |
| * @methodOf ui.router.util.type:Type |
| * |
| * @description |
| * Detects whether a value is of a particular type. Accepts a native (decoded) value |
| * and determines whether it matches the current `Type` object. |
| * |
| * @param {*} val The value to check. |
| * @param {string} key Optional. If the type check is happening in the context of a specific |
| * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the |
| * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. |
| * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. |
| */ |
| Type.prototype.is = function(val, key) { |
| return true; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:Type#encode |
| * @methodOf ui.router.util.type:Type |
| * |
| * @description |
| * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the |
| * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it |
| * only needs to be a representation of `val` that has been coerced to a string. |
| * |
| * @param {*} val The value to encode. |
| * @param {string} key The name of the parameter in which `val` is stored. Can be used for |
| * meta-programming of `Type` objects. |
| * @returns {string} Returns a string representation of `val` that can be encoded in a URL. |
| */ |
| Type.prototype.encode = function(val, key) { |
| return val; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:Type#decode |
| * @methodOf ui.router.util.type:Type |
| * |
| * @description |
| * Converts a parameter value (from URL string or transition param) to a custom/native value. |
| * |
| * @param {string} val The URL parameter value to decode. |
| * @param {string} key The name of the parameter in which `val` is stored. Can be used for |
| * meta-programming of `Type` objects. |
| * @returns {*} Returns a custom representation of the URL parameter value. |
| */ |
| Type.prototype.decode = function(val, key) { |
| return val; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.type:Type#equals |
| * @methodOf ui.router.util.type:Type |
| * |
| * @description |
| * Determines whether two decoded values are equivalent. |
| * |
| * @param {*} a A value to compare against. |
| * @param {*} b A value to compare against. |
| * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. |
| */ |
| Type.prototype.equals = function(a, b) { |
| return a == b; |
| }; |
| |
| Type.prototype.$subPattern = function() { |
| var sub = this.pattern.toString(); |
| return sub.substr(1, sub.length - 2); |
| }; |
| |
| Type.prototype.pattern = /.*/; |
| |
| Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; |
| |
| /** Given an encoded string, or a decoded object, returns a decoded object */ |
| Type.prototype.$normalize = function(val) { |
| return this.is(val) ? val : this.decode(val); |
| }; |
| |
| /* |
| * Wraps an existing custom Type as an array of Type, depending on 'mode'. |
| * e.g.: |
| * - urlmatcher pattern "/path?{queryParam[]:int}" |
| * - url: "/path?queryParam=1&queryParam=2 |
| * - $stateParams.queryParam will be [1, 2] |
| * if `mode` is "auto", then |
| * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 |
| * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] |
| */ |
| Type.prototype.$asArray = function(mode, isSearch) { |
| if (!mode) return this; |
| if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); |
| |
| function ArrayType(type, mode) { |
| function bindTo(type, callbackName) { |
| return function() { |
| return type[callbackName].apply(type, arguments); |
| }; |
| } |
| |
| // Wrap non-array value as array |
| function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } |
| // Unwrap array value for "auto" mode. Return undefined for empty array. |
| function arrayUnwrap(val) { |
| switch(val.length) { |
| case 0: return undefined; |
| case 1: return mode === "auto" ? val[0] : val; |
| default: return val; |
| } |
| } |
| function falsey(val) { return !val; } |
| |
| // Wraps type (.is/.encode/.decode) functions to operate on each value of an array |
| function arrayHandler(callback, allTruthyMode) { |
| return function handleArray(val) { |
| val = arrayWrap(val); |
| var result = map(val, callback); |
| if (allTruthyMode === true) |
| return filter(result, falsey).length === 0; |
| return arrayUnwrap(result); |
| }; |
| } |
| |
| // Wraps type (.equals) functions to operate on each value of an array |
| function arrayEqualsHandler(callback) { |
| return function handleArray(val1, val2) { |
| var left = arrayWrap(val1), right = arrayWrap(val2); |
| if (left.length !== right.length) return false; |
| for (var i = 0; i < left.length; i++) { |
| if (!callback(left[i], right[i])) return false; |
| } |
| return true; |
| }; |
| } |
| |
| this.encode = arrayHandler(bindTo(type, 'encode')); |
| this.decode = arrayHandler(bindTo(type, 'decode')); |
| this.is = arrayHandler(bindTo(type, 'is'), true); |
| this.equals = arrayEqualsHandler(bindTo(type, 'equals')); |
| this.pattern = type.pattern; |
| this.$normalize = arrayHandler(bindTo(type, '$normalize')); |
| this.name = type.name; |
| this.$arrayMode = mode; |
| } |
| |
| return new ArrayType(this, mode); |
| }; |
| |
| |
| |
| /** |
| * @ngdoc object |
| * @name ui.router.util.$urlMatcherFactory |
| * |
| * @description |
| * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory |
| * is also available to providers under the name `$urlMatcherFactoryProvider`. |
| */ |
| function $UrlMatcherFactory() { |
| $$UMFP = this; |
| |
| var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; |
| |
| function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; } |
| function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; } |
| |
| var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { |
| string: { |
| encode: valToString, |
| decode: valFromString, |
| // TODO: in 1.0, make string .is() return false if value is undefined/null by default. |
| // In 0.2.x, string params are optional by default for backwards compat |
| is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; }, |
| pattern: /[^/]*/ |
| }, |
| int: { |
| encode: valToString, |
| decode: function(val) { return parseInt(val, 10); }, |
| is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, |
| pattern: /\d+/ |
| }, |
| bool: { |
| encode: function(val) { return val ? 1 : 0; }, |
| decode: function(val) { return parseInt(val, 10) !== 0; }, |
| is: function(val) { return val === true || val === false; }, |
| pattern: /0|1/ |
| }, |
| date: { |
| encode: function (val) { |
| if (!this.is(val)) |
| return undefined; |
| return [ val.getFullYear(), |
| ('0' + (val.getMonth() + 1)).slice(-2), |
| ('0' + val.getDate()).slice(-2) |
| ].join("-"); |
| }, |
| decode: function (val) { |
| if (this.is(val)) return val; |
| var match = this.capture.exec(val); |
| return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; |
| }, |
| is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, |
| equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, |
| pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, |
| capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ |
| }, |
| json: { |
| encode: angular.toJson, |
| decode: angular.fromJson, |
| is: angular.isObject, |
| equals: angular.equals, |
| pattern: /[^/]*/ |
| }, |
| any: { // does not encode/decode |
| encode: angular.identity, |
| decode: angular.identity, |
| equals: angular.equals, |
| pattern: /.*/ |
| } |
| }; |
| |
| function getDefaultConfig() { |
| return { |
| strict: isStrictMode, |
| caseInsensitive: isCaseInsensitive |
| }; |
| } |
| |
| function isInjectable(value) { |
| return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); |
| } |
| |
| /** |
| * [Internal] Get the default value of a parameter, which may be an injectable function. |
| */ |
| $UrlMatcherFactory.$$getDefaultValue = function(config) { |
| if (!isInjectable(config.value)) return config.value; |
| if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); |
| return injector.invoke(config.value); |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.$urlMatcherFactory#caseInsensitive |
| * @methodOf ui.router.util.$urlMatcherFactory |
| * |
| * @description |
| * Defines whether URL matching should be case sensitive (the default behavior), or not. |
| * |
| * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; |
| * @returns {boolean} the current value of caseInsensitive |
| */ |
| this.caseInsensitive = function(value) { |
| if (isDefined(value)) |
| isCaseInsensitive = value; |
| return isCaseInsensitive; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.$urlMatcherFactory#strictMode |
| * @methodOf ui.router.util.$urlMatcherFactory |
| * |
| * @description |
| * Defines whether URLs should match trailing slashes, or not (the default behavior). |
| * |
| * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. |
| * @returns {boolean} the current value of strictMode |
| */ |
| this.strictMode = function(value) { |
| if (isDefined(value)) |
| isStrictMode = value; |
| return isStrictMode; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy |
| * @methodOf ui.router.util.$urlMatcherFactory |
| * |
| * @description |
| * Sets the default behavior when generating or matching URLs with default parameter values. |
| * |
| * @param {string} value A string that defines the default parameter URL squashing behavior. |
| * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL |
| * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the |
| * parameter is surrounded by slashes, squash (remove) one slash from the URL |
| * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) |
| * the parameter value from the URL and replace it with this string. |
| */ |
| this.defaultSquashPolicy = function(value) { |
| if (!isDefined(value)) return defaultSquashPolicy; |
| if (value !== true && value !== false && !isString(value)) |
| throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); |
| defaultSquashPolicy = value; |
| return value; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.$urlMatcherFactory#compile |
| * @methodOf ui.router.util.$urlMatcherFactory |
| * |
| * @description |
| * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. |
| * |
| * @param {string} pattern The URL pattern. |
| * @param {Object} config The config object hash. |
| * @returns {UrlMatcher} The UrlMatcher. |
| */ |
| this.compile = function (pattern, config) { |
| return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.$urlMatcherFactory#isMatcher |
| * @methodOf ui.router.util.$urlMatcherFactory |
| * |
| * @description |
| * Returns true if the specified object is a `UrlMatcher`, or false otherwise. |
| * |
| * @param {Object} object The object to perform the type check against. |
| * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by |
| * implementing all the same methods. |
| */ |
| this.isMatcher = function (o) { |
| if (!isObject(o)) return false; |
| var result = true; |
| |
| forEach(UrlMatcher.prototype, function(val, name) { |
| if (isFunction(val)) { |
| result = result && (isDefined(o[name]) && isFunction(o[name])); |
| } |
| }); |
| return result; |
| }; |
| |
| /** |
| * @ngdoc function |
| * @name ui.router.util.$urlMatcherFactory#type |
| * @methodOf ui.router.util.$urlMatcherFactory |
| * |
| * @description |
| * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to |
| * generate URLs with typed parameters. |
| * |
| * @param {string} name The type name. |
| * @param {Object|Function} definition The type definition. See |
| * {@link ui.router.util.type:Type `Type`} for information on the values accepted. |
| * @param {Object|Function} definitionFn (optional) A function that is injected before the app |
| * runtime starts. The result of this function is merged into the existing `definition`. |
| * See {@link ui.router.util.type:Type `Type`} for information on the values accepted. |
| * |
| * @returns {Object} Returns `$urlMatcherFactoryProvider`. |
| * |
| * @example |
| * This is a simple example of a custom type that encodes and decodes items from an |
| * array, using the array index as the URL-encoded value: |
| * |
| * <pre> |
| * var list = ['John', 'Paul', 'George', 'Ringo']; |
| * |
| * $urlMatcherFactoryProvider.type('listItem', { |
| * encode: function(item) { |
| * // Represent the list item in the URL using its corresponding index |
| * return list.indexOf(item); |
| * }, |
| * decode: function(item) { |
| * // Look up the list item by index |
| * return list[parseInt(item, 10)]; |
| * }, |
| * is: function(item) { |
| * // Ensure the item is valid by checking to see that it appears |
| * // in the list |
| * return list.indexOf(item) > -1; |
| * } |
| * }); |
| * |
| * $stateProvider.state('list', { |
| * url: "/list/{item:listItem}", |
| * controller: function($scope, $stateParams) { |
| * console.log($stateParams.item); |
| * } |
| * }); |
| * |
| * // ... |
| * |
| * // Changes URL to '/list/3', logs "Ringo" to the console |
| * $state.go('list', { item: "Ringo" }); |
| * </pre> |
| * |
| * This is a more complex example of a type that relies on dependency injection to |
| * interact with services, and uses the parameter name from the URL to infer how to |
| * handle encoding and decoding parameter values: |
| * |
| * <pre> |
| * // Defines a custom type that gets a value from a service, |
| * // where each service gets different types of values from |
| * // a backend API: |
| * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) { |
| * |
| * // Matches up services to URL parameter names |
| * var services = { |
| * user: Users, |
| * post: Posts |
| * }; |
| * |
| * return { |
| * encode: function(object) { |
| * // Represent the object in the URL using its unique ID |
| * return object.id; |
| * }, |
| * decode: function(value, key) { |
| * // Look up the object by ID, using the parameter |
| * // name (key) to call the correct service |
| * return services[key].findById(value); |
| * }, |
| * is: function(object, key) { |
| * // Check that object is a valid dbObject |
| * return angular.isObject(object) && object.id && services[key]; |
| * } |
| * equals: function(a, b) { |
| * // Check the equality of decoded objects by comparing |
| * // their unique IDs |
| * return a.id === b.id; |
| * } |
| * }; |
| * }); |
| * |
| * // In a config() block, you can then attach URLs with |
| * // type-annotated parameters: |
| * $stateProvider.state('users', { |
| * url: "/users", |
| * // ... |
| * }).state('users.item', { |
| * url: "/{user:dbObject}", |
| * controller: function($scope, $stateParams) { |
| * // $stateParams.user will now be an object returned from |
| * // the Users service |
| * }, |
| * // ... |
| * }); |
| * </pre> |
| */ |
| this.type = function (name, definition, definitionFn) { |
| if (!isDefined(definition)) return $types[name]; |
| if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); |
| |
| $types[name] = new Type(extend({ name: name }, definition)); |
| if (definitionFn) { |
| typeQueue.push({ name: name, def: definitionFn }); |
| if (!enqueue) flushTypeQueue(); |
| } |
| return this; |
| }; |
| |
| // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s |
| function flushTypeQueue() { |
| while(typeQueue.length) { |
| var type = typeQueue.shift(); |
| if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); |
| angular.extend($types[type.name], injector.invoke(type.def)); |
| } |
| } |
| |
| // Register default types. Store them in the prototype of $types. |
| forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); |
| $types = inherit($types, {}); |
| |
| /* No need to document $get, since it returns this */ |
| this.$get = ['$injector', function ($injector) { |
| injector = $injector; |
| enqueue = false; |
| flushTypeQueue(); |
| |
| forEach(defaultTypes, function(type, name) { |
| if (!$types[name]) $types[name] = new Type(type); |
| }); |
| return this; |
| }]; |
| |
| this.Param = function Param(id, type, config, location) { |
| var self = this; |
| config = unwrapShorthand(config); |
| type = getType(config, type, location); |
| var arrayMode = getArrayMode(); |
| type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; |
| if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined) |
| config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" |
| var isOptional = config.value !== undefined; |
| var squash = getSquashPolicy(config, isOptional); |
| var replace = getReplace(config, arrayMode, isOptional, squash); |
| |
| function unwrapShorthand(config) { |
| var keys = isObject(config) ? objectKeys(config) : []; |
| var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && |
| indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; |
| if (isShorthand) config = { value: config }; |
| config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; }; |
| return config; |
| } |
| |
| function getType(config, urlType, location) { |
| if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); |
| if (urlType) return urlType; |
| if (!config.type) return (location === "config" ? $types.any : $types.string); |
| return config.type instanceof Type ? config.type : new Type(config.type); |
| } |
| |
| // array config: param name (param[]) overrides default settings. explicit config overrides param name. |
| function getArrayMode() { |
| var arrayDefaults = { array: (location === "search" ? "auto" : false) }; |
| var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; |
| return extend(arrayDefaults, arrayParamNomenclature, config).array; |
| } |
| |
| /** |
| * returns false, true, or the squash value to indicate the "default parameter url squash policy". |
| */ |
| function getSquashPolicy(config, isOptional) { |
| var squash = config.squash; |
| if (!isOptional || squash === false) return false; |
| if (!isDefined(squash) || squash == null) return defaultSquashPolicy; |
| if (squash === true || isString(squash)) return squash; |
| throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); |
| } |
| |
| function getReplace(config, arrayMode, isOptional, squash) { |
| var replace, configuredKeys, defaultPolicy = [ |
| { from: "", to: (isOptional || arrayMode ? undefined : "") }, |
| { from: null, to: (isOptional || arrayMode ? undefined : "") } |
| ]; |
| replace = isArray(config.replace) ? config.replace : []; |
| if (isString(squash)) |
| replace.push({ from: squash, to: undefined }); |
| configuredKeys = map(replace, function(item) { return item.from; } ); |
| return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); |
| } |
| |
| /** |
| * [Internal] Get the default value of a parameter, which may be an injectable function. |
| */ |
| function $$getDefaultValue() { |
| if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); |
| var defaultValue = injector.invoke(config.$$fn); |
| if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue)) |
| throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")"); |
| return defaultValue; |
| } |
| |
| /** |
| * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the |
| * default value, which may be the result of an injectable function. |
| */ |
| function $value(value) { |
| function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } |
| function $replace(value) { |
| var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); |
| return replacement.length ? replacement[0] : value; |
| } |
| value = $replace(value); |
| return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value); |
| } |
| |
| function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } |
| |
| extend(this, { |
| id: id, |
| type: type, |
| location: location, |
| array: arrayMode, |
| squash: squash, |
| replace: replace, |
| isOptional: isOptional, |
| value: $value, |
| dynamic: undefined, |
| config: config, |
| toString: toString |
| }); |
| }; |
| |
| function ParamSet(params) { |
| extend(this, params || {}); |
| } |
| |
| ParamSet.prototype = { |
| $$new: function() { |
| return inherit(this, extend(new ParamSet(), { $$parent: this})); |
| }, |
| $$keys: function () { |
| var keys = [], chain = [], parent = this, |
| ignore = objectKeys(ParamSet.prototype); |
| while (parent) { chain.push(parent); parent = parent.$$parent; } |
| chain.reverse(); |
| forEach(chain, function(paramset) { |
| forEach(objectKeys(paramset), function(key) { |
| if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); |
| }); |
| }); |
| return keys; |
| }, |
| $$values: function(paramValues) { |
| var values = {}, self = this; |
| forEach(self.$$keys(), function(key) { |
| values[key] = self[key].value(paramValues && paramValues[key]); |
| }); |
| return values; |
| }, |
| $$equals: function(paramValues1, paramValues2) { |
| var equal = true, self = this; |
| forEach(self.$$keys(), function(key) { |
| var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; |
| if (!self[key].type.equals(left, right)) equal = false; |
| }); |
| return equal; |
| }, |
| $$validates: function $$validate(paramValues) { |
| var keys = this.$$keys(), i, param, rawVal, normalized, encoded; |
| for (i = 0; i < keys.length; i++) { |
| param = this[keys[i]]; |
| rawVal = paramValues[keys[i]]; |
| if ((rawVal === undefined || rawVal === null) && param.isOptional) |
| break; // There was no parameter value, but the param is optional |
| normalized = param.type.$normalize(rawVal); |
| if (!param.type.is(normalized)) |
| return false; // The value was not of the correct Type, and could not be decoded to the correct Type |
| encoded = param.type.encode(normalized); |
| if (angular.isString(encoded) && !param.type.pattern.exec(encoded)) |
| return false; // The value was of the correct type, but when encoded, did not match the Type's regexp |
| } |
| return true; |
| }, |
| $$parent: undefined |
| }; |
| |
| this.ParamSet = ParamSet; |
| } |
| |
| // Register as a provider so it's available to other providers |
| angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); |
| angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]); |