mirror of
https://github.com/Poniverse/Pony.fm.git
synced 2024-11-25 14:37:59 +01:00
1037 lines
No EOL
38 KiB
JavaScript
1037 lines
No EOL
38 KiB
JavaScript
/**
|
|
* State-based routing for AngularJS
|
|
* @version v0.0.1
|
|
* @link http://angular-ui.github.com/
|
|
* @license MIT License, http://www.opensource.org/licenses/MIT
|
|
*/
|
|
(function (window, angular, undefined) {
|
|
/*jshint globalstrict:true*/
|
|
/*global angular:false*/
|
|
'use strict';
|
|
|
|
var isDefined = angular.isDefined,
|
|
isFunction = angular.isFunction,
|
|
isString = angular.isString,
|
|
isObject = angular.isObject,
|
|
isArray = angular.isArray,
|
|
forEach = angular.forEach,
|
|
extend = angular.extend,
|
|
copy = angular.copy;
|
|
|
|
function inherit(parent, extra) {
|
|
return extend(new (extend(function() {}, { prototype: parent }))(), extra);
|
|
}
|
|
|
|
/**
|
|
* Extends the destination object `dst` by copying all of the properties from the `src` object(s)
|
|
* to `dst` if the `dst` object has no own property of the same name. You can specify multiple
|
|
* `src` objects.
|
|
*
|
|
* @param {Object} dst Destination object.
|
|
* @param {...Object} src Source object(s).
|
|
* @see angular.extend
|
|
*/
|
|
function merge(dst) {
|
|
forEach(arguments, function(obj) {
|
|
if (obj !== dst) {
|
|
forEach(obj, function(value, key) {
|
|
if (!dst.hasOwnProperty(key)) dst[key] = value;
|
|
});
|
|
}
|
|
});
|
|
return dst;
|
|
}
|
|
|
|
angular.module('ui.util', ['ng']);
|
|
angular.module('ui.router', ['ui.util']);
|
|
angular.module('ui.state', ['ui.router', 'ui.util']);
|
|
angular.module('ui.compat', ['ui.state']);
|
|
|
|
/**
|
|
* Service. Manages loading of templates.
|
|
* @constructor
|
|
* @name $templateFactory
|
|
* @requires $http
|
|
* @requires $templateCache
|
|
* @requires $injector
|
|
*/
|
|
$TemplateFactory.$inject = ['$http', '$templateCache', '$injector'];
|
|
function $TemplateFactory( $http, $templateCache, $injector) {
|
|
|
|
/**
|
|
* Creates a template from a configuration object.
|
|
* @function
|
|
* @name $templateFactory#fromConfig
|
|
* @methodOf $templateFactory
|
|
* @param {Object} config Configuration object for which to load a template. The following
|
|
* properties are search in the specified order, and the first one that is defined is
|
|
* used to create the template:
|
|
* @param {string|Function} config.template html string template or function to load via
|
|
* {@link $templateFactory#fromString fromString}.
|
|
* @param {string|Function} config.templateUrl url to load or a function returning the url
|
|
* to load via {@link $templateFactory#fromUrl fromUrl}.
|
|
* @param {Function} config.templateProvider function to invoke via
|
|
* {@link $templateFactory#fromProvider fromProvider}.
|
|
* @param {Object} params Parameters to pass to the template function.
|
|
* @param {Object} [locals] Locals to pass to `invoke` if the template is loaded via a
|
|
* `templateProvider`. Defaults to `{ params: params }`.
|
|
* @return {string|Promise.<string>} The template html as a string, or a promise for that string,
|
|
* or `null` if no template is configured.
|
|
*/
|
|
this.fromConfig = function (config, params, locals) {
|
|
return (
|
|
isDefined(config.template) ? this.fromString(config.template, params) :
|
|
isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
|
|
isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, locals) :
|
|
null
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Creates a template from a string or a function returning a string.
|
|
* @function
|
|
* @name $templateFactory#fromString
|
|
* @methodOf $templateFactory
|
|
* @param {string|Function} template html template as a string or function that returns an html
|
|
* template as a string.
|
|
* @param {Object} params Parameters to pass to the template function.
|
|
* @return {string|Promise.<string>} The template html as a string, or a promise for that string.
|
|
*/
|
|
this.fromString = function (template, params) {
|
|
return isFunction(template) ? template(params) : template;
|
|
};
|
|
|
|
/**
|
|
* Loads a template from the a URL via `$http` and `$templateCache`.
|
|
* @function
|
|
* @name $templateFactory#fromUrl
|
|
* @methodOf $templateFactory
|
|
* @param {string|Function} url url of the template to load, or a function that returns a url.
|
|
* @param {Object} params Parameters to pass to the url function.
|
|
* @return {string|Promise.<string>} The template html as a string, or a promise for that string.
|
|
*/
|
|
this.fromUrl = function (url, params) {
|
|
if (isFunction(url)) url = url(params);
|
|
if (url == null) return null;
|
|
else return $http
|
|
.get(url, { cache: $templateCache })
|
|
.then(function(response) { return response.data; });
|
|
};
|
|
|
|
/**
|
|
* Creates a template by invoking an injectable provider function.
|
|
* @function
|
|
* @name $templateFactory#fromUrl
|
|
* @methodOf $templateFactory
|
|
* @param {Function} provider Function to invoke via `$injector.invoke`
|
|
* @param {Object} params Parameters for the template.
|
|
* @param {Object} [locals] Locals to pass to `invoke`. Defaults to `{ params: params }`.
|
|
* @return {string|Promise.<string>} The template html as a string, or a promise for that string.
|
|
*/
|
|
this.fromProvider = function (provider, params, locals) {
|
|
return $injector.invoke(provider, null, locals || { params: params });
|
|
};
|
|
}
|
|
|
|
angular.module('ui.util').service('$templateFactory', $TemplateFactory);
|
|
|
|
/**
|
|
* 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 UrlMatcher#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 '}' - curly placeholder with regexp. 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.
|
|
*
|
|
* @constructor
|
|
* @param {string} pattern the pattern to compile into a matcher.
|
|
*
|
|
* @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 UrlMatcher#exec exec()} returns
|
|
* non-null) will start with this prefix.
|
|
*/
|
|
function UrlMatcher(pattern) {
|
|
|
|
// 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)
|
|
// \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4)
|
|
// (?: ... | ... | ... )+ 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,
|
|
names = {}, compiled = '^', last = 0, m,
|
|
segments = this.segments = [],
|
|
params = this.params = [];
|
|
|
|
function addParameter(id) {
|
|
if (!/^\w+$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
|
|
if (names[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
|
|
names[id] = true;
|
|
params.push(id);
|
|
}
|
|
|
|
function quoteRegExp(string) {
|
|
return string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
|
|
}
|
|
|
|
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.
|
|
var id, regexp, segment;
|
|
while ((m = placeholder.exec(pattern))) {
|
|
id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
|
|
regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
|
|
segment = pattern.substring(last, m.index);
|
|
if (segment.indexOf('?') >= 0) break; // we're into the search part
|
|
compiled += quoteRegExp(segment) + '(' + regexp + ')';
|
|
addParameter(id);
|
|
segments.push(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);
|
|
|
|
// Allow parameters to be separated by '?' as well as '&' to make concat() easier
|
|
forEach(search.substring(1).split(/[&?]/), addParameter);
|
|
} else {
|
|
this.sourcePath = pattern;
|
|
this.sourceSearch = '';
|
|
}
|
|
|
|
compiled += quoteRegExp(segment) + '$';
|
|
segments.push(segment);
|
|
this.regexp = new RegExp(compiled);
|
|
this.prefix = segments[0];
|
|
}
|
|
|
|
/**
|
|
* 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:
|
|
* ```
|
|
* new UrlMatcher('/user/{id}?q').concat('/details?date');
|
|
* new UrlMatcher('/user/{id}/details?q&date');
|
|
* ```
|
|
*
|
|
* @param {string} pattern The pattern to append.
|
|
* @return {UrlMatcher} A matcher for the concatenated pattern.
|
|
*/
|
|
UrlMatcher.prototype.concat = function (pattern) {
|
|
// 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.
|
|
return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch);
|
|
};
|
|
|
|
UrlMatcher.prototype.toString = function () {
|
|
return this.source;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
* ```
|
|
* new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { x:'1', q:'hello' });
|
|
* // returns { id:'bob', q:'hello', r:null }
|
|
* ```
|
|
*
|
|
* @param {string} path The URL path to match, e.g. `$location.path()`.
|
|
* @param {Object} searchParams URL search parameters, e.g. `$location.search()`.
|
|
* @return {Object} The captured parameter values.
|
|
*/
|
|
UrlMatcher.prototype.exec = function (path, searchParams) {
|
|
var m = this.regexp.exec(path);
|
|
if (!m) return null;
|
|
|
|
var params = this.params, nTotal = params.length,
|
|
nPath = this.segments.length-1,
|
|
values = {}, i;
|
|
|
|
for (i=0; i<nPath; i++) values[params[i]] = decodeURIComponent(m[i+1]);
|
|
for (/**/; i<nTotal; i++) values[params[i]] = searchParams[params[i]];
|
|
|
|
return values;
|
|
};
|
|
|
|
/**
|
|
* Returns the names of all path and search parameters of this pattern in an unspecified order.
|
|
* @return {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 () {
|
|
return this.params;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
* ```
|
|
* new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
|
|
* // returns '/user/bob?q=yes'
|
|
* ```
|
|
*
|
|
* @param {Object} values the values to substitute for the parameters in this pattern.
|
|
* @return {string} the formatted URL (path and optionally search part).
|
|
*/
|
|
UrlMatcher.prototype.format = function (values) {
|
|
var segments = this.segments, params = this.params;
|
|
if (!values) return segments.join('');
|
|
|
|
var nPath = segments.length-1, nTotal = params.length,
|
|
result = segments[0], i, search, value;
|
|
|
|
for (i=0; i<nPath; i++) {
|
|
value = values[params[i]];
|
|
// TODO: Maybe we should throw on null here? It's not really good style to use '' and null interchangeabley
|
|
if (value != null) result += value;
|
|
result += segments[i+1];
|
|
}
|
|
for (/**/; i<nTotal; i++) {
|
|
value = values[params[i]];
|
|
if (value != null) {
|
|
result += (search ? '&' : '?') + params[i] + '=' + encodeURIComponent(value);
|
|
search = true;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Service. Factory for {@link UrlMatcher} instances. The factory is also available to providers
|
|
* under the name `$urlMatcherFactoryProvider`.
|
|
* @constructor
|
|
* @name $urlMatcherFactory
|
|
*/
|
|
function $UrlMatcherFactory() {
|
|
/**
|
|
* Creates a {@link UrlMatcher} for the specified pattern.
|
|
* @function
|
|
* @name $urlMatcherFactory#compile
|
|
* @methodOf $urlMatcherFactory
|
|
* @param {string} pattern The URL pattern.
|
|
* @return {UrlMatcher} The UrlMatcher.
|
|
*/
|
|
this.compile = function (pattern) {
|
|
return new UrlMatcher(pattern);
|
|
};
|
|
|
|
/**
|
|
* Returns true if the specified object is a UrlMatcher, or false otherwise.
|
|
* @function
|
|
* @name $urlMatcherFactory#isMatcher
|
|
* @methodOf $urlMatcherFactory
|
|
* @param {Object} o
|
|
* @return {boolean}
|
|
*/
|
|
this.isMatcher = function (o) {
|
|
return o instanceof UrlMatcher;
|
|
};
|
|
|
|
this.$get = function () {
|
|
return this;
|
|
};
|
|
}
|
|
|
|
// Register as a provider so it's available to other providers
|
|
angular.module('ui.util').provider('$urlMatcherFactory', $UrlMatcherFactory);
|
|
|
|
|
|
$UrlRouterProvider.$inject = ['$urlMatcherFactoryProvider'];
|
|
function $UrlRouterProvider( $urlMatcherFactory) {
|
|
var rules = [],
|
|
otherwise = null;
|
|
|
|
// Returns a string that is a prefix of all strings matching the RegExp
|
|
function regExpPrefix(re) {
|
|
var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source);
|
|
return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : '';
|
|
}
|
|
|
|
// Interpolates matched values into a String.replace()-style pattern
|
|
function interpolate(pattern, match) {
|
|
return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) {
|
|
return match[what === '$' ? 0 : Number(what)];
|
|
});
|
|
}
|
|
|
|
this.rule =
|
|
function (rule) {
|
|
if (!isFunction(rule)) throw new Error("'rule' must be a function");
|
|
rules.push(rule);
|
|
return this;
|
|
};
|
|
|
|
this.otherwise =
|
|
function (rule) {
|
|
if (isString(rule)) {
|
|
var redirect = rule;
|
|
rule = function () { return redirect; };
|
|
}
|
|
else if (!isFunction(rule)) throw new Error("'rule' must be a function");
|
|
otherwise = rule;
|
|
return this;
|
|
};
|
|
|
|
|
|
function handleIfMatch($injector, handler, match) {
|
|
if (!match) return false;
|
|
var result = $injector.invoke(handler, handler, { $match: match });
|
|
return isDefined(result) ? result : true;
|
|
}
|
|
|
|
this.when =
|
|
function (what, handler) {
|
|
var rule, redirect;
|
|
if (isString(what))
|
|
what = $urlMatcherFactory.compile(what);
|
|
|
|
if ($urlMatcherFactory.isMatcher(what)) {
|
|
if (isString(handler)) {
|
|
redirect = $urlMatcherFactory.compile(handler);
|
|
handler = ['$match', function ($match) { return redirect.format($match); }];
|
|
}
|
|
else if (!isFunction(handler) && !isArray(handler))
|
|
throw new Error("invalid 'handler' in when()");
|
|
|
|
rule = function ($injector, $location) {
|
|
return handleIfMatch($injector, handler, what.exec($location.path(), $location.search()));
|
|
};
|
|
rule.prefix = isString(what.prefix) ? what.prefix : '';
|
|
}
|
|
else if (what instanceof RegExp) {
|
|
if (isString(handler)) {
|
|
redirect = handler;
|
|
handler = ['$match', function ($match) { return interpolate(redirect, $match); }];
|
|
}
|
|
else if (!isFunction(handler) && !isArray(handler))
|
|
throw new Error("invalid 'handler' in when()");
|
|
|
|
if (what.global || what.sticky)
|
|
throw new Error("when() RegExp must not be global or sticky");
|
|
|
|
rule = function ($injector, $location) {
|
|
return handleIfMatch($injector, handler, what.exec($location.path()));
|
|
};
|
|
rule.prefix = regExpPrefix(what);
|
|
}
|
|
else
|
|
throw new Error("invalid 'what' in when()");
|
|
|
|
return this.rule(rule);
|
|
};
|
|
|
|
this.$get =
|
|
[ '$location', '$rootScope', '$injector',
|
|
function ($location, $rootScope, $injector) {
|
|
if (otherwise) rules.push(otherwise);
|
|
|
|
// TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
|
|
function update() {
|
|
var n=rules.length, i, handled;
|
|
for (i=0; i<n; i++) {
|
|
handled = rules[i]($injector, $location);
|
|
if (handled) {
|
|
if (isString(handled)) $location.replace().url(handled);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$rootScope.$on('$locationChangeSuccess', update);
|
|
return {};
|
|
}];
|
|
}
|
|
|
|
angular.module('ui.router').provider('$urlRouter', $UrlRouterProvider);
|
|
|
|
$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider'];
|
|
function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
|
|
|
|
var root, states = {}, $state;
|
|
|
|
function findState(stateOrName) {
|
|
var state;
|
|
if (isString(stateOrName)) {
|
|
state = states[stateOrName];
|
|
if (!state) throw new Error("No such state '" + stateOrName + "'");
|
|
} else {
|
|
state = states[stateOrName.name];
|
|
if (!state || state !== stateOrName && state.self !== stateOrName)
|
|
throw new Error("Invalid or unregistered state");
|
|
}
|
|
return state;
|
|
}
|
|
|
|
function registerState(state) {
|
|
// Wrap a new object around the state so we can store our private details easily.
|
|
state = inherit(state, {
|
|
self: state,
|
|
toString: function () { return this.name; }
|
|
});
|
|
|
|
var name = state.name;
|
|
if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name");
|
|
if (states[name]) throw new Error("State '" + name + "'' is already defined");
|
|
|
|
// Derive parent state from a hierarchical name only if 'parent' is not explicitly defined.
|
|
var parent = root;
|
|
if (!isDefined(state.parent)) {
|
|
// regex matches any valid composite state name
|
|
// would match "contact.list" but not "contacts"
|
|
var compositeName = /^(.+)\.[^.]+$/.exec(name);
|
|
if (compositeName != null) {
|
|
parent = findState(compositeName[1]);
|
|
}
|
|
} else if (state.parent != null) {
|
|
parent = findState(state.parent);
|
|
}
|
|
state.parent = parent;
|
|
// state.children = [];
|
|
// if (parent) parent.children.push(state);
|
|
|
|
// Build a URLMatcher if necessary, either via a relative or absolute URL
|
|
var url = state.url;
|
|
if (isString(url)) {
|
|
if (url.charAt(0) == '^') {
|
|
url = state.url = $urlMatcherFactory.compile(url.substring(1));
|
|
} else {
|
|
url = state.url = (parent.navigable || root).url.concat(url);
|
|
}
|
|
} else if (isObject(url) &&
|
|
isFunction(url.exec) && isFunction(url.format) && isFunction(url.concat)) {
|
|
/* use UrlMatcher (or compatible object) as is */
|
|
} else if (url != null) {
|
|
throw new Error("Invalid url '" + url + "' in state '" + state + "'");
|
|
}
|
|
|
|
// Keep track of the closest ancestor state that has a URL (i.e. is navigable)
|
|
state.navigable = url ? state : parent ? parent.navigable : null;
|
|
|
|
// Derive parameters for this state and ensure they're a super-set of parent's parameters
|
|
var params = state.params;
|
|
if (params) {
|
|
if (!isArray(params)) throw new Error("Invalid params in state '" + state + "'");
|
|
if (url) throw new Error("Both params and url specicified in state '" + state + "'");
|
|
} else {
|
|
params = state.params = url ? url.parameters() : state.parent.params;
|
|
}
|
|
|
|
var paramNames = {}; forEach(params, function (p) { paramNames[p] = true; });
|
|
if (parent) {
|
|
forEach(parent.params, function (p) {
|
|
if (!paramNames[p]) {
|
|
throw new Error("Missing required parameter '" + p + "' in state '" + name + "'");
|
|
}
|
|
paramNames[p] = false;
|
|
});
|
|
|
|
var ownParams = state.ownParams = [];
|
|
forEach(paramNames, function (own, p) {
|
|
if (own) ownParams.push(p);
|
|
});
|
|
} else {
|
|
state.ownParams = params;
|
|
}
|
|
|
|
// If there is no explicit multi-view configuration, make one up so we don't have
|
|
// to handle both cases in the view directive later. Note that having an explicit
|
|
// 'views' property will mean the default unnamed view properties are ignored. This
|
|
// is also a good time to resolve view names to absolute names, so everything is a
|
|
// straight lookup at link time.
|
|
var views = {};
|
|
forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) {
|
|
if (name.indexOf('@') < 0) name = name + '@' + state.parent.name;
|
|
views[name] = view;
|
|
});
|
|
state.views = views;
|
|
|
|
// Keep a full path from the root down to this state as this is needed for state activation.
|
|
state.path = parent ? parent.path.concat(state) : []; // exclude root from path
|
|
|
|
// Speed up $state.contains() as it's used a lot
|
|
var includes = state.includes = parent ? extend({}, parent.includes) : {};
|
|
includes[name] = true;
|
|
|
|
if (!state.resolve) state.resolve = {}; // prevent null checks later
|
|
|
|
// Register the state in the global state list and with $urlRouter if necessary.
|
|
if (!state['abstract'] && url) {
|
|
$urlRouterProvider.when(url, ['$match', function ($match) {
|
|
$state.transitionTo(state, $match, false);
|
|
}]);
|
|
}
|
|
states[name] = state;
|
|
return state;
|
|
}
|
|
|
|
// Implicit root state that is always active
|
|
root = registerState({
|
|
name: '',
|
|
url: '^',
|
|
views: null,
|
|
'abstract': true
|
|
});
|
|
root.locals = { globals: { $stateParams: {} } };
|
|
root.navigable = null;
|
|
|
|
|
|
// .state(state)
|
|
// .state(name, state)
|
|
this.state = state;
|
|
function state(name, definition) {
|
|
/*jshint validthis: true */
|
|
if (isObject(name)) definition = name;
|
|
else definition.name = name;
|
|
registerState(definition);
|
|
return this;
|
|
}
|
|
|
|
// $urlRouter is injected just to ensure it gets instantiated
|
|
this.$get = $get;
|
|
$get.$inject = ['$rootScope', '$q', '$templateFactory', '$injector', '$stateParams', '$location', '$urlRouter'];
|
|
function $get( $rootScope, $q, $templateFactory, $injector, $stateParams, $location, $urlRouter) {
|
|
|
|
var TransitionSuperseded = $q.reject(new Error('transition superseded'));
|
|
var TransitionPrevented = $q.reject(new Error('transition prevented'));
|
|
|
|
$state = {
|
|
params: {},
|
|
current: root.self,
|
|
$current: root,
|
|
transition: null
|
|
};
|
|
|
|
// $state.go = function go(to, params) {
|
|
// };
|
|
|
|
$state.transitionTo = function transitionTo(to, toParams, updateLocation) {
|
|
if (!isDefined(updateLocation)) updateLocation = true;
|
|
|
|
to = findState(to);
|
|
if (to['abstract']) throw new Error("Cannot transition to abstract state '" + to + "'");
|
|
var toPath = to.path,
|
|
from = $state.$current, fromParams = $state.params, fromPath = from.path;
|
|
|
|
// Starting from the root of the path, keep all levels that haven't changed
|
|
var keep, state, locals = root.locals, toLocals = [];
|
|
for (keep = 0, state = toPath[keep];
|
|
state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams);
|
|
keep++, state = toPath[keep]) {
|
|
locals = toLocals[keep] = state.locals;
|
|
}
|
|
|
|
// If we're going to the same state and all locals are kept, we've got nothing to do.
|
|
// But clear 'transition', as we still want to cancel any other pending transitions.
|
|
// TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves,
|
|
// because we might accidentally abort a legitimate transition initiated from code?
|
|
if (to === from && locals === from.locals) {
|
|
$state.transition = null;
|
|
return $q.when($state.current);
|
|
}
|
|
|
|
// Normalize/filter parameters before we pass them to event handlers etc.
|
|
toParams = normalize(to.params, toParams || {});
|
|
|
|
// Broadcast start event and cancel the transition if requested
|
|
if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams)
|
|
.defaultPrevented) return TransitionPrevented;
|
|
|
|
// Resolve locals for the remaining states, but don't update any global state just
|
|
// yet -- if anything fails to resolve the current state needs to remain untouched.
|
|
// We also set up an inheritance chain for the locals here. This allows the view directive
|
|
// to quickly look up the correct definition for each view in the current state. Even
|
|
// though we create the locals object itself outside resolveState(), it is initially
|
|
// empty and gets filled asynchronously. We need to keep track of the promise for the
|
|
// (fully resolved) current locals, and pass this down the chain.
|
|
var resolved = $q.when(locals);
|
|
for (var l=keep; l<toPath.length; l++, state=toPath[l]) {
|
|
locals = toLocals[l] = inherit(locals);
|
|
resolved = resolveState(state, toParams, state===to, resolved, locals);
|
|
}
|
|
|
|
// Once everything is resolved, we are ready to perform the actual transition
|
|
// and return a promise for the new state. We also keep track of what the
|
|
// current promise is, so that we can detect overlapping transitions and
|
|
// keep only the outcome of the last transition.
|
|
var transition = $state.transition = resolved.then(function () {
|
|
var l, entering, exiting;
|
|
|
|
if ($state.transition !== transition) return TransitionSuperseded;
|
|
|
|
// Exit 'from' states not kept
|
|
for (l=fromPath.length-1; l>=keep; l--) {
|
|
exiting = fromPath[l];
|
|
if (exiting.self.onExit) {
|
|
$injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals);
|
|
}
|
|
exiting.locals = null;
|
|
}
|
|
|
|
// Enter 'to' states not kept
|
|
for (l=keep; l<toPath.length; l++) {
|
|
entering = toPath[l];
|
|
entering.locals = toLocals[l];
|
|
if (entering.self.onEnter) {
|
|
$injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals);
|
|
}
|
|
}
|
|
|
|
// Update globals in $state
|
|
$state.$current = to;
|
|
$state.current = to.self;
|
|
$state.params = toParams;
|
|
copy($state.params, $stateParams);
|
|
$state.transition = null;
|
|
|
|
// Update $location
|
|
var toNav = to.navigable;
|
|
if (updateLocation && toNav) {
|
|
$location.url(toNav.url.format(toNav.locals.globals.$stateParams));
|
|
}
|
|
|
|
$rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams);
|
|
|
|
return $state.current;
|
|
}, function (error) {
|
|
if ($state.transition !== transition) return TransitionSuperseded;
|
|
|
|
$state.transition = null;
|
|
$rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error);
|
|
|
|
return $q.reject(error);
|
|
});
|
|
|
|
return transition;
|
|
};
|
|
|
|
$state.is = function (stateOrName) {
|
|
return $state.$current === findState(stateOrName);
|
|
};
|
|
|
|
$state.includes = function (stateOrName) {
|
|
return $state.$current.includes[findState(stateOrName).name];
|
|
};
|
|
|
|
$state.href = function (stateOrName, params) {
|
|
var state = findState(stateOrName), nav = state.navigable;
|
|
if (!nav) throw new Error("State '" + state + "' is not navigable");
|
|
return nav.url.format(normalize(state.params, params || {}));
|
|
};
|
|
|
|
function resolveState(state, params, paramsAreFiltered, inherited, dst) {
|
|
// We need to track all the promises generated during the resolution process.
|
|
// The first of these is for the fully resolved parent locals.
|
|
var promises = [inherited];
|
|
|
|
// Make a restricted $stateParams with only the parameters that apply to this state if
|
|
// necessary. In addition to being available to the controller and onEnter/onExit callbacks,
|
|
// we also need $stateParams to be available for any $injector calls we make during the
|
|
// dependency resolution process.
|
|
var $stateParams;
|
|
if (paramsAreFiltered) $stateParams = params;
|
|
else {
|
|
$stateParams = {};
|
|
forEach(state.params, function (name) {
|
|
$stateParams[name] = params[name];
|
|
});
|
|
}
|
|
var locals = { $stateParams: $stateParams };
|
|
|
|
// Resolves the values from an individual 'resolve' dependency spec
|
|
function resolve(deps, dst) {
|
|
forEach(deps, function (value, key) {
|
|
promises.push($q
|
|
.when(isString(value) ?
|
|
$injector.get(value) :
|
|
$injector.invoke(value, state.self, locals))
|
|
.then(function (result) {
|
|
dst[key] = result;
|
|
}));
|
|
});
|
|
}
|
|
|
|
// Resolve 'global' dependencies for the state, i.e. those not specific to a view.
|
|
// We're also including $stateParams in this; that we're the parameters are restricted
|
|
// to the set that should be visible to the state, and are independent of when we update
|
|
// the global $state and $stateParams values.
|
|
var globals = dst.globals = { $stateParams: $stateParams };
|
|
resolve(state.resolve, globals);
|
|
globals.$$state = state; // Provide access to the state itself for internal use
|
|
|
|
// Resolve template and dependencies for all views.
|
|
forEach(state.views, function (view, name) {
|
|
// References to the controller (only instantiated at link time)
|
|
var $view = dst[name] = {
|
|
$$controller: view.controller
|
|
};
|
|
|
|
// Template
|
|
promises.push($q
|
|
.when($templateFactory.fromConfig(view, $stateParams, locals) || '')
|
|
.then(function (result) {
|
|
$view.$template = result;
|
|
}));
|
|
|
|
// View-local dependencies. If we've reused the state definition as the default
|
|
// view definition in .state(), we can end up with state.resolve === view.resolve.
|
|
// Avoid resolving everything twice in that case.
|
|
if (view.resolve !== state.resolve) resolve(view.resolve, $view);
|
|
});
|
|
|
|
// Once we've resolved all the dependencies for this state, merge
|
|
// in any inherited dependencies, and merge common state dependencies
|
|
// into the dependency set for each view. Finally return a promise
|
|
// for the fully popuplated state dependencies.
|
|
return $q.all(promises).then(function (values) {
|
|
merge(dst.globals, values[0].globals); // promises[0] === inherited
|
|
forEach(state.views, function (view, name) {
|
|
merge(dst[name], dst.globals);
|
|
});
|
|
return dst;
|
|
});
|
|
}
|
|
|
|
function normalize(keys, values) {
|
|
var normalized = {};
|
|
|
|
forEach(keys, function (name) {
|
|
var value = values[name];
|
|
normalized[name] = (value != null) ? String(value) : null;
|
|
});
|
|
return normalized;
|
|
}
|
|
|
|
function equalForKeys(a, b, keys) {
|
|
for (var i=0; i<keys.length; i++) {
|
|
var k = keys[i];
|
|
if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return $state;
|
|
}
|
|
}
|
|
|
|
angular.module('ui.state')
|
|
.value('$stateParams', {})
|
|
.provider('$state', $StateProvider);
|
|
|
|
|
|
$ViewDirective.$inject = ['$state', '$compile', '$controller', '$injector', '$anchorScroll'];
|
|
function $ViewDirective( $state, $compile, $controller, $injector, $anchorScroll) {
|
|
// Unfortunately there is no neat way to ask $injector if a service exists
|
|
var $animator; try { $animator = $injector.get('$animator'); } catch (e) { /* do nothing */ }
|
|
|
|
var directive = {
|
|
restrict: 'ECA',
|
|
terminal: true,
|
|
link: function(scope, element, attr) {
|
|
var viewScope, viewLocals,
|
|
name = attr[directive.name] || attr.name || '',
|
|
onloadExp = attr.onload || '',
|
|
animate = isDefined($animator) && $animator(scope, attr);
|
|
|
|
// Find the details of the parent view directive (if any) and use it
|
|
// to derive our own qualified view name, then hang our own details
|
|
// off the DOM so child directives can find it.
|
|
var parent = element.parent().inheritedData('$uiView');
|
|
if (name.indexOf('@') < 0) name = name + '@' + (parent ? parent.state.name : '');
|
|
var view = { name: name, state: null };
|
|
element.data('$uiView', view);
|
|
|
|
scope.$on('$stateChangeSuccess', function() { updateView(true); });
|
|
updateView(false);
|
|
|
|
function updateView(doAnimate) {
|
|
var locals = $state.$current && $state.$current.locals[name];
|
|
if (locals === viewLocals) return; // nothing to do
|
|
|
|
// Destroy previous view scope and remove content (if any)
|
|
if (viewScope) {
|
|
if (animate && doAnimate) animate.leave(element.contents(), element);
|
|
else element.html('');
|
|
|
|
viewScope.$destroy();
|
|
viewScope = null;
|
|
}
|
|
|
|
if (locals) {
|
|
viewLocals = locals;
|
|
view.state = locals.$$state;
|
|
|
|
var contents;
|
|
if (animate && doAnimate) {
|
|
contents = angular.element('<div></div>').html(locals.$template).contents();
|
|
animate.enter(contents, element);
|
|
} else {
|
|
element.html(locals.$template);
|
|
contents = element.contents();
|
|
}
|
|
|
|
var link = $compile(contents);
|
|
viewScope = scope.$new();
|
|
if (locals.$$controller) {
|
|
locals.$scope = viewScope;
|
|
var controller = $controller(locals.$$controller, locals);
|
|
element.children().data('$ngControllerController', controller);
|
|
}
|
|
link(viewScope);
|
|
viewScope.$emit('$viewContentLoaded');
|
|
viewScope.$eval(onloadExp);
|
|
|
|
// TODO: This seems strange, shouldn't $anchorScroll listen for $viewContentLoaded if necessary?
|
|
// $anchorScroll might listen on event...
|
|
$anchorScroll();
|
|
} else {
|
|
viewLocals = null;
|
|
view.state = null;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
return directive;
|
|
}
|
|
|
|
angular.module('ui.state').directive('uiView', $ViewDirective);
|
|
|
|
$RouteProvider.$inject = ['$stateProvider', '$urlRouterProvider'];
|
|
function $RouteProvider( $stateProvider, $urlRouterProvider) {
|
|
|
|
var routes = [];
|
|
|
|
onEnterRoute.$inject = ['$$state'];
|
|
function onEnterRoute( $$state) {
|
|
/*jshint validthis: true */
|
|
this.locals = $$state.locals.globals;
|
|
this.params = this.locals.$stateParams;
|
|
}
|
|
|
|
function onExitRoute() {
|
|
/*jshint validthis: true */
|
|
this.locals = null;
|
|
this.params = null;
|
|
}
|
|
|
|
this.when = when;
|
|
function when(url, route) {
|
|
/*jshint validthis: true */
|
|
if (route.redirectTo != null) {
|
|
// Redirect, configure directly on $urlRouterProvider
|
|
var redirect = route.redirectTo, handler;
|
|
if (isString(redirect)) {
|
|
handler = redirect; // leave $urlRouterProvider to handle
|
|
} else if (isFunction(redirect)) {
|
|
// Adapt to $urlRouterProvider API
|
|
handler = function (params, $location) {
|
|
return redirect(params, $location.path(), $location.search());
|
|
};
|
|
} else {
|
|
throw new Error("Invalid 'redirectTo' in when()");
|
|
}
|
|
$urlRouterProvider.when(url, handler);
|
|
} else {
|
|
// Regular route, configure as state
|
|
$stateProvider.state(inherit(route, {
|
|
parent: null,
|
|
name: 'route:' + encodeURIComponent(url),
|
|
url: url,
|
|
onEnter: onEnterRoute,
|
|
onExit: onExitRoute
|
|
}));
|
|
}
|
|
routes.push(route);
|
|
return this;
|
|
}
|
|
|
|
this.$get = $get;
|
|
$get.$inject = ['$state', '$rootScope', '$routeParams'];
|
|
function $get( $state, $rootScope, $routeParams) {
|
|
|
|
var $route = {
|
|
routes: routes,
|
|
params: $routeParams,
|
|
current: undefined
|
|
};
|
|
|
|
function stateAsRoute(state) {
|
|
return (state.name !== '') ? state : undefined;
|
|
}
|
|
|
|
$rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) {
|
|
$rootScope.$broadcast('$routeChangeStart', stateAsRoute(to), stateAsRoute(from));
|
|
});
|
|
|
|
$rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) {
|
|
$route.current = stateAsRoute(to);
|
|
$rootScope.$broadcast('$routeChangeSuccess', stateAsRoute(to), stateAsRoute(from));
|
|
copy(toParams, $route.params);
|
|
});
|
|
|
|
$rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) {
|
|
$rootScope.$broadcast('$routeChangeError', stateAsRoute(to), stateAsRoute(from), error);
|
|
});
|
|
|
|
return $route;
|
|
}
|
|
}
|
|
|
|
angular.module('ui.compat')
|
|
.provider('$route', $RouteProvider)
|
|
.directive('ngView', $ViewDirective);
|
|
})(window, window.angular); |