Skip to content

Instantly share code, notes, and snippets.

@christopherthielen
Created October 7, 2015 01:03
Show Gist options
  • Save christopherthielen/2b88d15f7f5252a3e298 to your computer and use it in GitHub Desktop.
Save christopherthielen/2b88d15f7f5252a3e298 to your computer and use it in GitHub Desktop.
ui-router extras 0.0.15-pre2
/**
* UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise
* Monolithic build (all modules)
* @version 0.0.15-pre2
* @link http://christopherthielen.github.io/ui-router-extras/
* @license MIT License, http://www.opensource.org/licenses/MIT
*/
(function (root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['angular'], function (angular) {
factory(angular);
});
} else if (typeof exports === 'object') {
factory(require('angular'));
} else {
factory(root.angular);
}
}(this, function (angular, undefined) {
var mod_core = angular.module("ct.ui.router.extras.core", [ "ui.router" ]);
var internalStates = {}, stateRegisteredCallbacks = [];
mod_core.config([ '$stateProvider', '$injector', function ($stateProvider, $injector) {
// Decorate any state attribute in order to get access to the internal state representation.
$stateProvider.decorator('parent', function (state, parentFn) {
// Capture each internal UI-Router state representations as opposed to the user-defined state object.
// The internal state is, e.g., the state returned by $state.$current as opposed to $state.current
internalStates[state.self.name] = state;
// Add an accessor for the internal state from the user defined state
state.self.$$state = function () {
return internalStates[state.self.name];
};
angular.forEach(stateRegisteredCallbacks, function(callback) { callback(state); });
return parentFn(state);
});
}]);
var DEBUG = false;
var forEach = angular.forEach;
var extend = angular.extend;
var isArray = angular.isArray;
var map = function (collection, callback) {
"use strict";
var result = [];
forEach(collection, function (item, index) {
result.push(callback(item, index));
});
return result;
};
var keys = function (collection) {
"use strict";
return map(collection, function (collection, key) {
return key;
});
};
var filter = function (collection, callback) {
"use strict";
var result = [];
forEach(collection, function (item, index) {
if (callback(item, index)) {
result.push(item);
}
});
return result;
};
var filterObj = function (collection, callback) {
"use strict";
var result = {};
forEach(collection, function (item, index) {
if (callback(item, index)) {
result[index] = item;
}
});
return result;
};
// Duplicates code in UI-Router common.js
function ancestors(first, second) {
var path = [];
for (var n in first.path) {
if (first.path[n] !== second.path[n]) break;
path.push(first.path[n]);
}
return path;
}
// Duplicates code in UI-Router common.js
function objectKeys(object) {
if (Object.keys) {
return Object.keys(object);
}
var result = [];
angular.forEach(object, function (val, key) {
result.push(key);
});
return result;
}
/**
* like objectKeys, but includes keys from prototype chain.
* @param object the object whose prototypal keys will be returned
* @param ignoreKeys an array of keys to ignore
*/
// Duplicates code in UI-Router common.js
function protoKeys(object, ignoreKeys) {
var result = [];
for (var key in object) {
if (!ignoreKeys || ignoreKeys.indexOf(key) === -1)
result.push(key);
}
return result;
}
// Duplicates code in UI-Router common.js
function arraySearch(array, value) {
if (Array.prototype.indexOf) {
return array.indexOf(value, Number(arguments[2]) || 0);
}
var len = array.length >>> 0, from = Number(arguments[2]) || 0;
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
if (from < 0) from += len;
for (; from < len; from++) {
if (from in array && array[from] === value) return from;
}
return -1;
}
// Duplicates code in UI-Router common.js
// Added compatibility code (isArray check) to support both 0.2.x and 0.3.x series of UI-Router.
function inheritParams(currentParams, newParams, $current, $to) {
var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];
for (var i in parents) {
if (!parents[i].params) continue;
// This test allows compatibility with 0.2.x and 0.3.x (optional and object params)
parentParams = isArray(parents[i].params) ? parents[i].params : objectKeys(parents[i].params);
if (!parentParams.length) continue;
for (var j in parentParams) {
if (arraySearch(inheritList, parentParams[j]) >= 0) continue;
inheritList.push(parentParams[j]);
inherited[parentParams[j]] = currentParams[parentParams[j]];
}
}
return extend({}, inherited, newParams);
}
function inherit(parent, extra) {
return extend(new (extend(function () { }, {prototype: parent}))(), extra);
}
function onStateRegistered(callback) { stateRegisteredCallbacks.push(callback); }
mod_core.provider("uirextras_core", function() {
var core = {
internalStates: internalStates,
onStateRegistered: onStateRegistered,
forEach: forEach,
extend: extend,
isArray: isArray,
map: map,
keys: keys,
filter: filter,
filterObj: filterObj,
ancestors: ancestors,
objectKeys: objectKeys,
protoKeys: protoKeys,
arraySearch: arraySearch,
inheritParams: inheritParams,
inherit: inherit
};
angular.extend(this, core);
this.$get = function() {
return core;
};
});
var ignoreDsr;
function resetIgnoreDsr() {
ignoreDsr = undefined;
}
// Decorate $state.transitionTo to gain access to the last transition.options variable.
// This is used to process the options.ignoreDsr option
angular.module('ct.ui.router.extras.dsr', [ 'ct.ui.router.extras.core' ]).config([ "$provide", function ($provide) {
var $state_transitionTo;
$provide.decorator("$state", ['$delegate', '$q', function ($state, $q) {
$state_transitionTo = $state.transitionTo;
$state.transitionTo = function (to, toParams, options) {
if (options.ignoreDsr) {
ignoreDsr = options.ignoreDsr;
}
return $state_transitionTo.apply($state, arguments).then(
function (result) {
resetIgnoreDsr();
return result;
},
function (err) {
resetIgnoreDsr();
return $q.reject(err);
}
);
};
return $state;
}]);
}]);
angular.module('ct.ui.router.extras.dsr').service("$deepStateRedirect", [ '$rootScope', '$state', '$injector', function ($rootScope, $state, $injector) {
var lastSubstate = {};
var deepStateRedirectsByName = {};
var REDIRECT = "Redirect", ANCESTOR_REDIRECT = "AncestorRedirect";
function computeDeepStateStatus(state) {
var name = state.name;
if (deepStateRedirectsByName.hasOwnProperty(name))
return deepStateRedirectsByName[name];
recordDeepStateRedirectStatus(name);
}
function getConfig(state) {
var declaration = state.deepStateRedirect || state.dsr;
if (!declaration) return { dsr: false };
var dsrCfg = { dsr: true };
if (angular.isFunction(declaration)) {
dsrCfg.fn = declaration;
} else if (angular.isObject(declaration)) {
dsrCfg = angular.extend(dsrCfg, declaration);
}
if (angular.isString(dsrCfg['default'])) {
dsrCfg['default'] = { state: dsrCfg['default'] };
}
if (!dsrCfg.fn) {
dsrCfg.fn = [ '$dsr$', function($dsr$) {
return $dsr$.redirect.state != $dsr$.to.state;
} ];
}
return dsrCfg;
}
function recordDeepStateRedirectStatus(stateName) {
var state = $state.get(stateName);
if (!state) return false;
var cfg = getConfig(state);
if (cfg.dsr) {
deepStateRedirectsByName[state.name] = REDIRECT;
if (lastSubstate[stateName] === undefined)
lastSubstate[stateName] = {};
}
var parent = state.$$state && state.$$state().parent;
if (parent) {
var parentStatus = recordDeepStateRedirectStatus(parent.self.name);
if (parentStatus && deepStateRedirectsByName[state.name] === undefined) {
deepStateRedirectsByName[state.name] = ANCESTOR_REDIRECT;
}
}
return deepStateRedirectsByName[state.name] || false;
}
function getMatchParams(params, dsrParams) {
if (dsrParams === true) dsrParams = Object.keys(params);
if (dsrParams === null || dsrParams === undefined) dsrParams = [];
var matchParams = {};
angular.forEach(dsrParams.sort(), function(name) { matchParams[name] = params[name]; });
return matchParams;
}
function getParamsString(params, dsrParams) {
var matchParams = getMatchParams(params, dsrParams);
function safeString(input) { return !input ? input : input.toString(); }
var paramsToString = {};
angular.forEach(matchParams, function(val, name) { paramsToString[name] = safeString(val); });
return angular.toJson(paramsToString);
}
$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
var cfg = getConfig(toState);
if (ignoreDsr || (computeDeepStateStatus(toState) !== REDIRECT) && !cfg['default']) return;
// We're changing directly to one of the redirect (tab) states.
// Get the DSR key for this state by calculating the DSRParams option
var key = getParamsString(toParams, cfg.params);
var redirect = lastSubstate[toState.name][key] || cfg['default'];
if (!redirect) return;
// we have a last substate recorded
var $dsr$ = { redirect: { state: redirect.state, params: redirect.params}, to: { state: toState.name, params: toParams } };
var result = $injector.invoke(cfg.fn, toState, { $dsr$: $dsr$ });
if (!result) return;
if (result.state) redirect = result;
event.preventDefault();
var redirectParams = getMatchParams(toParams, cfg.params);
$state.go(redirect.state, angular.extend(redirectParams, redirect.params));
});
$rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) {
var deepStateStatus = computeDeepStateStatus(toState);
if (deepStateStatus) {
var name = toState.name;
angular.forEach(lastSubstate, function (redirect, dsrState) {
// update Last-SubState&params for each DSR that this transition matches.
var cfg = getConfig($state.get(dsrState));
var key = getParamsString(toParams, cfg.params);
if (toState.$$state().includes[dsrState]) {
lastSubstate[dsrState][key] = { state: name, params: angular.copy(toParams) };
}
});
}
});
return {
getRedirect: function(dsrState, params) {
var state = $state.get(dsrState);
computeDeepStateStatus(state)
var cfg = getConfig(state);
var key = getParamsString(params, cfg.params);
var redirect = lastSubstate[state.name][key] || cfg['default'];
return redirect;
},
reset: function(stateOrName, params) {
if (!stateOrName) {
angular.forEach(lastSubstate, function(redirect, dsrState) { lastSubstate[dsrState] = {}; });
} else {
var state = $state.get(stateOrName);
if (!state) throw new Error("Unknown state: " + stateOrName);
if (lastSubstate[state.name]) {
if (params) {
var key = getParamsString(params, getConfig(state).params);
delete lastSubstate[state.name][key];
} else {
lastSubstate[state.name] = {};
}
}
}
}
};
}]);
angular.module('ct.ui.router.extras.dsr').run(['$deepStateRedirect', function ($deepStateRedirect) {
// Make sure $deepStateRedirect is instantiated
}]);
angular.module("ct.ui.router.extras.sticky", [ 'ct.ui.router.extras.core' ]);
var mod_sticky = angular.module("ct.ui.router.extras.sticky");
$StickyStateProvider.$inject = [ '$stateProvider', 'uirextras_coreProvider' ];
function $StickyStateProvider($stateProvider, uirextras_coreProvider) {
var core = uirextras_coreProvider;
var inheritParams = core.inheritParams;
var objectKeys = core.objectKeys;
var protoKeys = core.protoKeys;
var forEach = core.forEach;
var map = core.map;
// Holds all the states which are inactivated. Inactivated states can be either sticky states, or descendants of sticky states.
var inactiveStates = {}; // state.name -> (state)
var stickyStates = {}; // state.name -> true
var $state;
var DEBUG = false;
// Called by $stateProvider.registerState();
// registers a sticky state with $stickyStateProvider
this.registerStickyState = function (state) {
stickyStates[state.name] = state;
// console.log("Registered sticky state: ", state);
};
this.enableDebug = this.debugMode = function (enabled) {
if (angular.isDefined(enabled))
DEBUG = enabled;
return DEBUG;
};
this.$get = [ '$rootScope', '$state', '$stateParams', '$injector', '$log',
function ($rootScope, $state, $stateParams, $injector, $log) {
// Each inactive states is either a sticky state, or a child of a sticky state.
// This function finds the closest ancestor sticky state, then find that state's parent.
// Map all inactive states to their closest parent-to-sticky state.
function mapInactives() {
var mappedStates = {};
angular.forEach(inactiveStates, function (state, name) {
var stickyAncestors = getStickyStateStack(state);
for (var i = 0; i < stickyAncestors.length; i++) {
var parent = stickyAncestors[i].parent;
mappedStates[parent.name] = mappedStates[parent.name] || [];
mappedStates[parent.name].push(state);
}
if (mappedStates['']) {
// This is necessary to compute Transition.inactives when there are sticky states are children to root state.
mappedStates['__inactives'] = mappedStates['']; // jshint ignore:line
}
});
return mappedStates;
}
function mapInactivesByImmediateParent() {
var inactivesByAllParents ={};
forEach(inactiveStates, function(state) {
forEach(state.path, function(ancestor) {
if (ancestor === state) return;
inactivesByAllParents[ancestor.name] = inactivesByAllParents[ancestor.name] || [];
inactivesByAllParents[ancestor.name].push(state);
});
});
return inactivesByAllParents;
}
// Given a state, returns all ancestor states which are sticky.
// Walks up the view's state's ancestry tree and locates each ancestor state which is marked as sticky.
// Returns an array populated with only those ancestor sticky states.
function getStickyStateStack(state) {
var stack = [];
if (!state) return stack;
do {
if (state.sticky) stack.push(state);
state = state.parent;
} while (state);
stack.reverse();
return stack;
}
// Used by processTransition to determine if what kind of sticky state transition this is.
// returns { from: (bool), to: (bool) }
function getStickyTransitionType(fromPath, toPath, keep) {
if (fromPath[keep] === toPath[keep]) return { from: false, to: false };
var stickyFromState = keep < fromPath.length && fromPath[keep].self.sticky;
var stickyToState = keep < toPath.length && toPath[keep].self.sticky;
return { from: stickyFromState, to: stickyToState };
}
// Returns a sticky transition type necessary to enter the state.
// Transition can be: reactivate, updateStateParams, or enter
// Note: if a state is being reactivated but params dont match, we treat
// it as a Exit/Enter, thus the special "updateStateParams" transition.
// If a parent inactivated state has "updateStateParams" transition type, then
// all descendant states must also be exit/entered, thus the first line of this function.
function getEnterTransition(state, stateParams, reloadStateTree, ancestorParamsChanged) {
if (ancestorParamsChanged) return "updateStateParams";
var inactiveState = inactiveStates[state.self.name];
if (!inactiveState) return "enter";
if (state.self === reloadStateTree) return "updateStateParams";
// if (inactiveState.locals == null || inactiveState.locals.globals == null) debugger;
var paramsMatch = paramsEqualForState(state.ownParams, stateParams, inactiveState.locals.globals.$stateParams);
// if (DEBUG) $log.debug("getEnterTransition: " + state.name + (paramsMatch ? ": reactivate" : ": updateStateParams"));
return paramsMatch ? "reactivate" : "updateStateParams";
}
// Given a state and (optional) stateParams, returns the inactivated state from the inactive sticky state registry.
function getInactivatedState(state, stateParams) {
var inactiveState = inactiveStates[state.name];
if (!inactiveState) return null;
if (!stateParams) return inactiveState;
var paramsMatch = paramsEqualForState(state.ownParams, stateParams, inactiveState.locals.globals.$stateParams);
return paramsMatch ? inactiveState : null;
}
function paramsEqualForState(ownParams, stateParams, stateParams2) {
if (typeof ownParams.$$equals === 'function')
return paramsMatch = ownParams.$$equals(stateParams, stateParams2);
return equalForKeys(stateParams, stateParams2, ownParams);
}
// Duplicates logic in $state.transitionTo, primarily to find the pivot state (i.e., the "keep" value)
function equalForKeys(a, b, keys) {
if (!angular.isArray(keys) && angular.isObject(keys)) {
keys = protoKeys(keys, ["$$keys", "$$values", "$$equals", "$$validates", "$$new", "$$parent"]);
}
if (!keys) {
keys = [];
for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility
}
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;
}
var stickySupport = {
getInactiveStates: function () {
var states = [];
angular.forEach(inactiveStates, function (state) {
states.push(state);
});
return states;
},
getInactiveStatesByParent: function () {
return mapInactives();
},
// Main API for $stickyState, used by $state.
// Processes a potential transition, returns an object with the following attributes:
// {
// keep: The number of states being "kept"
// inactives: Array of all states which will be inactive if the transition is completed.
// reactivatingStates: Array of all states which will be reactivated if the transition is completed.
// inactiveOrphans: Array of inactive states being orphaned by the transition
// Note: Transitioning directly to an inactive state with inactive children will reactivate the state, but exit all the inactive children.
// enter: Enter transition type for all added states. This is a sticky array to "toStates" array in $state.transitionTo.
// exit: Exit transition type for all removed states. This is a sticky array to "fromStates" array in $state.transitionTo.
// }
processTransition: function (transition) {
// This object is returned
var result = { inactives: [], enter: [], exit: [], keep: 0 };
var fromPath = transition.fromState.path,
fromParams = transition.fromParams,
toPath = transition.toState.path,
toParams = transition.toParams,
reloadStateTree = transition.reloadStateTree,
options = transition.options;
var keep = 0, state = toPath[keep];
if (options.inherit) {
toParams = inheritParams($stateParams, toParams || {}, $state.$current, transition.toState);
}
while (state && state === fromPath[keep] && paramsEqualForState(state.ownParams, toParams, fromParams)) {
// We're "keeping" this state. bump keep var and get the next state in toPath for the next iteration.
state = toPath[++keep];
}
result.keep = keep;
var idx, deepestUpdatedParams, noLongerInactiveStates = {}, pType = getStickyTransitionType(fromPath, toPath, keep);
var ancestorUpdated = !!options.reload; // When ancestor params change, treat reactivation as exit/enter
var inactives = [], reactivatingStates = [], enteringStates = [], exitingStates = [];
// Calculate the "exit" transition for states not "kept", in fromPath.
// Exit transition can be one of:
// exit: standard state exit logic
// inactivate: register state as an inactive state
for (idx = keep; idx < fromPath.length; idx++) {
if (pType.from) {
// State is being inactivated, note this in result.inactives array
result.inactives.push(fromPath[idx]);
inactives.push(fromPath[idx]);
result.exit[idx] = "inactivate";
} else {
exitingStates.push(fromPath[idx]);
result.exit[idx] = "exit";
}
}
// Calculate the "enter" transitions for new states in toPath
// Enter transitions will be either "enter", "reactivate", or "updateStateParams" where
// enter: full resolve, no special logic
// reactivate: use previous locals
// updateStateParams: like 'enter', except exit the inactive state before entering it.
for (idx = keep; idx < toPath.length; idx++) {
var enterTrans = !pType.to ? "enter" : getEnterTransition(toPath[idx], toParams, reloadStateTree, ancestorUpdated);
ancestorUpdated = (ancestorUpdated || enterTrans == 'updateStateParams');
result.enter[idx] = enterTrans;
// If we're reactivating a state, make a note of it, so we can remove that state from the "inactive" list
if (enterTrans == 'reactivate') {
reactivatingStates.push(toPath[idx]);
noLongerInactiveStates[toPath[idx].name] = toPath[idx];
} else if (enterTrans == 'updateStateParams') {
deepestUpdatedParams = noLongerInactiveStates[toPath[idx].name] = toPath[idx];
}
enteringStates.push(toPath[idx]);
}
// Get the currently inactive states (before the transition is processed), mapped by parent state
var inactivesByAllParents = mapInactivesByImmediateParent();
var allInactives = map(inactiveStates, angular.identity);
function flattenReduce(memo, list) { return memo.concat(list); }
function uniqReduce(memo, orphan) { if (memo.indexOf(orphan) === -1) memo.push(orphan); return memo; }
function isChildOrSiblingOf(state) {
return function(other) { return other.parent === state || other.parent === state.parent; };
}
function isDefined(obj) { return obj != null; }
function notEntered(state) { return enteringStates.indexOf(state) === -1; }
function notSticky(state) { return !state.sticky; }
function inactiveDescendents(state) { return inactivesByAllParents[state.name] || []; }
// Find all the "orphaned" states: those states that are currently inactive, but should now be exited.
//
// Given:
// - states A (sticky: true), B, A.foo, A.bar
// - A.foo is currently inactive
// - B is currently active
// Orphan case 1)
// - Transition to A.bar orphans the inactive state A.foo; it should be exited
// Orphan case 2)
// - Transition directly to A orphans the inactive state A.foo; it should be exited
var orphanedParents = enteringStates
// For each entering state in the path, find any child or sibling states which are currently inactive
.map(function (entering) { return allInactives.filter(isChildOrSiblingOf(entering)); })
// Flatten nested arrays. Now we have an array of inactive states that are children of the ones being entered.
.reduce(flattenReduce, [])
.reduce(uniqReduce, [])
// Consider "orphaned": only those children that are themselves not currently being entered
.filter(notEntered)
// Consider "orphaned": only those children that are not themselves sticky states.
.filter(notSticky);
// orphanedParents may be root nodes of larger inactive state trees; the whole tree is orphaned.
// For each parent, find any orphaned children
var orphanedDescendents = orphanedParents.map(inactiveDescendents)
.reduce(flattenReduce, [])
.reduce(uniqReduce, []); // remove any dupes (necessary? not necessary? dunno)
var orphans = orphanedParents.concat(orphanedDescendents).reduce(uniqReduce, []);
// Add them to the list of states being exited.
exitingStates = exitingStates.concat(orphans);
// Now calculate the states that will be inactive if this transition succeeds.
// We have already pushed the transitionType == "inactivate" states to 'inactives'.
// Second, add all the existing inactive states
inactives = inactives.concat(map(inactiveStates, angular.identity));
// Finally, remove any states that are scheduled for "exit" or "enter", "reactivate", or "updateStateParams"
inactives = inactives.filter(function(state) {
return exitingStates.indexOf(state) === -1 && enteringStates.indexOf(state) === -1;
});
result.inactives = inactives;
result.reactivatingStates = reactivatingStates;
result.inactiveOrphans = orphans;
return result;
},
// Adds a state to the inactivated sticky state registry.
stateInactivated: function (state) {
// Keep locals around.
inactiveStates[state.self.name] = state;
// Notify states they are being Inactivated (i.e., a different
// sticky state tree is now active).
state.self.status = 'inactive';
if (state.self.onInactivate)
$injector.invoke(state.self.onInactivate, state.self, state.locals.globals);
},
// Removes a previously inactivated state from the inactive sticky state registry
stateReactivated: function (state) {
if (inactiveStates[state.self.name]) {
delete inactiveStates[state.self.name];
}
state.self.status = 'entered';
// if (state.locals == null || state.locals.globals == null) debugger;
if (state.self.onReactivate)
$injector.invoke(state.self.onReactivate, state.self, state.locals.globals);
},
// Exits all inactivated descendant substates when the ancestor state is exited.
// When transitionTo is exiting a state, this function is called with the state being exited. It checks the
// registry of inactivated states for descendants of the exited state and also exits those descendants. It then
// removes the locals and de-registers the state from the inactivated registry.
stateExiting: function (exiting, exitQueue, onExit) {
var exitingNames = {};
angular.forEach(exitQueue, function (state) {
exitingNames[state.self.name] = true;
});
angular.forEach(inactiveStates, function (inactiveExiting, name) {
// TODO: Might need to run the inactivations in the proper depth-first order?
if (!exitingNames[name] && inactiveExiting.includes[exiting.name]) {
if (DEBUG) $log.debug("Exiting " + name + " because it's a substate of " + exiting.name + " and wasn't found in ", exitingNames);
if (inactiveExiting.self.onExit)
$injector.invoke(inactiveExiting.self.onExit, inactiveExiting.self, inactiveExiting.locals.globals);
angular.forEach(inactiveExiting.locals, function(localval, key) {
delete inactivePseudoState.locals[key];
});
inactiveExiting.locals = null;
inactiveExiting.self.status = 'exited';
delete inactiveStates[name];
}
});
if (onExit)
$injector.invoke(onExit, exiting.self, exiting.locals.globals);
exiting.locals = null;
exiting.self.status = 'exited';
delete inactiveStates[exiting.self.name];
},
// Removes a previously inactivated state from the inactive sticky state registry
stateEntering: function (entering, params, onEnter, updateParams) {
var inactivatedState = getInactivatedState(entering);
if (inactivatedState && (updateParams || !getInactivatedState(entering, params))) {
var savedLocals = entering.locals;
this.stateExiting(inactivatedState);
entering.locals = savedLocals;
}
entering.self.status = 'entered';
if (onEnter)
$injector.invoke(onEnter, entering.self, entering.locals.globals);
},
reset: function reset(inactiveState, params) {
function resetOne(state) { stickySupport.reset(state); }
if (inactiveState === "*") {
angular.forEach(stickySupport.getInactiveStates(), resetOne);
return true;
}
var state = $state.get(inactiveState);
if (!state) return false;
var exiting = getInactivatedState(state, params);
if (!exiting) return false;
stickySupport.stateExiting(exiting);
$rootScope.$broadcast("$viewContentLoading");
return true;
}
};
return stickySupport;
}];
}
mod_sticky.provider("$stickyState", $StickyStateProvider);
/**
* Sticky States makes entire state trees "sticky". Sticky state trees are retained until their parent state is
* exited. This can be useful to allow multiple modules, peers to each other, each module having its own independent
* state tree. The peer modules can be activated and inactivated without any loss of their internal context, including
* DOM content such as unvalidated/partially filled in forms, and even scroll position.
*
* DOM content is retained by declaring a named ui-view in the parent state, and filling it in with a named view from the
* sticky state.
*
* Technical overview:
*
* ---PATHS---
* UI-Router uses state paths to manage entering and exiting of individual states. Each state "A.B.C.X" has its own path, starting
* from the root state ("") and ending at the state "X". The path is composed the final state "X"'s ancestors, e.g.,
* [ "", "A", "B", "C", "X" ].
*
* When a transition is processed, the previous path (fromState.path) is compared with the requested destination path
* (toState.path). All states that the from and to paths have in common are "kept" during the transition. The last
* "kept" element in the path is the "pivot".
*
* ---VIEWS---
* A View in UI-Router consists of a controller and a template. Each view belongs to one state, and a state can have many
* views. Each view plugs into a ui-view element in the DOM of one of the parent state's view(s).
*
* View context is managed in UI-Router using a 'state locals' concept. When a state's views are fully loaded, those views
* are placed on the states 'locals' object. Each locals object prototypally inherits from its parent state's locals object.
* This means that state "A.B.C.X"'s locals object also has all of state "A.B.C"'s locals as well as those from "A.B" and "A".
* The root state ("") defines no views, but it is included in the protypal inheritance chain.
*
* The locals object is used by the ui-view directive to load the template, render the content, create the child scope,
* initialize the controller, etc. The ui-view directives caches the locals in a closure variable. If the locals are
* identical (===), then the ui-view directive exits early, and does no rendering.
*
* In stock UI-Router, when a state is exited, that state's locals object is deleted and those views are cleaned up by
* the ui-view directive shortly.
*
* ---Sticky States---
* UI-Router Extras keeps views for inactive states live, even when UI-Router thinks it has exited them. It does this
* by creating a pseudo state called "__inactives" that is the parent of the root state. It also then defines a locals
* object on the "__inactives" state, which the root state protoypally inherits from. By doing this, views for inactive
* states are accessible through locals object's protoypal inheritance chain from any state in the system.
*
* ---Transitions---
* UI-Router Extras decorates the $state.transitionTo function. While a transition is in progress, the toState and
* fromState internal state representations are modified in order to coerce stock UI-Router's transitionTo() into performing
* the appropriate operations. When the transition promise is completed, the original toState and fromState values are
* restored.
*
* Stock UI-Router's $state.transitionTo function uses toState.path and fromState.path to manage entering and exiting
* states. UI-Router Extras takes advantage of those internal implementation details and prepares a toState.path and
* fromState.path which coerces UI-Router into entering and exiting the correct states, or more importantly, not entering
* and not exiting inactive or sticky states. It also replaces state.self.onEnter and state.self.onExit for elements in
* the paths when they are being inactivated or reactivated.
*/
// ------------------------ Sticky State module-level variables -----------------------------------------------
var _StickyState; // internal reference to $stickyStateProvider
var internalStates = {}; // Map { statename -> InternalStateObj } holds internal representation of all states
var root, // Root state, internal representation
pendingTransitions = [], // One transition may supersede another. This holds references to all pending transitions
pendingRestore, // The restore function from the superseded transition
inactivePseudoState, // This pseudo state holds all the inactive states' locals (resolved state data, such as views etc)
reactivatingLocals = { }, // This is a prent locals to the inactivePseudoState locals, used to hold locals for states being reactivated
versionHeuristics = { // Heuristics used to guess the current UI-Router Version
hasParamSet: false
};
// Creates a blank surrogate state
function SurrogateState(type) {
return {
resolve: { },
locals: {
globals: root && root.locals && root.locals.globals
},
views: { },
self: { },
params: { },
ownParams: ( versionHeuristics.hasParamSet ? { $$equals: function() { return true; } } : []),
surrogateType: type
};
}
// ------------------------ Sticky State registration and initialization code ----------------------------------
// Grab a copy of the $stickyState service for use by the transition management code
angular.module("ct.ui.router.extras.sticky").run(["$stickyState", function ($stickyState) {
_StickyState = $stickyState;
}]);
angular.module("ct.ui.router.extras.sticky").config(
[ "$provide", "$stateProvider", '$stickyStateProvider', '$urlMatcherFactoryProvider', 'uirextras_coreProvider',
function ($provide, $stateProvider, $stickyStateProvider, $urlMatcherFactoryProvider, uirextras_coreProvider) {
var core = uirextras_coreProvider;
var internalStates = core.internalStates;
var inherit = core.inherit;
var inheritParams = core.inheritParams;
var forEach = core.forEach;
var map = core.map;
var filterObj = core.filterObj;
versionHeuristics.hasParamSet = !!$urlMatcherFactoryProvider.ParamSet;
// inactivePseudoState (__inactives) holds all the inactive locals which includes resolved states data, i.e., views, scope, etc
inactivePseudoState = angular.extend(new SurrogateState("__inactives"), { self: { name: '__inactives' } });
// Reset other module scoped variables. This is to primarily to flush any previous state during karma runs.
root = pendingRestore = undefined;
pendingTransitions = [];
uirextras_coreProvider.onStateRegistered(function(state) {
// Register the ones marked as "sticky"
if (state.self.sticky === true) {
$stickyStateProvider.registerStickyState(state.self);
}
});
var $state_transitionTo; // internal reference to the real $state.transitionTo function
// Decorate the $state service, so we can decorate the $state.transitionTo() function with sticky state stuff.
$provide.decorator("$state", ['$delegate', '$log', '$q', function ($state, $log, $q) {
// Note: this code gets run only on the first state that is decorated
root = $state.$current;
internalStates[""] = root;
root.parent = inactivePseudoState; // Make inactivePsuedoState the parent of root. "wat"
inactivePseudoState.parent = undefined; // Make inactivePsuedoState the real root.
// Add another locals bucket, as a parent to inactivatePseudoState locals.
// This is for temporary storage of locals of states being reactivated while a transition is pending
// This is necessary in some cases where $viewContentLoading is triggered before the $state.$current is updated to the toState.
inactivePseudoState.locals = inherit(reactivatingLocals, inactivePseudoState.locals);
root.locals = inherit(inactivePseudoState.locals, root.locals); // make root locals extend the __inactives locals.
delete inactivePseudoState.locals.globals;
// Hold on to the real $state.transitionTo in a module-scope variable.
$state_transitionTo = $state.transitionTo;
// ------------------------ Decorated transitionTo implementation begins here ---------------------------
$state.transitionTo = function (to, toParams, options) {
var DEBUG = $stickyStateProvider.debugMode();
// TODO: Move this to module.run?
// TODO: I'd rather have root.locals prototypally inherit from inactivePseudoState.locals
// Link root.locals and inactives.locals. Do this at runtime, after root.locals has been set.
if (!inactivePseudoState.locals)
inactivePseudoState.locals = root.locals;
var idx = pendingTransitions.length;
if (pendingRestore) {
pendingRestore();
if (DEBUG) {
$log.debug("Restored paths from pending transition");
}
}
var fromState = $state.$current, fromParams = $state.params;
var rel = options && options.relative || $state.$current; // Not sure if/when $state.$current is appropriate here.
var toStateSelf = $state.get(to, rel); // exposes findState relative path functionality, returns state.self
var savedToStatePath, savedFromStatePath, stickyTransitions;
var reactivated = [], exited = [], terminalReactivatedState;
toParams = toParams || {};
arguments[1] = toParams;
var noop = function () {
};
// Sticky states works by modifying the internal state objects of toState and fromState, especially their .path(s).
// The restore() function is a closure scoped function that restores those states' definitions to their original values.
var restore = function () {
if (savedToStatePath) {
toState.path = savedToStatePath;
savedToStatePath = null;
}
if (savedFromStatePath) {
fromState.path = savedFromStatePath;
savedFromStatePath = null;
}
angular.forEach(restore.restoreFunctions, function (restoreFunction) {
restoreFunction();
});
// Restore is done, now set the restore function to noop in case it gets called again.
restore = noop;
// pendingRestore keeps track of a transition that is in progress. It allows the decorated transitionTo
// method to be re-entrant (for example, when superceding a transition, i.e., redirect). The decorated
// transitionTo checks right away if there is a pending transition in progress and restores the paths
// if so using pendingRestore.
pendingRestore = null;
pendingTransitions.splice(idx, 1); // Remove this transition from the list
};
// All decorated transitions have their toState.path and fromState.path replaced. Surrogate states also make
// additional changes to the states definition before handing the transition off to UI-Router. In particular,
// certain types of surrogate states modify the state.self object's onEnter or onExit callbacks.
// Those surrogate states must then register additional restore steps using restore.addRestoreFunction(fn)
restore.restoreFunctions = [];
restore.addRestoreFunction = function addRestoreFunction(fn) {
this.restoreFunctions.push(fn);
};
// --------------------- Surrogate State Functions ------------------------
// During a transition, the .path arrays in toState and fromState are replaced. Individual path elements
// (states) which aren't being "kept" are replaced with surrogate elements (states). This section of the code
// has factory functions for all the different types of surrogate states.
function stateReactivatedSurrogatePhase1(state) {
var surrogate = angular.extend(new SurrogateState("reactivate_phase1"), { locals: state.locals });
surrogate.self = angular.extend({}, state.self);
return surrogate;
}
function stateReactivatedSurrogatePhase2(state) {
var surrogate = angular.extend(new SurrogateState("reactivate_phase2"), state);
var oldOnEnter = surrogate.self.onEnter;
surrogate.resolve = {}; // Don't re-resolve when reactivating states (fixes issue #22)
// TODO: Not 100% sure if this is necessary. I think resolveState will load the views if I don't do this.
surrogate.views = {}; // Don't re-activate controllers when reactivating states (fixes issue #22)
surrogate.self.onEnter = function () {
// ui-router sets locals on the surrogate to a blank locals (because we gave it nothing to resolve)
// Re-set it back to the already loaded state.locals here.
surrogate.locals = state.locals;
_StickyState.stateReactivated(state);
};
restore.addRestoreFunction(function () {
state.self.onEnter = oldOnEnter;
});
return surrogate;
}
function stateInactivatedSurrogate(state) {
var surrogate = new SurrogateState("inactivate");
surrogate.self = state.self;
var oldOnExit = state.self.onExit;
surrogate.self.onExit = function () {
_StickyState.stateInactivated(state);
};
restore.addRestoreFunction(function () {
state.self.onExit = oldOnExit;
});
return surrogate;
}
function stateEnteredSurrogate(state, toParams) {
var oldOnEnter = state.self.onEnter;
state.self.onEnter = function () {
_StickyState.stateEntering(state, toParams, oldOnEnter);
};
restore.addRestoreFunction(function () {
state.self.onEnter = oldOnEnter;
});
return state;
}
// TODO: This may be completely unnecessary now that we're using $$uirouterextrasreload temp param
function stateUpdateParamsSurrogate(state, toParams) {
var oldOnEnter = state.self.onEnter;
state.self.onEnter = function () {
_StickyState.stateEntering(state, toParams, oldOnEnter, true);
};
restore.addRestoreFunction(function () {
state.self.onEnter = oldOnEnter;
});
return state;
}
function stateExitedSurrogate(state) {
var oldOnExit = state.self.onExit;
state.self.onExit = function () {
_StickyState.stateExiting(state, exited, oldOnExit);
};
restore.addRestoreFunction(function () {
state.self.onExit = oldOnExit;
});
return state;
}
// --------------------- decorated .transitionTo() logic starts here ------------------------
if (toStateSelf) {
var toState = internalStates[toStateSelf.name]; // have the state, now grab the internal state representation
if (toState) {
// Save the toState and fromState paths to be restored using restore()
savedToStatePath = toState.path;
savedFromStatePath = fromState.path;
// Try to resolve options.reload to a state. If so, we'll reload only up to the given state.
var reload = options && options.reload || false;
var reloadStateTree = reload && (reload === true ? savedToStatePath[0].self : $state.get(reload, rel));
// If options.reload is a string or a state, we want to handle reload ourselves and not
// let ui-router reload the entire toPath.
if (options && reload && reload !== true)
delete options.reload;
var currentTransition = {
toState: toState,
toParams: toParams || {},
fromState: fromState,
fromParams: fromParams || {},
options: options,
reloadStateTree: reloadStateTree
};
pendingTransitions.push(currentTransition); // TODO: See if a list of pending transitions is necessary.
pendingRestore = restore;
// If we're reloading from a state and below, temporarily add a param to the top of the state tree
// being reloaded, and add a param value to the transition. This will cause the "has params changed
// for state" check to return true, and the states will be reloaded.
if (reloadStateTree) {
currentTransition.toParams.$$uirouterextrasreload = Math.random();
var params = reloadStateTree.$$state().params;
var ownParams = reloadStateTree.$$state().ownParams;
if (versionHeuristics.hasParamSet) {
var tempParam = new $urlMatcherFactoryProvider.Param('$$uirouterextrasreload');
params.$$uirouterextrasreload = ownParams.$$uirouterextrasreload = tempParam;
restore.restoreFunctions.push(function() {
delete params.$$uirouterextrasreload;
delete ownParams.$$uirouterextrasreload;
});
} else {
params.push('$$uirouterextrasreload');
ownParams.push('$$uirouterextrasreload');
restore.restoreFunctions.push(function() {
params.length = params.length -1;
ownParams.length = ownParams.length -1;
});
}
}
// $StickyStateProvider.processTransition analyzes the states involved in the pending transition. It
// returns an object that tells us:
// 1) if we're involved in a sticky-type transition
// 2) what types of exit transitions will occur for each "exited" path element
// 3) what types of enter transitions will occur for each "entered" path element
// 4) which states will be inactive if the transition succeeds.
stickyTransitions = _StickyState.processTransition(currentTransition);
if (DEBUG) debugTransition($log, currentTransition, stickyTransitions);
// Begin processing of surrogate to and from paths.
var surrogateToPath = toState.path.slice(0, stickyTransitions.keep);
var surrogateFromPath = fromState.path.slice(0, stickyTransitions.keep);
// Clear out and reload inactivePseudoState.locals each time transitionTo is called
angular.forEach(inactivePseudoState.locals, function (local, name) {
if (name.indexOf("@") != -1) delete inactivePseudoState.locals[name];
});
var saveViewsToLocals = function (targetObj) {
return function(view, name) {
if (name.indexOf("@") !== -1) { // Only grab this state's "view" locals
targetObj[name] = view; // Add all inactive views not already included.
}
}
};
// For each state that will be inactive when the transition is complete, place its view-locals on the
// __inactives pseudostate's .locals. This allows the ui-view directive to access them and
// render the inactive views.
forEach(stickyTransitions.inactives, function(state) {
forEach(state.locals, saveViewsToLocals(inactivePseudoState.locals))
});
// For each state that will be reactivated during the transition, place its view-locals on a separate
// locals object (prototypal parent of __inactives.locals, and remove them when the transition is complete.
// This is necessary when we a transition will reactivate one state, but enter a second.
// Gory details:
// - the entering of a new state causes $view.load() to fire $viewContentLoading while the transition is
// still in process
// - all ui-view(s) check if they should re-render themselves in response to this event.
// - ui-view checks if previousLocals is equal to currentLocals
// - it uses $state.$current.locals[myViewName] for previousLocals
// - Because the transition is not completed, $state.$current is set to the from state, and
// the ui-view for a reactivated state cannot find its previous locals.
forEach(stickyTransitions.reactivatingStates, function(state) {
forEach(state.locals, saveViewsToLocals(reactivatingLocals));
});
// When the transition is complete, remove the copies of the view locals from reactivatingLocals.
restore.addRestoreFunction(function clearReactivatingLocals() {
forEach(reactivatingLocals, function (val, viewname) {
delete reactivatingLocals[viewname];
})
});
// Find all the states the transition will be entering. For each entered state, check entered-state-transition-type
// Depending on the entered-state transition type, place the proper surrogate state on the surrogate toPath.
angular.forEach(stickyTransitions.enter, function (value, idx) {
var surrogate;
var enteringState = toState.path[idx];
if (value === "reactivate") {
// Reactivated states require TWO surrogates. The "phase 1 reactivated surrogates" are added to both
// to.path and from.path, and as such, are considered to be "kept" by UI-Router.
// This is required to get UI-Router to add the surrogate locals to the protoypal locals object
surrogate = stateReactivatedSurrogatePhase1(enteringState);
surrogateToPath.push(surrogate);
surrogateFromPath.push(surrogate); // so toPath[i] === fromPath[i]
// The "phase 2 reactivated surrogate" is added to the END of the .path, after all the phase 1
// surrogates have been added.
reactivated.push(stateReactivatedSurrogatePhase2(enteringState));
terminalReactivatedState = enteringState;
} else if (value === "updateStateParams") {
// If the state params have been changed, we need to exit any inactive states and re-enter them.
surrogateToPath.push(stateUpdateParamsSurrogate(enteringState));
terminalReactivatedState = enteringState;
} else if (value === "enter") {
// Standard enter transition. We still wrap it in a surrogate.
surrogateToPath.push(stateEnteredSurrogate(enteringState));
}
});
// Find all the states the transition will be exiting. For each exited state, check the exited-state-transition-type.
// Depending on the exited-state transition type, place a surrogate state on the surrogate fromPath.
angular.forEach(stickyTransitions.exit, function (value, idx) {
var exiting = fromState.path[idx];
if (value === "inactivate") {
surrogateFromPath.push(stateInactivatedSurrogate(exiting));
exited.push(exiting);
} else if (value === "exit") {
surrogateFromPath.push(stateExitedSurrogate(exiting));
exited.push(exiting);
}
});
// Add surrogate states for reactivated to ToPath again (phase 2), this time without a matching FromPath entry
// This is to get ui-router to call the surrogate's onEnter callback.
if (reactivated.length) {
angular.forEach(reactivated, function (surrogate) {
surrogateToPath.push(surrogate);
});
}
// We may transition directly to an inactivated state, reactivating it. In this case, we should
// exit all of that state's inactivated children.
var inactiveOrphans = stickyTransitions.inactiveOrphans;
// Add surrogate exited states for all orphaned descendants of the Deepest Reactivated State
surrogateFromPath = surrogateFromPath.concat(map(stickyTransitions.inactiveOrphans, function (exiting) {
return stateExitedSurrogate(exiting);
}));
exited = exited.concat(inactiveOrphans);
// Replace the .path variables. toState.path and fromState.path are now ready for a sticky transition.
fromState.path = surrogateFromPath;
toState.path = surrogateToPath;
var pathMessage = function (state) {
return (state.surrogateType ? state.surrogateType + ":" : "") + state.self.name;
};
if (DEBUG) $log.debug("SurrogateFromPath: ", map(surrogateFromPath, pathMessage));
if (DEBUG) $log.debug("SurrogateToPath: ", map(surrogateToPath, pathMessage));
}
}
// toState and fromState are all set up; now run stock UI-Router's $state.transitionTo().
var transitionPromise = $state_transitionTo.apply($state, arguments);
// Add post-transition promise handlers, then return the promise to the original caller.
return transitionPromise.then(function transitionSuccess(state) {
// First, restore toState and fromState to their original values.
restore();
if (DEBUG) debugViewsAfterSuccess($log, internalStates[state.name], $state);
state.status = 'active'; // TODO: This status is used in statevis.js, and almost certainly belongs elsewhere.
return state;
}, function transitionFailed(err) {
restore();
if (DEBUG &&
err.message !== "transition prevented" &&
err.message !== "transition aborted" &&
err.message !== "transition superseded") {
$log.debug("transition failed", err);
$log.debug(err.stack);
}
return $q.reject(err);
});
};
return $state;
}]);
function debugTransition($log, currentTransition, stickyTransition) {
function message(path, index, state) {
return (path[index] ? path[index].toUpperCase() + ": " + state.self.name : "(" + state.self.name + ")");
}
var inactiveLogVar = map(stickyTransition.inactives, function (state) {
return state.self.name;
});
var enterLogVar = map(currentTransition.toState.path, function (state, index) {
return message(stickyTransition.enter, index, state);
});
var exitLogVar = map(currentTransition.fromState.path, function (state, index) {
return message(stickyTransition.exit, index, state);
});
var transitionMessage = currentTransition.fromState.self.name + ": " +
angular.toJson(currentTransition.fromParams) + ": " +
" -> " +
currentTransition.toState.self.name + ": " +
angular.toJson(currentTransition.toParams);
$log.debug(" Current transition: ", transitionMessage);
$log.debug("Before transition, inactives are: : ", map(_StickyState.getInactiveStates(), function (s) {
return s.self.name;
}));
$log.debug("After transition, inactives will be: ", inactiveLogVar);
$log.debug("Transition will exit: ", exitLogVar);
$log.debug("Transition will enter: ", enterLogVar);
}
function debugViewsAfterSuccess($log, currentState, $state) {
$log.debug("Current state: " + currentState.self.name + ", inactive states: ", map(_StickyState.getInactiveStates(), function (s) {
return s.self.name;
}));
var viewMsg = function (local, name) {
return "'" + name + "' (" + local.$$state.name + ")";
};
var statesOnly = function (local, name) {
return name != 'globals' && name != 'resolve';
};
var viewsForState = function (state) {
var views = map(filterObj(state.locals, statesOnly), viewMsg).join(", ");
return "(" + (state.self.name ? state.self.name : "root") + ".locals" + (views.length ? ": " + views : "") + ")";
};
var message = viewsForState(currentState);
var parent = currentState.parent;
while (parent && parent !== currentState) {
if (parent.self.name === "") {
// Show the __inactives before showing root state.
message = viewsForState($state.$current.path[0]) + " / " + message;
}
message = viewsForState(parent) + " / " + message;
currentState = parent;
parent = currentState.parent;
}
$log.debug("Views: " + message);
}
}
]
);
(function(angular, undefined) {
var app = angular.module('ct.ui.router.extras.future', [ 'ct.ui.router.extras.core' ]);
_futureStateProvider.$inject = [ '$stateProvider', '$urlRouterProvider', '$urlMatcherFactoryProvider', 'uirextras_coreProvider' ];
function _futureStateProvider($stateProvider, $urlRouterProvider, $urlMatcherFactory, uirextras_coreProvider) {
var core = uirextras_coreProvider;
var internalStates = core.internalStates;
var stateFactories = {}, futureStates = {};
var lazyloadInProgress = false, resolveFunctions = [], initPromise, initDone = false;
var provider = this;
// This function registers a promiseFn, to be resolved before the url/state matching code
// will reject a route. The promiseFn is injected/executed using the runtime $injector.
// The function should return a promise.
// When all registered promises are resolved, then the route is re-sync'ed.
// Example: function($http) {
// return $http.get('//server.com/api/DynamicFutureStates').then(function(data) {
// angular.forEach(data.futureStates, function(fstate) { $futureStateProvider.futureState(fstate); });
// };
// }
this.addResolve = function (promiseFn) {
resolveFunctions.push(promiseFn);
};
// Register a state factory function for a particular future-state type. This factory, given a future-state object,
// should create a ui-router state.
// The factory function is injected/executed using the runtime $injector. The future-state is injected as 'futureState'.
// Example:
// $futureStateProvider.stateFactory('test', function(futureState) {
// return {
// name: futureState.stateName,
// url: futureState.urlFragment,
// template: '<h3>Future State Template</h3>',
// controller: function() {
// console.log("Entered state " + futureState.stateName);
// }
// }
// });
this.stateFactory = function (futureStateType, factory) {
stateFactories[futureStateType] = factory;
};
this.futureState = function (futureState) {
if (futureState.stateName) // backwards compat for now
futureState.name = futureState.stateName;
if (futureState.urlPrefix) // backwards compat for now
futureState.url = "^" + futureState.urlPrefix;
futureStates[futureState.name] = futureState;
var parentMatcher, parentName = futureState.name.split(/\./).slice(0, -1).join("."),
realParent = findState(futureState.parent || parentName);
if (realParent) {
parentMatcher = realParent.url || realParent.navigable && realParent.navigable.url;
} else if (parentName === "") {
parentMatcher = $urlMatcherFactory.compile("");
} else {
var futureParent = findState((futureState.parent || parentName), true);
if (!futureParent) throw new Error("Couldn't determine parent state of future state. FutureState:" + angular.toJson(futureState));
var pattern = futureParent.urlMatcher.source.replace(/\*rest$/, "");
parentMatcher = $urlMatcherFactory.compile(pattern);
futureState.parentFutureState = futureParent;
}
if (futureState.url) {
futureState.urlMatcher = futureState.url.charAt(0) === "^" ?
$urlMatcherFactory.compile(futureState.url.substring(1) + "*rest") :
parentMatcher.concat(futureState.url + "*rest");
}
};
this.get = function () {
return angular.extend({}, futureStates);
};
function findState(stateOrName, findFutureState) {
var statename = angular.isObject(stateOrName) ? stateOrName.name : stateOrName;
return !findFutureState ? internalStates[statename] : futureStates[statename];
}
/* options is an object with at least a name or url attribute */
function findFutureState($state, options) {
if (options.name) {
var nameComponents = options.name.split(/\./);
if (options.name.charAt(0) === '.')
nameComponents[0] = $state.current.name;
while (nameComponents.length) {
var stateName = nameComponents.join(".");
if ($state.get(stateName, { relative: $state.current }))
return null; // State is already defined; nothing to do
if (futureStates[stateName])
return futureStates[stateName];
nameComponents.pop();
}
}
if (options.url) {
var matches = [];
for(var future in futureStates) {
var matcher = futureStates[future].urlMatcher;
if (matcher && matcher.exec(options.url)) {
matches.push(futureStates[future]);
}
}
// Find most specific by ignoring matching parents from matches
var copy = matches.slice(0);
for (var i = matches.length - 1; i >= 0; i--) {
for (var j = 0; j < copy.length; j++) {
if (matches[i] === copy[j].parentFutureState) matches.splice(i, 1);
}
}
return matches[0];
}
}
function lazyLoadState($injector, futureState) {
lazyloadInProgress = true;
var $q = $injector.get("$q");
if (!futureState) {
var deferred = $q.defer();
deferred.reject("No lazyState passed in " + futureState);
return deferred.promise;
}
var parentPromises = $q.when([]), parentFuture = futureState.parentFutureState;
if (parentFuture && futureStates[parentFuture.name]) {
parentPromises = lazyLoadState($injector, futureStates[parentFuture.name]);
}
var type = futureState.type;
var factory = stateFactories[type];
if (!factory) throw Error("No state factory for futureState.type: " + (futureState && futureState.type));
var failedLoadPolicy = factory.$options && factory.$options.failedLazyLoadPolicy || "remove";
function deregisterFutureState() { delete(futureStates[futureState.name]); }
function errorHandler(err) {
if (failedLoadPolicy === "remove") deregisterFutureState();
return $q.reject(err);
}
return parentPromises.then(function(array) {
var factoryPromise = $injector.invoke(factory, factory, { futureState: futureState });
return factoryPromise.then(function(fullState) {
deregisterFutureState(); // Success; remove future state
if (fullState) { array.push(fullState); } // Pass a chain of realized states back
return array;
});
}).catch(errorHandler)
}
var otherwiseFunc = [ '$log', '$location',
function otherwiseFunc($log, $location) {
//$log.debug("Unable to map " + $location.path());
}];
function futureState_otherwise($injector, $location) {
var resyncing = false;
var lazyLoadMissingState =
['$rootScope', '$urlRouter', '$state',
function lazyLoadMissingState($rootScope, $urlRouter, $state) {
function resync() {
resyncing = true; $urlRouter.sync(); resyncing = false;
}
if (!initDone) {
// Asynchronously load state definitions, then resync URL
initPromise().then(resync);
initDone = true;
return;
}
var futureState = findFutureState($state, { url: $location.path() });
if (!futureState) {
return $injector.invoke(otherwiseFunc);
}
// Config loaded. Asynchronously lazy-load state definition from URL fragment, if mapped.
lazyLoadState($injector, futureState).then(function lazyLoadedStateCallback(states) {
states.forEach(function (state) {
if (state && (!$state.get(state) || (state.name && !$state.get(state.name))))
$stateProvider.state(state);
});
lazyloadInProgress = false;
resync();
}, function lazyLoadStateAborted() {
lazyloadInProgress = false;
resync();
});
}];
if (lazyloadInProgress) return;
var nextFn = resyncing ? otherwiseFunc : lazyLoadMissingState;
return $injector.invoke(nextFn);
}
$urlRouterProvider.otherwise(futureState_otherwise);
$urlRouterProvider.otherwise = function(rule) {
if (angular.isString(rule)) {
var redirect = rule;
rule = function () { return redirect; };
}
else if (!angular.isFunction(rule)) throw new Error("'rule' must be a function");
otherwiseFunc = ['$injector', '$location', rule];
return $urlRouterProvider;
};
var serviceObject = {
getResolvePromise: function () {
return initPromise();
}
};
// Used in .run() block to init
this.$get = [ '$injector', '$state', '$q', '$rootScope', '$urlRouter', '$timeout', '$log',
function futureStateProvider_get($injector, $state, $q, $rootScope, $urlRouter, $timeout, $log) {
function init() {
$rootScope.$on("$stateNotFound", function futureState_notFound(event, unfoundState, fromState, fromParams) {
if (lazyloadInProgress) return;
//$log.debug("event, unfoundState, fromState, fromParams", event, unfoundState, fromState, fromParams);
var futureState = findFutureState($state, { name: unfoundState.to });
if (!futureState) return;
event.preventDefault();
var promise = lazyLoadState($injector, futureState);
promise.then(function (states) {
states.forEach(function (state) {
if (state && (!$state.get(state) || (state.name && !$state.get(state.name))))
$stateProvider.state(state);
});
$state.go(unfoundState.to, unfoundState.toParams);
lazyloadInProgress = false;
}, function (error) {
console.log("failed to lazy load state ", error);
if (fromState.name) $state.go(fromState, fromParams);
lazyloadInProgress = false;
});
});
// Do this better. Want to load remote config once, before everything else
if (!initPromise) {
var promises = [];
angular.forEach(resolveFunctions, function (promiseFn) {
promises.push($injector.invoke(promiseFn));
});
initPromise = function () {
return $q.all(promises);
};
}
// TODO: analyze this. I'm calling $urlRouter.sync() in two places for retry-initial-transition.
// TODO: I should only need to do this once. Pick the better place and remove the extra resync.
initPromise().then(function retryInitialState() {
$timeout(function () {
if ($state.transition) {
$state.transition.then(retryInitialState, retryInitialState);
} else {
$urlRouter.sync();
}
});
});
}
init();
serviceObject.state = $stateProvider.state;
serviceObject.futureState = provider.futureState;
serviceObject.get = provider.get;
return serviceObject;
}
];
}
app.provider('$futureState', _futureStateProvider);
var statesAddedQueue = {
state: function(state) {
if (statesAddedQueue.$rootScope)
statesAddedQueue.$rootScope.$broadcast("$stateAdded", state);
},
itsNowRuntimeOhWhatAHappyDay: function($rootScope) {
statesAddedQueue.$rootScope = $rootScope;
},
$rootScope: undefined
};
app.config([ '$stateProvider', function($stateProvider) {
// decorate $stateProvider.state so we can broadcast when a real state was added
var realStateFn = $stateProvider.state;
$stateProvider.state = function state_announce() {
var val = realStateFn.apply($stateProvider, arguments);
var state = angular.isObject(arguments[0]) ? arguments[0] : arguments[1];
statesAddedQueue.state(state);
return val;
};
}]);
// inject $futureState so the service gets initialized via $get();
app.run(['$futureState', function ($futureState, $rootScope) {
statesAddedQueue.itsNowRuntimeOhWhatAHappyDay($rootScope);
} ]);
})(angular);
angular.module('ct.ui.router.extras.previous', [ 'ct.ui.router.extras.core', 'ct.ui.router.extras.transition' ]).service("$previousState",
[ '$rootScope', '$state', '$q',
function ($rootScope, $state, $q) {
var previous = null, lastPrevious = null, memos = {};
$rootScope.$on("$transitionStart", function(evt, $transition$) {
var from = $transition$.from;
// Check if the fromState is navigable before tracking it.
// Root state doesn't get decorated with $$state(). Doh.
var fromState = from.state && from.state.$$state && from.state.$$state();
function commit() { lastPrevious = null; }
function revert() { previous = lastPrevious; }
if (fromState) {
lastPrevious = previous;
previous = $transition$.from;
$transition$.promise.then(commit)['catch'](revert);
}
});
var $previousState = {
get: function (memoName) {
return memoName ? memos[memoName] : previous;
},
go: function (memoName, options) {
var to = $previousState.get(memoName);
if (memoName && !to) {
return $q.reject(new Error('undefined memo'));
}
return $state.go(to.state, to.params, options);
},
memo: function (memoName, defaultStateName, defaultStateParams) {
memos[memoName] = previous || { state: $state.get(defaultStateName), params: defaultStateParams };
},
forget: function (memoName) {
if (memoName) {
delete memos[memoName];
} else {
previous = undefined;
}
}
};
return $previousState;
}
]
);
angular.module('ct.ui.router.extras.previous').run(['$previousState', function ($previousState) {
// Inject $previousState so it can register $rootScope events
}]);
angular.module("ct.ui.router.extras.transition", [ 'ct.ui.router.extras.core' ]).config( [ "$provide", function ($provide) {
// Decorate the $state service, so we can replace $state.transitionTo()
$provide.decorator("$state", ['$delegate', '$rootScope', '$q', '$injector',
function ($state, $rootScope, $q, $injector) {
// Keep an internal reference to the real $state.transitionTo function
var $state_transitionTo = $state.transitionTo;
// $state.transitionTo can be re-entered. Keep track of re-entrant stack
var transitionDepth = -1;
var tDataStack = [];
var restoreFnStack = [];
// This function decorates the $injector, adding { $transition$: tData } to invoke() and instantiate() locals.
// It returns a function that restores $injector to its previous state.
function decorateInjector(tData) {
var oldinvoke = $injector.invoke;
var oldinstantiate = $injector.instantiate;
$injector.invoke = function (fn, self, locals) {
return oldinvoke(fn, self, angular.extend({$transition$: tData}, locals));
};
$injector.instantiate = function (fn, locals) {
return oldinstantiate(fn, angular.extend({$transition$: tData}, locals));
};
return function restoreItems() {
$injector.invoke = oldinvoke;
$injector.instantiate = oldinstantiate;
};
}
function popStack() {
restoreFnStack.pop()();
tDataStack.pop();
transitionDepth--;
}
// This promise callback (for when the real transitionTo is successful) runs the restore function for the
// current stack level, then broadcasts the $transitionSuccess event.
function transitionSuccess(deferred, tSuccess) {
return function successFn(data) {
popStack();
$rootScope.$broadcast("$transitionSuccess", tSuccess);
deferred.resolve(data); // $transition$ deferred
return data;
};
}
// This promise callback (for when the real transitionTo fails) runs the restore function for the
// current stack level, then broadcasts the $transitionError event.
function transitionFailure(deferred, tFail) {
return function failureFn(error) {
popStack();
$rootScope.$broadcast("$transitionError", tFail, error);
deferred.reject(error); // $transition$ deferred
return $q.reject(error);
};
}
// Decorate $state.transitionTo.
$state.transitionTo = function (to, toParams, options) {
// Create a deferred/promise which can be used earlier than UI-Router's transition promise.
var deferred = $q.defer();
// Place the promise in a transition data, and place it on the stack to be used in $stateChangeStart
var tData = tDataStack[++transitionDepth] = {
promise: deferred.promise
};
// placeholder restoreFn in case transitionTo doesn't reach $stateChangeStart (state not found, etc)
restoreFnStack[transitionDepth] = function() { };
// Invoke the real $state.transitionTo
var tPromise = $state_transitionTo.apply($state, arguments);
// insert our promise callbacks into the chain.
return tPromise.then(transitionSuccess(deferred, tData), transitionFailure(deferred, tData));
};
// This event is handled synchronously in transitionTo call stack
$rootScope.$on("$stateChangeStart", function (evt, toState, toParams, fromState, fromParams) {
var depth = transitionDepth;
// To/From is now normalized by ui-router. Add this information to the transition data object.
var tData = angular.extend(tDataStack[depth], {
to: { state: toState, params: toParams },
from: { state: fromState, params: fromParams }
});
var restoreFn = decorateInjector(tData);
restoreFnStack[depth] = restoreFn;
$rootScope.$broadcast("$transitionStart", tData);
}
);
return $state;
}]);
}
]
);
// statevis requires d3.
(function () {
"use strict";
var app = angular.module("ct.ui.router.extras.statevis", [ 'ct.ui.router.extras.core', 'ct.ui.router.extras.sticky' ]);
app.directive('stateVis', [ '$state', '$timeout', '$interval', stateVisDirective ]);
/**
* This directive gets all the current states using $state.get() and displays them in a tree using D3 lib.
* It then listens for state events and updates the tree.
*
* Usage:
* <state-vis height="1000px" width="1000px"></state-vis>
*/
function stateVisDirective($state, $timeout, $interval) {
return {
scope: {
width: '@',
height: '@'
},
restrict: 'AE',
template: '<svg></svg>',
link: function (_scope, _elem, _attrs) {
var stateMap = {};
var width = _scope.width || 400,
height = _scope.height || 400;
var tree = d3.layout.tree()
.size([width - 20, height - 20])
.separation(function (a, b) {
return a.parent == b.parent ? 10 : 25;
});
var root = $state.get().filter(function (state) { return state.name === ""; })[0];
var nodes = tree(root);
root.parent = root;
root.px = root.x = width / 2;
root.py = root.y = height / 2;
var activeNode = { };
activeNode.px = activeNode.x = root.px;
activeNode.py = activeNode.y = root.py;
var diagonal = d3.svg.diagonal();
var svg = d3.select(_elem.find("svg")[0])
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(10, 10)");
var node = svg.selectAll(".node"),
link = svg.selectAll(".link"),
active = svg.selectAll(".active")
;
var updateInterval = 200,
transLength = 200,
timer = setInterval(update, updateInterval);
function addStates(data) {
// *********** Convert flat data into a nice tree ***************
data = data.map(function (node) {
return node.name === "" ? root : angular.copy(node);
});
angular.extend(stateMap, data.reduce(function (map, node) {
map[node.name] = node;
return map;
}, {}));
data.forEach(function (node) {
// add to parent
var parentName = node.name.split(/\./).slice(0, -1).join(".");
var parent = node.name != parentName && stateMap[parentName];
if (parent) {
(parent.children || (parent.children = [])).push(node); // create child array if it doesn't exist
node.px = parent.px;
node.py = parent.py;
nodes.push(node);
}
});
}
$interval(function () {
_scope.states = $state.get();
angular.forEach(nodes, function (n) {
var s = $state.get(n.name);
if (s) {
n.status = s.status || 'exited';
}
});
// _scope.futureStates = $futureState.get();
}, 250);
_scope.$watchCollection("states", function (newval, oldval) {
var oldstates = (oldval || []).map(function (s) { return s.name; });
addStates((newval || []).filter(function(state) { return oldstates.indexOf(state.name) == -1; } ));
// addStates(_.reject(newval, function (state) { return _.contains(oldstates, state.name); }));
});
// addStates($state.get());
update(updateInterval);
function update() {
// Recompute the layout and data join.
node = node.data(tree.nodes(root), function (d) { return d.name; });
link = link.data(tree.links(nodes), function (d) { return d.target.name; });
active = active.data(activeNode);
nodes.forEach(function (d) { d.y = d.depth * 70; });
// Add entering nodes in the parent’s old position.
var nodeEnter = node.enter();
function stateName(node) {
var name = node.name.split(".").pop();
if (node.sticky) { name += " (STICKY)"; }
if (node.deepStateRedirect) { name += " (DSR)"; }
return name;
}
active.enter()
.append("circle")
.attr("class", "active")
.attr("r", 13)
.attr("cx", function (d) { return d.parent.px || 100; })
.attr("cy", function (d) { return d.parent.py || 100; })
;
nodeEnter.append("circle")
.attr("class", "node")
.attr("r", 9)
.attr("cx", function (d) { return d.parent.px; })
.attr("cy", function (d) { return d.parent.py; });
nodeEnter.append("text")
.attr("class", "label")
.attr("x", function (d) { return d.parent.px; })
.attr("y", function (d) { return d.parent.py; })
.attr("text-anchor", function (d) { return "middle"; })
.text(stateName)
.style("fill-opacity", 1);
// Add entering links in the parent’s old position.
link.enter().insert("path", ".node")
.attr("class", "link")
.attr("d", function (d) {
var o = {x: d.source.px, y: d.source.py};
return diagonal({source: o, target: o});
});
// Transition nodes and links to their new positions.
var t = svg.transition()
.duration(transLength);
t.selectAll(".link")
.attr("d", diagonal);
/* jshint -W093 */
var circleColors = { entered: '#AF0', exited: '#777', active: '#0f0', inactive: '#55F', future: '#009' };
t.selectAll(".node")
.attr("cx", function (d) { return d.px = d.x; })
.attr("cy", function (d) { return d.py = d.y; })
.attr("r", function (d) { return d.status === 'active' ? 15 : 10; })
.style("fill", function (d) { return circleColors[d.status] || "#FFF"; });
t.selectAll(".label")
.attr("x", function (d) { return d.px = d.x; })
.attr("y", function (d) { return d.py = d.y - 15; })
.attr("transform", function (d) { return "rotate(-25 " + d.x + " " + d.y + ")"; })
;
t.selectAll(".active")
.attr("x", function (d) { return d.px = d.x; })
.attr("y", function (d) { return d.py = d.y - 15; });
}
}
};
}
})();
angular.module("ct.ui.router.extras",
[
'ct.ui.router.extras.core',
'ct.ui.router.extras.dsr',
'ct.ui.router.extras.future',
'ct.ui.router.extras.previous',
'ct.ui.router.extras.statevis',
'ct.ui.router.extras.sticky',
'ct.ui.router.extras.transition'
]);
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment