From a01c95647a02891a960099c5a137a0cbcddb4aa0 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Wed, 27 Aug 2014 11:00:06 -0500 Subject: [PATCH] Build and release 0.0.10 --- release/ct-ui-router-extras.js | 1738 +++++++++++++++------------- release/ct-ui-router-extras.min.js | 2 +- 2 files changed, 928 insertions(+), 812 deletions(-) diff --git a/release/ct-ui-router-extras.js b/release/ct-ui-router-extras.js index 33bdf23..0d3ae0c 100644 --- a/release/ct-ui-router-extras.js +++ b/release/ct-ui-router-extras.js @@ -1,6 +1,6 @@ /** - * UI-Router Extras - * @version v0.0.9 + * UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise + * @version v0.0.10 * @link http://christopherthielen.github.io/ui-router-extras/ * @license MIT License, http://www.opensource.org/licenses/MIT */ @@ -24,17 +24,17 @@ var map = function (collection, callback) { return result; }; -var keys = function(collection) { +var keys = function (collection) { "use strict"; - return map(collection, function(collection, key) { + return map(collection, function (collection, key) { return key; }); }; -var filter = function(collection, callback) { +var filter = function (collection, callback) { "use strict"; var result = []; - forEach(collection, function(item, index) { + forEach(collection, function (item, index) { if (callback(item, index)) { result.push(item); } @@ -42,10 +42,10 @@ var filter = function(collection, callback) { return result; }; -var filterObj = function(collection, callback) { +var filterObj = function (collection, callback) { "use strict"; var result = {}; - forEach(collection, function(item, index) { + forEach(collection, function (item, index) { if (callback(item, index)) { result[index] = item; } @@ -71,7 +71,7 @@ function objectKeys(object) { } var result = []; - angular.forEach(object, function(val, key) { + angular.forEach(object, function (val, key) { result.push(key); }); return result; @@ -114,106 +114,112 @@ function inheritParams(currentParams, newParams, $current, $to) { } function inherit(parent, extra) { - return extend(new (extend(function() {}, {prototype:parent}))(), extra); + return extend(new (extend(function () { }, {prototype: parent}))(), extra); } -//define(['angularAMD'], function (angularAMD) { - - var app = angular.module("ct.ui.router.extras"); +var ignoreDsr; +function resetIgnoreDsr() { + ignoreDsr = undefined; +} - 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").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; + } - // Decorate $state.transitionTo to gain access to the last transition.options variable. - // This is used to process the options.ignoreDsr option - app.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_transitionTo.apply($state, arguments).then( - function(result) { resetIgnoreDsr(); return result; }, - function(err) { resetIgnoreDsr(); return $q.reject(err); } - ); - }; - return $state; - }]); + ); + }; + return $state; }]); +}]); - app.service("$deepStateRedirect", [ '$rootScope', '$state', function ($rootScope, $state) { - var lastSubstate = {}; - var lastParams = {}; - var deepStateRedirectsByName = {}; - - var REDIRECT = "Redirect", ANCESTOR_REDIRECT = "AncestorRedirect"; +angular.module("ct.ui.router.extras").service("$deepStateRedirect", [ '$rootScope', '$state', '$injector', function ($rootScope, $state, $injector) { + var lastSubstate = {}; + var lastParams = {}; + var deepStateRedirectsByName = {}; - function computeDeepStateStatus(state) { - var name = state.name; - if (deepStateRedirectsByName.hasOwnProperty(name)) - return deepStateRedirectsByName[name]; - recordDeepStateRedirectStatus(name); - } + var REDIRECT = "Redirect", ANCESTOR_REDIRECT = "AncestorRedirect"; - function recordDeepStateRedirectStatus(stateName) { - var state = $state.get(stateName); - if (state && state.deepStateRedirect === true) { - deepStateRedirectsByName[stateName] = REDIRECT; - if (lastSubstate[stateName] === undefined) - lastSubstate[stateName] = stateName; - } + function computeDeepStateStatus(state) { + var name = state.name; + if (deepStateRedirectsByName.hasOwnProperty(name)) + return deepStateRedirectsByName[name]; + recordDeepStateRedirectStatus(name); + } - var lastDot = stateName.lastIndexOf("."); - if (lastDot != -1) { - var parentStatus = recordDeepStateRedirectStatus(stateName.substr(0, lastDot)); - if (parentStatus && deepStateRedirectsByName[stateName] === undefined) { - deepStateRedirectsByName[stateName] = ANCESTOR_REDIRECT; - } - } - return deepStateRedirectsByName[stateName] || false; + function recordDeepStateRedirectStatus(stateName) { + var state = $state.get(stateName); + if (state && state.deepStateRedirect) { + deepStateRedirectsByName[stateName] = REDIRECT; + if (lastSubstate[stateName] === undefined) + lastSubstate[stateName] = stateName; } - $rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) { - function shouldRedirect() { - var deepStateStatus = computeDeepStateStatus(toState); - var substate = lastSubstate[toState.name]; - // We're changing directly to one of the redirect (tab) states and we have a last substate recorded - return !ignoreDsr && deepStateStatus === REDIRECT && substate && substate != toState.name ? true : false; + var lastDot = stateName.lastIndexOf("."); + if (lastDot != -1) { + var parentStatus = recordDeepStateRedirectStatus(stateName.substr(0, lastDot)); + if (parentStatus && deepStateRedirectsByName[stateName] === undefined) { + deepStateRedirectsByName[stateName] = ANCESTOR_REDIRECT; } + } + return deepStateRedirectsByName[stateName] || false; + } - if (shouldRedirect()) { // send them to the last known state for that tab - event.preventDefault(); - $state.go(lastSubstate[toState.name], lastParams[toState.name]); - } - }); + $rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) { + function shouldRedirect() { + if (ignoreDsr) return false; - $rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) { var deepStateStatus = computeDeepStateStatus(toState); - if (deepStateStatus) { - var name = toState.name; - angular.forEach(lastSubstate, function (deepState, redirectState) { - if (name == redirectState || name.indexOf(redirectState + ".") != -1) { - lastSubstate[redirectState] = name; - lastParams[redirectState] = angular.copy(toParams); - } - }); - } - }); + var substate = lastSubstate[toState.name]; - return {}; - }]); + // We're changing directly to one of the redirect (tab) states and we have a last substate recorded + var isDSR = (deepStateStatus === REDIRECT && substate && substate != toState.name ? true : false); + if (isDSR && angular.isFunction(toState.deepStateRedirect)) + return $injector.invoke(toState.deepStateRedirect, toState); - app.run(['$deepStateRedirect', function ($deepStateRedirect) { - // Make sure $deepStateRedirect is instantiated - }]); + return isDSR; + } + + if (shouldRedirect()) { // send them to the last known state for that tab + event.preventDefault(); + $state.go(lastSubstate[toState.name], lastParams[toState.name]); + } + }); -// return app; -//}); + $rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) { + var deepStateStatus = computeDeepStateStatus(toState); + if (deepStateStatus) { + var name = toState.name; + angular.forEach(lastSubstate, function (deepState, redirectState) { + if (name == redirectState || name.indexOf(redirectState + ".") != -1) { + lastSubstate[redirectState] = name; + lastParams[redirectState] = angular.copy(toParams); + } + }); + } + }); + + return {}; +}]); + +angular.module("ct.ui.router.extras").run(['$deepStateRedirect', function ($deepStateRedirect) { + // Make sure $deepStateRedirect is instantiated +}]); $StickyStateProvider.$inject = [ '$stateProvider' ]; function $StickyStateProvider($stateProvider) { @@ -228,259 +234,261 @@ function $StickyStateProvider($stateProvider) { stickyStates[state.name] = state; // console.log("Registered sticky state: ", state); }; - - this.enableDebug = function(enabled) { + + this.enableDebug = function (enabled) { DEBUG = enabled; }; - 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; - } + 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; + } - // 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; - } + // 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 }; - } + // 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, ancestorParamsChanged) { - if (ancestorParamsChanged) return "updateStateParams"; - var inactiveState = inactiveStates[state.self.name]; - if (!inactiveState) return "enter"; + // 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, ancestorParamsChanged) { + if (ancestorParamsChanged) return "updateStateParams"; + var inactiveState = inactiveStates[state.self.name]; + if (!inactiveState) return "enter"; // if (inactiveState.locals == null || inactiveState.locals.globals == null) debugger; - var paramsMatch = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams); + var paramsMatch = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams); // 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 = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams); - return paramsMatch ? inactiveState : null; - } + return paramsMatch ? "reactivate" : "updateStateParams"; + } - // Duplicates logic in $state.transitionTo, primarily to find the pivot state (i.e., the "keep" value) - function equalForKeys(a, b, keys) { - if (!keys) { - keys = []; - for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + // 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 = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams); + return paramsMatch ? inactiveState : null; } - 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 + // Duplicates logic in $state.transitionTo, primarily to find the pivot state (i.e., the "keep" value) + function equalForKeys(a, b, keys) { + 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; } - 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: - // { - // inactives: Array of all states which will be inactive if the transition is completed. (both previously and newly inactivated) - // 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, + 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: + // { + // inactives: Array of all states which will be inactive if the transition is completed. (both previously and newly inactivated) + // 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, options = transition.options; - var keep = 0, state = toPath[keep]; + var keep = 0, state = toPath[keep]; - if (options.inherit) { - toParams = inheritParams($stateParams, toParams || {}, $state.$current, transition.toState); - } + if (options.inherit) { + toParams = inheritParams($stateParams, toParams || {}, $state.$current, transition.toState); + } - while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) { - state = toPath[++keep]; - } + while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) { + state = toPath[++keep]; + } - result.keep = keep; - - var idx, deepestUpdatedParams, deepestReactivate, reactivatedStatesByName = {}, pType = getStickyTransitionType(fromPath, toPath, keep); - var ancestorUpdated = false; // When ancestor params change, treat reactivation as exit/enter - - // 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], transition.toParams, 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') - deepestReactivate = reactivatedStatesByName[toPath[idx].name] = toPath[idx]; - if (enterTrans == 'updateStateParams') - deepestUpdatedParams = toPath[idx]; - } - deepestReactivate = deepestReactivate ? deepestReactivate.self.name + "." : ""; - deepestUpdatedParams = deepestUpdatedParams ? deepestUpdatedParams.self.name + "." : ""; - - // Inactive states, before the transition is processed, mapped to the parent to the sticky state. - var inactivesByParent = mapInactives(); - - // root ("") is always kept. Find the remaining names of the kept path. - var keptStateNames = [""].concat(map(fromPath.slice(0, keep), function(state) { return state.self.name; })); - - // Locate currently and newly inactive states (at pivot and above) and store them in the output array 'inactives'. - angular.forEach(keptStateNames, function(name) { - var inactiveChildren = inactivesByParent[name]; - for (var i = 0; inactiveChildren && i < inactiveChildren.length; i++) { - var child = inactiveChildren[i]; - // Don't organize state as inactive if we're about to reactivate it. - if (!reactivatedStatesByName[child.name] && + result.keep = keep; + + var idx, deepestUpdatedParams, deepestReactivate, reactivatedStatesByName = {}, pType = getStickyTransitionType(fromPath, toPath, keep); + var ancestorUpdated = false; // When ancestor params change, treat reactivation as exit/enter + + // 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], transition.toParams, 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') + deepestReactivate = reactivatedStatesByName[toPath[idx].name] = toPath[idx]; + if (enterTrans == 'updateStateParams') + deepestUpdatedParams = toPath[idx]; + } + deepestReactivate = deepestReactivate ? deepestReactivate.self.name + "." : ""; + deepestUpdatedParams = deepestUpdatedParams ? deepestUpdatedParams.self.name + "." : ""; + + // Inactive states, before the transition is processed, mapped to the parent to the sticky state. + var inactivesByParent = mapInactives(); + + // root ("") is always kept. Find the remaining names of the kept path. + var keptStateNames = [""].concat(map(fromPath.slice(0, keep), function (state) { + return state.self.name; + })); + + // Locate currently and newly inactive states (at pivot and above) and store them in the output array 'inactives'. + angular.forEach(keptStateNames, function (name) { + var inactiveChildren = inactivesByParent[name]; + for (var i = 0; inactiveChildren && i < inactiveChildren.length; i++) { + var child = inactiveChildren[i]; + // Don't organize state as inactive if we're about to reactivate it. + if (!reactivatedStatesByName[child.name] && (!deepestReactivate || (child.self.name.indexOf(deepestReactivate) !== 0)) && (!deepestUpdatedParams || (child.self.name.indexOf(deepestUpdatedParams) !== 0))) - result.inactives.push(child); - } - }); + result.inactives.push(child); + } + }); - // 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++) { - var exitTrans = "exit"; - if (pType.from) { - // State is being inactivated, note this in result.inactives array - result.inactives.push(fromPath[idx]); - exitTrans = "inactivate"; + // 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++) { + var exitTrans = "exit"; + if (pType.from) { + // State is being inactivated, note this in result.inactives array + result.inactives.push(fromPath[idx]); + exitTrans = "inactivate"; + } + result.exit[idx] = exitTrans; } - result.exit[idx] = exitTrans; - } - 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'; + 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 substatePrefix = exiting.self.name + "."; // All descendant states will start with this prefix - 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] && name.indexOf(substatePrefix) === 0) { // inactivated state's name starts with the prefix. - if (DEBUG) $log.debug("Exiting " + name + " because it's a substate of " + substatePrefix + " and wasn't found in ", exitingNames); - if (inactiveExiting.self.onExit) - $injector.invoke(inactiveExiting.self.onExit, inactiveExiting.self, inactiveExiting.locals.globals); - inactiveExiting.locals = null; - inactiveExiting.self.status = 'exited'; - delete inactiveStates[name]; + 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); + 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) { + var inactivatedState = getInactivatedState(entering); + if (inactivatedState && !getInactivatedState(entering, params)) { + var savedLocals = entering.locals; + this.stateExiting(inactivatedState); + entering.locals = savedLocals; } - }); - - 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) { - var inactivatedState = getInactivatedState(entering); - if (inactivatedState && !getInactivatedState(entering, params)) { - var savedLocals = entering.locals; - this.stateExiting(inactivatedState); - entering.locals = savedLocals; - } - entering.self.status = 'entered'; + entering.self.status = 'entered'; - if (onEnter) - $injector.invoke(onEnter, entering.self, entering.locals.globals); - } - }; + if (onEnter) + $injector.invoke(onEnter, entering.self, entering.locals.globals); + } + }; - return stickySupport; - }]; + return stickySupport; + }]; } angular.module("ct.ui.router.extras").provider("$stickyState", $StickyStateProvider); @@ -546,9 +554,9 @@ angular.module("ct.ui.router.extras").provider("$stickyState", $StickyStateProvi 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) + 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) // Creates a blank surrogate state function SurrogateState(type) { @@ -567,325 +575,330 @@ function SurrogateState(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").run(["$stickyState", function ($stickyState) { _StickyState = $stickyState; }]); +angular.module("ct.ui.router.extras").run(["$stickyState", function ($stickyState) { + _StickyState = $stickyState; +}]); angular.module("ct.ui.router.extras").config( [ "$provide", "$stateProvider", '$stickyStateProvider', function ($provide, $stateProvider, $stickyStateProvider) { // 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 = []; - - // Need access to the internal 'root' state object. Get it by decorating the StateBuilder parent function. - // Known bug: At least one state must be defined by the application. If no states are defined, then the - // decorator never gets invoked. If the decorator never gets invoked, then the root state variable remains undefined. - $stateProvider.decorator('parent', function (state, parentFn) { - if (root === undefined) { - // Note: this code gets run only on the first state that is decorated - root = parentFn({}); // StateBuilder.parent({}) returns the root internal state object - root.parent = inactivePseudoState; // Hook root.parent up to the inactivePsuedoState - inactivePseudoState.parent = undefined; - inactivePseudoState.locals = undefined; - } + // Reset other module scoped variables. This is to primarily to flush any previous state during karma runs. + root = pendingRestore = undefined; + pendingTransitions = []; + + // 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]; + }; + + // Register the ones marked as "sticky" + if (state.self.sticky === true) { + $stickyStateProvider.registerStickyState(state.self); + } - // 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]; }; + return parentFn(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; + root.parent = inactivePseudoState; // Make inactivePsuedoState the parent of root. "wat" + inactivePseudoState.parent = undefined; // Make inactivePsuedoState the real root. + 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) { + // 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"); + } } - return parentFn(state); - }); - - 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) { - // Hold on to the real $state.transitionTo in a module-scope variable. - $state_transitionTo = $state.transitionTo; + 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; - // ------------------------ Decorated transitionTo implementation begins here --------------------------- - $state.transitionTo = function (to, toParams, options) { - // 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 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; } - 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; - - 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; - } + 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 - }; + 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); - }; + // 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. + // --------------------- 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 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 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 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; - }); + 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; - } + 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; - }); + 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; - } + 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; - // --------------------- 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; + var currentTransition = {toState: toState, toParams: toParams || {}, fromState: fromState, fromParams: fromParams || {}, options: options}; - var currentTransition = {toState: toState, toParams: toParams || {}, fromState: fromState, fromParams: fromParams || {}, options: options}; + pendingTransitions.push(currentTransition); // TODO: See if a list of pending transitions is necessary. + pendingRestore = restore; - pendingTransitions.push(currentTransition); // TODO: See if a list of pending transitions is necessary. - pendingRestore = restore; + // $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); - // $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); - 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); - // 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]; + }); - // Clear out and reload inactivePseudoState.locals each time transitionTo is called - angular.forEach(inactivePseudoState.locals, function(local, name) { - delete inactivePseudoState.locals[name]; + // Find all states that will be inactive once the transition succeeds. For each of those states, + // place its view-locals on the __inactives pseudostate's .locals. This allows the ui-view directive + // to access them and render the inactive views. + for (var i = 0; i < stickyTransitions.inactives.length; i++) { + var iLocals = stickyTransitions.inactives[i].locals; + angular.forEach(iLocals, function (view, name) { + if (iLocals.hasOwnProperty(name) && name.indexOf("@") != -1) { // Only grab this state's "view" locals + inactivePseudoState.locals[name] = view; // Add all inactive views not already included. + } }); + } - // Find all states that will be inactive once the transition succeeds. For each of those states, - // place its view-locals on the __inactives pseudostate's .locals. This allows the ui-view directive - // to access them and render the inactive views. - for (var i = 0; i < stickyTransitions.inactives.length; i++) { - var iLocals = stickyTransitions.inactives[i].locals; - angular.forEach(iLocals, function(view, name) { - if (iLocals.hasOwnProperty(name) && name.indexOf("@") != -1) { // Only grab this state's "view" locals - inactivePseudoState.locals[name] = view; // Add all inactive views not already included. - } - }); + // 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; + 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(toState.path[idx]); + 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(toState.path[idx])); + terminalReactivatedState = surrogate; + } else if (value === "updateStateParams") { + // If the state params have been changed, we need to exit any inactive states and re-enter them. + surrogate = stateEnteredSurrogate(toState.path[idx]); + surrogateToPath.push(surrogate); + terminalReactivatedState = surrogate; + } else if (value === "enter") { + // Standard enter transition. We still wrap it in a surrogate. + surrogateToPath.push(stateEnteredSurrogate(toState.path[idx])); } + }); - // 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; - 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(toState.path[idx]); - 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(toState.path[idx])); - terminalReactivatedState = surrogate; - } else if (value === "updateStateParams") { - // If the state params have been changed, we need to exit any inactive states and re-enter them. - surrogate = stateEnteredSurrogate(toState.path[idx]); - surrogateToPath.push(surrogate); - terminalReactivatedState = surrogate; - } else if (value === "enter") { - // Standard enter transition. We still wrap it in a surrogate. - surrogateToPath.push(stateEnteredSurrogate(toState.path[idx])); - } + // 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 for reactivated to ToPath again, 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); }); + } - // 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); + // In some cases, we may be some state, but not its children states. If that's the case, we have to + // exit all the children of the deepest reactivated state. + if (terminalReactivatedState) { + var prefix = terminalReactivatedState.self.name + "."; + var inactiveStates = _StickyState.getInactiveStates(); + var inactiveOrphans = []; + inactiveStates.forEach(function (exiting) { + if (exiting.self.name.indexOf(prefix) === 0) { + inactiveOrphans.push(exiting); } }); + inactiveOrphans.sort(); + inactiveOrphans.reverse(); + // Add surrogate exited states for all orphaned descendants of the Deepest Reactivated State + surrogateFromPath = surrogateFromPath.concat(map(inactiveOrphans, function (exiting) { + return stateExitedSurrogate(exiting); + })); + exited = exited.concat(inactiveOrphans); + } - // Add surrogate for reactivated to ToPath again, 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); - }); - } - - // In some cases, we may be some state, but not its children states. If that's the case, we have to - // exit all the children of the deepest reactivated state. - if (terminalReactivatedState) { - var prefix = terminalReactivatedState.self.name + "."; - var inactiveStates = _StickyState.getInactiveStates(); - var inactiveOrphans = []; - inactiveStates.forEach(function (exiting) { - if (exiting.self.name.indexOf(prefix) === 0) { - inactiveOrphans.push(exiting); - } - }); - inactiveOrphans.sort(); - inactiveOrphans.reverse(); - // Add surrogate exited states for all orphaned descendants of the Deepest Reactivated State - surrogateFromPath = surrogateFromPath.concat(map(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. - toState.path = surrogateToPath; - fromState.path = surrogateFromPath; + // Replace the .path variables. toState.path and fromState.path are now ready for a sticky transition. + toState.path = surrogateToPath; + fromState.path = surrogateFromPath; - 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)); - } + 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); - console.log(err.stack); - } - return $q.when(err); - }); - }; - return $state; - }]); + // 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); + console.log(err.stack); + } + return $q.reject(err); + }); + }; + return $state; }]); + } + ] +); function debugTransition($log, currentTransition, stickyTransition) { function message(path, index, state) { @@ -949,264 +962,367 @@ function debugViewsAfterSuccess($log, currentState, $state) { } -//define(['angularAMD'], function (angularAMD) { - angular.module('ct.ui.router.extras').provider('$futureState', function _futureStateProvider($stateProvider, $urlRouterProvider) { - var stateFactories = {}, futureStates = {}, futureUrlPrefixes = {}; - var transitionPending = 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); - }; +angular.module('ct.ui.router.extras').provider('$futureState', + [ '$stateProvider', '$urlRouterProvider', + function _futureStateProvider($stateProvider, $urlRouterProvider) { + var stateFactories = {}, futureStates = {}, futureUrlPrefixes = {}; + var transitionPending = false, resolveFunctions = [], initPromise, initDone = false; + var provider = this; - // 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: '

Future State Template

', - // controller: function() { - // console.log("Entered state " + futureState.stateName); - // } - // } - // }); - this.stateFactory = function (futureStateType, factory) { - stateFactories[futureStateType] = factory; - }; + // 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. - this.futureState = function (futureState) { - futureStates[futureState.stateName] = futureState; - futureUrlPrefixes[futureState.urlPrefix] = futureState; - }; - - this.get = function() { - return angular.extend({}, futureStates); - }; - - /* options is an object with at least a name or url attribute */ - function findFutureState($state, options) { - if (options.name) { - var nameComponents = options.name.split(/\./); - while (nameComponents.length) { - var stateName = nameComponents.join("."); - if ($state.get(stateName)) - return null; // State is already defined; nothing to do - if (futureStates[stateName]) - return futureStates[stateName]; - nameComponents.pop(); + // 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: '

Future State Template

', + // controller: function() { + // console.log("Entered state " + futureState.stateName); + // } + // } + // }); + this.stateFactory = function (futureStateType, factory) { + stateFactories[futureStateType] = factory; + }; + + this.futureState = function (futureState) { + futureStates[futureState.stateName] = futureState; + futureUrlPrefixes[futureState.urlPrefix] = futureState; + }; + + this.get = function () { + return angular.extend({}, futureStates); + }; + + /* options is an object with at least a name or url attribute */ + function findFutureState($state, options) { + if (options.name) { + var nameComponents = options.name.split(/\./); + while (nameComponents.length) { + var stateName = nameComponents.join("."); + if ($state.get(stateName)) + return null; // State is already defined; nothing to do + if (futureStates[stateName]) + return futureStates[stateName]; + nameComponents.pop(); + } } - } - if (options.url) { - var urlComponents = options.url.split(/\//); - while (urlComponents.length) { - var urlPrefix = urlComponents.join("/"); - if (futureUrlPrefixes[urlPrefix]) - return futureUrlPrefixes[urlPrefix]; - urlComponents.pop(); + if (options.url) { + var urlComponents = options.url.split(/\//); + while (urlComponents.length) { + var urlPrefix = urlComponents.join("/"); + if (futureUrlPrefixes[urlPrefix]) + return futureUrlPrefixes[urlPrefix]; + urlComponents.pop(); + } } } - } - function lazyLoadState($injector, futureState) { - if (!futureState) { - var deferred = $q.defer(); - deferred.reject("No lazyState passed in " + futureState); - return deferred.promise; + function lazyLoadState($injector, futureState) { + if (!futureState) { + var deferred = $q.defer(); + deferred.reject("No lazyState passed in " + futureState); + return deferred.promise; + } + + var type = futureState.type; + var factory = stateFactories[type]; + if (!factory) throw Error("No state factory for futureState.type: " + (futureState && futureState.type)); + return $injector.invoke(factory, factory, { futureState: futureState }); } - var type = futureState.type; - var factory = stateFactories[type]; - if (!factory) throw Error("No state factory for futureState.type: " + (futureState && futureState.type)); - return $injector.invoke(factory, factory, { futureState: futureState }); - } + function futureState_otherwise($injector, $location) { + var resyncing = false; + var $log = $injector.get("$log"); + + var otherwiseFunc = [ '$state', + function otherwiseFunc($state) { + $log.debug("Unable to map " + $location.path()); + $location.url("/"); + }]; + + var lazyLoadMissingState = + ['$rootScope', '$urlRouter', '$state', + function lazyLoadMissingState($rootScope, $urlRouter, $state) { + if (!initDone) { + // Asynchronously load state definitions, then resync URL + initPromise().then(function initialResync() { + resyncing = true; + $urlRouter.sync(); + resyncing = false; + }); + initDone = true; + return; + } - function futureState_otherwise($injector, $location) { - var resyncing = false; - var $log = $injector.get("$log"); - var otherwiseFunc = function otherwiseFunc($state) { - $log.debug("Unable to map " + $location.path()); - $location.url("/"); - }; + var futureState = findFutureState($state, { url: $location.path() }); + if (!futureState) { + return $injector.invoke(otherwiseFunc); + } - var lazyLoadMissingState = function lazyLoadMissingState($rootScope, $urlRouter, $state) { - if (!initDone) { - // Asynchronously load state definitions, then resync URL - initPromise().then(function initialResync() { - resyncing = true; - $urlRouter.sync(); - resyncing = false; - }); - initDone = true; - return; - } + transitionPending = true; + // Config loaded. Asynchronously lazy-load state definition from URL fragment, if mapped. + lazyLoadState($injector, futureState).then(function lazyLoadedStateCallback(state) { + // TODO: Should have a specific resolve value that says 'dont register a state because I already did' + if (state && !$state.get(state)) + $stateProvider.state(state); + resyncing = true; + $urlRouter.sync(); + resyncing = false; + transitionPending = false; + }, function lazyLoadStateAborted() { + transitionPending = false; + $state.go("top"); + }); + }]; + if (transitionPending) return; - - var futureState = findFutureState($state, { url: $location.path() }); - if (!futureState) { - return $injector.invoke(otherwiseFunc); - } + var nextFn = resyncing ? otherwiseFunc : lazyLoadMissingState; + return $injector.invoke(nextFn); + } - transitionPending = true; - // Config loaded. Asynchronously lazy-load state definition from URL fragment, if mapped. - lazyLoadState($injector, futureState).then(function lazyLoadedStateCallback(state) { - // TODO: Should have a specific resolve value that says 'dont register a state because I already did' - if (state && !$state.get(state)) - $stateProvider.state(state); - resyncing = true; - $urlRouter.sync(); - resyncing = false; - transitionPending = false; - }, function lazyLoadStateAborted() { - transitionPending = false; - $state.go("top"); - }); - }; - if (transitionPending) return; + $urlRouterProvider.otherwise(futureState_otherwise); - var nextFn = resyncing ? otherwiseFunc : lazyLoadMissingState; - return $injector.invoke(nextFn); - } - $urlRouterProvider.otherwise(futureState_otherwise); + var serviceObject = { + getResolvePromise: function () { + return initPromise(); + } + }; - var serviceObject = { - getResolvePromise: function () { - return initPromise(); - } - }; - - // Used in .run() block to init - this.$get = function futureStateProvider_get($injector, $state, $q, $rootScope, $urlRouter, $timeout, $log) { - function init() { - $rootScope.$on("$stateNotFound", function futureState_notFound(event, unfoundState, fromState, fromParams) { - if (transitionPending) return; - $log.debug("event, unfoundState, fromState, fromParams", event, unfoundState, fromState, fromParams); - - var futureState = findFutureState($state, { name: unfoundState.to }); - if (!futureState) return; - - event.preventDefault(); - transitionPending = true; - - var promise = lazyLoadState($injector, futureState); - promise.then(function (state) { - // TODO: Should have a specific resolve value that says 'dont register a state because I already did' - if (state) - $stateProvider.state(state); - $state.go(unfoundState.to, unfoundState.toParams); - transitionPending = false; - }, function (error) { - console.log("failed to lazy load state ", error); - $state.go(fromState, fromParams); - transitionPending = false; - }); - }); + // 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 (transitionPending) return; + $log.debug("event, unfoundState, fromState, fromParams", event, unfoundState, fromState, fromParams); + + var futureState = findFutureState($state, { name: unfoundState.to }); + if (!futureState) return; + + event.preventDefault(); + transitionPending = true; + + var promise = lazyLoadState($injector, futureState); + promise.then(function (state) { + // TODO: Should have a specific resolve value that says 'dont register a state because I already did' + if (state) + $stateProvider.state(state); + $state.go(unfoundState.to, unfoundState.toParams); + transitionPending = false; + }, function (error) { + console.log("failed to lazy load state ", error); + $state.go(fromState, fromParams); + transitionPending = 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); }; + // 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); + }; // initPromise = _.once(function flattenFutureStates() { // var allPromises = $q.all(promises); // return allPromises.then(function(data) { // return _.flatten(data); // }); // }); - } - - // 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($urlRouter.sync, $urlRouter.sync); - } else { - $urlRouter.sync(); } - }); - }); - } - init(); - serviceObject.state = $stateProvider.state; - serviceObject.futureState = provider.futureState; - serviceObject.get = provider.get; - - return serviceObject; - }; - }); + // 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($urlRouter.sync, $urlRouter.sync); + } else { + $urlRouter.sync(); + } + }); + }); + } - angular.module('ct.ui.router.extras').run(['$futureState', - // Just inject $futureState so it gets initialized. - function ($futureState) { } - ]); + init(); -// return app; -//}); + serviceObject.state = $stateProvider.state; + serviceObject.futureState = provider.futureState; + serviceObject.get = provider.get; -angular.module('ct.ui.router.extras').service("$previousState", - [ '$rootScope', '$state', -function($rootScope, $state) { - var previous = null; - var memos = {}; + return serviceObject; + }]; + }]); - var lastPrevious = null; +angular.module('ct.ui.router.extras').run(['$futureState', + // Just inject $futureState so it gets initialized. + function ($futureState) { + } +]); - $rootScope.$on("$stateChangeStart", function(evt, toState, toStateParams, fromState, fromStateParams) { - // State change is starting. Keep track of the CURRENT previous state in case we have to restore it - lastPrevious = previous; - previous = { state: fromState, params: fromStateParams }; - }); +angular.module('ct.ui.router.extras').service("$previousState", + [ '$rootScope', '$state', + function ($rootScope, $state) { + var previous = null; + var memos = {}; - $rootScope.$on("$stateChangeError", function() { - // State change did not occur due to an error. Restore the previous previous state. - previous = lastPrevious; - lastPrevious = null; - }); + var lastPrevious = null; - $rootScope.$on("$stateChangeSuccess", function() { - lastPrevious = null; - }); + $rootScope.$on("$stateChangeStart", function (evt, toState, toStateParams, fromState, fromStateParams) { + // State change is starting. Keep track of the CURRENT previous state in case we have to restore it + lastPrevious = previous; + previous = { state: fromState, params: fromStateParams }; + }); - var $previousState = { - get: function(memoName) { - return memoName ? memos[memoName] : previous; - }, - go: function(memoName, options) { - var to = $previousState.get(memoName); - return $state.go(to.state, to.params, options); - }, - memo: function(memoName) { - memos[memoName] = previous; - }, - forget: function(memoName) { - delete memos[memoName]; - } - }; + $rootScope.$on("$stateChangeError", function () { + // State change did not occur due to an error. Restore the previous previous state. + previous = lastPrevious; + lastPrevious = null; + }); - return $previousState; -}]); + $rootScope.$on("$stateChangeSuccess", function () { + lastPrevious = null; + }); -angular.module('ct.ui.router.extras').run(['$previousState', function($previousState) { + var $previousState = { + get: function (memoName) { + return memoName ? memos[memoName] : previous; + }, + go: function (memoName, options) { + var to = $previousState.get(memoName); + return $state.go(to.state, to.params, options); + }, + memo: function (memoName) { + memos[memoName] = previous; + }, + forget: function (memoName) { + delete memos[memoName]; + } + }; + + return $previousState; + } + ] +); + +angular.module('ct.ui.router.extras').run(['$previousState', function ($previousState) { // Inject $previousState so it can register $rootScope events }]); + +angular.module("ct.ui.router.extras").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); + return deferred.resolve(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); + return deferred.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; + }]); + } + ] +); + })(window, window.angular); \ No newline at end of file diff --git a/release/ct-ui-router-extras.min.js b/release/ct-ui-router-extras.min.js index 429af6a..d73ef5d 100644 --- a/release/ct-ui-router-extras.min.js +++ b/release/ct-ui-router-extras.min.js @@ -1 +1 @@ -/*! ui-router-extras - v0.0.9 - 2014-07-27 */!function(window,angular,undefined){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}function objectKeys(object){if(Object.keys)return Object.keys(object);var result=[];return angular.forEach(object,function(val,key){result.push(key)}),result}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;for(from=0>from?Math.ceil(from):Math.floor(from),0>from&&(from+=len);len>from;from++)if(from in array&&array[from]===value)return from;return-1}function inheritParams(currentParams,newParams,$current,$to){var parentParams,parents=ancestors($current,$to),inherited={},inheritList=[];for(var i in parents)if(parents[i].params&&(parentParams=isArray(parents[i].params)?parents[i].params:objectKeys(parents[i].params),parentParams.length))for(var j in parentParams)arraySearch(inheritList,parentParams[j])>=0||(inheritList.push(parentParams[j]),inherited[parentParams[j]]=currentParams[parentParams[j]]);return extend({},inherited,newParams)}function resetIgnoreDsr(){ignoreDsr=undefined}function $StickyStateProvider(){var inactiveStates={},stickyStates={};this.registerStickyState=function(state){stickyStates[state.name]=state},this.enableDebug=function(enabled){DEBUG=enabled},this.$get=["$rootScope","$state","$stateParams","$injector","$log",function($rootScope,$state,$stateParams,$injector,$log){function mapInactives(){var mappedStates={};return angular.forEach(inactiveStates,function(state){for(var stickyAncestors=getStickyStateStack(state),i=0;i "+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}));for(var viewMsg=function(local,name){return"'"+name+"' ("+local.$$state.name+")"},statesOnly=function(local,name){return"globals"!=name&&"resolve"!=name},viewsForState=function(state){var views=map(filterObj(state.locals,statesOnly),viewMsg).join(", ");return"("+(state.self.name?state.self.name:"root")+".locals"+(views.length?": "+views:"")+")"},message=viewsForState(currentState),parent=currentState.parent;parent&&parent!==currentState;)""===parent.self.name&&(message=viewsForState($state.$current.path[0])+" / "+message),message=viewsForState(parent)+" / "+message,currentState=parent,parent=currentState.parent;$log.debug("Views: "+message)}angular.module("ct.ui.router.extras",["ui.router"]);var ignoreDsr,DEBUG=!1,forEach=angular.forEach,extend=angular.extend,isArray=angular.isArray,map=function(collection,callback){"use strict";var result=[];return forEach(collection,function(item,index){result.push(callback(item,index))}),result},filterObj=function(collection,callback){"use strict";var result={};return forEach(collection,function(item,index){callback(item,index)&&(result[index]=item)}),result},app=angular.module("ct.ui.router.extras");app.config(["$provide",function($provide){var $state_transitionTo;$provide.decorator("$state",["$delegate","$q",function($state,$q){return $state_transitionTo=$state.transitionTo,$state.transitionTo=function(to,toParams,options){return options.ignoreDsr&&(ignoreDsr=options.ignoreDsr),$state_transitionTo.apply($state,arguments).then(function(result){return resetIgnoreDsr(),result},function(err){return resetIgnoreDsr(),$q.reject(err)})},$state}])}]),app.service("$deepStateRedirect",["$rootScope","$state",function($rootScope,$state){function computeDeepStateStatus(state){var name=state.name;return deepStateRedirectsByName.hasOwnProperty(name)?deepStateRedirectsByName[name]:void recordDeepStateRedirectStatus(name)}function recordDeepStateRedirectStatus(stateName){var state=$state.get(stateName);state&&state.deepStateRedirect===!0&&(deepStateRedirectsByName[stateName]=REDIRECT,lastSubstate[stateName]===undefined&&(lastSubstate[stateName]=stateName));var lastDot=stateName.lastIndexOf(".");if(-1!=lastDot){var parentStatus=recordDeepStateRedirectStatus(stateName.substr(0,lastDot));parentStatus&&deepStateRedirectsByName[stateName]===undefined&&(deepStateRedirectsByName[stateName]=ANCESTOR_REDIRECT)}return deepStateRedirectsByName[stateName]||!1}var lastSubstate={},lastParams={},deepStateRedirectsByName={},REDIRECT="Redirect",ANCESTOR_REDIRECT="AncestorRedirect";return $rootScope.$on("$stateChangeStart",function(event,toState){function shouldRedirect(){var deepStateStatus=computeDeepStateStatus(toState),substate=lastSubstate[toState.name];return!ignoreDsr&&deepStateStatus===REDIRECT&&substate&&substate!=toState.name?!0:!1}shouldRedirect()&&(event.preventDefault(),$state.go(lastSubstate[toState.name],lastParams[toState.name]))}),$rootScope.$on("$stateChangeSuccess",function(event,toState,toParams){var deepStateStatus=computeDeepStateStatus(toState);if(deepStateStatus){var name=toState.name;angular.forEach(lastSubstate,function(deepState,redirectState){(name==redirectState||-1!=name.indexOf(redirectState+"."))&&(lastSubstate[redirectState]=name,lastParams[redirectState]=angular.copy(toParams))})}}),{}}]),app.run(["$deepStateRedirect",function(){}]),$StickyStateProvider.$inject=["$stateProvider"],angular.module("ct.ui.router.extras").provider("$stickyState",$StickyStateProvider);var _StickyState,root,pendingRestore,inactivePseudoState,internalStates={},pendingTransitions=[];angular.module("ct.ui.router.extras").run(["$stickyState",function($stickyState){_StickyState=$stickyState}]),angular.module("ct.ui.router.extras").config(["$provide","$stateProvider","$stickyStateProvider",function($provide,$stateProvider,$stickyStateProvider){inactivePseudoState=angular.extend(new SurrogateState("__inactives"),{self:{name:"__inactives"}}),root=pendingRestore=undefined,pendingTransitions=[],$stateProvider.decorator("parent",function(state,parentFn){return root===undefined&&(root=parentFn({}),root.parent=inactivePseudoState,inactivePseudoState.parent=undefined,inactivePseudoState.locals=undefined),internalStates[state.self.name]=state,state.self.$$state=function(){return internalStates[state.self.name]},state.self.sticky===!0&&$stickyStateProvider.registerStickyState(state.self),parentFn(state)});var $state_transitionTo;$provide.decorator("$state",["$delegate","$log","$q",function($state,$log,$q){return $state_transitionTo=$state.transitionTo,$state.transitionTo=function(to,toParams,options){function stateReactivatedSurrogatePhase1(state){var surrogate=angular.extend(new SurrogateState("reactivate_phase1"),{locals:state.locals});return surrogate.self=angular.extend({},state.self),surrogate}function stateReactivatedSurrogatePhase2(state){var surrogate=angular.extend(new SurrogateState("reactivate_phase2"),state),oldOnEnter=surrogate.self.onEnter;return surrogate.resolve={},surrogate.views={},surrogate.self.onEnter=function(){surrogate.locals=state.locals,_StickyState.stateReactivated(state)},restore.addRestoreFunction(function(){state.self.onEnter=oldOnEnter}),surrogate}function stateInactivatedSurrogate(state){var surrogate=new SurrogateState("inactivate");surrogate.self=state.self;var oldOnExit=state.self.onExit;return surrogate.self.onExit=function(){_StickyState.stateInactivated(state)},restore.addRestoreFunction(function(){state.self.onExit=oldOnExit}),surrogate}function stateEnteredSurrogate(state,toParams){var oldOnEnter=state.self.onEnter;return state.self.onEnter=function(){_StickyState.stateEntering(state,toParams,oldOnEnter)},restore.addRestoreFunction(function(){state.self.onEnter=oldOnEnter}),state}function stateExitedSurrogate(state){var oldOnExit=state.self.onExit;return state.self.onExit=function(){_StickyState.stateExiting(state,exited,oldOnExit)},restore.addRestoreFunction(function(){state.self.onExit=oldOnExit}),state}inactivePseudoState.locals||(inactivePseudoState.locals=root.locals);var idx=pendingTransitions.length;pendingRestore&&(pendingRestore(),DEBUG&&$log.debug("Restored paths from pending transition"));var savedToStatePath,savedFromStatePath,stickyTransitions,terminalReactivatedState,fromState=$state.$current,fromParams=$state.params,rel=options&&options.relative||$state.$current,toStateSelf=$state.get(to,rel),reactivated=[],exited=[],noop=function(){},restore=function(){savedToStatePath&&(toState.path=savedToStatePath,savedToStatePath=null),savedFromStatePath&&(fromState.path=savedFromStatePath,savedFromStatePath=null),angular.forEach(restore.restoreFunctions,function(restoreFunction){restoreFunction()}),restore=noop,pendingRestore=null,pendingTransitions.splice(idx,1)};if(restore.restoreFunctions=[],restore.addRestoreFunction=function(fn){this.restoreFunctions.push(fn)},toStateSelf){var toState=internalStates[toStateSelf.name];if(toState){savedToStatePath=toState.path,savedFromStatePath=fromState.path;var currentTransition={toState:toState,toParams:toParams||{},fromState:fromState,fromParams:fromParams||{},options:options};pendingTransitions.push(currentTransition),pendingRestore=restore,stickyTransitions=_StickyState.processTransition(currentTransition),DEBUG&&debugTransition($log,currentTransition,stickyTransitions);var surrogateToPath=toState.path.slice(0,stickyTransitions.keep),surrogateFromPath=fromState.path.slice(0,stickyTransitions.keep);angular.forEach(inactivePseudoState.locals,function(local,name){delete inactivePseudoState.locals[name]});for(var i=0;i>>0,from=Number(arguments[2])||0;for(from=0>from?Math.ceil(from):Math.floor(from),0>from&&(from+=len);len>from;from++)if(from in array&&array[from]===value)return from;return-1}function inheritParams(currentParams,newParams,$current,$to){var parentParams,parents=ancestors($current,$to),inherited={},inheritList=[];for(var i in parents)if(parents[i].params&&(parentParams=isArray(parents[i].params)?parents[i].params:objectKeys(parents[i].params),parentParams.length))for(var j in parentParams)arraySearch(inheritList,parentParams[j])>=0||(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 resetIgnoreDsr(){ignoreDsr=undefined}function $StickyStateProvider(){var inactiveStates={},stickyStates={};this.registerStickyState=function(state){stickyStates[state.name]=state},this.enableDebug=function(enabled){DEBUG=enabled},this.$get=["$rootScope","$state","$stateParams","$injector","$log",function($rootScope,$state,$stateParams,$injector,$log){function mapInactives(){var mappedStates={};return angular.forEach(inactiveStates,function(state){for(var stickyAncestors=getStickyStateStack(state),i=0;i "+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}));for(var viewMsg=function(local,name){return"'"+name+"' ("+local.$$state.name+")"},statesOnly=function(local,name){return"globals"!=name&&"resolve"!=name},viewsForState=function(state){var views=map(filterObj(state.locals,statesOnly),viewMsg).join(", ");return"("+(state.self.name?state.self.name:"root")+".locals"+(views.length?": "+views:"")+")"},message=viewsForState(currentState),parent=currentState.parent;parent&&parent!==currentState;)""===parent.self.name&&(message=viewsForState($state.$current.path[0])+" / "+message),message=viewsForState(parent)+" / "+message,currentState=parent,parent=currentState.parent;$log.debug("Views: "+message)}angular.module("ct.ui.router.extras",["ui.router"]);var ignoreDsr,DEBUG=!1,forEach=angular.forEach,extend=angular.extend,isArray=angular.isArray,map=function(collection,callback){"use strict";var result=[];return forEach(collection,function(item,index){result.push(callback(item,index))}),result},filterObj=function(collection,callback){"use strict";var result={};return forEach(collection,function(item,index){callback(item,index)&&(result[index]=item)}),result};angular.module("ct.ui.router.extras").config(["$provide",function($provide){var $state_transitionTo;$provide.decorator("$state",["$delegate","$q",function($state,$q){return $state_transitionTo=$state.transitionTo,$state.transitionTo=function(to,toParams,options){return options.ignoreDsr&&(ignoreDsr=options.ignoreDsr),$state_transitionTo.apply($state,arguments).then(function(result){return resetIgnoreDsr(),result},function(err){return resetIgnoreDsr(),$q.reject(err)})},$state}])}]),angular.module("ct.ui.router.extras").service("$deepStateRedirect",["$rootScope","$state","$injector",function($rootScope,$state,$injector){function computeDeepStateStatus(state){var name=state.name;return deepStateRedirectsByName.hasOwnProperty(name)?deepStateRedirectsByName[name]:void recordDeepStateRedirectStatus(name)}function recordDeepStateRedirectStatus(stateName){var state=$state.get(stateName);state&&state.deepStateRedirect&&(deepStateRedirectsByName[stateName]=REDIRECT,lastSubstate[stateName]===undefined&&(lastSubstate[stateName]=stateName));var lastDot=stateName.lastIndexOf(".");if(-1!=lastDot){var parentStatus=recordDeepStateRedirectStatus(stateName.substr(0,lastDot));parentStatus&&deepStateRedirectsByName[stateName]===undefined&&(deepStateRedirectsByName[stateName]=ANCESTOR_REDIRECT)}return deepStateRedirectsByName[stateName]||!1}var lastSubstate={},lastParams={},deepStateRedirectsByName={},REDIRECT="Redirect",ANCESTOR_REDIRECT="AncestorRedirect";return $rootScope.$on("$stateChangeStart",function(event,toState){function shouldRedirect(){if(ignoreDsr)return!1;var deepStateStatus=computeDeepStateStatus(toState),substate=lastSubstate[toState.name],isDSR=deepStateStatus===REDIRECT&&substate&&substate!=toState.name?!0:!1;return isDSR&&angular.isFunction(toState.deepStateRedirect)?$injector.invoke(toState.deepStateRedirect,toState):isDSR}shouldRedirect()&&(event.preventDefault(),$state.go(lastSubstate[toState.name],lastParams[toState.name]))}),$rootScope.$on("$stateChangeSuccess",function(event,toState,toParams){var deepStateStatus=computeDeepStateStatus(toState);if(deepStateStatus){var name=toState.name;angular.forEach(lastSubstate,function(deepState,redirectState){(name==redirectState||-1!=name.indexOf(redirectState+"."))&&(lastSubstate[redirectState]=name,lastParams[redirectState]=angular.copy(toParams))})}}),{}}]),angular.module("ct.ui.router.extras").run(["$deepStateRedirect",function(){}]),$StickyStateProvider.$inject=["$stateProvider"],angular.module("ct.ui.router.extras").provider("$stickyState",$StickyStateProvider);var _StickyState,root,pendingRestore,inactivePseudoState,internalStates={},pendingTransitions=[];angular.module("ct.ui.router.extras").run(["$stickyState",function($stickyState){_StickyState=$stickyState}]),angular.module("ct.ui.router.extras").config(["$provide","$stateProvider","$stickyStateProvider",function($provide,$stateProvider,$stickyStateProvider){inactivePseudoState=angular.extend(new SurrogateState("__inactives"),{self:{name:"__inactives"}}),root=pendingRestore=undefined,pendingTransitions=[],$stateProvider.decorator("parent",function(state,parentFn){return internalStates[state.self.name]=state,state.self.$$state=function(){return internalStates[state.self.name]},state.self.sticky===!0&&$stickyStateProvider.registerStickyState(state.self),parentFn(state)});var $state_transitionTo;$provide.decorator("$state",["$delegate","$log","$q",function($state,$log,$q){return root=$state.$current,root.parent=inactivePseudoState,inactivePseudoState.parent=undefined,root.locals=inherit(inactivePseudoState.locals,root.locals),delete inactivePseudoState.locals.globals,$state_transitionTo=$state.transitionTo,$state.transitionTo=function(to,toParams,options){function stateReactivatedSurrogatePhase1(state){var surrogate=angular.extend(new SurrogateState("reactivate_phase1"),{locals:state.locals});return surrogate.self=angular.extend({},state.self),surrogate}function stateReactivatedSurrogatePhase2(state){var surrogate=angular.extend(new SurrogateState("reactivate_phase2"),state),oldOnEnter=surrogate.self.onEnter;return surrogate.resolve={},surrogate.views={},surrogate.self.onEnter=function(){surrogate.locals=state.locals,_StickyState.stateReactivated(state)},restore.addRestoreFunction(function(){state.self.onEnter=oldOnEnter}),surrogate}function stateInactivatedSurrogate(state){var surrogate=new SurrogateState("inactivate");surrogate.self=state.self;var oldOnExit=state.self.onExit;return surrogate.self.onExit=function(){_StickyState.stateInactivated(state)},restore.addRestoreFunction(function(){state.self.onExit=oldOnExit}),surrogate}function stateEnteredSurrogate(state,toParams){var oldOnEnter=state.self.onEnter;return state.self.onEnter=function(){_StickyState.stateEntering(state,toParams,oldOnEnter)},restore.addRestoreFunction(function(){state.self.onEnter=oldOnEnter}),state}function stateExitedSurrogate(state){var oldOnExit=state.self.onExit;return state.self.onExit=function(){_StickyState.stateExiting(state,exited,oldOnExit)},restore.addRestoreFunction(function(){state.self.onExit=oldOnExit}),state}inactivePseudoState.locals||(inactivePseudoState.locals=root.locals);var idx=pendingTransitions.length;pendingRestore&&(pendingRestore(),DEBUG&&$log.debug("Restored paths from pending transition"));var savedToStatePath,savedFromStatePath,stickyTransitions,terminalReactivatedState,fromState=$state.$current,fromParams=$state.params,rel=options&&options.relative||$state.$current,toStateSelf=$state.get(to,rel),reactivated=[],exited=[],noop=function(){},restore=function(){savedToStatePath&&(toState.path=savedToStatePath,savedToStatePath=null),savedFromStatePath&&(fromState.path=savedFromStatePath,savedFromStatePath=null),angular.forEach(restore.restoreFunctions,function(restoreFunction){restoreFunction()}),restore=noop,pendingRestore=null,pendingTransitions.splice(idx,1)};if(restore.restoreFunctions=[],restore.addRestoreFunction=function(fn){this.restoreFunctions.push(fn)},toStateSelf){var toState=internalStates[toStateSelf.name];if(toState){savedToStatePath=toState.path,savedFromStatePath=fromState.path;var currentTransition={toState:toState,toParams:toParams||{},fromState:fromState,fromParams:fromParams||{},options:options};pendingTransitions.push(currentTransition),pendingRestore=restore,stickyTransitions=_StickyState.processTransition(currentTransition),DEBUG&&debugTransition($log,currentTransition,stickyTransitions);var surrogateToPath=toState.path.slice(0,stickyTransitions.keep),surrogateFromPath=fromState.path.slice(0,stickyTransitions.keep);angular.forEach(inactivePseudoState.locals,function(local,name){-1!=name.indexOf("@")&&delete inactivePseudoState.locals[name]});for(var i=0;i