From e3cc6a2555c364e975e97e33c03b70b775c67040 Mon Sep 17 00:00:00 2001 From: Pavel Gonzales Date: Mon, 6 Mar 2017 16:29:07 +0300 Subject: [PATCH] patch ct-ui-router-extras --- .gitignore | 2 +- .vscode/settings.json | 3 + build/ct-ui-router-extras.js | 1965 +++++++++++++++++ build/ct-ui-router-extras.min.js | 2 + build/modular/ct-ui-router-extras.core.js | 181 ++ build/modular/ct-ui-router-extras.core.min.js | 2 + build/modular/ct-ui-router-extras.dsr.js | 185 ++ build/modular/ct-ui-router-extras.dsr.min.js | 2 + build/modular/ct-ui-router-extras.future.js | 326 +++ .../modular/ct-ui-router-extras.future.min.js | 2 + build/modular/ct-ui-router-extras.previous.js | 65 + .../ct-ui-router-extras.previous.min.js | 2 + build/modular/ct-ui-router-extras.statevis.js | 194 ++ .../ct-ui-router-extras.statevis.min.js | 2 + build/modular/ct-ui-router-extras.sticky.js | 944 ++++++++ .../modular/ct-ui-router-extras.sticky.min.js | 2 + .../modular/ct-ui-router-extras.transition.js | 107 + .../ct-ui-router-extras.transition.min.js | 2 + src/transition.js | 2 +- 19 files changed, 3988 insertions(+), 2 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 build/ct-ui-router-extras.js create mode 100644 build/ct-ui-router-extras.min.js create mode 100644 build/modular/ct-ui-router-extras.core.js create mode 100644 build/modular/ct-ui-router-extras.core.min.js create mode 100644 build/modular/ct-ui-router-extras.dsr.js create mode 100644 build/modular/ct-ui-router-extras.dsr.min.js create mode 100644 build/modular/ct-ui-router-extras.future.js create mode 100644 build/modular/ct-ui-router-extras.future.min.js create mode 100644 build/modular/ct-ui-router-extras.previous.js create mode 100644 build/modular/ct-ui-router-extras.previous.min.js create mode 100644 build/modular/ct-ui-router-extras.statevis.js create mode 100644 build/modular/ct-ui-router-extras.statevis.min.js create mode 100644 build/modular/ct-ui-router-extras.sticky.js create mode 100644 build/modular/ct-ui-router-extras.sticky.min.js create mode 100644 build/modular/ct-ui-router-extras.transition.js create mode 100644 build/modular/ct-ui-router-extras.transition.min.js diff --git a/.gitignore b/.gitignore index fde8a71..93499fd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,11 +11,11 @@ *.ipr *.iws *.DS_Store +.vscode lib-cov pids logs results -build test/temp.js node_modules diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a8d6186 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "vsicons.presets.angular": false +} \ No newline at end of file diff --git a/build/ct-ui-router-extras.js b/build/ct-ui-router-extras.js new file mode 100644 index 0000000..65b9b18 --- /dev/null +++ b/build/ct-ui-router-extras.js @@ -0,0 +1,1965 @@ +/** + * UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise + * Monolithic build (all modules) + * @version 0.1.3 + * @link http://christopherthielen.github.io/ui-router-extras/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function (root, factory) { + 'use strict'; + + if (typeof define === 'function' && define.amd) { + define(['angular'], function (angular) { + factory(angular); + }); + } else if (typeof exports === 'object') { + factory(require('angular')); + } else { + factory(root.angular); + } +}(this, function (angular, undefined) { + var mod_core = angular.module("ct.ui.router.extras.core", [ "ui.router" ]); + +var internalStates = {}, stateRegisteredCallbacks = []; +mod_core.config([ '$stateProvider', '$injector', function ($stateProvider, $injector) { + // Decorate any state attribute in order to get access to the internal state representation. + $stateProvider.decorator('parent', function (state, parentFn) { + // Capture each internal UI-Router state representations as opposed to the user-defined state object. + // The internal state is, e.g., the state returned by $state.$current as opposed to $state.current + internalStates[state.self.name] = state; + // Add an accessor for the internal state from the user defined state + state.self.$$state = function () { + return internalStates[state.self.name]; + }; + + angular.forEach(stateRegisteredCallbacks, function(callback) { callback(state); }); + return parentFn(state); + }); +}]); + +var DEBUG = false; + +var forEach = angular.forEach; +var extend = angular.extend; +var isArray = angular.isArray; + +var map = function (collection, callback) { + "use strict"; + var result = []; + forEach(collection, function (item, index) { + result.push(callback(item, index)); + }); + return result; +}; + +var keys = function (collection) { + "use strict"; + return map(collection, function (collection, key) { + return key; + }); +}; + +var filter = function (collection, callback) { + "use strict"; + var result = []; + forEach(collection, function (item, index) { + if (callback(item, index)) { + result.push(item); + } + }); + return result; +}; + +var filterObj = function (collection, callback) { + "use strict"; + var result = {}; + forEach(collection, function (item, index) { + if (callback(item, index)) { + result[index] = item; + } + }); + return result; +}; + +// Duplicates code in UI-Router common.js +function ancestors(first, second) { + var path = []; + + for (var n in first.path) { + if (first.path[n] !== second.path[n]) break; + path.push(first.path[n]); + } + return path; +} + +// Duplicates code in UI-Router common.js +function objectKeys(object) { + if (Object.keys) { + return Object.keys(object); + } + var result = []; + + angular.forEach(object, function (val, key) { + result.push(key); + }); + return result; +} + +/** + * like objectKeys, but includes keys from prototype chain. + * @param object the object whose prototypal keys will be returned + * @param ignoreKeys an array of keys to ignore + */ +// Duplicates code in UI-Router common.js +function protoKeys(object, ignoreKeys) { + var result = []; + for (var key in object) { + if (!ignoreKeys || ignoreKeys.indexOf(key) === -1) + result.push(key); + } + return result; +} + +// Duplicates code in UI-Router common.js +function arraySearch(array, value) { + if (Array.prototype.indexOf) { + return array.indexOf(value, Number(arguments[2]) || 0); + } + var len = array.length >>> 0, from = Number(arguments[2]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + + if (from < 0) from += len; + + for (; from < len; from++) { + if (from in array && array[from] === value) return from; + } + return -1; +} + +// Duplicates code in UI-Router common.js +// Added compatibility code (isArray check) to support both 0.2.x and 0.3.x series of UI-Router. +function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i].params) continue; + // This test allows compatibility with 0.2.x and 0.3.x (optional and object params) + parentParams = isArray(parents[i].params) ? parents[i].params : objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (arraySearch(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); +} + +function inherit(parent, extra) { + return extend(new (extend(function () { }, {prototype: parent}))(), extra); +} + +function onStateRegistered(callback) { stateRegisteredCallbacks.push(callback); } + +mod_core.provider("uirextras_core", function() { + var core = { + internalStates: internalStates, + onStateRegistered: onStateRegistered, + forEach: forEach, + extend: extend, + isArray: isArray, + map: map, + keys: keys, + filter: filter, + filterObj: filterObj, + ancestors: ancestors, + objectKeys: objectKeys, + protoKeys: protoKeys, + arraySearch: arraySearch, + inheritParams: inheritParams, + inherit: inherit + }; + + angular.extend(this, core); + + this.$get = function() { + return core; + }; +}); + + +var ignoreDsr; +function resetIgnoreDsr() { + ignoreDsr = undefined; +} + +// Decorate $state.transitionTo to gain access to the last transition.options variable. +// This is used to process the options.ignoreDsr option +angular.module('ct.ui.router.extras.dsr', [ 'ct.ui.router.extras.core' ]).config([ "$provide", function ($provide) { + var $state_transitionTo; + $provide.decorator("$state", ['$delegate', '$q', function ($state, $q) { + $state_transitionTo = $state.transitionTo; + $state.transitionTo = function (to, toParams, options) { + if (options && options.ignoreDsr) { + ignoreDsr = options.ignoreDsr; + } + + return $state_transitionTo.apply($state, arguments).then( + function (result) { + resetIgnoreDsr(); + return result; + }, + function (err) { + resetIgnoreDsr(); + return $q.reject(err); + } + ); + }; + return $state; + }]); +}]); + +angular.module('ct.ui.router.extras.dsr').service("$deepStateRedirect", [ '$rootScope', '$state', '$injector', function ($rootScope, $state, $injector) { + var lastSubstate = {}; + var deepStateRedirectsByName = {}; + + var REDIRECT = "Redirect", ANCESTOR_REDIRECT = "AncestorRedirect"; + + function computeDeepStateStatus(state) { + var name = state.name; + if (deepStateRedirectsByName.hasOwnProperty(name)) + return deepStateRedirectsByName[name]; + recordDeepStateRedirectStatus(name); + } + + function getConfig(state) { + var declaration = state.deepStateRedirect || state.dsr; + if (!declaration) return { dsr: false }; + var dsrCfg = { dsr: true }; + + if (angular.isFunction(declaration)) { + dsrCfg.fn = declaration; + } else if (angular.isObject(declaration)) { + dsrCfg = angular.extend(dsrCfg, declaration); + } + + if (angular.isString(dsrCfg['default'])) { + dsrCfg['default'] = { state: dsrCfg['default'] }; + } + + if (!dsrCfg.fn) { + dsrCfg.fn = [ '$dsr$', function($dsr$) { + return $dsr$.redirect.state != $dsr$.to.state; + } ]; + } + return dsrCfg; + } + + function recordDeepStateRedirectStatus(stateName) { + var state = $state.get(stateName); + if (!state) return false; + var cfg = getConfig(state); + if (cfg.dsr) { + deepStateRedirectsByName[state.name] = REDIRECT; + if (lastSubstate[stateName] === undefined) + lastSubstate[stateName] = {}; + } + + var parent = state.$$state && state.$$state().parent; + if (parent) { + var parentStatus = recordDeepStateRedirectStatus(parent.self.name); + if (parentStatus && deepStateRedirectsByName[state.name] === undefined) { + deepStateRedirectsByName[state.name] = ANCESTOR_REDIRECT; + } + } + return deepStateRedirectsByName[state.name] || false; + } + + function getMatchParams(params, dsrParams) { + if (dsrParams === true) dsrParams = Object.keys(params); + if (dsrParams === null || dsrParams === undefined) dsrParams = []; + + var matchParams = {}; + angular.forEach(dsrParams.sort(), function(name) { matchParams[name] = params[name]; }); + return matchParams; + } + + function getParamsString(params, dsrParams) { + var matchParams = getMatchParams(params, dsrParams); + function safeString(input) { return !input ? input : input.toString(); } + var paramsToString = {}; + angular.forEach(matchParams, function(val, name) { paramsToString[name] = safeString(val); }); + return angular.toJson(paramsToString); + } + + $rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) { + var cfg = getConfig(toState); + if (ignoreDsr || (computeDeepStateStatus(toState) !== REDIRECT) && !cfg['default']) return; + // We're changing directly to one of the redirect (tab) states. + // Get the DSR key for this state by calculating the DSRParams option + var key = getParamsString(toParams, cfg.params); + var redirect = lastSubstate[toState.name][key] || cfg['default']; + if (!redirect) return; + + // we have a last substate recorded + var $dsr$ = { redirect: { state: redirect.state, params: redirect.params}, to: { state: toState.name, params: toParams } }; + var result = $injector.invoke(cfg.fn, toState, { $dsr$: $dsr$ }); + if (!result) return; + if (result.state) redirect = result; + event.preventDefault(); + var redirectParams = getMatchParams(toParams, cfg.params); + $state.go(redirect.state, angular.extend(redirectParams, redirect.params)); + }); + + $rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) { + var deepStateStatus = computeDeepStateStatus(toState); + if (deepStateStatus) { + var name = toState.name; + angular.forEach(lastSubstate, function (redirect, dsrState) { + // update Last-SubState¶ms for each DSR that this transition matches. + var cfg = getConfig($state.get(dsrState)); + var key = getParamsString(toParams, cfg.params); + if (toState.$$state().includes[dsrState]) { + lastSubstate[dsrState][key] = { state: name, params: angular.copy(toParams) }; + } + }); + } + }); + + return { + getRedirect: function(dsrState, params) { + var state = $state.get(dsrState); + computeDeepStateStatus(state) + var cfg = getConfig(state); + var key = getParamsString(params, cfg.params); + var redirect = lastSubstate[state.name]; + if (redirect && redirect[key]) { + redirect = redirect[key]; + } else { + redirect = cfg['default']; + } + return redirect; + }, + reset: function(stateOrName, params) { + if (!stateOrName) { + angular.forEach(lastSubstate, function(redirect, dsrState) { lastSubstate[dsrState] = {}; }); + } else { + var state = $state.get(stateOrName); + if (!state) throw new Error("Unknown state: " + stateOrName); + if (lastSubstate[state.name]) { + if (params) { + var key = getParamsString(params, getConfig(state).params); + delete lastSubstate[state.name][key]; + } else { + lastSubstate[state.name] = {}; + } + } + } + } + }; +}]); + +angular.module('ct.ui.router.extras.dsr').run(['$deepStateRedirect', function ($deepStateRedirect) { + // Make sure $deepStateRedirect is instantiated +}]); + +angular.module("ct.ui.router.extras.sticky", [ 'ct.ui.router.extras.core' ]); + +var mod_sticky = angular.module("ct.ui.router.extras.sticky"); + +$StickyStateProvider.$inject = [ '$stateProvider', 'uirextras_coreProvider' ]; +function $StickyStateProvider($stateProvider, uirextras_coreProvider) { + var core = uirextras_coreProvider; + var inheritParams = core.inheritParams; + var objectKeys = core.objectKeys; + var protoKeys = core.protoKeys; + var forEach = core.forEach; + var map = core.map; + + // Holds all the states which are inactivated. Inactivated states can be either sticky states, or descendants of sticky states. + var inactiveStates = {}; // state.name -> (state) + var stickyStates = {}; // state.name -> true + var $state; + var DEBUG = false; + + // Called by $stateProvider.registerState(); + // registers a sticky state with $stickyStateProvider + this.registerStickyState = function (state) { + stickyStates[state.name] = state; + // console.log("Registered sticky state: ", state); + }; + + this.enableDebug = this.debugMode = function (enabled) { + if (angular.isDefined(enabled)) + DEBUG = enabled; + return DEBUG; + }; + + this.$get = [ '$rootScope', '$state', '$stateParams', '$injector', '$log', + function ($rootScope, $state, $stateParams, $injector, $log) { + // Each inactive states is either a sticky state, or a child of a sticky state. + // This function finds the closest ancestor sticky state, then find that state's parent. + // Map all inactive states to their closest parent-to-sticky state. + function mapInactives() { + var mappedStates = {}; + angular.forEach(inactiveStates, function (state, name) { + var stickyAncestors = getStickyStateStack(state); + for (var i = 0; i < stickyAncestors.length; i++) { + var parent = stickyAncestors[i].parent; + mappedStates[parent.name] = mappedStates[parent.name] || []; + mappedStates[parent.name].push(state); + } + if (mappedStates['']) { + // This is necessary to compute Transition.inactives when there are sticky states are children to root state. + mappedStates['__inactives'] = mappedStates['']; // jshint ignore:line + } + }); + return mappedStates; + } + + function mapInactivesByImmediateParent() { + var inactivesByAllParents ={}; + forEach(inactiveStates, function(state) { + forEach(state.path, function(ancestor) { + if (ancestor === state) return; + inactivesByAllParents[ancestor.name] = inactivesByAllParents[ancestor.name] || []; + inactivesByAllParents[ancestor.name].push(state); + }); + }); + return inactivesByAllParents; + } + + // Given a state, returns all ancestor states which are sticky. + // Walks up the view's state's ancestry tree and locates each ancestor state which is marked as sticky. + // Returns an array populated with only those ancestor sticky states. + function getStickyStateStack(state) { + var stack = []; + if (!state) return stack; + do { + if (state.sticky) stack.push(state); + state = state.parent; + } while (state); + stack.reverse(); + return stack; + } + + // Returns a sticky transition type necessary to enter the state. + // Transition can be: reactivate, reload, or enter + + // Note: if a state is being reactivated but params dont match, we treat + // it as a Exit/Enter, thus the special "reload" transition. + // If a parent inactivated state has "reload" transition type, then + // all descendant states must also be exit/entered, thus the first line of this function. + function getEnterTransition(state, stateParams, reloadStateTree, ancestorReloaded) { + if (ancestorReloaded) return "reload"; + var inactiveState = inactiveStates[state.self.name]; + if (!inactiveState) return "enter"; + if (state.self === reloadStateTree) return "reload"; + var paramsMatch = paramsEqualForState(state.ownParams, stateParams, inactiveState.locals.globals.$stateParams); + return paramsMatch ? "reactivate" : "reload"; + } + + // Given a state and (optional) stateParams, returns the inactivated state from the inactive sticky state registry. + function getInactivatedState(state, stateParams) { + var inactiveState = inactiveStates[state.name]; + if (!inactiveState) return null; + if (!stateParams) return inactiveState; + var paramsMatch = paramsEqualForState(state.ownParams, stateParams, inactiveState.locals.globals.$stateParams); + return paramsMatch ? inactiveState : null; + } + + function paramsEqualForState(ownParams, stateParams, stateParams2) { + if (typeof ownParams.$$equals === 'function') + return ownParams.$$equals(stateParams, stateParams2); + return equalForKeys(stateParams, stateParams2, ownParams); + } + + // Duplicates logic in $state.transitionTo, primarily to find the pivot state (i.e., the "keep" value) + function equalForKeys(a, b, keys) { + if (!angular.isArray(keys) && angular.isObject(keys)) { + keys = protoKeys(keys, ["$$keys", "$$values", "$$equals", "$$validates", "$$new", "$$parent"]); + } + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized + } + return true; + } + + function calcTreeChanges(transition) { + var fromPath = transition.fromState.path; + var toPath = transition.toState.path; + var toParams = transition.toParams; + var keep = 0, state = toPath[keep]; + + if (transition.options && transition.options.inherit) { + toParams = transition.toParams = + inheritParams($stateParams, toParams || {}, $state.$current, transition.toState); + } + + while (state && state === fromPath[keep] && paramsEqualForState(state.ownParams, toParams, transition.fromParams)) { + // We're "keeping" this state. bump keep var and get the next state in toPath for the next iteration. + state = toPath[++keep]; + } + + return { + keep: keep, + retained: fromPath.slice(0, keep), + exiting: fromPath.slice(keep), + entering: toPath.slice(keep) + }; + } + + function sortByStateDepth(a,b) { + return a.name.split(".").length - b.name.split(".").length; + } + + var stickySupport = { + getInactiveStates: function () { + return map(inactiveStates, angular.identity).sort(sortByStateDepth); + }, + getInactiveStatesByParent: function () { + return mapInactives(); + }, + // Main API for $stickyState, used by $state. + // Processes a potential transition, returns an object with the following attributes: + // { + // keep: The number of states being "kept" + // inactives: Array of all states which will be inactive if the transition is completed. + // reactivatingStates: Array of all states which will be reactivated if the transition is completed. + // orphans: Array of previously inactive states, which are being orphaned by the transition + // Note: Transitioning directly to an inactive state with inactive children will reactivate the state, but exit all the inactive children. + // enter: Enter transition type for all added states. This is a parallel array to "toStates" array in $state.transitionTo. + // exit: Exit transition type for all removed states. This is a parallel array to "fromStates" array in $state.transitionTo. + // } + processTransition: function (transition) { + var treeChanges = calcTreeChanges(transition); + var currentInactives = stickySupport.getInactiveStates(); + var futureInactives, exitingTypes, enteringTypes; + var keep = treeChanges.keep; + + + ///////////////////////////////////////// + // helper functions + function notIn(array) { return function (elem) { return array.indexOf(elem) === -1; }; } + function flattenReduce(memo, list) { return memo.concat(list); } + function uniqReduce(memo, orphan) { if (notIn(memo)(orphan)) memo.push(orphan); return memo; } + function prop(attr) { return function(obj) { return obj[attr]; } } + function typeIs(type) { return function(obj) { return obj.type === type; } } + function isChildOf(state) { return function(other) { return other.parent === state; }; } + var notEntering = notIn(treeChanges.entering); + function notSticky(state) { return !state.sticky; } + //////////////////////////////////// + + + // Calculate the "exit" transition types for states being exited in fromPath + // Exit types will be either "inactivate" or "exit" + // Two things must be satisfied in order to inactivate the "exiting" states (instead of exit them): + // - The first element of the exiting path must be sticky + // - We must be entering any sibling state of the sticky (we can check this using entering.length) + var shouldInactivate = treeChanges.exiting[0] && treeChanges.exiting[0].sticky && treeChanges.entering.length > 0; + exitingTypes = treeChanges.exiting.map(function (state) { + var stateRentering = treeChanges.entering.indexOf(state) !== -1; + var type = shouldInactivate && !stateRentering ? "inactivate" : "exit"; + return { type: type, state: state }; + }); + + + // Calculate the "enter" transition types for states being entered in toPath + // Enter types will be either "enter", "reactivate", or "reload" where: + // enter: full resolve, no special logic + // reactivate: use previous locals + // reload: like 'enter', except exit the inactive state before entering it. + var reloaded = transition.options && !!transition.options.reload; + enteringTypes = treeChanges.entering.map(function(state) { + var type = getEnterTransition(state, transition.toParams, transition.reloadStateTree, reloaded); + reloaded = reloaded || type === 'reload'; + return { type: type, state: state }; + }); + + // Find all the "orphaned" states. those states that are : + // - are siblings of the entering states + // - previously inactive + // - are not being reactivated (entered) + // - are not sticky + // unioned with: + // - children of the toState + // - previously inactive + // + // Given: + // - states A (sticky: true), B, A.foo, A.bar + // - A.foo is currently inactive + // - B is currently active + // Orphan case 1) + // - Transition to A.bar orphans the inactive state A.foo; it should be exited + // Orphan case 2) + // - Transition directly to A orphans the inactive state A.foo; it should be exited + // + // Given: + // - states A (sticky: true), B, A.foo (sticky), A.bar + // - A.foo is currently inactive + // - B is currently active + // Orphan case 3) + // - Transition directly to A orphans the inactive sticky state A.foo; it should be exited + // Note: transition from B to A.bar does not orphan A.foo + // Note 2: each orphaned state might be the parent of a larger inactive subtree. + var orphanedRoots = treeChanges.entering + // For each entering state in the path, find all sibling states which are currently inactive + .map(function (entering) { return currentInactives.filter(isChildOf(entering.parent)); }) + // Flatten nested arrays. Now we have an array of inactive states that are children of the ones being entered. + .reduce(flattenReduce, []) + // Consider "orphaned": only those children that are themselves not currently being entered + .filter(notEntering) + // Consider "orphaned": only those children that are not themselves sticky states. + .filter(notSticky) + // Finally, union that set with any inactive children of the "to state" + .concat(currentInactives.filter(isChildOf(transition.toState))); + + var currentInactivesByParent = mapInactivesByImmediateParent(); + var allOrphans = orphanedRoots + .map(function(root) { return currentInactivesByParent[root.name] }) + .filter(angular.isDefined) + .reduce(flattenReduce, []) + .concat(orphanedRoots) + // Sort by depth to exit orphans in proper order + .sort(sortByStateDepth); + + // Add them to the list of states being exited. + var exitOrOrphaned = exitingTypes + .filter(typeIs("exit")) + .map(prop("state")) + .concat(allOrphans); + + // Now calculate the states that will be inactive if this transition succeeds. + // We have already pushed the transitionType == "inactivate" states to 'inactives'. + // Second, add all the existing inactive states + futureInactives = currentInactives + .filter(notIn(exitOrOrphaned)) + .filter(notIn(treeChanges.entering)) + .concat(exitingTypes.filter(typeIs("inactivate")).map(prop("state"))) + .sort(sortByStateDepth); + + return { + keep: keep, + enter: new Array(keep).concat(enteringTypes.map(prop("type"))), + exit: new Array(keep).concat(exitingTypes.map(prop("type"))), + inactives: futureInactives, + reactivatingStates: enteringTypes.filter(typeIs("reactivate")).map(prop("state")), + orphans: allOrphans + }; + }, + + // Adds a state to the inactivated sticky state registry. + stateInactivated: function (state) { + // Keep locals around. + inactiveStates[state.self.name] = state; + // Notify states they are being Inactivated (i.e., a different + // sticky state tree is now active). + state.self.status = 'inactive'; + if (state.self.onInactivate) + $injector.invoke(state.self.onInactivate, state.self, state.locals.globals); + }, + + // Removes a previously inactivated state from the inactive sticky state registry + stateReactivated: function (state) { + if (inactiveStates[state.self.name]) { + delete inactiveStates[state.self.name]; + } + state.self.status = 'entered'; +// if (state.locals == null || state.locals.globals == null) debugger; + if (state.self.onReactivate) + $injector.invoke(state.self.onReactivate, state.self, state.locals.globals); + }, + + // Exits all inactivated descendant substates when the ancestor state is exited. + // When transitionTo is exiting a state, this function is called with the state being exited. It checks the + // registry of inactivated states for descendants of the exited state and also exits those descendants. It then + // removes the locals and de-registers the state from the inactivated registry. + stateExiting: function (exiting, exitQueue, onExit) { + var exitingNames = {}; + angular.forEach(exitQueue, function (state) { + exitingNames[state.self.name] = true; + }); + + angular.forEach(inactiveStates, function (inactiveExiting, name) { + // TODO: Might need to run the inactivations in the proper depth-first order? + if (!exitingNames[name] && inactiveExiting.includes[exiting.name]) { + if (DEBUG) $log.debug("Exiting " + name + " because it's a substate of " + exiting.name + " and wasn't found in ", exitingNames); + if (inactiveExiting.self.onExit) + $injector.invoke(inactiveExiting.self.onExit, inactiveExiting.self, inactiveExiting.locals.globals); + angular.forEach(inactiveExiting.locals, function(localval, key) { + delete inactivePseudoState.locals[key]; + }); + inactiveExiting.locals = null; + inactiveExiting.self.status = 'exited'; + delete inactiveStates[name]; + } + }); + + if (onExit) + $injector.invoke(onExit, exiting.self, exiting.locals.globals); + exiting.locals = null; + exiting.self.status = 'exited'; + delete inactiveStates[exiting.self.name]; + }, + + // Removes a previously inactivated state from the inactive sticky state registry + stateEntering: function (entering, params, onEnter, updateParams) { + var inactivatedState = getInactivatedState(entering); + if (inactivatedState && (updateParams || !getInactivatedState(entering, params))) { + var savedLocals = entering.locals; + this.stateExiting(inactivatedState); + entering.locals = savedLocals; + } + entering.self.status = 'entered'; + + if (onEnter) + $injector.invoke(onEnter, entering.self, entering.locals.globals); + }, + reset: function reset(inactiveState, params) { + function resetOne(state) { stickySupport.reset(state); } + if (inactiveState === "*") { + angular.forEach(stickySupport.getInactiveStates(), resetOne); + return true; + } + var state = $state.get(inactiveState); + if (!state) return false; + var exiting = getInactivatedState(state, params); + if (!exiting) return false; + stickySupport.stateExiting(exiting); + $rootScope.$broadcast("$viewContentLoading"); + return true; + } + }; + + return stickySupport; + }]; +} + +mod_sticky.provider("$stickyState", $StickyStateProvider); + +/** + * Sticky States makes entire state trees "sticky". Sticky state trees are retained until their parent state is + * exited. This can be useful to allow multiple modules, peers to each other, each module having its own independent + * state tree. The peer modules can be activated and inactivated without any loss of their internal context, including + * DOM content such as unvalidated/partially filled in forms, and even scroll position. + * + * DOM content is retained by declaring a named ui-view in the parent state, and filling it in with a named view from the + * sticky state. + * + * Technical overview: + * + * ---PATHS--- + * UI-Router uses state paths to manage entering and exiting of individual states. Each state "A.B.C.X" has its own path, starting + * from the root state ("") and ending at the state "X". The path is composed the final state "X"'s ancestors, e.g., + * [ "", "A", "B", "C", "X" ]. + * + * When a transition is processed, the previous path (fromState.path) is compared with the requested destination path + * (toState.path). All states that the from and to paths have in common are "kept" during the transition. The last + * "kept" element in the path is the "pivot". + * + * ---VIEWS--- + * A View in UI-Router consists of a controller and a template. Each view belongs to one state, and a state can have many + * views. Each view plugs into a ui-view element in the DOM of one of the parent state's view(s). + * + * View context is managed in UI-Router using a 'state locals' concept. When a state's views are fully loaded, those views + * are placed on the states 'locals' object. Each locals object prototypally inherits from its parent state's locals object. + * This means that state "A.B.C.X"'s locals object also has all of state "A.B.C"'s locals as well as those from "A.B" and "A". + * The root state ("") defines no views, but it is included in the protypal inheritance chain. + * + * The locals object is used by the ui-view directive to load the template, render the content, create the child scope, + * initialize the controller, etc. The ui-view directives caches the locals in a closure variable. If the locals are + * identical (===), then the ui-view directive exits early, and does no rendering. + * + * In stock UI-Router, when a state is exited, that state's locals object is deleted and those views are cleaned up by + * the ui-view directive shortly. + * + * ---Sticky States--- + * UI-Router Extras keeps views for inactive states live, even when UI-Router thinks it has exited them. It does this + * by creating a pseudo state called "__inactives" that is the parent of the root state. It also then defines a locals + * object on the "__inactives" state, which the root state protoypally inherits from. By doing this, views for inactive + * states are accessible through locals object's protoypal inheritance chain from any state in the system. + * + * ---Transitions--- + * UI-Router Extras decorates the $state.transitionTo function. While a transition is in progress, the toState and + * fromState internal state representations are modified in order to coerce stock UI-Router's transitionTo() into performing + * the appropriate operations. When the transition promise is completed, the original toState and fromState values are + * restored. + * + * Stock UI-Router's $state.transitionTo function uses toState.path and fromState.path to manage entering and exiting + * states. UI-Router Extras takes advantage of those internal implementation details and prepares a toState.path and + * fromState.path which coerces UI-Router into entering and exiting the correct states, or more importantly, not entering + * and not exiting inactive or sticky states. It also replaces state.self.onEnter and state.self.onExit for elements in + * the paths when they are being inactivated or reactivated. + */ + + + +// ------------------------ Sticky State module-level variables ----------------------------------------------- +var _StickyState; // internal reference to $stickyStateProvider +var internalStates = {}; // Map { statename -> InternalStateObj } holds internal representation of all states +var root, // Root state, internal representation + pendingTransitions = [], // One transition may supersede another. This holds references to all pending transitions + pendingRestore, // The restore function from the superseded transition + inactivePseudoState, // This pseudo state holds all the inactive states' locals (resolved state data, such as views etc) + reactivatingLocals = { }, // This is a prent locals to the inactivePseudoState locals, used to hold locals for states being reactivated + versionHeuristics = { // Heuristics used to guess the current UI-Router Version + hasParamSet: false + }; + +// Creates a blank surrogate state +function SurrogateState(type) { + return { + resolve: { }, + locals: { + globals: root && root.locals && root.locals.globals + }, + views: { }, + self: { }, + params: { }, + ownParams: ( versionHeuristics.hasParamSet ? { $$equals: function() { return true; } } : []), + surrogateType: type + }; +} + +// ------------------------ Sticky State registration and initialization code ---------------------------------- +// Grab a copy of the $stickyState service for use by the transition management code +angular.module("ct.ui.router.extras.sticky").run(["$stickyState", function ($stickyState) { + _StickyState = $stickyState; +}]); + +angular.module("ct.ui.router.extras.sticky").config( + [ "$provide", "$stateProvider", '$stickyStateProvider', '$urlMatcherFactoryProvider', 'uirextras_coreProvider', + function ($provide, $stateProvider, $stickyStateProvider, $urlMatcherFactoryProvider, uirextras_coreProvider) { + var core = uirextras_coreProvider; + var internalStates = core.internalStates; + var inherit = core.inherit; + var inheritParams = core.inheritParams; + var forEach = core.forEach; + var map = core.map; + var filterObj = core.filterObj; + + versionHeuristics.hasParamSet = !!$urlMatcherFactoryProvider.ParamSet; + // inactivePseudoState (__inactives) holds all the inactive locals which includes resolved states data, i.e., views, scope, etc + inactivePseudoState = angular.extend(new SurrogateState("__inactives"), { self: { name: '__inactives' } }); + // Reset other module scoped variables. This is to primarily to flush any previous state during karma runs. + root = pendingRestore = undefined; + pendingTransitions = []; + + uirextras_coreProvider.onStateRegistered(function(state) { + // Register the ones marked as "sticky" + if (state.self.sticky === true) { + $stickyStateProvider.registerStickyState(state.self); + } + }); + + var $state_transitionTo; // internal reference to the real $state.transitionTo function + // Decorate the $state service, so we can decorate the $state.transitionTo() function with sticky state stuff. + $provide.decorator("$state", ['$delegate', '$log', '$q', function ($state, $log, $q) { + // Note: this code gets run only on the first state that is decorated + root = $state.$current; + internalStates[""] = root; + root.parent = inactivePseudoState; // Make inactivePsuedoState the parent of root. "wat" + inactivePseudoState.parent = undefined; // Make inactivePsuedoState the real root. + // Add another locals bucket, as a parent to inactivatePseudoState locals. + // This is for temporary storage of locals of states being reactivated while a transition is pending + // This is necessary in some cases where $viewContentLoading is triggered before the $state.$current is updated to the toState. + inactivePseudoState.locals = inherit(reactivatingLocals, inactivePseudoState.locals); + root.locals = inherit(inactivePseudoState.locals, root.locals); // make root locals extend the __inactives locals. + delete inactivePseudoState.locals.globals; + + // Hold on to the real $state.transitionTo in a module-scope variable. + $state_transitionTo = $state.transitionTo; + + // ------------------------ Decorated transitionTo implementation begins here --------------------------- + $state.transitionTo = function (to, toParams, options) { + var DEBUG = $stickyStateProvider.debugMode(); + // TODO: Move this to module.run? + // TODO: I'd rather have root.locals prototypally inherit from inactivePseudoState.locals + // Link root.locals and inactives.locals. Do this at runtime, after root.locals has been set. + if (!inactivePseudoState.locals) + inactivePseudoState.locals = root.locals; + var idx = pendingTransitions.length; + if (pendingRestore) { + pendingRestore(); + if (DEBUG) { + $log.debug("Restored paths from pending transition"); + } + } + + var fromState = $state.$current, fromParams = $state.params; + var rel = options && options.relative || $state.$current; // Not sure if/when $state.$current is appropriate here. + var toStateSelf = $state.get(to, rel); // exposes findState relative path functionality, returns state.self + var savedToStatePath, savedFromStatePath, stickyTransitions; + var reactivated = [], exited = [], terminalReactivatedState; + toParams = toParams || {}; + arguments[1] = toParams; + + var noop = function () { + }; + // Sticky states works by modifying the internal state objects of toState and fromState, especially their .path(s). + // The restore() function is a closure scoped function that restores those states' definitions to their original values. + var restore = function () { + if (savedToStatePath) { + toState.path = savedToStatePath; + savedToStatePath = null; + } + + if (savedFromStatePath) { + fromState.path = savedFromStatePath; + savedFromStatePath = null; + } + + angular.forEach(restore.restoreFunctions, function (restoreFunction) { + restoreFunction(); + }); + // Restore is done, now set the restore function to noop in case it gets called again. + restore = noop; + // pendingRestore keeps track of a transition that is in progress. It allows the decorated transitionTo + // method to be re-entrant (for example, when superceding a transition, i.e., redirect). The decorated + // transitionTo checks right away if there is a pending transition in progress and restores the paths + // if so using pendingRestore. + pendingRestore = null; + pendingTransitions.splice(idx, 1); // Remove this transition from the list + }; + + // All decorated transitions have their toState.path and fromState.path replaced. Surrogate states also make + // additional changes to the states definition before handing the transition off to UI-Router. In particular, + // certain types of surrogate states modify the state.self object's onEnter or onExit callbacks. + // Those surrogate states must then register additional restore steps using restore.addRestoreFunction(fn) + restore.restoreFunctions = []; + restore.addRestoreFunction = function addRestoreFunction(fn) { + this.restoreFunctions.push(fn); + }; + + + // --------------------- Surrogate State Functions ------------------------ + // During a transition, the .path arrays in toState and fromState are replaced. Individual path elements + // (states) which aren't being "kept" are replaced with surrogate elements (states). This section of the code + // has factory functions for all the different types of surrogate states. + + + function stateReactivatedSurrogatePhase1(state) { + var surrogate = angular.extend(new SurrogateState("reactivate_phase1"), { locals: state.locals }); + surrogate.self = angular.extend({}, state.self); + return surrogate; + } + + function stateReactivatedSurrogatePhase2(state) { + var surrogate = angular.extend(new SurrogateState("reactivate_phase2"), state); + var oldOnEnter = surrogate.self.onEnter; + surrogate.resolve = {}; // Don't re-resolve when reactivating states (fixes issue #22) + // TODO: Not 100% sure if this is necessary. I think resolveState will load the views if I don't do this. + surrogate.views = {}; // Don't re-activate controllers when reactivating states (fixes issue #22) + surrogate.self.onEnter = function () { + // ui-router sets locals on the surrogate to a blank locals (because we gave it nothing to resolve) + // Re-set it back to the already loaded state.locals here. + surrogate.locals = state.locals; + _StickyState.stateReactivated(state); + }; + restore.addRestoreFunction(function () { + state.self.onEnter = oldOnEnter; + }); + return surrogate; + } + + function stateInactivatedSurrogate(state) { + var surrogate = new SurrogateState("inactivate"); + surrogate.self = state.self; + var oldOnExit = state.self.onExit; + surrogate.self.onExit = function () { + _StickyState.stateInactivated(state); + }; + restore.addRestoreFunction(function () { + state.self.onExit = oldOnExit; + }); + return surrogate; + } + + function stateEnteredSurrogate(state, toParams) { + var oldOnEnter = state.self.onEnter; + state.self.onEnter = function () { + _StickyState.stateEntering(state, toParams, oldOnEnter); + }; + restore.addRestoreFunction(function () { + state.self.onEnter = oldOnEnter; + }); + + return state; + } + + // TODO: This may be completely unnecessary now that we're using $$uirouterextrasreload temp param + function stateUpdateParamsSurrogate(state, toParams) { + var oldOnEnter = state.self.onEnter; + state.self.onEnter = function () { + _StickyState.stateEntering(state, toParams, oldOnEnter, true); + }; + restore.addRestoreFunction(function () { + state.self.onEnter = oldOnEnter; + }); + + return state; + } + + function stateExitedSurrogate(state) { + var oldOnExit = state.self.onExit; + state.self.onExit = function () { + _StickyState.stateExiting(state, exited, oldOnExit); + }; + restore.addRestoreFunction(function () { + state.self.onExit = oldOnExit; + }); + + return state; + } + + + // --------------------- decorated .transitionTo() logic starts here ------------------------ + if (toStateSelf) { + var toState = internalStates[toStateSelf.name]; // have the state, now grab the internal state representation + if (toState) { + // Save the toState and fromState paths to be restored using restore() + savedToStatePath = toState.path; + savedFromStatePath = fromState.path; + + // Try to resolve options.reload to a state. If so, we'll reload only up to the given state. + var reload = options && options.reload || false; + var reloadStateTree = reload && (reload === true ? savedToStatePath[0].self : $state.get(reload, rel)); + // If options.reload is a string or a state, we want to handle reload ourselves and not + // let ui-router reload the entire toPath. + if (options && reload && reload !== true) + delete options.reload; + + var currentTransition = { + toState: toState, + toParams: toParams || {}, + fromState: fromState, + fromParams: fromParams || {}, + options: options, + reloadStateTree: reloadStateTree + }; + + pendingTransitions.push(currentTransition); // TODO: See if a list of pending transitions is necessary. + pendingRestore = restore; + + // If we're reloading from a state and below, temporarily add a param to the top of the state tree + // being reloaded, and add a param value to the transition. This will cause the "has params changed + // for state" check to return true, and the states will be reloaded. + if (reloadStateTree) { + currentTransition.toParams.$$uirouterextrasreload = Math.random(); + var params = reloadStateTree.$$state().params; + var ownParams = reloadStateTree.$$state().ownParams; + + if (versionHeuristics.hasParamSet) { + var tempParam = new $urlMatcherFactoryProvider.Param('$$uirouterextrasreload'); + params.$$uirouterextrasreload = ownParams.$$uirouterextrasreload = tempParam; + restore.restoreFunctions.push(function() { + delete params.$$uirouterextrasreload; + delete ownParams.$$uirouterextrasreload; + }); + } else { + params.push('$$uirouterextrasreload'); + ownParams.push('$$uirouterextrasreload'); + restore.restoreFunctions.push(function() { + params.length = params.length -1; + ownParams.length = ownParams.length -1; + }); + } + } + + // $StickyStateProvider.processTransition analyzes the states involved in the pending transition. It + // returns an object that tells us: + // 1) if we're involved in a sticky-type transition + // 2) what types of exit transitions will occur for each "exited" path element + // 3) what types of enter transitions will occur for each "entered" path element + // 4) which states will be inactive if the transition succeeds. + stickyTransitions = _StickyState.processTransition(currentTransition); + + if (DEBUG) debugTransition($log, currentTransition, stickyTransitions); + + // Begin processing of surrogate to and from paths. + var surrogateToPath = toState.path.slice(0, stickyTransitions.keep); + var surrogateFromPath = fromState.path.slice(0, stickyTransitions.keep); + + // Clear out and reload inactivePseudoState.locals each time transitionTo is called + angular.forEach(inactivePseudoState.locals, function (local, name) { + if (name.indexOf("@") != -1) delete inactivePseudoState.locals[name]; + }); + + var saveViewsToLocals = function (targetObj) { + return function(view, name) { + if (name.indexOf("@") !== -1) { // Only grab this state's "view" locals + targetObj[name] = view; // Add all inactive views not already included. + } + } + }; + + // For each state that will be inactive when the transition is complete, place its view-locals on the + // __inactives pseudostate's .locals. This allows the ui-view directive to access them and + // render the inactive views. + forEach(stickyTransitions.inactives, function(state) { + forEach(state.locals, saveViewsToLocals(inactivePseudoState.locals)) + }); + + // For each state that will be reactivated during the transition, place its view-locals on a separate + // locals object (prototypal parent of __inactives.locals, and remove them when the transition is complete. + // This is necessary when we a transition will reactivate one state, but enter a second. + // Gory details: + // - the entering of a new state causes $view.load() to fire $viewContentLoading while the transition is + // still in process + // - all ui-view(s) check if they should re-render themselves in response to this event. + // - ui-view checks if previousLocals is equal to currentLocals + // - it uses $state.$current.locals[myViewName] for previousLocals + // - Because the transition is not completed, $state.$current is set to the from state, and + // the ui-view for a reactivated state cannot find its previous locals. + forEach(stickyTransitions.reactivatingStates, function(state) { + forEach(state.locals, saveViewsToLocals(reactivatingLocals)); + }); + + // When the transition is complete, remove the copies of the view locals from reactivatingLocals. + restore.addRestoreFunction(function clearReactivatingLocals() { + forEach(reactivatingLocals, function (val, viewname) { + delete reactivatingLocals[viewname]; + }) + }); + + // Find all the states the transition will be entering. For each entered state, check entered-state-transition-type + // Depending on the entered-state transition type, place the proper surrogate state on the surrogate toPath. + angular.forEach(stickyTransitions.enter, function (value, idx) { + var surrogate; + var enteringState = toState.path[idx]; + if (value === "reactivate") { + // Reactivated states require TWO surrogates. The "phase 1 reactivated surrogates" are added to both + // to.path and from.path, and as such, are considered to be "kept" by UI-Router. + // This is required to get UI-Router to add the surrogate locals to the protoypal locals object + surrogate = stateReactivatedSurrogatePhase1(enteringState); + surrogateToPath.push(surrogate); + surrogateFromPath.push(surrogate); // so toPath[i] === fromPath[i] + + // The "phase 2 reactivated surrogate" is added to the END of the .path, after all the phase 1 + // surrogates have been added. + reactivated.push(stateReactivatedSurrogatePhase2(enteringState)); + terminalReactivatedState = enteringState; + } else if (value === "reload") { + // If the state params have been changed, we need to exit any inactive states and re-enter them. + surrogateToPath.push(stateUpdateParamsSurrogate(enteringState)); + terminalReactivatedState = enteringState; + } else if (value === "enter") { + // Standard enter transition. We still wrap it in a surrogate. + surrogateToPath.push(stateEnteredSurrogate(enteringState)); + } + }); + + // Find all the states the transition will be exiting. For each exited state, check the exited-state-transition-type. + // Depending on the exited-state transition type, place a surrogate state on the surrogate fromPath. + angular.forEach(stickyTransitions.exit, function (value, idx) { + var exiting = fromState.path[idx]; + if (value === "inactivate") { + surrogateFromPath.push(stateInactivatedSurrogate(exiting)); + exited.push(exiting); + } else if (value === "exit") { + surrogateFromPath.push(stateExitedSurrogate(exiting)); + exited.push(exiting); + } + }); + + // Add surrogate states for reactivated to ToPath again (phase 2), this time without a matching FromPath entry + // This is to get ui-router to call the surrogate's onEnter callback. + if (reactivated.length) { + angular.forEach(reactivated, function (surrogate) { + surrogateToPath.push(surrogate); + }); + } + + // We may transition directly to an inactivated state, reactivating it. In this case, we should + // exit all of that state's inactivated children. + var orphans = stickyTransitions.orphans; + // Add surrogate exited states for all orphaned descendants of the Deepest Reactivated State + surrogateFromPath = surrogateFromPath.concat(map(orphans, function (exiting) { + return stateExitedSurrogate(exiting); + })); + exited = exited.concat(orphans); + + // Replace the .path variables. toState.path and fromState.path are now ready for a sticky transition. + fromState.path = surrogateFromPath; + toState.path = surrogateToPath; + + var pathMessage = function (state) { + return (state.surrogateType ? state.surrogateType + ":" : "") + state.self.name; + }; + if (DEBUG) $log.debug("SurrogateFromPath: ", map(surrogateFromPath, pathMessage)); + if (DEBUG) $log.debug("SurrogateToPath: ", map(surrogateToPath, pathMessage)); + } + } + + // toState and fromState are all set up; now run stock UI-Router's $state.transitionTo(). + var transitionPromise = $state_transitionTo.apply($state, arguments); + + // Add post-transition promise handlers, then return the promise to the original caller. + return transitionPromise.then(function transitionSuccess(state) { + // First, restore toState and fromState to their original values. + restore(); + if (DEBUG) debugViewsAfterSuccess($log, internalStates[state.name], $state); + + state.status = 'active'; // TODO: This status is used in statevis.js, and almost certainly belongs elsewhere. + + return state; + }, function transitionFailed(err) { + restore(); + if (DEBUG && + err.message !== "transition prevented" && + err.message !== "transition aborted" && + err.message !== "transition superseded") { + $log.debug("transition failed", err); + $log.debug(err.stack); + } + return $q.reject(err); + }); + }; + return $state; + }]); + + + + function debugTransition($log, currentTransition, stickyTransition) { + function message(path, index, state) { + return (path[index] ? path[index].toUpperCase() + ": " + state.self.name : "(" + state.self.name + ")"); + } + + var inactiveLogVar = map(stickyTransition.inactives, function (state) { + return state.self.name; + }); + var enterLogVar = map(currentTransition.toState.path, function (state, index) { + return message(stickyTransition.enter, index, state); + }); + var exitLogVar = map(currentTransition.fromState.path, function (state, index) { + return message(stickyTransition.exit, index, state); + }); + + var transitionMessage = currentTransition.fromState.self.name + ": " + + angular.toJson(currentTransition.fromParams) + ": " + + " -> " + + currentTransition.toState.self.name + ": " + + angular.toJson(currentTransition.toParams); + + $log.debug("------------------------------------------------------"); + $log.debug(" Current transition: ", transitionMessage); + $log.debug("Before transition, inactives are: : ", map(_StickyState.getInactiveStates(), function (s) { + return s.self.name; + })); + $log.debug("After transition, inactives will be: ", inactiveLogVar); + $log.debug("Transition will exit: ", exitLogVar); + $log.debug("Transition will enter: ", enterLogVar); + } + + function debugViewsAfterSuccess($log, currentState, $state) { + $log.debug("Current state: " + currentState.self.name + ", inactive states: ", map(_StickyState.getInactiveStates(), function (s) { + return s.self.name; + })); + + var statesOnly = function (local, name) { + return name != 'globals' && name != 'resolve'; + }; + + var viewsForState = function (state) { + var viewLocals = filterObj(state.locals, statesOnly); + + if (!Object.keys(viewLocals).length) { + viewLocals[''] = { $$state: { name: null } }; + } + + return map(viewLocals, function(local, name) { + return { + localsFor: state.self.name ? state.self.name : "(root)", + uiViewName: name || null, + filledByState: local.$$state.name + }; + }); + }; + + var viewsByState = viewsForState(currentState); + var parent = currentState.parent; + while (parent && parent !== currentState) { + viewsByState = viewsByState.concat(viewsForState(parent)); + currentState = parent; + parent = currentState.parent; + } + + $log.debug("Views active on each state:"); + console.table(viewsByState.reverse()); + } + } + ] +); + +(function(angular, undefined) { + var app = angular.module('ct.ui.router.extras.future', [ 'ct.ui.router.extras.core' ]); + + _futureStateProvider.$inject = [ '$stateProvider', '$urlRouterProvider', '$urlMatcherFactoryProvider', 'uirextras_coreProvider' ]; + function _futureStateProvider($stateProvider, $urlRouterProvider, $urlMatcherFactory, uirextras_coreProvider) { + var core = uirextras_coreProvider; + var internalStates = core.internalStates; + var stateFactories = {}, futureStates = {}; + var lazyloadInProgress = false, resolveFunctions = [], initPromise, initDone = false; + var provider = this; + + // This function registers a promiseFn, to be resolved before the url/state matching code + // will reject a route. The promiseFn is injected/executed using the runtime $injector. + // The function should return a promise. + // When all registered promises are resolved, then the route is re-sync'ed. + + // Example: function($http) { + // return $http.get('//server.com/api/DynamicFutureStates').then(function(data) { + // angular.forEach(data.futureStates, function(fstate) { $futureStateProvider.futureState(fstate); }); + // }; + // } + this.addResolve = function (promiseFn) { + resolveFunctions.push(promiseFn); + }; + + // Register a state factory function for a particular future-state type. This factory, given a future-state object, + // should create a ui-router state. + // The factory function is injected/executed using the runtime $injector. The future-state is injected as 'futureState'. + + // Example: + // $futureStateProvider.stateFactory('test', function(futureState) { + // return { + // name: futureState.stateName, + // url: futureState.urlFragment, + // template: '

Future State Template

', + // controller: function() { + // console.log("Entered state " + futureState.stateName); + // } + // } + // }); + this.stateFactory = function (futureStateType, factory) { + stateFactories[futureStateType] = factory; + }; + + this.futureState = function (futureState) { + if (futureState.stateName) // backwards compat for now + futureState.name = futureState.stateName; + if (futureState.urlPrefix) // backwards compat for now + futureState.url = "^" + futureState.urlPrefix; + + futureStates[futureState.name] = futureState; + var parentMatcher, parentName = futureState.name.split(/\./).slice(0, -1).join("."), + realParent = findState(futureState.parent || parentName); + if (realParent) { + parentMatcher = realParent.url || realParent.navigable && realParent.navigable.url; + } else if (parentName === "") { + parentMatcher = $urlMatcherFactory.compile(""); + } else { + var futureParent = findState((futureState.parent || parentName), true); + if (!futureParent) throw new Error("Couldn't determine parent state of future state. FutureState:" + angular.toJson(futureState)); + var pattern; + if (futureParent.urlMatcher) { + pattern = futureParent.urlMatcher.source.replace(/\*rest$/, ""); + } + else { + // if the futureParent doesn't have a urlMatcher, then we are still + // starting from the beginning of the path + pattern = ""; + } + parentMatcher = $urlMatcherFactory.compile(pattern); + futureState.parentFutureState = futureParent; + } + if (futureState.url) { + futureState.urlMatcher = futureState.url.charAt(0) === "^" ? + $urlMatcherFactory.compile(futureState.url.substring(1) + "*rest") : + parentMatcher.concat(futureState.url + "*rest"); + } + }; + + this.get = function () { + return angular.extend({}, futureStates); + }; + + function findState(stateOrName, findFutureState) { + var statename = angular.isObject(stateOrName) ? stateOrName.name : stateOrName; + return !findFutureState ? internalStates[statename] : futureStates[statename]; + } + + /* options is an object with at least a name or url attribute */ + function findFutureState($state, options) { + if (options.name) { + var nameComponents = options.name.split(/\./); + if (options.name.charAt(0) === '.') + nameComponents[0] = $state.current.name; + while (nameComponents.length) { + var stateName = nameComponents.join("."); + if ($state.get(stateName, { relative: $state.current })) + return null; // State is already defined; nothing to do + if (futureStates[stateName]) + return futureStates[stateName]; + nameComponents.pop(); + } + } + + if (options.url) { + var matches = []; + for(var future in futureStates) { + var matcher = futureStates[future].urlMatcher; + if (matcher && matcher.exec(options.url)) { + matches.push(futureStates[future]); + } + } + // Find most specific by ignoring matching parents from matches + var copy = matches.slice(0); + for (var i = matches.length - 1; i >= 0; i--) { + for (var j = 0; j < copy.length; j++) { + if (matches[i] === copy[j].parentFutureState) matches.splice(i, 1); + } + } + return matches[0]; + } + } + + function lazyLoadState($injector, futureState) { + lazyloadInProgress = true; + var $q = $injector.get("$q"); + if (!futureState) { + var deferred = $q.defer(); + deferred.reject("No lazyState passed in " + futureState); + return deferred.promise; + } + + var parentPromises = $q.when([]), parentFuture = futureState.parentFutureState; + if (parentFuture && futureStates[parentFuture.name]) { + parentPromises = lazyLoadState($injector, futureStates[parentFuture.name]); + } + + var type = futureState.type; + var factory = stateFactories[type]; + if (!factory) throw Error("No state factory for futureState.type: " + (futureState && futureState.type)); + + var failedLoadPolicy = factory.$options && factory.$options.failedLazyLoadPolicy || "remove"; + function deregisterFutureState() { delete(futureStates[futureState.name]); } + function errorHandler(err) { + if (failedLoadPolicy === "remove") deregisterFutureState(); + return $q.reject(err); + } + + return parentPromises.then(function(array) { + var factoryPromise = $injector.invoke(factory, factory, { futureState: futureState }); + + return factoryPromise.then(function(fullState) { + deregisterFutureState(); // Success; remove future state + if (fullState) { array.push(fullState); } // Pass a chain of realized states back + return array; + }); + }).catch(errorHandler) + } + + var otherwiseFunc = [ '$log', '$location', + function otherwiseFunc($log, $location) { + //$log.debug("Unable to map " + $location.path()); + }]; + + function futureState_otherwise($injector, $location) { + var resyncing = false; + + var lazyLoadMissingState = + ['$rootScope', '$urlRouter', '$state', + function lazyLoadMissingState($rootScope, $urlRouter, $state) { + function resync() { + resyncing = true; $urlRouter.sync(); resyncing = false; + } + if (!initDone) { + // Asynchronously load state definitions, then resync URL + initPromise().then(resync); + initDone = true; + return; + } + + var futureState = findFutureState($state, { url: $location.path() }); + if (!futureState) { + return $injector.invoke(otherwiseFunc); + } + + // Config loaded. Asynchronously lazy-load state definition from URL fragment, if mapped. + lazyLoadState($injector, futureState).then(function lazyLoadedStateCallback(states) { + states.forEach(function (state) { + if (state && (!$state.get(state) || (state.name && !$state.get(state.name)))) + $stateProvider.state(state); + }); + lazyloadInProgress = false; + resync(); + }, function lazyLoadStateAborted() { + lazyloadInProgress = false; + resync(); + }); + }]; + if (lazyloadInProgress) return; + + var nextFn = resyncing ? otherwiseFunc : lazyLoadMissingState; + return $injector.invoke(nextFn); + } + + $urlRouterProvider.otherwise(futureState_otherwise); + + $urlRouterProvider.otherwise = function(rule) { + if (angular.isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!angular.isFunction(rule)) throw new Error("'rule' must be a function"); + otherwiseFunc = ['$injector', '$location', rule]; + return $urlRouterProvider; + }; + + var serviceObject = { + getResolvePromise: function () { + return initPromise(); + } + }; + + // Used in .run() block to init + this.$get = [ '$injector', '$state', '$q', '$rootScope', '$urlRouter', '$timeout', '$log', + function futureStateProvider_get($injector, $state, $q, $rootScope, $urlRouter, $timeout, $log) { + function init() { + $rootScope.$on("$stateNotFound", function futureState_notFound(event, unfoundState, fromState, fromParams) { + if (lazyloadInProgress) return; + //$log.debug("event, unfoundState, fromState, fromParams", event, unfoundState, fromState, fromParams); + + var futureState = findFutureState($state, { name: unfoundState.to }); + if (!futureState) return; + + event.preventDefault(); + var promise = lazyLoadState($injector, futureState); + promise.then(function (states) { + states.forEach(function (state) { + if (state && (!$state.get(state) || (state.name && !$state.get(state.name)))) + $stateProvider.state(state); + }); + $state.go(unfoundState.to, unfoundState.toParams, unfoundState.options); + lazyloadInProgress = false; + }, function (error) { + console.log("failed to lazy load state ", error); + if (fromState.name) $state.go(fromState, fromParams); + lazyloadInProgress = false; + }); + }); + + // Do this better. Want to load remote config once, before everything else + if (!initPromise) { + var promises = []; + angular.forEach(resolveFunctions, function (promiseFn) { + promises.push($injector.invoke(promiseFn)); + }); + initPromise = function () { + return $q.all(promises); + }; + } + + // TODO: analyze this. I'm calling $urlRouter.sync() in two places for retry-initial-transition. + // TODO: I should only need to do this once. Pick the better place and remove the extra resync. + initPromise().then(function retryInitialState() { + $timeout(function () { + if ($state.transition) { + $state.transition.then(retryInitialState, retryInitialState); + } else { + $urlRouter.sync(); + } + }); + }); + } + + init(); + + serviceObject.state = $stateProvider.state; + serviceObject.futureState = provider.futureState; + serviceObject.get = provider.get; + + return serviceObject; + } + ]; + } + + app.provider('$futureState', _futureStateProvider); + + var statesAddedQueue = { + state: function(state) { + if (statesAddedQueue.$rootScope) + statesAddedQueue.$rootScope.$broadcast("$stateAdded", state); + }, + itsNowRuntimeOhWhatAHappyDay: function($rootScope) { + statesAddedQueue.$rootScope = $rootScope; + }, + $rootScope: undefined + }; + + app.config([ '$stateProvider', function($stateProvider) { + // decorate $stateProvider.state so we can broadcast when a real state was added + var realStateFn = $stateProvider.state; + $stateProvider.state = function state_announce() { + var val = realStateFn.apply($stateProvider, arguments); + + var state = angular.isObject(arguments[0]) ? arguments[0] : arguments[1]; + statesAddedQueue.state(state); + return val; + }; + }]); + + // inject $futureState so the service gets initialized via $get(); + app.run(['$futureState', function ($futureState, $rootScope) { + statesAddedQueue.itsNowRuntimeOhWhatAHappyDay($rootScope); + } ]); + +})(angular); + +angular.module('ct.ui.router.extras.previous', [ 'ct.ui.router.extras.core', 'ct.ui.router.extras.transition' ]).service("$previousState", + [ '$rootScope', '$state', '$q', + function ($rootScope, $state, $q) { + var previous = null, lastPrevious = null, memos = {}; + + $rootScope.$on("$transitionStart", function(evt, $transition$) { + var from = $transition$.from; + // Check if the fromState is navigable before tracking it. + // Root state doesn't get decorated with $$state(). Doh. + var fromState = from.state && from.state.$$state && from.state.$$state(); + function commit() { lastPrevious = null; } + function revert() { previous = lastPrevious; } + if (fromState) { + lastPrevious = previous; + previous = $transition$.from; + + $transition$.promise.then(commit)['catch'](revert); + } + }); + + var $previousState = { + get: function (memoName) { + return memoName ? memos[memoName] : previous; + }, + set: function (memoName, previousState, previousParams) { + memos[memoName] = { state: $state.get(previousState), params: previousParams }; + }, + go: function (memoName, options) { + var to = $previousState.get(memoName); + if (!to) { + return $q.reject(new Error('no previous state ' + (memoName ? 'for memo: ' + memoName : ''))); + } + return $state.go(to.state, to.params, options); + }, + memo: function (memoName, defaultStateName, defaultStateParams) { + memos[memoName] = previous || { state: $state.get(defaultStateName), params: defaultStateParams }; + }, + forget: function (memoName) { + if (memoName) { + delete memos[memoName]; + } else { + previous = undefined; + } + } + }; + + return $previousState; + } + ] +); + +angular.module('ct.ui.router.extras.previous').run(['$previousState', function ($previousState) { + // Inject $previousState so it can register $rootScope events +}]); + + +angular.module("ct.ui.router.extras.transition", [ 'ct.ui.router.extras.core' ]).config( [ "$provide", function ($provide) { + // Decorate the $state service, so we can replace $state.transitionTo() + $provide.decorator("$state", ['$delegate', '$rootScope', '$q', '$injector', + function ($state, $rootScope, $q, $injector) { + // Keep an internal reference to the real $state.transitionTo function + var $state_transitionTo = $state.transitionTo; + // $state.transitionTo can be re-entered. Keep track of re-entrant stack + var transitionDepth = -1; + var tDataStack = []; + var restoreFnStack = []; + + // This function decorates the $injector, adding { $transition$: tData } to invoke() and instantiate() locals. + // It returns a function that restores $injector to its previous state. + function decorateInjector(tData) { + var oldinvoke = $injector.invoke; + var oldinstantiate = $injector.instantiate; + $injector.invoke = function (fn, self, locals) { + return oldinvoke(fn, self, angular.extend({$transition$: tData}, locals)); + }; + $injector.instantiate = function (fn, locals) { + return oldinstantiate(fn, angular.extend({$transition$: tData}, locals)); + }; + + return function restoreItems() { + $injector.invoke = oldinvoke; + $injector.instantiate = oldinstantiate; + }; + } + + function popStack() { + restoreFnStack.pop()(); + tDataStack.pop(); + transitionDepth--; + } + + // This promise callback (for when the real transitionTo is successful) runs the restore function for the + // current stack level, then broadcasts the $transitionSuccess event. + function transitionSuccess(deferred, tSuccess) { + return function successFn(data) { + popStack(); + $rootScope.$broadcast("$transitionSuccess", tSuccess); + deferred.resolve(data); // $transition$ deferred + return data; + }; + } + + // This promise callback (for when the real transitionTo fails) runs the restore function for the + // current stack level, then broadcasts the $transitionError event. + function transitionFailure(deferred, tFail) { + return function failureFn(error) { + popStack(); + $rootScope.$broadcast("$transitionError", tFail, error); + deferred.reject(error); // $transition$ deferred + return $q.reject(error); + }; + } + + // Decorate $state.transitionTo. + $state.transitionTo = function (to, toParams, options) { + // Create a deferred/promise which can be used earlier than UI-Router's transition promise. + var deferred = $q.defer(); + // Place the promise in a transition data, and place it on the stack to be used in $stateChangeStart + var tData = tDataStack[++transitionDepth] = { + promise: deferred.promise + }; + // placeholder restoreFn in case transitionTo doesn't reach $stateChangeStart (state not found, etc) + restoreFnStack[transitionDepth] = function() { }; + // Invoke the real $state.transitionTo + var tPromise = $state_transitionTo.apply($state, arguments); + + // insert our promise callbacks into the chain. + return tPromise.then(transitionSuccess(deferred, tData), transitionFailure(deferred, tData)); + }; + + // This event is handled synchronously in transitionTo call stack + $rootScope.$on("$stateChangeStart", function (evt, toState, toParams, fromState, fromParams) { + if (transitionDepth >= tDataStack.length || transitionDepth < 0) return; + var depth = transitionDepth; + // To/From is now normalized by ui-router. Add this information to the transition data object. + var tData = angular.extend(tDataStack[depth], { + to: { state: toState, params: toParams }, + from: { state: fromState, params: fromParams } + }); + + var restoreFn = decorateInjector(tData); + restoreFnStack[depth] = restoreFn; + $rootScope.$broadcast("$transitionStart", tData); + } + ); + + return $state; + }]); + } + ] +); + +// statevis requires d3. +(function () { + "use strict"; + var app = angular.module("ct.ui.router.extras.statevis", [ 'ct.ui.router.extras.core', 'ct.ui.router.extras.sticky' ]); + + app.directive('stateVis', [ '$state', '$timeout', '$interval', stateVisDirective ]); + + /** + * This directive gets all the current states using $state.get() and displays them in a tree using D3 lib. + * It then listens for state events and updates the tree. + * + * Usage: + * + */ + function stateVisDirective($state, $timeout, $interval) { + return { + scope: { + width: '@', + height: '@' + }, + restrict: 'AE', + template: '', + link: function (_scope, _elem, _attrs) { + var stateMap = {}; + var width = _scope.width || 400, + height = _scope.height || 400; + + var tree = d3.layout.tree() + .size([width - 20, height - 20]) + .separation(function (a, b) { + return a.parent == b.parent ? 10 : 25; + }); + + var root = $state.get().filter(function (state) { return state.name === ""; })[0]; + var nodes = tree(root); + + root.parent = root; + root.px = root.x = width / 2; + root.py = root.y = height / 2; + + var activeNode = { }; + activeNode.px = activeNode.x = root.px; + activeNode.py = activeNode.y = root.py; + + var diagonal = d3.svg.diagonal(); + + var svg = d3.select(_elem.find("svg")[0]) + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", "translate(10, 10)"); + + var node = svg.selectAll(".node"), + link = svg.selectAll(".link"), + active = svg.selectAll(".active") + ; + + var updateInterval = 200, + transLength = 200, + timer = setInterval(update, updateInterval); + + function addStates(data) { + // *********** Convert flat data into a nice tree *************** + data = data.map(function (node) { + return node.name === "" ? root : angular.copy(node); + }); + angular.extend(stateMap, data.reduce(function (map, node) { + map[node.name] = node; + return map; + }, {})); + + data.forEach(function (node) { + // add to parent + var parentName = node.name.split(/\./).slice(0, -1).join("."); + var parent = node.name != parentName && stateMap[parentName]; + if (parent) { + (parent.children || (parent.children = [])).push(node); // create child array if it doesn't exist + node.px = parent.px; + node.py = parent.py; + nodes.push(node); + } + }); + } + + $interval(function () { + _scope.states = $state.get(); + angular.forEach(nodes, function (n) { + var s = $state.get(n.name); + if (s) { + n.status = s.status || 'exited'; + } + }); +// _scope.futureStates = $futureState.get(); + }, 250); + + _scope.$watchCollection("states", function (newval, oldval) { + var oldstates = (oldval || []).map(function (s) { return s.name; }); + addStates((newval || []).filter(function(state) { return oldstates.indexOf(state.name) == -1; } )); +// addStates(_.reject(newval, function (state) { return _.contains(oldstates, state.name); })); + }); + +// addStates($state.get()); + update(updateInterval); + + function update() { + // Recompute the layout and data join. + node = node.data(tree.nodes(root), function (d) { return d.name; }); + link = link.data(tree.links(nodes), function (d) { return d.target.name; }); + active = active.data(activeNode); + + nodes.forEach(function (d) { d.y = d.depth * 70; }); + + // Add entering nodes in the parent’s old position. + var nodeEnter = node.enter(); + + function stateName(node) { + var name = node.name.split(".").pop(); + if (node.sticky) { name += " (STICKY)"; } + if (node.deepStateRedirect) { name += " (DSR)"; } + return name; + } + + active.enter() + .append("circle") + .attr("class", "active") + .attr("r", 13) + .attr("cx", function (d) { return d.parent.px || 100; }) + .attr("cy", function (d) { return d.parent.py || 100; }) + ; + + nodeEnter.append("circle") + .attr("class", "node") + .attr("r", 9) + .attr("cx", function (d) { return d.parent.px; }) + .attr("cy", function (d) { return d.parent.py; }); + + nodeEnter.append("text") + .attr("class", "label") + .attr("x", function (d) { return d.parent.px; }) + .attr("y", function (d) { return d.parent.py; }) + .attr("text-anchor", function (d) { return "middle"; }) + .text(stateName) + .style("fill-opacity", 1); + + + // Add entering links in the parent’s old position. + link.enter().insert("path", ".node") + .attr("class", "link") + .attr("d", function (d) { + var o = {x: d.source.px, y: d.source.py}; + return diagonal({source: o, target: o}); + }); + + // Transition nodes and links to their new positions. + var t = svg.transition() + .duration(transLength); + + t.selectAll(".link") + .attr("d", diagonal); + + /* jshint -W093 */ + var circleColors = { entered: '#AF0', exited: '#777', active: '#0f0', inactive: '#55F', future: '#009' }; + t.selectAll(".node") + .attr("cx", function (d) { return d.px = d.x; }) + .attr("cy", function (d) { return d.py = d.y; }) + .attr("r", function (d) { return d.status === 'active' ? 15 : 10; }) + .style("fill", function (d) { return circleColors[d.status] || "#FFF"; }); + + t.selectAll(".label") + .attr("x", function (d) { return d.px = d.x; }) + .attr("y", function (d) { return d.py = d.y - 15; }) + .attr("transform", function (d) { return "rotate(-25 " + d.x + " " + d.y + ")"; }) + ; + + t.selectAll(".active") + .attr("x", function (d) { return d.px = d.x; }) + .attr("y", function (d) { return d.py = d.y - 15; }); + } + } + }; + } +})(); + + +angular.module("ct.ui.router.extras", + [ + 'ct.ui.router.extras.core', + 'ct.ui.router.extras.dsr', + 'ct.ui.router.extras.future', + 'ct.ui.router.extras.previous', + 'ct.ui.router.extras.statevis', + 'ct.ui.router.extras.sticky', + 'ct.ui.router.extras.transition' + ]); + + +})); \ No newline at end of file diff --git a/build/ct-ui-router-extras.min.js b/build/ct-ui-router-extras.min.js new file mode 100644 index 0000000..8b7ef0a --- /dev/null +++ b/build/ct-ui-router-extras.min.js @@ -0,0 +1,2 @@ +/** UI-Router Extras v.0.1.3 Monolithic build (all modules) http://christopherthielen.github.io/ui-router-extras/ - MIT License */ +!function(t,e){"use strict";"function"==typeof define&&define.amd?define(["angular"],function(t){e(t)}):e("object"==typeof exports?require("angular"):t.angular)}(this,function(t,e){function r(t,e){var r=[];for(var n in t.path){if(t.path[n]!==e.path[n])break;r.push(t.path[n])}return r}function n(e){if(Object.keys)return Object.keys(e);var r=[];return t.forEach(e,function(t,e){r.push(e)}),r}function a(t,e){var r=[];for(var n in t)e&&e.indexOf(n)!==-1||r.push(n);return r}function o(t,e){if(Array.prototype.indexOf)return t.indexOf(e,Number(arguments[2])||0);var r=t.length>>>0,n=Number(arguments[2])||0;for(n=n<0?Math.ceil(n):Math.floor(n),n<0&&(n+=r);n=0||(f.push(u[p]),c[u[p]]=t[u[p]]);return h({},c,e)}function u(t,e){return h(new(h(function(){},{prototype:t})),e)}function s(t){d.push(t)}function c(){b=e}function f(e,r){var n=r,a=n.inheritParams,o=(n.objectKeys,n.protoKeys),i=n.forEach,u=n.map,s={},c={},f=!1;this.registerStickyState=function(t){c[t.name]=t},this.enableDebug=this.debugMode=function(e){return t.isDefined(e)&&(f=e),f},this.$get=["$rootScope","$state","$stateParams","$injector","$log",function(e,r,n,c,l){function p(){var e={};return t.forEach(s,function(t,r){for(var n=d(t),a=0;a0;c=l.exiting.map(function(t){var e=l.entering.indexOf(t)!==-1,r=$&&!e?"inactivate":"exit";return{type:r,state:t}});var g=e.options&&!!e.options.reload;f=l.entering.map(function(t){var r=m(t,e.toParams,e.reloadStateTree,g);return g=g||"reload"===r,{type:r,state:t}});var b=l.entering.map(function(t){return p.filter(i(t.parent))}).reduce(n,[]).filter(h).filter(u).concat(p.filter(i(e.toState))),E=v(),k=b.map(function(t){return E[t.name]}).filter(t.isDefined).reduce(n,[]).concat(b).sort(y),P=c.filter(o("exit")).map(a("state")).concat(k);return s=p.filter(r(P)).filter(r(l.entering)).concat(c.filter(o("inactivate")).map(a("state"))).sort(y),{keep:d,enter:new Array(d).concat(f.map(a("type"))),exit:new Array(d).concat(c.map(a("type"))),inactives:s,reactivatingStates:f.filter(o("reactivate")).map(a("state")),orphans:k}},stateInactivated:function(t){s[t.self.name]=t,t.self.status="inactive",t.self.onInactivate&&c.invoke(t.self.onInactivate,t.self,t.locals.globals)},stateReactivated:function(t){s[t.self.name]&&delete s[t.self.name],t.self.status="entered",t.self.onReactivate&&c.invoke(t.self.onReactivate,t.self,t.locals.globals)},stateExiting:function(e,r,n){var a={};t.forEach(r,function(t){a[t.self.name]=!0}),t.forEach(s,function(r,n){!a[n]&&r.includes[e.name]&&(f&&l.debug("Exiting "+n+" because it's a substate of "+e.name+" and wasn't found in ",a),r.self.onExit&&c.invoke(r.self.onExit,r.self,r.locals.globals),t.forEach(r.locals,function(t,e){delete j.locals[e]}),r.locals=null,r.self.status="exited",delete s[n])}),n&&c.invoke(n,e.self,e.locals.globals),e.locals=null,e.self.status="exited",delete s[e.self.name]},stateEntering:function(t,e,r,n){var a=h(t);if(a&&(n||!h(t,e))){var o=t.locals;this.stateExiting(a),t.locals=o}t.self.status="entered",r&&c.invoke(r,t.self,t.locals.globals)},reset:function(n,a){function o(t){S.reset(t)}if("*"===n)return t.forEach(S.getInactiveStates(),o),!0;var i=r.get(n);if(!i)return!1;var u=h(i,a);return!!u&&(S.stateExiting(u),e.$broadcast("$viewContentLoading"),!0)}};return S}]}function l(t){return{resolve:{},locals:{globals:P&&P.locals&&P.locals.globals},views:{},self:{},params:{},ownParams:A.hasParamSet?{$$equals:function(){return!0}}:[],surrogateType:t}}var p=t.module("ct.ui.router.extras.core",["ui.router"]),v={},d=[];p.config(["$stateProvider","$injector",function(e,r){e.decorator("parent",function(e,r){return v[e.self.name]=e,e.self.$$state=function(){return v[e.self.name]},t.forEach(d,function(t){t(e)}),r(e)})}]);var m=t.forEach,h=t.extend,$=t.isArray,g=function(t,e){"use strict";var r=[];return m(t,function(t,n){r.push(e(t,n))}),r},x=function(t){"use strict";return g(t,function(t,e){return e})},y=function(t,e){"use strict";var r=[];return m(t,function(t,n){e(t,n)&&r.push(t)}),r},S=function(t,e){"use strict";var r={};return m(t,function(t,n){e(t,n)&&(r[n]=t)}),r};p.provider("uirextras_core",function(){var e={internalStates:v,onStateRegistered:s,forEach:m,extend:h,isArray:$,map:g,keys:x,filter:y,filterObj:S,ancestors:r,objectKeys:n,protoKeys:a,arraySearch:o,inheritParams:i,inherit:u};t.extend(this,e),this.$get=function(){return e}});var b;t.module("ct.ui.router.extras.dsr",["ct.ui.router.extras.core"]).config(["$provide",function(t){var e;t.decorator("$state",["$delegate","$q",function(t,r){return e=t.transitionTo,t.transitionTo=function(n,a,o){return o&&o.ignoreDsr&&(b=o.ignoreDsr),e.apply(t,arguments).then(function(t){return c(),t},function(t){return c(),r.reject(t)})},t}])}]),t.module("ct.ui.router.extras.dsr").service("$deepStateRedirect",["$rootScope","$state","$injector",function(r,n,a){function o(t){var e=t.name;return l.hasOwnProperty(e)?l[e]:void u(e)}function i(e){var r=e.deepStateRedirect||e.dsr;if(!r)return{dsr:!1};var n={dsr:!0};return t.isFunction(r)?n.fn=r:t.isObject(r)&&(n=t.extend(n,r)),t.isString(n["default"])&&(n["default"]={state:n["default"]}),n.fn||(n.fn=["$dsr$",function(t){return t.redirect.state!=t.to.state}]),n}function u(t){var r=n.get(t);if(!r)return!1;var a=i(r);a.dsr&&(l[r.name]=p,f[t]===e&&(f[t]={}));var o=r.$$state&&r.$$state().parent;if(o){var s=u(o.self.name);s&&l[r.name]===e&&(l[r.name]=v)}return l[r.name]||!1}function s(r,n){n===!0&&(n=Object.keys(r)),null!==n&&n!==e||(n=[]);var a={};return t.forEach(n.sort(),function(t){a[t]=r[t]}),a}function c(e,r){function n(t){return t?t.toString():t}var a=s(e,r),o={};return t.forEach(a,function(t,e){o[e]=n(t)}),t.toJson(o)}var f={},l={},p="Redirect",v="AncestorRedirect";return r.$on("$stateChangeStart",function(e,r,u,l,v){var d=i(r);if(!b&&(o(r)===p||d["default"])){var m=c(u,d.params),h=f[r.name][m]||d["default"];if(h){var $={redirect:{state:h.state,params:h.params},to:{state:r.name,params:u}},g=a.invoke(d.fn,r,{$dsr$:$});if(g){g.state&&(h=g),e.preventDefault();var x=s(u,d.params);n.go(h.state,t.extend(x,h.params))}}}}),r.$on("$stateChangeSuccess",function(e,r,a,u,s){var l=o(r);if(l){var p=r.name;t.forEach(f,function(e,o){var u=i(n.get(o)),s=c(a,u.params);r.$$state().includes[o]&&(f[o][s]={state:p,params:t.copy(a)})})}}),{getRedirect:function(t,e){var r=n.get(t);o(r);var a=i(r),u=c(e,a.params),s=f[r.name];return s=s&&s[u]?s[u]:a["default"]},reset:function(e,r){if(e){var a=n.get(e);if(!a)throw new Error("Unknown state: "+e);if(f[a.name])if(r){var o=c(r,i(a).params);delete f[a.name][o]}else f[a.name]={}}else t.forEach(f,function(t,e){f[e]={}})}}}]),t.module("ct.ui.router.extras.dsr").run(["$deepStateRedirect",function(t){}]),t.module("ct.ui.router.extras.sticky",["ct.ui.router.extras.core"]);var E=t.module("ct.ui.router.extras.sticky");f.$inject=["$stateProvider","uirextras_coreProvider"],E.provider("$stickyState",f);var k,P,w,j,v={},F=[],R={},A={hasParamSet:!1};t.module("ct.ui.router.extras.sticky").run(["$stickyState",function(t){k=t}]),t.module("ct.ui.router.extras.sticky").config(["$provide","$stateProvider","$stickyStateProvider","$urlMatcherFactoryProvider","uirextras_coreProvider",function(r,n,a,o,i){function u(e,r,n){function a(t,e,r){return t[e]?t[e].toUpperCase()+": "+r.self.name:"("+r.self.name+")"}var o=d(n.inactives,function(t){return t.self.name}),i=d(r.toState.path,function(t,e){return a(n.enter,e,t)}),u=d(r.fromState.path,function(t,e){return a(n.exit,e,t)}),s=r.fromState.self.name+": "+t.toJson(r.fromParams)+": -> "+r.toState.self.name+": "+t.toJson(r.toParams);e.debug("------------------------------------------------------"),e.debug(" Current transition: ",s),e.debug("Before transition, inactives are: : ",d(k.getInactiveStates(),function(t){return t.self.name})),e.debug("After transition, inactives will be: ",o),e.debug("Transition will exit: ",u),e.debug("Transition will enter: ",i)}function s(t,e,r){t.debug("Current state: "+e.self.name+", inactive states: ",d(k.getInactiveStates(),function(t){return t.self.name}));for(var n=function(t,e){return"globals"!=e&&"resolve"!=e},a=function(t){var e=m(t.locals,n);return Object.keys(e).length||(e[""]={$$state:{name:null}}),d(e,function(e,r){return{localsFor:t.self.name?t.self.name:"(root)",uiViewName:r||null,filledByState:e.$$state.name}})},o=a(e),i=e.parent;i&&i!==e;)o=o.concat(a(i)),e=i,i=e.parent;t.debug("Views active on each state:"),console.table(o.reverse())}var c=i,f=c.internalStates,p=c.inherit,v=(c.inheritParams,c.forEach),d=c.map,m=c.filterObj;A.hasParamSet=!!o.ParamSet,j=t.extend(new l("__inactives"),{self:{name:"__inactives"}}),P=w=e,F=[],i.onStateRegistered(function(t){t.self.sticky===!0&&a.registerStickyState(t.self)});var h;r.decorator("$state",["$delegate","$log","$q",function(r,n,i){return P=r.$current,f[""]=P,P.parent=j,j.parent=e,j.locals=p(R,j.locals),P.locals=p(j.locals,P.locals),delete j.locals.globals,h=r.transitionTo,r.transitionTo=function(e,c,p){function m(e){var r=t.extend(new l("reactivate_phase1"),{locals:e.locals});return r.self=t.extend({},e.self),r}function $(e){var r=t.extend(new l("reactivate_phase2"),e),n=r.self.onEnter;return r.resolve={},r.views={},r.self.onEnter=function(){r.locals=e.locals,k.stateReactivated(e)},J.addRestoreFunction(function(){e.self.onEnter=n}),r}function g(t){var e=new l("inactivate");e.self=t.self;var r=t.self.onExit;return e.self.onExit=function(){k.stateInactivated(t)},J.addRestoreFunction(function(){t.self.onExit=r}),e}function x(t,e){var r=t.self.onEnter;return t.self.onEnter=function(){k.stateEntering(t,e,r)},J.addRestoreFunction(function(){t.self.onEnter=r}),t}function y(t,e){var r=t.self.onEnter;return t.self.onEnter=function(){k.stateEntering(t,e,r,!0)},J.addRestoreFunction(function(){t.self.onEnter=r}),t}function S(t){var e=t.self.onExit;return t.self.onExit=function(){k.stateExiting(t,K,e)},J.addRestoreFunction(function(){t.self.onExit=e}),t}var b=a.debugMode();j.locals||(j.locals=P.locals);var E=F.length;w&&(w(),b&&n.debug("Restored paths from pending transition"));var O,T,I,_,q=r.$current,M=r.params,C=p&&p.relative||r.$current,D=r.get(e,C),N=[],K=[];c=c||{},arguments[1]=c;var z=function(){},J=function(){O&&(B.path=O,O=null),T&&(q.path=T,T=null),t.forEach(J.restoreFunctions,function(t){t()}),J=z,w=null,F.splice(E,1)};if(J.restoreFunctions=[],J.addRestoreFunction=function(t){this.restoreFunctions.push(t)},D){var B=f[D.name];if(B){O=B.path,T=q.path;var L=p&&p.reload||!1,V=L&&(L===!0?O[0].self:r.get(L,C));p&&L&&L!==!0&&delete p.reload;var H={toState:B,toParams:c||{},fromState:q,fromParams:M||{},options:p,reloadStateTree:V};if(F.push(H),w=J,V){H.toParams.$$uirouterextrasreload=Math.random();var U=V.$$state().params,W=V.$$state().ownParams;if(A.hasParamSet){var Y=new o.Param("$$uirouterextrasreload");U.$$uirouterextrasreload=W.$$uirouterextrasreload=Y,J.restoreFunctions.push(function(){delete U.$$uirouterextrasreload,delete W.$$uirouterextrasreload})}else U.push("$$uirouterextrasreload"),W.push("$$uirouterextrasreload"),J.restoreFunctions.push(function(){U.length=U.length-1,W.length=W.length-1})}I=k.processTransition(H),b&&u(n,H,I);var G=B.path.slice(0,I.keep),Q=q.path.slice(0,I.keep);t.forEach(j.locals,function(t,e){e.indexOf("@")!=-1&&delete j.locals[e]});var X=function(t){return function(e,r){r.indexOf("@")!==-1&&(t[r]=e)}};v(I.inactives,function(t){v(t.locals,X(j.locals))}),v(I.reactivatingStates,function(t){v(t.locals,X(R))}),J.addRestoreFunction(function(){v(R,function(t,e){delete R[e]})}),t.forEach(I.enter,function(t,e){var r,n=B.path[e];"reactivate"===t?(r=m(n),G.push(r),Q.push(r),N.push($(n)),_=n):"reload"===t?(G.push(y(n)),_=n):"enter"===t&&G.push(x(n))}),t.forEach(I.exit,function(t,e){var r=q.path[e];"inactivate"===t?(Q.push(g(r)),K.push(r)):"exit"===t&&(Q.push(S(r)),K.push(r))}),N.length&&t.forEach(N,function(t){G.push(t)});var Z=I.orphans;Q=Q.concat(d(Z,function(t){return S(t)})),K=K.concat(Z),q.path=Q,B.path=G;var tt=function(t){return(t.surrogateType?t.surrogateType+":":"")+t.self.name};b&&n.debug("SurrogateFromPath: ",d(Q,tt)),b&&n.debug("SurrogateToPath: ",d(G,tt))}}var et=h.apply(r,arguments);return et.then(function(t){return J(),b&&s(n,f[t.name],r),t.status="active",t},function(t){return J(),b&&"transition prevented"!==t.message&&"transition aborted"!==t.message&&"transition superseded"!==t.message&&(n.debug("transition failed",t),n.debug(t.stack)),i.reject(t)})},r}])}]),function(t,e){function r(e,r,n,a){function o(e,r){var n=t.isObject(e)?e.name:e;return r?v[n]:l[n]}function i(t,e){if(e.name){var r=e.name.split(/\./);for("."===e.name.charAt(0)&&(r[0]=t.current.name);r.length;){var n=r.join(".");if(t.get(n,{relative:t.current}))return null;if(v[n])return v[n];r.pop()}}if(e.url){var a=[];for(var o in v){var i=v[o].urlMatcher;i&&i.exec(e.url)&&a.push(v[o])}for(var u=a.slice(0),s=a.length-1;s>=0;s--)for(var c=0;c=l.length||f<0)){var s=f,c=t.extend(l[s],{to:{state:n,params:a},from:{state:i,params:u}}),v=o(c);p[s]=v,r.$broadcast("$transitionStart",c)}}),e}])}]),function(){"use strict";function e(e,r,n){return{scope:{width:"@",height:"@"},restrict:"AE",template:"",link:function(r,a,o){function i(e){e=e.map(function(e){return""===e.name?p:t.copy(e)}),t.extend(s,e.reduce(function(t,e){return t[e.name]=e,t},{})),e.forEach(function(t){var e=t.name.split(/\./).slice(0,-1).join("."),r=t.name!=e&&s[e];r&&((r.children||(r.children=[])).push(t),t.px=r.px,t.py=r.py,v.push(t))})}function u(){function t(t){var e=t.name.split(".").pop();return t.sticky&&(e+=" (STICKY)"),t.deepStateRedirect&&(e+=" (DSR)"),e}$=$.data(l.nodes(p),function(t){return t.name}),g=g.data(l.links(v),function(t){return t.target.name}),x=x.data(d),v.forEach(function(t){t.y=70*t.depth});var e=$.enter();x.enter().append("circle").attr("class","active").attr("r",13).attr("cx",function(t){return t.parent.px||100}).attr("cy",function(t){return t.parent.py||100}),e.append("circle").attr("class","node").attr("r",9).attr("cx",function(t){return t.parent.px}).attr("cy",function(t){return t.parent.py}),e.append("text").attr("class","label").attr("x",function(t){return t.parent.px}).attr("y",function(t){return t.parent.py}).attr("text-anchor",function(t){return"middle"}).text(t).style("fill-opacity",1),g.enter().insert("path",".node").attr("class","link").attr("d",function(t){var e={x:t.source.px,y:t.source.py};return m({source:e,target:e})});var r=h.transition().duration(S);r.selectAll(".link").attr("d",m);var n={entered:"#AF0",exited:"#777",active:"#0f0",inactive:"#55F",future:"#009"};r.selectAll(".node").attr("cx",function(t){return t.px=t.x}).attr("cy",function(t){return t.py=t.y}).attr("r",function(t){return"active"===t.status?15:10}).style("fill",function(t){return n[t.status]||"#FFF"}),r.selectAll(".label").attr("x",function(t){return t.px=t.x}).attr("y",function(t){return t.py=t.y-15}).attr("transform",function(t){return"rotate(-25 "+t.x+" "+t.y+")"}),r.selectAll(".active").attr("x",function(t){return t.px=t.x}).attr("y",function(t){return t.py=t.y-15})}var s={},c=r.width||400,f=r.height||400,l=d3.layout.tree().size([c-20,f-20]).separation(function(t,e){return t.parent==e.parent?10:25}),p=e.get().filter(function(t){return""===t.name})[0],v=l(p);p.parent=p,p.px=p.x=c/2,p.py=p.y=f/2;var d={};d.px=d.x=p.px,d.py=d.y=p.py;var m=d3.svg.diagonal(),h=d3.select(a.find("svg")[0]).attr("width",c).attr("height",f).append("g").attr("transform","translate(10, 10)"),$=h.selectAll(".node"),g=h.selectAll(".link"),x=h.selectAll(".active"),y=200,S=200;setInterval(u,y);n(function(){r.states=e.get(),t.forEach(v,function(t){var r=e.get(t.name);r&&(t.status=r.status||"exited")})},250),r.$watchCollection("states",function(t,e){var r=(e||[]).map(function(t){return t.name});i((t||[]).filter(function(t){return r.indexOf(t.name)==-1}))}),u(y)}}}var r=t.module("ct.ui.router.extras.statevis",["ct.ui.router.extras.core","ct.ui.router.extras.sticky"]);r.directive("stateVis",["$state","$timeout","$interval",e])}(),t.module("ct.ui.router.extras",["ct.ui.router.extras.core","ct.ui.router.extras.dsr","ct.ui.router.extras.future","ct.ui.router.extras.previous","ct.ui.router.extras.statevis","ct.ui.router.extras.sticky","ct.ui.router.extras.transition"])}); \ No newline at end of file diff --git a/build/modular/ct-ui-router-extras.core.js b/build/modular/ct-ui-router-extras.core.js new file mode 100644 index 0000000..a3e5e33 --- /dev/null +++ b/build/modular/ct-ui-router-extras.core.js @@ -0,0 +1,181 @@ +/** + * UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise + * Module: core + * @version 0.1.3 + * @link http://christopherthielen.github.io/ui-router-extras/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function(angular, undefined){ +"use strict"; +var mod_core = angular.module("ct.ui.router.extras.core", [ "ui.router" ]); + +var internalStates = {}, stateRegisteredCallbacks = []; +mod_core.config([ '$stateProvider', '$injector', function ($stateProvider, $injector) { + // Decorate any state attribute in order to get access to the internal state representation. + $stateProvider.decorator('parent', function (state, parentFn) { + // Capture each internal UI-Router state representations as opposed to the user-defined state object. + // The internal state is, e.g., the state returned by $state.$current as opposed to $state.current + internalStates[state.self.name] = state; + // Add an accessor for the internal state from the user defined state + state.self.$$state = function () { + return internalStates[state.self.name]; + }; + + angular.forEach(stateRegisteredCallbacks, function(callback) { callback(state); }); + return parentFn(state); + }); +}]); + +var DEBUG = false; + +var forEach = angular.forEach; +var extend = angular.extend; +var isArray = angular.isArray; + +var map = function (collection, callback) { + "use strict"; + var result = []; + forEach(collection, function (item, index) { + result.push(callback(item, index)); + }); + return result; +}; + +var keys = function (collection) { + "use strict"; + return map(collection, function (collection, key) { + return key; + }); +}; + +var filter = function (collection, callback) { + "use strict"; + var result = []; + forEach(collection, function (item, index) { + if (callback(item, index)) { + result.push(item); + } + }); + return result; +}; + +var filterObj = function (collection, callback) { + "use strict"; + var result = {}; + forEach(collection, function (item, index) { + if (callback(item, index)) { + result[index] = item; + } + }); + return result; +}; + +// Duplicates code in UI-Router common.js +function ancestors(first, second) { + var path = []; + + for (var n in first.path) { + if (first.path[n] !== second.path[n]) break; + path.push(first.path[n]); + } + return path; +} + +// Duplicates code in UI-Router common.js +function objectKeys(object) { + if (Object.keys) { + return Object.keys(object); + } + var result = []; + + angular.forEach(object, function (val, key) { + result.push(key); + }); + return result; +} + +/** + * like objectKeys, but includes keys from prototype chain. + * @param object the object whose prototypal keys will be returned + * @param ignoreKeys an array of keys to ignore + */ +// Duplicates code in UI-Router common.js +function protoKeys(object, ignoreKeys) { + var result = []; + for (var key in object) { + if (!ignoreKeys || ignoreKeys.indexOf(key) === -1) + result.push(key); + } + return result; +} + +// Duplicates code in UI-Router common.js +function arraySearch(array, value) { + if (Array.prototype.indexOf) { + return array.indexOf(value, Number(arguments[2]) || 0); + } + var len = array.length >>> 0, from = Number(arguments[2]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + + if (from < 0) from += len; + + for (; from < len; from++) { + if (from in array && array[from] === value) return from; + } + return -1; +} + +// Duplicates code in UI-Router common.js +// Added compatibility code (isArray check) to support both 0.2.x and 0.3.x series of UI-Router. +function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i].params) continue; + // This test allows compatibility with 0.2.x and 0.3.x (optional and object params) + parentParams = isArray(parents[i].params) ? parents[i].params : objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (arraySearch(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); +} + +function inherit(parent, extra) { + return extend(new (extend(function () { }, {prototype: parent}))(), extra); +} + +function onStateRegistered(callback) { stateRegisteredCallbacks.push(callback); } + +mod_core.provider("uirextras_core", function() { + var core = { + internalStates: internalStates, + onStateRegistered: onStateRegistered, + forEach: forEach, + extend: extend, + isArray: isArray, + map: map, + keys: keys, + filter: filter, + filterObj: filterObj, + ancestors: ancestors, + objectKeys: objectKeys, + protoKeys: protoKeys, + arraySearch: arraySearch, + inheritParams: inheritParams, + inherit: inherit + }; + + angular.extend(this, core); + + this.$get = function() { + return core; + }; +}); + + +})(angular); \ No newline at end of file diff --git a/build/modular/ct-ui-router-extras.core.min.js b/build/modular/ct-ui-router-extras.core.min.js new file mode 100644 index 0000000..d5a0b4b --- /dev/null +++ b/build/modular/ct-ui-router-extras.core.min.js @@ -0,0 +1,2 @@ +/** UI-Router Extras v.0.1.3 Module: core http://christopherthielen.github.io/ui-router-extras/ - MIT License */ +!function(r,n){"use strict";function t(r,n){var t=[];for(var e in r.path){if(r.path[e]!==n.path[e])break;t.push(r.path[e])}return t}function e(n){if(Object.keys)return Object.keys(n);var t=[];return r.forEach(n,function(r,n){t.push(n)}),t}function i(r,n){var t=[];for(var e in r)n&&n.indexOf(e)!==-1||t.push(e);return t}function u(r,n){if(Array.prototype.indexOf)return r.indexOf(n,Number(arguments[2])||0);var t=r.length>>>0,e=Number(arguments[2])||0;for(e=e<0?Math.ceil(e):Math.floor(e),e<0&&(e+=t);e=0||(s.push(o[p]),c[o[p]]=r[o[p]]);return v({},c,n)}function o(r,n){return v(new(v(function(){},{prototype:r})),n)}function f(r){h.push(r)}var c=r.module("ct.ui.router.extras.core",["ui.router"]),s={},h=[];c.config(["$stateProvider","$injector",function(n,t){n.decorator("parent",function(n,t){return s[n.self.name]=n,n.self.$$state=function(){return s[n.self.name]},r.forEach(h,function(r){r(n)}),t(n)})}]);var p=r.forEach,v=r.extend,l=r.isArray,d=function(r,n){var t=[];return p(r,function(r,e){t.push(n(r,e))}),t},m=function(r){return d(r,function(r,n){return n})},y=function(r,n){var t=[];return p(r,function(r,e){n(r,e)&&t.push(r)}),t},x=function(r,n){var t={};return p(r,function(r,e){n(r,e)&&(t[e]=r)}),t};c.provider("uirextras_core",function(){var n={internalStates:s,onStateRegistered:f,forEach:p,extend:v,isArray:l,map:d,keys:m,filter:y,filterObj:x,ancestors:t,objectKeys:e,protoKeys:i,arraySearch:u,inheritParams:a,inherit:o};r.extend(this,n),this.$get=function(){return n}})}(angular); \ No newline at end of file diff --git a/build/modular/ct-ui-router-extras.dsr.js b/build/modular/ct-ui-router-extras.dsr.js new file mode 100644 index 0000000..17d4d94 --- /dev/null +++ b/build/modular/ct-ui-router-extras.dsr.js @@ -0,0 +1,185 @@ +/** + * UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise + * Module: dsr + * @version 0.1.3 + * @link http://christopherthielen.github.io/ui-router-extras/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function(angular, undefined){ +"use strict"; +var ignoreDsr; +function resetIgnoreDsr() { + ignoreDsr = undefined; +} + +// Decorate $state.transitionTo to gain access to the last transition.options variable. +// This is used to process the options.ignoreDsr option +angular.module('ct.ui.router.extras.dsr', [ 'ct.ui.router.extras.core' ]).config([ "$provide", function ($provide) { + var $state_transitionTo; + $provide.decorator("$state", ['$delegate', '$q', function ($state, $q) { + $state_transitionTo = $state.transitionTo; + $state.transitionTo = function (to, toParams, options) { + if (options && options.ignoreDsr) { + ignoreDsr = options.ignoreDsr; + } + + return $state_transitionTo.apply($state, arguments).then( + function (result) { + resetIgnoreDsr(); + return result; + }, + function (err) { + resetIgnoreDsr(); + return $q.reject(err); + } + ); + }; + return $state; + }]); +}]); + +angular.module('ct.ui.router.extras.dsr').service("$deepStateRedirect", [ '$rootScope', '$state', '$injector', function ($rootScope, $state, $injector) { + var lastSubstate = {}; + var deepStateRedirectsByName = {}; + + var REDIRECT = "Redirect", ANCESTOR_REDIRECT = "AncestorRedirect"; + + function computeDeepStateStatus(state) { + var name = state.name; + if (deepStateRedirectsByName.hasOwnProperty(name)) + return deepStateRedirectsByName[name]; + recordDeepStateRedirectStatus(name); + } + + function getConfig(state) { + var declaration = state.deepStateRedirect || state.dsr; + if (!declaration) return { dsr: false }; + var dsrCfg = { dsr: true }; + + if (angular.isFunction(declaration)) { + dsrCfg.fn = declaration; + } else if (angular.isObject(declaration)) { + dsrCfg = angular.extend(dsrCfg, declaration); + } + + if (angular.isString(dsrCfg['default'])) { + dsrCfg['default'] = { state: dsrCfg['default'] }; + } + + if (!dsrCfg.fn) { + dsrCfg.fn = [ '$dsr$', function($dsr$) { + return $dsr$.redirect.state != $dsr$.to.state; + } ]; + } + return dsrCfg; + } + + function recordDeepStateRedirectStatus(stateName) { + var state = $state.get(stateName); + if (!state) return false; + var cfg = getConfig(state); + if (cfg.dsr) { + deepStateRedirectsByName[state.name] = REDIRECT; + if (lastSubstate[stateName] === undefined) + lastSubstate[stateName] = {}; + } + + var parent = state.$$state && state.$$state().parent; + if (parent) { + var parentStatus = recordDeepStateRedirectStatus(parent.self.name); + if (parentStatus && deepStateRedirectsByName[state.name] === undefined) { + deepStateRedirectsByName[state.name] = ANCESTOR_REDIRECT; + } + } + return deepStateRedirectsByName[state.name] || false; + } + + function getMatchParams(params, dsrParams) { + if (dsrParams === true) dsrParams = Object.keys(params); + if (dsrParams === null || dsrParams === undefined) dsrParams = []; + + var matchParams = {}; + angular.forEach(dsrParams.sort(), function(name) { matchParams[name] = params[name]; }); + return matchParams; + } + + function getParamsString(params, dsrParams) { + var matchParams = getMatchParams(params, dsrParams); + function safeString(input) { return !input ? input : input.toString(); } + var paramsToString = {}; + angular.forEach(matchParams, function(val, name) { paramsToString[name] = safeString(val); }); + return angular.toJson(paramsToString); + } + + $rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) { + var cfg = getConfig(toState); + if (ignoreDsr || (computeDeepStateStatus(toState) !== REDIRECT) && !cfg['default']) return; + // We're changing directly to one of the redirect (tab) states. + // Get the DSR key for this state by calculating the DSRParams option + var key = getParamsString(toParams, cfg.params); + var redirect = lastSubstate[toState.name][key] || cfg['default']; + if (!redirect) return; + + // we have a last substate recorded + var $dsr$ = { redirect: { state: redirect.state, params: redirect.params}, to: { state: toState.name, params: toParams } }; + var result = $injector.invoke(cfg.fn, toState, { $dsr$: $dsr$ }); + if (!result) return; + if (result.state) redirect = result; + event.preventDefault(); + var redirectParams = getMatchParams(toParams, cfg.params); + $state.go(redirect.state, angular.extend(redirectParams, redirect.params)); + }); + + $rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) { + var deepStateStatus = computeDeepStateStatus(toState); + if (deepStateStatus) { + var name = toState.name; + angular.forEach(lastSubstate, function (redirect, dsrState) { + // update Last-SubState¶ms for each DSR that this transition matches. + var cfg = getConfig($state.get(dsrState)); + var key = getParamsString(toParams, cfg.params); + if (toState.$$state().includes[dsrState]) { + lastSubstate[dsrState][key] = { state: name, params: angular.copy(toParams) }; + } + }); + } + }); + + return { + getRedirect: function(dsrState, params) { + var state = $state.get(dsrState); + computeDeepStateStatus(state) + var cfg = getConfig(state); + var key = getParamsString(params, cfg.params); + var redirect = lastSubstate[state.name]; + if (redirect && redirect[key]) { + redirect = redirect[key]; + } else { + redirect = cfg['default']; + } + return redirect; + }, + reset: function(stateOrName, params) { + if (!stateOrName) { + angular.forEach(lastSubstate, function(redirect, dsrState) { lastSubstate[dsrState] = {}; }); + } else { + var state = $state.get(stateOrName); + if (!state) throw new Error("Unknown state: " + stateOrName); + if (lastSubstate[state.name]) { + if (params) { + var key = getParamsString(params, getConfig(state).params); + delete lastSubstate[state.name][key]; + } else { + lastSubstate[state.name] = {}; + } + } + } + } + }; +}]); + +angular.module('ct.ui.router.extras.dsr').run(['$deepStateRedirect', function ($deepStateRedirect) { + // Make sure $deepStateRedirect is instantiated +}]); + +})(angular); \ No newline at end of file diff --git a/build/modular/ct-ui-router-extras.dsr.min.js b/build/modular/ct-ui-router-extras.dsr.min.js new file mode 100644 index 0000000..b97ebdc --- /dev/null +++ b/build/modular/ct-ui-router-extras.dsr.min.js @@ -0,0 +1,2 @@ +/** UI-Router Extras v.0.1.3 Module: dsr http://christopherthielen.github.io/ui-router-extras/ - MIT License */ +!function(t,e){"use strict";function r(){n=e}var n;t.module("ct.ui.router.extras.dsr",["ct.ui.router.extras.core"]).config(["$provide",function(t){var e;t.decorator("$state",["$delegate","$q",function(t,a){return e=t.transitionTo,t.transitionTo=function(o,i,u){return u&&u.ignoreDsr&&(n=u.ignoreDsr),e.apply(t,arguments).then(function(t){return r(),t},function(t){return r(),a.reject(t)})},t}])}]),t.module("ct.ui.router.extras.dsr").service("$deepStateRedirect",["$rootScope","$state","$injector",function(r,a,o){function i(t){var e=t.name;return v.hasOwnProperty(e)?v[e]:void s(e)}function u(e){var r=e.deepStateRedirect||e.dsr;if(!r)return{dsr:!1};var n={dsr:!0};return t.isFunction(r)?n.fn=r:t.isObject(r)&&(n=t.extend(n,r)),t.isString(n["default"])&&(n["default"]={state:n["default"]}),n.fn||(n.fn=["$dsr$",function(t){return t.redirect.state!=t.to.state}]),n}function s(t){var r=a.get(t);if(!r)return!1;var n=u(r);n.dsr&&(v[r.name]=m,d[t]===e&&(d[t]={}));var o=r.$$state&&r.$$state().parent;if(o){var i=s(o.self.name);i&&v[r.name]===e&&(v[r.name]=$)}return v[r.name]||!1}function c(r,n){n===!0&&(n=Object.keys(r)),null!==n&&n!==e||(n=[]);var a={};return t.forEach(n.sort(),function(t){a[t]=r[t]}),a}function f(e,r){function n(t){return t?t.toString():t}var a=c(e,r),o={};return t.forEach(a,function(t,e){o[e]=n(t)}),t.toJson(o)}var d={},v={},m="Redirect",$="AncestorRedirect";return r.$on("$stateChangeStart",function(e,r,s,v,$){var p=u(r);if(!n&&(i(r)===m||p["default"])){var l=f(s,p.params),g=d[r.name][l]||p["default"];if(g){var h={redirect:{state:g.state,params:g.params},to:{state:r.name,params:s}},S=o.invoke(p.fn,r,{$dsr$:h});if(S){S.state&&(g=S),e.preventDefault();var x=c(s,p.params);a.go(g.state,t.extend(x,g.params))}}}}),r.$on("$stateChangeSuccess",function(e,r,n,o,s){var c=i(r);if(c){var v=r.name;t.forEach(d,function(e,o){var i=u(a.get(o)),s=f(n,i.params);r.$$state().includes[o]&&(d[o][s]={state:v,params:t.copy(n)})})}}),{getRedirect:function(t,e){var r=a.get(t);i(r);var n=u(r),o=f(e,n.params),s=d[r.name];return s=s&&s[o]?s[o]:n["default"]},reset:function(e,r){if(e){var n=a.get(e);if(!n)throw new Error("Unknown state: "+e);if(d[n.name])if(r){var o=f(r,u(n).params);delete d[n.name][o]}else d[n.name]={}}else t.forEach(d,function(t,e){d[e]={}})}}}]),t.module("ct.ui.router.extras.dsr").run(["$deepStateRedirect",function(t){}])}(angular); \ No newline at end of file diff --git a/build/modular/ct-ui-router-extras.future.js b/build/modular/ct-ui-router-extras.future.js new file mode 100644 index 0000000..4caf7ba --- /dev/null +++ b/build/modular/ct-ui-router-extras.future.js @@ -0,0 +1,326 @@ +/** + * UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise + * Module: future + * @version 0.1.3 + * @link http://christopherthielen.github.io/ui-router-extras/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function(angular, undefined){ +"use strict"; +(function(angular, undefined) { + var app = angular.module('ct.ui.router.extras.future', [ 'ct.ui.router.extras.core' ]); + + _futureStateProvider.$inject = [ '$stateProvider', '$urlRouterProvider', '$urlMatcherFactoryProvider', 'uirextras_coreProvider' ]; + function _futureStateProvider($stateProvider, $urlRouterProvider, $urlMatcherFactory, uirextras_coreProvider) { + var core = uirextras_coreProvider; + var internalStates = core.internalStates; + var stateFactories = {}, futureStates = {}; + var lazyloadInProgress = false, resolveFunctions = [], initPromise, initDone = false; + var provider = this; + + // This function registers a promiseFn, to be resolved before the url/state matching code + // will reject a route. The promiseFn is injected/executed using the runtime $injector. + // The function should return a promise. + // When all registered promises are resolved, then the route is re-sync'ed. + + // Example: function($http) { + // return $http.get('//server.com/api/DynamicFutureStates').then(function(data) { + // angular.forEach(data.futureStates, function(fstate) { $futureStateProvider.futureState(fstate); }); + // }; + // } + this.addResolve = function (promiseFn) { + resolveFunctions.push(promiseFn); + }; + + // Register a state factory function for a particular future-state type. This factory, given a future-state object, + // should create a ui-router state. + // The factory function is injected/executed using the runtime $injector. The future-state is injected as 'futureState'. + + // Example: + // $futureStateProvider.stateFactory('test', function(futureState) { + // return { + // name: futureState.stateName, + // url: futureState.urlFragment, + // template: '

Future State Template

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