Pony.fm/app/scripts/base/angular-ui-router.js

1037 lines
38 KiB
JavaScript
Raw Normal View History

2013-07-25 23:33:04 +02:00
/**
* 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);