From b6db4cfe7408307c7b1813d0d12c8d39d4541001 Mon Sep 17 00:00:00 2001 From: christopherthielen Date: Sat, 31 Jan 2015 20:28:22 -0600 Subject: [PATCH] release 0.0.12 --- bower.json | 2 +- gulpfile.js | 2 +- package.json | 4 +- release/ct-ui-router-extras.js | 1004 ++++++++++----- release/ct-ui-router-extras.min.js | 2 +- release/modular/ct-ui-router-extras.core.js | 174 +++ .../modular/ct-ui-router-extras.core.min.js | 1 + release/modular/ct-ui-router-extras.dsr.js | 165 +++ .../modular/ct-ui-router-extras.dsr.min.js | 1 + release/modular/ct-ui-router-extras.future.js | 302 +++++ .../modular/ct-ui-router-extras.future.min.js | 1 + .../modular/ct-ui-router-extras.previous.js | 52 + .../ct-ui-router-extras.previous.min.js | 1 + .../modular/ct-ui-router-extras.statevis.js | 187 +++ .../ct-ui-router-extras.statevis.min.js | 1 + release/modular/ct-ui-router-extras.sticky.js | 821 ++++++++++++ .../modular/ct-ui-router-extras.sticky.min.js | 1 + .../modular/ct-ui-router-extras.transition.js | 97 ++ .../ct-ui-router-extras.transition.min.js | 1 + .../{0.2.11 => 0.2.12}/angular-ui-router.js | 1095 ++++++++++++----- 20 files changed, 3304 insertions(+), 610 deletions(-) create mode 100644 release/modular/ct-ui-router-extras.core.js create mode 100644 release/modular/ct-ui-router-extras.core.min.js create mode 100644 release/modular/ct-ui-router-extras.dsr.js create mode 100644 release/modular/ct-ui-router-extras.dsr.min.js create mode 100644 release/modular/ct-ui-router-extras.future.js create mode 100644 release/modular/ct-ui-router-extras.future.min.js create mode 100644 release/modular/ct-ui-router-extras.previous.js create mode 100644 release/modular/ct-ui-router-extras.previous.min.js create mode 100644 release/modular/ct-ui-router-extras.statevis.js create mode 100644 release/modular/ct-ui-router-extras.statevis.min.js create mode 100644 release/modular/ct-ui-router-extras.sticky.js create mode 100644 release/modular/ct-ui-router-extras.sticky.min.js create mode 100644 release/modular/ct-ui-router-extras.transition.js create mode 100644 release/modular/ct-ui-router-extras.transition.min.js rename ui-router-versions/{0.2.11 => 0.2.12}/angular-ui-router.js (76%) diff --git a/bower.json b/bower.json index 806c040..e29b7b1 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "ui-router-extras", - "version": "0.0.12-pre1", + "version": "0.0.12", "authors": [ "Chris Thielen " ], diff --git a/gulpfile.js b/gulpfile.js index 083bc78..1708f6f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -97,7 +97,7 @@ gulp.task('karma:modules', ['scripts'], function() { gulp.task('karma:versions', ['scripts'], function() { var karma = require('karma').server; var Q = require('q'); - var versions = [ '0.2.8', '0.2.10', '0.2.11', '0.2.13' ]; + var versions = [ '0.2.8', '0.2.10', '0.2.12', '0.2.13' ]; var dynamicconf = require("./test/conf/karma.dynamic.conf"); var promise = Q(true); diff --git a/package.json b/package.json index 581b95b..ec4363c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "Christopher Thielen", "name": "ui-router-extras", - "version": "0.0.12-pre1", + "version": "0.0.12", "description": "UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise", "homepage": "http://christopherthielen.github.io/ui-router-extras/", "dependencies": {}, @@ -18,9 +18,9 @@ "gulp-uglify": "^1.1.0", "gulp-jshint": "^1.9.0", "gulp-notify": "^2.1.0", - "del": "^1.1.1", "gulp-wrap": "^0.10.1", "gulp-karma": "0.0.4", + "del": "^1.1.1", "q": "^1.1.2", "run-sequence": "^1.0.2" }, diff --git a/release/ct-ui-router-extras.js b/release/ct-ui-router-extras.js index db13931..0490e34 100644 --- a/release/ct-ui-router-extras.js +++ b/release/ct-ui-router-extras.js @@ -1,13 +1,23 @@ -/** - * UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise - * @version v0.0.11 - * @link http://christopherthielen.github.io/ui-router-extras/ - * @license MIT License, http://www.opensource.org/licenses/MIT - */ - -(function (window, angular, undefined) { -angular.module("ct.ui.router.extras", [ 'ui.router' ]); +(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; @@ -77,6 +87,21 @@ function objectKeys(object) { 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) { @@ -117,6 +142,35 @@ 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; @@ -124,7 +178,7 @@ function resetIgnoreDsr() { // Decorate $state.transitionTo to gain access to the last transition.options variable. // This is used to process the options.ignoreDsr option -angular.module("ct.ui.router.extras").config([ "$provide", function ($provide) { +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; @@ -148,7 +202,7 @@ angular.module("ct.ui.router.extras").config([ "$provide", function ($provide) { }]); }]); -angular.module("ct.ui.router.extras").service("$deepStateRedirect", [ '$rootScope', '$state', '$injector', function ($rootScope, $state, $injector) { +angular.module('ct.ui.router.extras.dsr').service("$deepStateRedirect", [ '$rootScope', '$state', '$injector', function ($rootScope, $state, $injector) { var lastSubstate = {}; var deepStateRedirectsByName = {}; @@ -162,14 +216,19 @@ angular.module("ct.ui.router.extras").service("$deepStateRedirect", [ '$rootScop } function getConfig(state) { - var declaration = state.deepStateRedirect; + var declaration = state.deepStateRedirect || state.dsr; if (!declaration) return { dsr: false }; var dsrCfg = { dsr: true }; - if (angular.isFunction(declaration)) + if (angular.isFunction(declaration)) { dsrCfg.fn = declaration; - else if (angular.isObject(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$) { @@ -199,23 +258,30 @@ angular.module("ct.ui.router.extras").service("$deepStateRedirect", [ '$rootScop return deepStateRedirectsByName[state.name] || false; } - function getParamsString(params, dsrParams) { - function safeString(input) { return !input ? input : input.toString(); } + 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(dsrParams.sort(), function(name) { paramsToString[name] = safeString(params[name]); }); + angular.forEach(matchParams, function(val, name) { paramsToString[name] = safeString(val); }); return angular.toJson(paramsToString); } $rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) { - if (ignoreDsr || computeDeepStateStatus(toState) !== REDIRECT) return; + 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 cfg = getConfig(toState); var key = getParamsString(toParams, cfg.params); - var redirect = lastSubstate[toState.name][key]; + var redirect = lastSubstate[toState.name][key] || cfg.default; if (!redirect) return; // we have a last substate recorded @@ -224,7 +290,8 @@ angular.module("ct.ui.router.extras").service("$deepStateRedirect", [ '$rootScop if (!result) return; if (result.state) redirect = result; event.preventDefault(); - $state.go(redirect.state, redirect.params); + var redirectParams = getMatchParams(toParams, cfg.params); + $state.go(redirect.state, angular.extend(redirectParams, redirect.params)); }); $rootScope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) { @@ -243,29 +310,40 @@ angular.module("ct.ui.router.extras").service("$deepStateRedirect", [ '$rootScop }); return { - reset: function(stateOrName) { + 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]) - lastSubstate[state.name] = {}; + 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").run(['$deepStateRedirect', function ($deepStateRedirect) { +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' ]; function $StickyStateProvider($stateProvider) { // 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 @@ -274,8 +352,10 @@ function $StickyStateProvider($stateProvider) { // console.log("Registered sticky state: ", state); }; - this.enableDebug = function (enabled) { - DEBUG = enabled; + this.enableDebug = this.debugMode = function (enabled) { + if (angular.isDefined(enabled)) + DEBUG = enabled; + return DEBUG; }; this.$get = [ '$rootScope', '$state', '$stateParams', '$injector', '$log', @@ -330,10 +410,11 @@ function $StickyStateProvider($stateProvider) { // it as a Exit/Enter, thus the special "updateStateParams" transition. // If a parent inactivated state has "updateStateParams" transition type, then // all descendant states must also be exit/entered, thus the first line of this function. - function getEnterTransition(state, stateParams, ancestorParamsChanged) { + function getEnterTransition(state, stateParams, reloadStateTree, ancestorParamsChanged) { if (ancestorParamsChanged) return "updateStateParams"; var inactiveState = inactiveStates[state.self.name]; if (!inactiveState) return "enter"; + if (state.self === reloadStateTree) return "updateStateParams"; // if (inactiveState.locals == null || inactiveState.locals.globals == null) debugger; var paramsMatch = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams); // if (DEBUG) $log.debug("getEnterTransition: " + state.name + (paramsMatch ? ": reactivate" : ": updateStateParams")); @@ -351,6 +432,9 @@ function $StickyStateProvider($stateProvider) { // 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 @@ -388,6 +472,7 @@ function $StickyStateProvider($stateProvider) { fromParams = transition.fromParams, toPath = transition.toState.path, toParams = transition.toParams, + reloadStateTree = transition.reloadStateTree, options = transition.options; var keep = 0, state = toPath[keep]; @@ -396,13 +481,14 @@ function $StickyStateProvider($stateProvider) { } while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) { + // We're "keeping" this state. bump keep var and get the next state in toPath for the next iteration. state = toPath[++keep]; } result.keep = keep; - var idx, deepestUpdatedParams, deepestReactivate, reactivatedStatesByName = {}, pType = getStickyTransitionType(fromPath, toPath, keep); - var ancestorUpdated = false; // When ancestor params change, treat reactivation as exit/enter + var idx, deepestUpdatedParams, deepestReactivate, noLongerInactiveStates = {}, pType = getStickyTransitionType(fromPath, toPath, keep); + var ancestorUpdated = !!options.reload; // When ancestor params change, treat reactivation as exit/enter // Calculate the "enter" transitions for new states in toPath // Enter transitions will be either "enter", "reactivate", or "updateStateParams" where @@ -410,14 +496,14 @@ function $StickyStateProvider($stateProvider) { // reactivate: use previous locals // updateStateParams: like 'enter', except exit the inactive state before entering it. for (idx = keep; idx < toPath.length; idx++) { - var enterTrans = !pType.to ? "enter" : getEnterTransition(toPath[idx], transition.toParams, ancestorUpdated); + var enterTrans = !pType.to ? "enter" : getEnterTransition(toPath[idx], toParams, reloadStateTree, ancestorUpdated); ancestorUpdated = (ancestorUpdated || enterTrans == 'updateStateParams'); result.enter[idx] = enterTrans; // If we're reactivating a state, make a note of it, so we can remove that state from the "inactive" list if (enterTrans == 'reactivate') - deepestReactivate = reactivatedStatesByName[toPath[idx].name] = toPath[idx]; + deepestReactivate = noLongerInactiveStates[toPath[idx].name] = toPath[idx]; if (enterTrans == 'updateStateParams') - deepestUpdatedParams = toPath[idx]; + deepestUpdatedParams = noLongerInactiveStates[toPath[idx].name] = toPath[idx]; } deepestReactivate = deepestReactivate ? deepestReactivate.self.name + "." : ""; deepestUpdatedParams = deepestUpdatedParams ? deepestUpdatedParams.self.name + "." : ""; @@ -436,7 +522,7 @@ function $StickyStateProvider($stateProvider) { for (var i = 0; inactiveChildren && i < inactiveChildren.length; i++) { var child = inactiveChildren[i]; // Don't organize state as inactive if we're about to reactivate it. - if (!reactivatedStatesByName[child.name] && + if (!noLongerInactiveStates[child.name] && (!deepestReactivate || (child.self.name.indexOf(deepestReactivate) !== 0)) && (!deepestUpdatedParams || (child.self.name.indexOf(deepestUpdatedParams) !== 0))) result.inactives.push(child); @@ -515,9 +601,9 @@ function $StickyStateProvider($stateProvider) { }, // Removes a previously inactivated state from the inactive sticky state registry - stateEntering: function (entering, params, onEnter) { + stateEntering: function (entering, params, onEnter, updateParams) { var inactivatedState = getInactivatedState(entering); - if (inactivatedState && !getInactivatedState(entering, params)) { + if (inactivatedState && (updateParams || !getInactivatedState(entering, params))) { var savedLocals = entering.locals; this.stateExiting(inactivatedState); entering.locals = savedLocals; @@ -541,7 +627,7 @@ function $StickyStateProvider($stateProvider) { }]; } -angular.module("ct.ui.router.extras").provider("$stickyState", $StickyStateProvider); +mod_sticky.provider("$stickyState", $StickyStateProvider); /** * Sticky States makes entire state trees "sticky". Sticky state trees are retained until their parent state is @@ -628,13 +714,15 @@ function SurrogateState(type) { // ------------------------ Sticky State registration and initialization code ---------------------------------- // Grab a copy of the $stickyState service for use by the transition management code -angular.module("ct.ui.router.extras").run(["$stickyState", function ($stickyState) { +angular.module("ct.ui.router.extras.sticky").run(["$stickyState", function ($stickyState) { _StickyState = $stickyState; }]); -angular.module("ct.ui.router.extras").config( - [ "$provide", "$stateProvider", '$stickyStateProvider', '$urlMatcherFactoryProvider', - function ($provide, $stateProvider, $stickyStateProvider, $urlMatcherFactoryProvider) { +angular.module("ct.ui.router.extras.sticky").config( + [ "$provide", "$stateProvider", '$stickyStateProvider', '$urlMatcherFactoryProvider', 'uirextras_coreProvider', + function ($provide, $stateProvider, $stickyStateProvider, $urlMatcherFactoryProvider, uirextras_coreProvider) { + var internalStates = uirextras_coreProvider.internalStates; + 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' } }); @@ -642,22 +730,11 @@ angular.module("ct.ui.router.extras").config( root = pendingRestore = undefined; pendingTransitions = []; - // Decorate any state attribute in order to get access to the internal state representation. - $stateProvider.decorator('parent', function (state, parentFn) { - // Capture each internal UI-Router state representations as opposed to the user-defined state object. - // The internal state is, e.g., the state returned by $state.$current as opposed to $state.current - internalStates[state.self.name] = state; - // Add an accessor for the internal state from the user defined state - state.self.$$state = function () { - return internalStates[state.self.name]; - }; - + uirextras_coreProvider.onStateRegistered(function(state) { // Register the ones marked as "sticky" if (state.self.sticky === true) { $stickyStateProvider.registerStickyState(state.self); } - - return parentFn(state); }); var $state_transitionTo; // internal reference to the real $state.transitionTo function @@ -676,6 +753,7 @@ angular.module("ct.ui.router.extras").config( // ------------------------ 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. @@ -694,6 +772,8 @@ angular.module("ct.ui.router.extras").config( 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 () { }; @@ -788,6 +868,19 @@ angular.module("ct.ui.router.extras").config( 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 () { @@ -809,11 +902,51 @@ angular.module("ct.ui.router.extras").config( savedToStatePath = toState.path; savedFromStatePath = fromState.path; - var currentTransition = {toState: toState, toParams: toParams || {}, fromState: fromState, fromParams: fromParams || {}, options: options}; + // 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 false, 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 @@ -849,26 +982,27 @@ angular.module("ct.ui.router.extras").config( // 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(toState.path[idx]); + 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(toState.path[idx])); - terminalReactivatedState = surrogate; + reactivated.push(stateReactivatedSurrogatePhase2(enteringState)); + terminalReactivatedState = enteringState; } else if (value === "updateStateParams") { // If the state params have been changed, we need to exit any inactive states and re-enter them. - surrogate = stateEnteredSurrogate(toState.path[idx]); + surrogate = stateUpdateParamsSurrogate(enteringState); surrogateToPath.push(surrogate); - terminalReactivatedState = surrogate; + terminalReactivatedState = enteringState; } else if (value === "enter") { // Standard enter transition. We still wrap it in a surrogate. - surrogateToPath.push(stateEnteredSurrogate(toState.path[idx])); + surrogateToPath.push(stateEnteredSurrogate(enteringState)); } }); @@ -893,9 +1027,9 @@ angular.module("ct.ui.router.extras").config( }); } - // In some cases, we may be some state, but not its children states. If that's the case, we have to - // exit all the children of the deepest reactivated state. - if (terminalReactivatedState) { + // We may transition directly to an inactivated state, reactivating it. In this case, we should + // exit all of that state's inactivated children. + if (toState === terminalReactivatedState) { var prefix = terminalReactivatedState.self.name + "."; var inactiveStates = _StickyState.getInactiveStates(); var inactiveOrphans = []; @@ -1017,307 +1151,323 @@ function debugViewsAfterSuccess($log, currentState, $state) { } -angular.module('ct.ui.router.extras').provider('$futureState', - [ '$stateProvider', '$urlRouterProvider', '$urlMatcherFactoryProvider', - function _futureStateProvider($stateProvider, $urlRouterProvider, $urlMatcherFactory) { - var stateFactories = {}, futureStates = {}; - var transitionPending = false, resolveFunctions = [], initPromise, initDone = false; - var provider = this; - - // This function registers a promiseFn, to be resolved before the url/state matching code - // will reject a route. The promiseFn is injected/executed using the runtime $injector. - // The function should return a promise. - // When all registered promises are resolved, then the route is re-sync'ed. - - // Example: function($http) { - // return $http.get('//server.com/api/DynamicFutureStates').then(function(data) { - // angular.forEach(data.futureStates, function(fstate) { $futureStateProvider.futureState(fstate); }); - // }; - // } - this.addResolve = function (promiseFn) { - resolveFunctions.push(promiseFn); - }; +(function(angular, undefined) { + var app = angular.module('ct.ui.router.extras.future', [ 'ct.ui.router.extras.core' ]); - // 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; - }; + function _futureStateProvider($stateProvider, $urlRouterProvider, $urlMatcherFactory) { + var stateFactories = {}, futureStates = {}; + var lazyloadInProgress = false, resolveFunctions = [], initPromise, initDone = false; + var provider = this; - 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.navigable.url; - } else if (parentName === "") { - parentMatcher = $urlMatcherFactory.compile(""); - } else { - var futureParent = findState((futureState.parent || parentName), true); - if (!futureParent) throw new Error("Couldn't determine parent state of future state. FutureState:" + angular.toJson(futureState)); - var pattern = futureParent.urlMatcher.source.replace(/\*rest$/, ""); - parentMatcher = $urlMatcherFactory.compile(pattern); - futureState.parentFutureState = futureParent; - } + // 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.url; + } else if (parentName === "") { + parentMatcher = $urlMatcherFactory.compile(""); + } else { + var futureParent = findState((futureState.parent || parentName), true); + if (!futureParent) throw new Error("Couldn't determine parent state of future state. FutureState:" + angular.toJson(futureState)); + var pattern = futureParent.urlMatcher.source.replace(/\*rest$/, ""); + parentMatcher = $urlMatcherFactory.compile(pattern); + futureState.parentFutureState = futureParent; + } + if (futureState.url) { futureState.urlMatcher = futureState.url.charAt(0) === "^" ? $urlMatcherFactory.compile(futureState.url.substring(1) + "*rest") : parentMatcher.concat(futureState.url + "*rest"); - }; + } + }; - this.get = function () { - return angular.extend({}, futureStates); - }; + 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]; - } + 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(); - } + /* 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) { - if (futureStates[future].urlMatcher.exec(options.url)) { - matches.push(futureStates[future]); - } + 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); - } + } + // 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]; } + return matches[0]; } + } - function lazyLoadState($injector, futureState) { - var $q = $injector.get("$q"); - if (!futureState) { - var deferred = $q.defer(); - deferred.reject("No lazyState passed in " + futureState); - return deferred.promise; - } + 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 promise = $q.when([]), parentFuture = futureState.parentFutureState; - if (parentFuture && futureStates[parentFuture.name]) { - promise = lazyLoadState($injector, futureStates[parentFuture.name]); - } + var promise = $q.when([]), parentFuture = futureState.parentFutureState; + if (parentFuture && futureStates[parentFuture.name]) { + promise = 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)); - return promise - .then(function(array) { - var injectorPromise = $injector.invoke(factory, factory, { futureState: futureState }); - return injectorPromise.then(function(fullState) { - if (fullState) { array.push(fullState); } // Pass a chain of realized states back - return array; - }); - }) - ["finally"](function() { // IE8 hack - delete(futureStates[futureState.name]); + var type = futureState.type; + var factory = stateFactories[type]; + if (!factory) throw Error("No state factory for futureState.type: " + (futureState && futureState.type)); + return promise + .then(function(array) { + var injectorPromise = $injector.invoke(factory, factory, { futureState: futureState }); + return injectorPromise.then(function(fullState) { + if (fullState) { array.push(fullState); } // Pass a chain of realized states back + return array; }); - } + }) + ["finally"](function() { // IE8 hack + delete(futureStates[futureState.name]); + }); + } - 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) { - if (!initDone) { - // Asynchronously load state definitions, then resync URL - initPromise().then(function initialResync() { - resyncing = true; - $urlRouter.sync(); - resyncing = false; - }); - initDone = true; - return; - } + var otherwiseFunc = [ '$log', '$location', + function otherwiseFunc($log, $location) { + $log.debug("Unable to map " + $location.path()); + }]; - var futureState = findFutureState($state, { url: $location.path() }); - if (!futureState) { - return $injector.invoke(otherwiseFunc); - } + function futureState_otherwise($injector, $location) { + var resyncing = false; - transitionPending = true; - // 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); - }); - resyncing = true; - $urlRouter.sync(); - resyncing = false; - transitionPending = false; - }, function lazyLoadStateAborted() { - transitionPending = false; - return $injector.invoke(otherwiseFunc); + 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); }); - }]; - if (transitionPending) return; + lazyloadInProgress = false; + resync(); + }, function lazyLoadStateAborted() { + lazyloadInProgress = false; + resync(); + }); + }]; + if (lazyloadInProgress) return; - var nextFn = resyncing ? otherwiseFunc : lazyLoadMissingState; - return $injector.invoke(nextFn); - } + var nextFn = resyncing ? otherwiseFunc : lazyLoadMissingState; + return $injector.invoke(nextFn); + } - $urlRouterProvider.otherwise(futureState_otherwise); + $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 = rule; - return $urlRouterProvider; - }; - - var serviceObject = { - getResolvePromise: function () { - return initPromise(); - } - }; + $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; + }; - // Used in .run() block to init - this.$get = [ '$injector', '$state', '$q', '$rootScope', '$urlRouter', '$timeout', '$log', - function futureStateProvider_get($injector, $state, $q, $rootScope, $urlRouter, $timeout, $log) { - function init() { - $rootScope.$on("$stateNotFound", function futureState_notFound(event, unfoundState, fromState, fromParams) { - if (transitionPending) return; - $log.debug("event, unfoundState, fromState, fromParams", event, unfoundState, fromState, fromParams); - - var futureState = findFutureState($state, { name: unfoundState.to }); - if (!futureState) return; - - event.preventDefault(); - transitionPending = true; - - var promise = lazyLoadState($injector, futureState); - promise.then(function (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); - transitionPending = false; - }, function (error) { - console.log("failed to lazy load state ", error); - $state.go(fromState, fromParams); - transitionPending = false; - }); - }); + var serviceObject = { + getResolvePromise: function () { + return initPromise(); + } + }; - // 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)); + // 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); }); - initPromise = function () { - return $q.all(promises); - }; -// initPromise = _.once(function flattenFutureStates() { -// var allPromises = $q.all(promises); -// return allPromises.then(function(data) { -// return _.flatten(data); -// }); -// }); - } + $state.go(unfoundState.to, unfoundState.toParams); + lazyloadInProgress = false; + }, function (error) { + console.log("failed to lazy load state ", error); + $state.go(fromState, fromParams); + lazyloadInProgress = false; + }); + }); - // TODO: analyze this. I'm calling $urlRouter.sync() in two places for retry-initial-transition. - // TODO: I should only need to do this once. Pick the better place and remove the extra resync. - initPromise().then(function retryInitialState() { - $timeout(function () { - if ($state.transition) { - $state.transition.then($urlRouter.sync, $urlRouter.sync); - } else { - $urlRouter.sync(); - } - }); + // 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); + }; } - init(); + // TODO: analyze this. I'm calling $urlRouter.sync() in two places for retry-initial-transition. + // TODO: I should only need to do this once. Pick the better place and remove the extra resync. + initPromise().then(function retryInitialState() { + $timeout(function () { + if ($state.transition) { + $state.transition.then($urlRouter.sync, $urlRouter.sync); + } else { + $urlRouter.sync(); + } + }); + }); + } - serviceObject.state = $stateProvider.state; - serviceObject.futureState = provider.futureState; - serviceObject.get = provider.get; + init(); - return serviceObject; - }]; - }]); + serviceObject.state = $stateProvider.state; + serviceObject.futureState = provider.futureState; + serviceObject.get = provider.get; -angular.module('ct.ui.router.extras').run(['$futureState', - // Just inject $futureState so it gets initialized. - function ($futureState) { + return serviceObject; + } + ]; } -]); -angular.module('ct.ui.router.extras').service("$previousState", - [ '$rootScope', '$state', - function ($rootScope, $state) { - var previous = null; - var memos = {}; + app.provider('$futureState', [ '$stateProvider', '$urlRouterProvider', '$urlMatcherFactoryProvider', _futureStateProvider]); - var lastPrevious = null; + var statesAddedQueue = { + state: function(state) { + if (statesAddedQueue.$rootScope) + statesAddedQueue.$rootScope.$broadcast("$stateAdded", state); + }, + itsNowRuntimeOhWhatAHappyDay: function($rootScope) { + statesAddedQueue.$rootScope = $rootScope; + }, + $rootScope: undefined + }; - $rootScope.$on("$stateChangeStart", function (evt, toState, toStateParams, fromState, fromStateParams) { - // State change is starting. Keep track of the CURRENT previous state in case we have to restore it - lastPrevious = previous; - previous = { state: fromState, params: fromStateParams }; - }); + 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); - $rootScope.$on("$stateChangeError", function () { - // State change did not occur due to an error. Restore the previous previous state. - previous = lastPrevious; - lastPrevious = null; - }); + var state = angular.isObject(arguments[0]) ? arguments[0] : arguments[1]; + statesAddedQueue.state(state); + return val; + }; + }]); - $rootScope.$on("$stateChangeSuccess", function () { - lastPrevious = null; + // 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', + function ($rootScope, $state) { + 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(); + if (fromState && fromState.navigable) { + lastPrevious = previous; + previous = $transition$.from; + } + + $transition$.promise.then(commit).catch(revert); + function commit() { lastPrevious = null; } + function revert() { previous = lastPrevious; } }); var $previousState = { @@ -1332,7 +1482,11 @@ angular.module('ct.ui.router.extras').service("$previousState", memos[memoName] = previous || { state: $state.get(defaultStateName), params: defaultStateParams }; }, forget: function (memoName) { - delete memos[memoName]; + if (memoName) { + delete memos[memoName]; + } else { + previous = undefined; + } } }; @@ -1341,12 +1495,12 @@ angular.module('ct.ui.router.extras').service("$previousState", ] ); -angular.module('ct.ui.router.extras').run(['$previousState', function ($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").config( [ "$provide", function ($provide) { +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) { @@ -1439,4 +1593,200 @@ angular.module("ct.ui.router.extras").config( [ "$provide", function ($provide) ] ); -})(window, window.angular); \ No newline at end of file +// statevis requires d3. +(function () { + "use strict"; + var app = angular.module("ct.ui.router.extras.statevis", [ 'ct.ui.router.extras.core' ]); + + 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' + ]); + + +})(angular); \ No newline at end of file diff --git a/release/ct-ui-router-extras.min.js b/release/ct-ui-router-extras.min.js index 1ce5a52..3c707df 100644 --- a/release/ct-ui-router-extras.min.js +++ b/release/ct-ui-router-extras.min.js @@ -1 +1 @@ -/*! ui-router-extras - v0.0.11 - 2014-11-21 */!function(a,b,c){function d(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function e(a){if(Object.keys)return Object.keys(a);var c=[];return b.forEach(a,function(a,b){c.push(b)}),c}function f(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function g(a,b,c,g){var h,i=d(c,g),j={},k=[];for(var l in i)if(i[l].params&&(h=r(i[l].params)?i[l].params:e(i[l].params),h.length))for(var m in h)f(k,h[m])>=0||(k.push(h[m]),j[h[m]]=a[h[m]]);return q({},j,b)}function h(a,b){return q(new(q(function(){},{prototype:a})),b)}function i(){n=c}function j(){var a={},c={};this.registerStickyState=function(a){c[a.name]=a},this.enableDebug=function(a){o=a},this.$get=["$rootScope","$state","$stateParams","$injector","$log",function(c,d,e,f,h){function i(){var c={};return b.forEach(a,function(a){for(var b=j(a),d=0;d "+c.toState.self.name+": "+b.toJson(c.toParams);a.debug(" Current transition: ",i),a.debug("Before transition, inactives are: : ",s(u.getInactiveStates(),function(a){return a.self.name})),a.debug("After transition, inactives will be: ",f),a.debug("Transition will exit: ",h),a.debug("Transition will enter: ",g)}function m(a,b,c){a.debug("Current state: "+b.self.name+", inactive states: ",s(u.getInactiveStates(),function(a){return a.self.name}));for(var d=function(a,b){return"'"+b+"' ("+a.$$state.name+")"},e=function(a,b){return"globals"!=b&&"resolve"!=b},f=function(a){var b=s(t(a.locals,e),d).join(", ");return"("+(a.self.name?a.self.name:"root")+".locals"+(b.length?": "+b:"")+")"},g=f(b),h=b.parent;h&&h!==b;)""===h.self.name&&(g=f(c.$current.path[0])+" / "+g),g=f(h)+" / "+g,b=h,h=b.parent;a.debug("Views: "+g)}b.module("ct.ui.router.extras",["ui.router"]);var n,o=!1,p=b.forEach,q=b.extend,r=b.isArray,s=function(a,b){"use strict";var c=[];return p(a,function(a,d){c.push(b(a,d))}),c},t=function(a,b){"use strict";var c={};return p(a,function(a,d){b(a,d)&&(c[d]=a)}),c};b.module("ct.ui.router.extras").config(["$provide",function(a){var b;a.decorator("$state",["$delegate","$q",function(a,c){return b=a.transitionTo,a.transitionTo=function(d,e,f){return f.ignoreDsr&&(n=f.ignoreDsr),b.apply(a,arguments).then(function(a){return i(),a},function(a){return i(),c.reject(a)})},a}])}]),b.module("ct.ui.router.extras").service("$deepStateRedirect",["$rootScope","$state","$injector",function(a,d,e){function f(a){var b=a.name;return k.hasOwnProperty(b)?k[b]:void h(b)}function g(a){var c=a.deepStateRedirect;if(!c)return{dsr:!1};var d={dsr:!0};return b.isFunction(c)?d.fn=c:b.isObject(c)&&(d=b.extend(d,c)),d.fn||(d.fn=["$dsr$",function(a){return a.redirect.state!=a.to.state}]),d}function h(a){var b=d.get(a);if(!b)return!1;var e=g(b);e.dsr&&(k[b.name]=l,j[a]===c&&(j[a]={}));var f=b.$$state&&b.$$state().parent;if(f){var i=h(f.self.name);i&&k[b.name]===c&&(k[b.name]=m)}return k[b.name]||!1}function i(a,d){function e(a){return a?a.toString():a}d===!0&&(d=Object.keys(a)),(null===d||d===c)&&(d=[]);var f={};return b.forEach(d.sort(),function(b){f[b]=e(a[b])}),b.toJson(f)}var j={},k={},l="Redirect",m="AncestorRedirect";return a.$on("$stateChangeStart",function(a,b,c){if(!n&&f(b)===l){var h=g(b),k=i(c,h.params),m=j[b.name][k];if(m){var o={redirect:{state:m.state,params:m.params},to:{state:b.name,params:c}},p=e.invoke(h.fn,b,{$dsr$:o});p&&(p.state&&(m=p),a.preventDefault(),d.go(m.state,m.params))}}}),a.$on("$stateChangeSuccess",function(a,c,e){var h=f(c);if(h){var k=c.name;b.forEach(j,function(a,c){var f=g(d.get(c)),h=i(e,f.params);(k==c||-1!=k.indexOf(c+"."))&&(j[c][h]={state:k,params:b.copy(e)})})}}),{reset:function(a){if(a){var c=d.get(a);if(!c)throw new Error("Unknown state: "+a);j[c.name]&&(j[c.name]={})}else b.forEach(j,function(a,b){j[b]={}})}}}]),b.module("ct.ui.router.extras").run(["$deepStateRedirect",function(){}]),j.$inject=["$stateProvider"],b.module("ct.ui.router.extras").provider("$stickyState",j);var u,v,w,x,y={},z=[],A={hasParamSet:!1};b.module("ct.ui.router.extras").run(["$stickyState",function(a){u=a}]),b.module("ct.ui.router.extras").config(["$provide","$stateProvider","$stickyStateProvider","$urlMatcherFactoryProvider",function(a,d,e,f){A.hasParamSet=!!f.ParamSet,x=b.extend(new k("__inactives"),{self:{name:"__inactives"}}),v=w=c,z=[],d.decorator("parent",function(a,b){return y[a.self.name]=a,a.self.$$state=function(){return y[a.self.name]},a.self.sticky===!0&&e.registerStickyState(a.self),b(a)});var g;a.decorator("$state",["$delegate","$log","$q",function(a,d,e){return v=a.$current,y[""]=v,v.parent=x,x.parent=c,v.locals=h(x.locals,v.locals),delete x.locals.globals,g=a.transitionTo,a.transitionTo=function(c,f,h){function i(a){var c=b.extend(new k("reactivate_phase1"),{locals:a.locals});return c.self=b.extend({},a.self),c}function j(a){var c=b.extend(new k("reactivate_phase2"),a),d=c.self.onEnter;return c.resolve={},c.views={},c.self.onEnter=function(){c.locals=a.locals,u.stateReactivated(a)},K.addRestoreFunction(function(){a.self.onEnter=d}),c}function n(a){var b=new k("inactivate");b.self=a.self;var c=a.self.onExit;return b.self.onExit=function(){u.stateInactivated(a)},K.addRestoreFunction(function(){a.self.onExit=c}),b}function p(a,b){var c=a.self.onEnter;return a.self.onEnter=function(){u.stateEntering(a,b,c)},K.addRestoreFunction(function(){a.self.onEnter=c}),a}function q(a){var b=a.self.onExit;return a.self.onExit=function(){u.stateExiting(a,I,b)},K.addRestoreFunction(function(){a.self.onExit=b}),a}x.locals||(x.locals=v.locals);var r=z.length;w&&(w(),o&&d.debug("Restored paths from pending transition"));var t,A,B,C,D=a.$current,E=a.params,F=h&&h.relative||a.$current,G=a.get(c,F),H=[],I=[],J=function(){},K=function(){t&&(L.path=t,t=null),A&&(D.path=A,A=null),b.forEach(K.restoreFunctions,function(a){a()}),K=J,w=null,z.splice(r,1)};if(K.restoreFunctions=[],K.addRestoreFunction=function(a){this.restoreFunctions.push(a)},G){var L=y[G.name];if(L){t=L.path,A=D.path;var M={toState:L,toParams:f||{},fromState:D,fromParams:E||{},options:h};z.push(M),w=K,B=u.processTransition(M),o&&l(d,M,B);var N=L.path.slice(0,B.keep),O=D.path.slice(0,B.keep);b.forEach(x.locals,function(a,b){-1!=b.indexOf("@")&&delete x.locals[b]});for(var P=0;P=0;h--)for(var i=0;i>>0,n=Number(arguments[2])||0;for(n=0>n?Math.ceil(n):Math.floor(n),0>n&&(n+=r);r>n;n++)if(n in t&&t[n]===e)return n;return-1}function i(t,e,a,i){var u,s=r(a,i),c={},f=[];for(var l in s)if(s[l].params&&(u=x(s[l].params)?s[l].params:n(s[l].params),u.length))for(var v in u)o(f,u[v])>=0||(f.push(u[v]),c[u[v]]=t[u[v]]);return $({},c,e)}function u(t,e){return $(new($(function(){},{prototype:t})),e)}function s(t){h.push(t)}function c(){P=e}function f(){var e={},r={},n=!1;this.registerStickyState=function(t){r[t.name]=t},this.enableDebug=this.debugMode=function(e){return t.isDefined(e)&&(n=e),n},this.$get=["$rootScope","$state","$stateParams","$injector","$log",function(r,o,u,s,c){function f(){var r={};return t.forEach(e,function(t){for(var e=l(t),n=0;n "+r.toState.self.name+": "+t.toJson(r.toParams);e.debug(" Current transition: ",s),e.debug("Before transition, inactives are: : ",y(w.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 p(t,e,r){t.debug("Current state: "+e.self.name+", inactive states: ",y(w.getInactiveStates(),function(t){return t.self.name}));for(var n=function(t,e){return"'"+e+"' ("+t.$$state.name+")"},a=function(t,e){return"globals"!=e&&"resolve"!=e},o=function(t){var e=y(b(t.locals,a),n).join(", ");return"("+(t.self.name?t.self.name:"root")+".locals"+(e.length?": "+e:"")+")"},i=o(e),u=e.parent;u&&u!==e;)""===u.self.name&&(i=o(r.$current.path[0])+" / "+i),i=o(u)+" / "+i,e=u,u=e.parent;t.debug("Views: "+i)}var d=t.module("ct.ui.router.extras.core",["ui.router"]),m={},h=[];d.config(["$stateProvider","$injector",function(e){e.decorator("parent",function(e,r){return m[e.self.name]=e,e.self.$$state=function(){return m[e.self.name]},t.forEach(h,function(t){t(e)}),r(e)})}]);var g=t.forEach,$=t.extend,x=t.isArray,y=function(t,e){var r=[];return g(t,function(t,n){r.push(e(t,n))}),r},S=function(t){return y(t,function(t,e){return e})},E=function(t,e){var r=[];return g(t,function(t,n){e(t,n)&&r.push(t)}),r},b=function(t,e){var r={};return g(t,function(t,n){e(t,n)&&(r[n]=t)}),r};d.provider("uirextras_core",function(){var e={internalStates:m,onStateRegistered:s,forEach:g,extend:$,isArray:x,map:y,keys:S,filter:E,filterObj:b,ancestors:r,objectKeys:n,protoKeys:a,arraySearch:o,inheritParams:i,inherit:u};t.extend(this,e),this.$get=function(){return e}});var P;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.ignoreDsr&&(P=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]=v,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]=p)}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={},v="Redirect",p="AncestorRedirect";return r.$on("$stateChangeStart",function(e,r,u){var l=i(r);if(!P&&(o(r)===v||l["default"])){var p=c(u,l.params),d=f[r.name][p]||l["default"];if(d){var m={redirect:{state:d.state,params:d.params},to:{state:r.name,params:u}},h=a.invoke(l.fn,r,{$dsr$:m});if(h){h.state&&(d=h),e.preventDefault();var g=s(u,l.params);n.go(d.state,t.extend(g,d.params))}}}}),r.$on("$stateChangeSuccess",function(e,r,a){var u=o(r);if(u){var s=r.name;t.forEach(f,function(e,r){var o=i(n.get(r)),u=c(a,o.params);(s==r||-1!=s.indexOf(r+"."))&&(f[r][u]={state:s,params:t.copy(a)})})}}),{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.module("ct.ui.router.extras.sticky",["ct.ui.router.extras.core"]);var k=t.module("ct.ui.router.extras.sticky");f.$inject=["$stateProvider"],k.provider("$stickyState",f);var w,F,R,j,m={},O=[],A={hasParamSet:!1};t.module("ct.ui.router.extras.sticky").run(["$stickyState",function(t){w=t}]),t.module("ct.ui.router.extras.sticky").config(["$provide","$stateProvider","$stickyStateProvider","$urlMatcherFactoryProvider","uirextras_coreProvider",function(r,n,a,o,i){var s=i.internalStates;A.hasParamSet=!!o.ParamSet,j=t.extend(new l("__inactives"),{self:{name:"__inactives"}}),F=R=e,O=[],i.onStateRegistered(function(t){t.self.sticky===!0&&a.registerStickyState(t.self)});var c;r.decorator("$state",["$delegate","$log","$q",function(r,n,i){return F=r.$current,s[""]=F,F.parent=j,j.parent=e,F.locals=u(j.locals,F.locals),delete j.locals.globals,c=r.transitionTo,r.transitionTo=function(e,u,f){function d(e){var r=t.extend(new l("reactivate_phase1"),{locals:e.locals});return r.self=t.extend({},e.self),r}function m(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,w.stateReactivated(e)},J.addRestoreFunction(function(){e.self.onEnter=n}),r}function h(t){var e=new l("inactivate");e.self=t.self;var r=t.self.onExit;return e.self.onExit=function(){w.stateInactivated(t)},J.addRestoreFunction(function(){t.self.onExit=r}),e}function g(t,e){var r=t.self.onEnter;return t.self.onEnter=function(){w.stateEntering(t,e,r)},J.addRestoreFunction(function(){t.self.onEnter=r}),t}function $(t,e){var r=t.self.onEnter;return t.self.onEnter=function(){w.stateEntering(t,e,r,!0)},J.addRestoreFunction(function(){t.self.onEnter=r}),t}function x(t){var e=t.self.onExit;return t.self.onExit=function(){w.stateExiting(t,N,e)},J.addRestoreFunction(function(){t.self.onExit=e}),t}var S=a.debugMode();j.locals||(j.locals=F.locals);var E=O.length;R&&(R(),S&&n.debug("Restored paths from pending transition"));var b,P,k,T,I=r.$current,C=r.params,M=f&&f.relative||r.$current,_=r.get(e,M),D=[],N=[];u=u||{},arguments[1]=u;var q=function(){},J=function(){b&&(z.path=b,b=null),P&&(I.path=P,P=null),t.forEach(J.restoreFunctions,function(t){t()}),J=q,R=null,O.splice(E,1)};if(J.restoreFunctions=[],J.addRestoreFunction=function(t){this.restoreFunctions.push(t)},_){var z=s[_.name];if(z){b=z.path,P=I.path;var K=f&&f.reload||!1,U=K&&(K===!0?b[0].self:r.get(K,M));f&&K&&K!==!0&&delete f.reload;var B={toState:z,toParams:u||{},fromState:I,fromParams:C||{},options:f,reloadStateTree:U};if(O.push(B),R=J,U){B.toParams.$$uirouterextrasreload=Math.random();var H=U.$$state().params,V=U.$$state().ownParams;if(A.hasParamSet){var W=new o.Param("$$uirouterextrasreload");H.$$uirouterextrasreload=V.$$uirouterextrasreload=W,J.restoreFunctions.push(function(){delete H.$$uirouterextrasreload,delete V.$$uirouterextrasreload})}else H.push("$$uirouterextrasreload"),V.push("$$uirouterextrasreload"),J.restoreFunctions.push(function(){H.length=H.length-1,V.length=V.length-1})}k=w.processTransition(B),S&&v(n,B,k);var L=z.path.slice(0,k.keep),Y=I.path.slice(0,k.keep);t.forEach(j.locals,function(t,e){-1!=e.indexOf("@")&&delete j.locals[e]});for(var G=0;G=0;s--)for(var c=0;c",link:function(r,a){function o(e){e=e.map(function(e){return""===e.name?l:t.copy(e)}),t.extend(u,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&&u[e];r&&((r.children||(r.children=[])).push(t),t.px=r.px,t.py=r.py,v.push(t))})}function i(){function t(t){var e=t.name.split(".").pop();return t.sticky&&(e+=" (STICKY)"),t.deepStateRedirect&&(e+=" (DSR)"),e}h=h.data(f.nodes(l),function(t){return t.name}),g=g.data(f.links(v),function(t){return t.target.name}),$=$.data(p),v.forEach(function(t){t.y=70*t.depth});var e=h.enter();$.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(){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 d({source:e,target:e})});var r=m.transition().duration(y);r.selectAll(".link").attr("d",d);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 u={},s=r.width||400,c=r.height||400,f=d3.layout.tree().size([s-20,c-20]).separation(function(t,e){return t.parent==e.parent?10:25}),l=e.get().filter(function(t){return""===t.name})[0],v=f(l);l.parent=l,l.px=l.x=s/2,l.py=l.y=c/2;var p={};p.px=p.x=l.px,p.py=p.y=l.py;{var d=d3.svg.diagonal(),m=d3.select(a.find("svg")[0]).attr("width",s).attr("height",c).append("g").attr("transform","translate(10, 10)"),h=m.selectAll(".node"),g=m.selectAll(".link"),$=m.selectAll(".active"),x=200,y=200;setInterval(i,x)}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});o((t||[]).filter(function(t){return-1==r.indexOf(t.name)}))}),i(x)}}}var r=t.module("ct.ui.router.extras.statevis",["ct.ui.router.extras.core"]);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"])}(angular); \ No newline at end of file diff --git a/release/modular/ct-ui-router-extras.core.js b/release/modular/ct-ui-router-extras.core.js new file mode 100644 index 0000000..616da4d --- /dev/null +++ b/release/modular/ct-ui-router-extras.core.js @@ -0,0 +1,174 @@ +(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/release/modular/ct-ui-router-extras.core.min.js b/release/modular/ct-ui-router-extras.core.min.js new file mode 100644 index 0000000..54d135c --- /dev/null +++ b/release/modular/ct-ui-router-extras.core.min.js @@ -0,0 +1 @@ +!function(r){"use strict";function n(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 t(n){if(Object.keys)return Object.keys(n);var t=[];return r.forEach(n,function(r,n){t.push(n)}),t}function e(r,n){var t=[];for(var e in r)n&&-1!==n.indexOf(e)||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=0>e?Math.ceil(e):Math.floor(e),0>e&&(e+=t);t>e;e++)if(e in r&&r[e]===n)return e;return-1}function a(r,e,a,i){var o,f=n(a,i),c={},s=[];for(var h in f)if(f[h].params&&(o=v(f[h].params)?f[h].params:t(f[h].params),o.length))for(var m in o)u(s,o[m])>=0||(s.push(o[m]),c[o[m]]=r[o[m]]);return p({},c,e)}function i(r,n){return p(new(p(function(){},{prototype:r})),n)}function o(r){s.push(r)}var f=r.module("ct.ui.router.extras.core",["ui.router"]),c={},s=[];f.config(["$stateProvider","$injector",function(n){n.decorator("parent",function(n,t){return c[n.self.name]=n,n.self.$$state=function(){return c[n.self.name]},r.forEach(s,function(r){r(n)}),t(n)})}]);var h=r.forEach,p=r.extend,v=r.isArray,m=function(r,n){var t=[];return h(r,function(r,e){t.push(n(r,e))}),t},l=function(r){return m(r,function(r,n){return n})},d=function(r,n){var t=[];return h(r,function(r,e){n(r,e)&&t.push(r)}),t},y=function(r,n){var t={};return h(r,function(r,e){n(r,e)&&(t[e]=r)}),t};f.provider("uirextras_core",function(){var f={internalStates:c,onStateRegistered:o,forEach:h,extend:p,isArray:v,map:m,keys:l,filter:d,filterObj:y,ancestors:n,objectKeys:t,protoKeys:e,arraySearch:u,inheritParams:a,inherit:i};r.extend(this,f),this.$get=function(){return f}})}(angular); \ No newline at end of file diff --git a/release/modular/ct-ui-router-extras.dsr.js b/release/modular/ct-ui-router-extras.dsr.js new file mode 100644 index 0000000..c282f10 --- /dev/null +++ b/release/modular/ct-ui-router-extras.dsr.js @@ -0,0 +1,165 @@ +(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.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 (name == dsrState || name.indexOf(dsrState + ".") != -1) { + lastSubstate[dsrState][key] = { state: name, params: angular.copy(toParams) }; + } + }); + } + }); + + return { + 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/release/modular/ct-ui-router-extras.dsr.min.js b/release/modular/ct-ui-router-extras.dsr.min.js new file mode 100644 index 0000000..41836cd --- /dev/null +++ b/release/modular/ct-ui-router-extras.dsr.min.js @@ -0,0 +1 @@ +!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.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){var v=u(r);if(!n&&(i(r)===m||v["default"])){var $=f(s,v.params),p=d[r.name][$]||v["default"];if(p){var l={redirect:{state:p.state,params:p.params},to:{state:r.name,params:s}},g=o.invoke(v.fn,r,{$dsr$:l});if(g){g.state&&(p=g),e.preventDefault();var h=c(s,v.params);a.go(p.state,t.extend(h,p.params))}}}}),r.$on("$stateChangeSuccess",function(e,r,n){var o=i(r);if(o){var s=r.name;t.forEach(d,function(e,r){var o=u(a.get(r)),i=f(n,o.params);(s==r||-1!=s.indexOf(r+"."))&&(d[r][i]={state:s,params:t.copy(n)})})}}),{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(){}])}(angular); \ No newline at end of file diff --git a/release/modular/ct-ui-router-extras.future.js b/release/modular/ct-ui-router-extras.future.js new file mode 100644 index 0000000..d58100b --- /dev/null +++ b/release/modular/ct-ui-router-extras.future.js @@ -0,0 +1,302 @@ +(function(angular, undefined){ +"use strict"; +(function(angular, undefined) { + var app = angular.module('ct.ui.router.extras.future', [ 'ct.ui.router.extras.core' ]); + + function _futureStateProvider($stateProvider, $urlRouterProvider, $urlMatcherFactory) { + 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.url; + } else if (parentName === "") { + parentMatcher = $urlMatcherFactory.compile(""); + } else { + var futureParent = findState((futureState.parent || parentName), true); + if (!futureParent) throw new Error("Couldn't determine parent state of future state. FutureState:" + angular.toJson(futureState)); + var pattern = futureParent.urlMatcher.source.replace(/\*rest$/, ""); + parentMatcher = $urlMatcherFactory.compile(pattern); + futureState.parentFutureState = futureParent; + } + if (futureState.url) { + futureState.urlMatcher = futureState.url.charAt(0) === "^" ? + $urlMatcherFactory.compile(futureState.url.substring(1) + "*rest") : + parentMatcher.concat(futureState.url + "*rest"); + } + }; + + this.get = function () { + return angular.extend({}, futureStates); + }; + + function findState(stateOrName, findFutureState) { + var statename = angular.isObject(stateOrName) ? stateOrName.name : stateOrName; + return !findFutureState ? internalStates[statename] : futureStates[statename]; + } + + /* options is an object with at least a name or url attribute */ + function findFutureState($state, options) { + if (options.name) { + var nameComponents = options.name.split(/\./); + if (options.name.charAt(0) === '.') + nameComponents[0] = $state.current.name; + while (nameComponents.length) { + var stateName = nameComponents.join("."); + if ($state.get(stateName, { relative: $state.current })) + return null; // State is already defined; nothing to do + if (futureStates[stateName]) + return futureStates[stateName]; + nameComponents.pop(); + } + } + + if (options.url) { + var matches = []; + for(var future in futureStates) { + var matcher = futureStates[future].urlMatcher; + if (matcher && matcher.exec(options.url)) { + matches.push(futureStates[future]); + } + } + // Find most specific by ignoring matching parents from matches + var copy = matches.slice(0); + for (var i = matches.length - 1; i >= 0; i--) { + for (var j = 0; j < copy.length; j++) { + if (matches[i] === copy[j].parentFutureState) matches.splice(i, 1); + } + } + return matches[0]; + } + } + + function lazyLoadState($injector, futureState) { + lazyloadInProgress = true; + var $q = $injector.get("$q"); + if (!futureState) { + var deferred = $q.defer(); + deferred.reject("No lazyState passed in " + futureState); + return deferred.promise; + } + + var promise = $q.when([]), parentFuture = futureState.parentFutureState; + if (parentFuture && futureStates[parentFuture.name]) { + promise = 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)); + return promise + .then(function(array) { + var injectorPromise = $injector.invoke(factory, factory, { futureState: futureState }); + return injectorPromise.then(function(fullState) { + if (fullState) { array.push(fullState); } // Pass a chain of realized states back + return array; + }); + }) + ["finally"](function() { // IE8 hack + delete(futureStates[futureState.name]); + }); + } + + var otherwiseFunc = [ '$log', '$location', + function otherwiseFunc($log, $location) { + $log.debug("Unable to map " + $location.path()); + }]; + + function futureState_otherwise($injector, $location) { + var resyncing = false; + + var lazyLoadMissingState = + ['$rootScope', '$urlRouter', '$state', + function lazyLoadMissingState($rootScope, $urlRouter, $state) { + function resync() { + resyncing = true; $urlRouter.sync(); resyncing = false; + } + if (!initDone) { + // Asynchronously load state definitions, then resync URL + initPromise().then(resync); + initDone = true; + return; + } + + var futureState = findFutureState($state, { url: $location.path() }); + if (!futureState) { + return $injector.invoke(otherwiseFunc); + } + + // Config loaded. Asynchronously lazy-load state definition from URL fragment, if mapped. + lazyLoadState($injector, futureState).then(function lazyLoadedStateCallback(states) { + states.forEach(function (state) { + if (state && (!$state.get(state) || (state.name && !$state.get(state.name)))) + $stateProvider.state(state); + }); + lazyloadInProgress = false; + resync(); + }, function lazyLoadStateAborted() { + lazyloadInProgress = false; + resync(); + }); + }]; + if (lazyloadInProgress) return; + + var nextFn = resyncing ? otherwiseFunc : lazyLoadMissingState; + return $injector.invoke(nextFn); + } + + $urlRouterProvider.otherwise(futureState_otherwise); + + $urlRouterProvider.otherwise = function(rule) { + if (angular.isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!angular.isFunction(rule)) throw new Error("'rule' must be a function"); + otherwiseFunc = ['$injector', '$location', rule]; + return $urlRouterProvider; + }; + + var serviceObject = { + getResolvePromise: function () { + return initPromise(); + } + }; + + // Used in .run() block to init + this.$get = [ '$injector', '$state', '$q', '$rootScope', '$urlRouter', '$timeout', '$log', + function futureStateProvider_get($injector, $state, $q, $rootScope, $urlRouter, $timeout, $log) { + function init() { + $rootScope.$on("$stateNotFound", function futureState_notFound(event, unfoundState, fromState, fromParams) { + if (lazyloadInProgress) return; + $log.debug("event, unfoundState, fromState, fromParams", event, unfoundState, fromState, fromParams); + + var futureState = findFutureState($state, { name: unfoundState.to }); + if (!futureState) return; + + event.preventDefault(); + var promise = lazyLoadState($injector, futureState); + promise.then(function (states) { + states.forEach(function (state) { + if (state && (!$state.get(state) || (state.name && !$state.get(state.name)))) + $stateProvider.state(state); + }); + $state.go(unfoundState.to, unfoundState.toParams); + lazyloadInProgress = false; + }, function (error) { + console.log("failed to lazy load state ", error); + $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($urlRouter.sync, $urlRouter.sync); + } else { + $urlRouter.sync(); + } + }); + }); + } + + init(); + + serviceObject.state = $stateProvider.state; + serviceObject.futureState = provider.futureState; + serviceObject.get = provider.get; + + return serviceObject; + } + ]; + } + + app.provider('$futureState', [ '$stateProvider', '$urlRouterProvider', '$urlMatcherFactoryProvider', _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/release/modular/ct-ui-router-extras.future.min.js b/release/modular/ct-ui-router-extras.future.min.js new file mode 100644 index 0000000..58d1f81 --- /dev/null +++ b/release/modular/ct-ui-router-extras.future.min.js @@ -0,0 +1 @@ +!function(t){"use strict";!function(t,e){function r(e,r,n){function a(e,r){var n=t.isObject(e)?e.name:e;return r?s[n]:internalStates[n]}function o(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(s[n])return s[n];r.pop()}}if(e.url){var a=[];for(var o in s){var u=s[o].urlMatcher;u&&u.exec(e.url)&&a.push(s[o])}for(var i=a.slice(0),c=a.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/release/modular/ct-ui-router-extras.statevis.min.js b/release/modular/ct-ui-router-extras.statevis.min.js new file mode 100644 index 0000000..e61bed5 --- /dev/null +++ b/release/modular/ct-ui-router-extras.statevis.min.js @@ -0,0 +1 @@ +!function(t){"use strict";!function(){function n(n,e,r){return{scope:{width:"@",height:"@"},restrict:"AE",template:"",link:function(e,a){function i(n){n=n.map(function(n){return""===n.name?p:t.copy(n)}),t.extend(u,n.reduce(function(t,n){return t[n.name]=n,t},{})),n.forEach(function(t){var n=t.name.split(/\./).slice(0,-1).join("."),e=t.name!=n&&u[n];e&&((e.children||(e.children=[])).push(t),t.px=e.px,t.py=e.py,f.push(t))})}function c(){function t(t){var n=t.name.split(".").pop();return t.sticky&&(n+=" (STICKY)"),t.deepStateRedirect&&(n+=" (DSR)"),n}v=v.data(l.nodes(p),function(t){return t.name}),h=h.data(l.links(f),function(t){return t.target.name}),m=m.data(d),f.forEach(function(t){t.y=70*t.depth});var n=v.enter();m.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}),n.append("circle").attr("class","node").attr("r",9).attr("cx",function(t){return t.parent.px}).attr("cy",function(t){return t.parent.py}),n.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(){return"middle"}).text(t).style("fill-opacity",1),h.enter().insert("path",".node").attr("class","link").attr("d",function(t){var n={x:t.source.px,y:t.source.py};return x({source:n,target:n})});var e=y.transition().duration(A);e.selectAll(".link").attr("d",x);var r={entered:"#AF0",exited:"#777",active:"#0f0",inactive:"#55F",future:"#009"};e.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"}),e.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+")"}),e.selectAll(".active").attr("x",function(t){return t.px=t.x}).attr("y",function(t){return t.py=t.y-15})}var u={},o=e.width||400,s=e.height||400,l=d3.layout.tree().size([o-20,s-20]).separation(function(t,n){return t.parent==n.parent?10:25}),p=n.get().filter(function(t){return""===t.name})[0],f=l(p);p.parent=p,p.px=p.x=o/2,p.py=p.y=s/2;var d={};d.px=d.x=p.px,d.py=d.y=p.py;{var x=d3.svg.diagonal(),y=d3.select(a.find("svg")[0]).attr("width",o).attr("height",s).append("g").attr("transform","translate(10, 10)"),v=y.selectAll(".node"),h=y.selectAll(".link"),m=y.selectAll(".active"),g=200,A=200;setInterval(c,g)}r(function(){e.states=n.get(),t.forEach(f,function(t){var e=n.get(t.name);e&&(t.status=e.status||"exited")})},250),e.$watchCollection("states",function(t,n){var e=(n||[]).map(function(t){return t.name});i((t||[]).filter(function(t){return-1==e.indexOf(t.name)}))}),c(g)}}}var e=t.module("ct.ui.router.extras.statevis",["ct.ui.router.extras.core"]);e.directive("stateVis",["$state","$timeout","$interval",n])}()}(angular); \ No newline at end of file diff --git a/release/modular/ct-ui-router-extras.sticky.js b/release/modular/ct-ui-router-extras.sticky.js new file mode 100644 index 0000000..10086a5 --- /dev/null +++ b/release/modular/ct-ui-router-extras.sticky.js @@ -0,0 +1,821 @@ +(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' ]; +function $StickyStateProvider($stateProvider) { + // 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; + } + + // Given a state, returns all ancestor states which are sticky. + // Walks up the view's state's ancestry tree and locates each ancestor state which is marked as sticky. + // Returns an array populated with only those ancestor sticky states. + function getStickyStateStack(state) { + var stack = []; + if (!state) return stack; + do { + if (state.sticky) stack.push(state); + state = state.parent; + } while (state); + stack.reverse(); + return stack; + } + + // Used by processTransition to determine if what kind of sticky state transition this is. + // returns { from: (bool), to: (bool) } + function getStickyTransitionType(fromPath, toPath, keep) { + if (fromPath[keep] === toPath[keep]) return { from: false, to: false }; + var stickyFromState = keep < fromPath.length && fromPath[keep].self.sticky; + var stickyToState = keep < toPath.length && toPath[keep].self.sticky; + return { from: stickyFromState, to: stickyToState }; + } + + // Returns a sticky transition type necessary to enter the state. + // Transition can be: reactivate, updateStateParams, or enter + + // Note: if a state is being reactivated but params dont match, we treat + // it as a Exit/Enter, thus the special "updateStateParams" transition. + // If a parent inactivated state has "updateStateParams" transition type, then + // all descendant states must also be exit/entered, thus the first line of this function. + function getEnterTransition(state, stateParams, reloadStateTree, ancestorParamsChanged) { + if (ancestorParamsChanged) return "updateStateParams"; + var inactiveState = inactiveStates[state.self.name]; + if (!inactiveState) return "enter"; + if (state.self === reloadStateTree) return "updateStateParams"; +// if (inactiveState.locals == null || inactiveState.locals.globals == null) debugger; + var paramsMatch = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams); +// if (DEBUG) $log.debug("getEnterTransition: " + state.name + (paramsMatch ? ": reactivate" : ": updateStateParams")); + return paramsMatch ? "reactivate" : "updateStateParams"; + } + + // Given a state and (optional) stateParams, returns the inactivated state from the inactive sticky state registry. + function getInactivatedState(state, stateParams) { + var inactiveState = inactiveStates[state.name]; + if (!inactiveState) return null; + if (!stateParams) return inactiveState; + var paramsMatch = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams); + return paramsMatch ? inactiveState : null; + } + + // Duplicates logic in $state.transitionTo, primarily to find the pivot state (i.e., the "keep" value) + function equalForKeys(a, b, keys) { + if (!angular.isArray(keys) && angular.isObject(keys)) { + keys = protoKeys(keys, ["$$keys", "$$values", "$$equals", "$$validates", "$$new", "$$parent"]); + } + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized + } + return true; + } + + var stickySupport = { + getInactiveStates: function () { + var states = []; + angular.forEach(inactiveStates, function (state) { + states.push(state); + }); + return states; + }, + getInactiveStatesByParent: function () { + return mapInactives(); + }, + // Main API for $stickyState, used by $state. + // Processes a potential transition, returns an object with the following attributes: + // { + // inactives: Array of all states which will be inactive if the transition is completed. (both previously and newly inactivated) + // enter: Enter transition type for all added states. This is a sticky array to "toStates" array in $state.transitionTo. + // exit: Exit transition type for all removed states. This is a sticky array to "fromStates" array in $state.transitionTo. + // } + processTransition: function (transition) { + // This object is returned + var result = { inactives: [], enter: [], exit: [], keep: 0 }; + var fromPath = transition.fromState.path, + fromParams = transition.fromParams, + toPath = transition.toState.path, + toParams = transition.toParams, + reloadStateTree = transition.reloadStateTree, + options = transition.options; + var keep = 0, state = toPath[keep]; + + if (options.inherit) { + toParams = inheritParams($stateParams, toParams || {}, $state.$current, transition.toState); + } + + while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) { + // We're "keeping" this state. bump keep var and get the next state in toPath for the next iteration. + state = toPath[++keep]; + } + + result.keep = keep; + + var idx, deepestUpdatedParams, deepestReactivate, noLongerInactiveStates = {}, pType = getStickyTransitionType(fromPath, toPath, keep); + var ancestorUpdated = !!options.reload; // When ancestor params change, treat reactivation as exit/enter + + // Calculate the "enter" transitions for new states in toPath + // Enter transitions will be either "enter", "reactivate", or "updateStateParams" where + // enter: full resolve, no special logic + // reactivate: use previous locals + // updateStateParams: like 'enter', except exit the inactive state before entering it. + for (idx = keep; idx < toPath.length; idx++) { + var enterTrans = !pType.to ? "enter" : getEnterTransition(toPath[idx], toParams, reloadStateTree, ancestorUpdated); + ancestorUpdated = (ancestorUpdated || enterTrans == 'updateStateParams'); + result.enter[idx] = enterTrans; + // If we're reactivating a state, make a note of it, so we can remove that state from the "inactive" list + if (enterTrans == 'reactivate') + deepestReactivate = noLongerInactiveStates[toPath[idx].name] = toPath[idx]; + if (enterTrans == 'updateStateParams') + deepestUpdatedParams = noLongerInactiveStates[toPath[idx].name] = toPath[idx]; + } + deepestReactivate = deepestReactivate ? deepestReactivate.self.name + "." : ""; + deepestUpdatedParams = deepestUpdatedParams ? deepestUpdatedParams.self.name + "." : ""; + + // Inactive states, before the transition is processed, mapped to the parent to the sticky state. + var inactivesByParent = mapInactives(); + + // root ("") is always kept. Find the remaining names of the kept path. + var keptStateNames = [""].concat(map(fromPath.slice(0, keep), function (state) { + return state.self.name; + })); + + // Locate currently and newly inactive states (at pivot and above) and store them in the output array 'inactives'. + angular.forEach(keptStateNames, function (name) { + var inactiveChildren = inactivesByParent[name]; + for (var i = 0; inactiveChildren && i < inactiveChildren.length; i++) { + var child = inactiveChildren[i]; + // Don't organize state as inactive if we're about to reactivate it. + if (!noLongerInactiveStates[child.name] && + (!deepestReactivate || (child.self.name.indexOf(deepestReactivate) !== 0)) && + (!deepestUpdatedParams || (child.self.name.indexOf(deepestUpdatedParams) !== 0))) + result.inactives.push(child); + } + }); + + // Calculate the "exit" transition for states not kept, in fromPath. + // Exit transition can be one of: + // exit: standard state exit logic + // inactivate: register state as an inactive state + for (idx = keep; idx < fromPath.length; idx++) { + var exitTrans = "exit"; + if (pType.from) { + // State is being inactivated, note this in result.inactives array + result.inactives.push(fromPath[idx]); + exitTrans = "inactivate"; + } + result.exit[idx] = exitTrans; + } + + return result; + }, + + // Adds a state to the inactivated sticky state registry. + stateInactivated: function (state) { + // Keep locals around. + inactiveStates[state.self.name] = state; + // Notify states they are being Inactivated (i.e., a different + // sticky state tree is now active). + state.self.status = 'inactive'; + if (state.self.onInactivate) + $injector.invoke(state.self.onInactivate, state.self, state.locals.globals); + }, + + // Removes a previously inactivated state from the inactive sticky state registry + stateReactivated: function (state) { + if (inactiveStates[state.self.name]) { + delete inactiveStates[state.self.name]; + } + state.self.status = 'entered'; +// 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) { + var state = $state.get(inactiveState); + 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) + 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 internalStates = uirextras_coreProvider.internalStates; + + 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. + 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 false, 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]; + }); + + // Find all states that will be inactive once the transition succeeds. For each of those states, + // place its view-locals on the __inactives pseudostate's .locals. This allows the ui-view directive + // to access them and render the inactive views. + for (var i = 0; i < stickyTransitions.inactives.length; i++) { + var iLocals = stickyTransitions.inactives[i].locals; + angular.forEach(iLocals, function (view, name) { + if (iLocals.hasOwnProperty(name) && name.indexOf("@") != -1) { // Only grab this state's "view" locals + inactivePseudoState.locals[name] = view; // Add all inactive views not already included. + } + }); + } + + // Find all the states the transition will be entering. For each entered state, check entered-state-transition-type + // Depending on the entered-state transition type, place the proper surrogate state on the surrogate toPath. + angular.forEach(stickyTransitions.enter, function (value, idx) { + var surrogate; + var enteringState = toState.path[idx]; + if (value === "reactivate") { + // Reactivated states require TWO surrogates. The "phase 1 reactivated surrogates" are added to both + // to.path and from.path, and as such, are considered to be "kept" by UI-Router. + // This is required to get UI-Router to add the surrogate locals to the protoypal locals object + surrogate = stateReactivatedSurrogatePhase1(enteringState); + surrogateToPath.push(surrogate); + surrogateFromPath.push(surrogate); // so toPath[i] === fromPath[i] + + // The "phase 2 reactivated surrogate" is added to the END of the .path, after all the phase 1 + // surrogates have been added. + reactivated.push(stateReactivatedSurrogatePhase2(enteringState)); + terminalReactivatedState = enteringState; + } else if (value === "updateStateParams") { + // If the state params have been changed, we need to exit any inactive states and re-enter them. + surrogate = stateUpdateParamsSurrogate(enteringState); + surrogateToPath.push(surrogate); + 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 for reactivated to ToPath again, this time without a matching FromPath entry + // This is to get ui-router to call the surrogate's onEnter callback. + if (reactivated.length) { + angular.forEach(reactivated, function (surrogate) { + surrogateToPath.push(surrogate); + }); + } + + // We may transition directly to an inactivated state, reactivating it. In this case, we should + // exit all of that state's inactivated children. + if (toState === terminalReactivatedState) { + var prefix = terminalReactivatedState.self.name + "."; + var inactiveStates = _StickyState.getInactiveStates(); + var inactiveOrphans = []; + inactiveStates.forEach(function (exiting) { + if (exiting.self.name.indexOf(prefix) === 0) { + inactiveOrphans.push(exiting); + } + }); + inactiveOrphans.sort(); + inactiveOrphans.reverse(); + // Add surrogate exited states for all orphaned descendants of the Deepest Reactivated State + surrogateFromPath = surrogateFromPath.concat(map(inactiveOrphans, function (exiting) { + return stateExitedSurrogate(exiting); + })); + exited = exited.concat(inactiveOrphans); + } + + // Replace the .path variables. toState.path and fromState.path are now ready for a sticky transition. + toState.path = surrogateToPath; + fromState.path = surrogateFromPath; + + var pathMessage = function (state) { + return (state.surrogateType ? state.surrogateType + ":" : "") + state.self.name; + }; + if (DEBUG) $log.debug("SurrogateFromPath: ", map(surrogateFromPath, pathMessage)); + if (DEBUG) $log.debug("SurrogateToPath: ", map(surrogateToPath, pathMessage)); + } + } + + // toState and fromState are all set up; now run stock UI-Router's $state.transitionTo(). + var transitionPromise = $state_transitionTo.apply($state, arguments); + + // Add post-transition promise handlers, then return the promise to the original caller. + return transitionPromise.then(function transitionSuccess(state) { + // First, restore toState and fromState to their original values. + restore(); + if (DEBUG) debugViewsAfterSuccess($log, internalStates[state.name], $state); + + state.status = 'active'; // TODO: This status is used in statevis.js, and almost certainly belongs elsewhere. + + return state; + }, function transitionFailed(err) { + restore(); + if (DEBUG && + err.message !== "transition prevented" && + err.message !== "transition aborted" && + err.message !== "transition superseded") { + $log.debug("transition failed", err); + console.log(err.stack); + } + return $q.reject(err); + }); + }; + return $state; + }]); + } + ] +); + +function debugTransition($log, currentTransition, stickyTransition) { + function message(path, index, state) { + return (path[index] ? path[index].toUpperCase() + ": " + state.self.name : "(" + state.self.name + ")"); + } + + var inactiveLogVar = map(stickyTransition.inactives, function (state) { + return state.self.name; + }); + var enterLogVar = map(currentTransition.toState.path, function (state, index) { + return message(stickyTransition.enter, index, state); + }); + var exitLogVar = map(currentTransition.fromState.path, function (state, index) { + return message(stickyTransition.exit, index, state); + }); + + var transitionMessage = currentTransition.fromState.self.name + ": " + + angular.toJson(currentTransition.fromParams) + ": " + + " -> " + + currentTransition.toState.self.name + ": " + + angular.toJson(currentTransition.toParams); + + $log.debug(" Current transition: ", transitionMessage); + $log.debug("Before transition, inactives are: : ", map(_StickyState.getInactiveStates(), function (s) { + return s.self.name; + })); + $log.debug("After transition, inactives will be: ", inactiveLogVar); + $log.debug("Transition will exit: ", exitLogVar); + $log.debug("Transition will enter: ", enterLogVar); +} + +function debugViewsAfterSuccess($log, currentState, $state) { + $log.debug("Current state: " + currentState.self.name + ", inactive states: ", map(_StickyState.getInactiveStates(), function (s) { + return s.self.name; + })); + + var viewMsg = function (local, name) { + return "'" + name + "' (" + local.$$state.name + ")"; + }; + var statesOnly = function (local, name) { + return name != 'globals' && name != 'resolve'; + }; + var viewsForState = function (state) { + var views = map(filterObj(state.locals, statesOnly), viewMsg).join(", "); + return "(" + (state.self.name ? state.self.name : "root") + ".locals" + (views.length ? ": " + views : "") + ")"; + }; + + var message = viewsForState(currentState); + var parent = currentState.parent; + while (parent && parent !== currentState) { + if (parent.self.name === "") { + // Show the __inactives before showing root state. + message = viewsForState($state.$current.path[0]) + " / " + message; + } + message = viewsForState(parent) + " / " + message; + currentState = parent; + parent = currentState.parent; + } + + $log.debug("Views: " + message); +} + + +})(angular); \ No newline at end of file diff --git a/release/modular/ct-ui-router-extras.sticky.min.js b/release/modular/ct-ui-router-extras.sticky.min.js new file mode 100644 index 0000000..a281f40 --- /dev/null +++ b/release/modular/ct-ui-router-extras.sticky.min.js @@ -0,0 +1 @@ +!function(e,t){"use strict";function a(){var t={},a={},n=!1;this.registerStickyState=function(e){a[e.name]=e},this.enableDebug=this.debugMode=function(t){return e.isDefined(t)&&(n=t),n},this.$get=["$rootScope","$state","$stateParams","$injector","$log",function(a,r,s,o,i){function u(){var a={};return e.forEach(t,function(e){for(var t=l(e),n=0;n "+a.toState.self.name+": "+e.toJson(a.toParams);t.debug(" Current transition: ",l),t.debug("Before transition, inactives are: : ",map(i.getInactiveStates(),function(e){return e.self.name})),t.debug("After transition, inactives will be: ",s),t.debug("Transition will exit: ",u),t.debug("Transition will enter: ",o)}function s(e,t,a){e.debug("Current state: "+t.self.name+", inactive states: ",map(i.getInactiveStates(),function(e){return e.self.name}));for(var n=function(e,t){return"'"+t+"' ("+e.$$state.name+")"},r=function(e,t){return"globals"!=t&&"resolve"!=t},s=function(e){var t=map(filterObj(e.locals,r),n).join(", ");return"("+(e.self.name?e.self.name:"root")+".locals"+(t.length?": "+t:"")+")"},o=s(t),u=t.parent;u&&u!==t;)""===u.self.name&&(o=s(a.$current.path[0])+" / "+o),o=s(u)+" / "+o,t=u,u=t.parent;e.debug("Views: "+o)}e.module("ct.ui.router.extras.sticky",["ct.ui.router.extras.core"]);var o=e.module("ct.ui.router.extras.sticky");a.$inject=["$stateProvider"],o.provider("$stickyState",a);var i,u,l,c,f=[],v={hasParamSet:!1};e.module("ct.ui.router.extras.sticky").run(["$stickyState",function(e){i=e}]),e.module("ct.ui.router.extras.sticky").config(["$provide","$stateProvider","$stickyStateProvider","$urlMatcherFactoryProvider","uirextras_coreProvider",function(a,o,d,m,p){var h=p.internalStates;v.hasParamSet=!!m.ParamSet,c=e.extend(new n("__inactives"),{self:{name:"__inactives"}}),u=l=t,f=[],p.onStateRegistered(function(e){e.self.sticky===!0&&d.registerStickyState(e.self)});var g;a.decorator("$state",["$delegate","$log","$q",function(a,o,p){return u=a.$current,h[""]=u,u.parent=c,c.parent=t,u.locals=inherit(c.locals,u.locals),delete c.locals.globals,g=a.transitionTo,a.transitionTo=function(t,$,x){function S(t){var a=e.extend(new n("reactivate_phase1"),{locals:t.locals});return a.self=e.extend({},t.self),a}function E(t){var a=e.extend(new n("reactivate_phase2"),t),r=a.self.onEnter;return a.resolve={},a.views={},a.self.onEnter=function(){a.locals=t.locals,i.stateReactivated(t)},D.addRestoreFunction(function(){t.self.onEnter=r}),a}function P(e){var t=new n("inactivate");t.self=e.self;var a=e.self.onExit;return t.self.onExit=function(){i.stateInactivated(e)},D.addRestoreFunction(function(){e.self.onExit=a}),t}function b(e,t){var a=e.self.onEnter;return e.self.onEnter=function(){i.stateEntering(e,t,a)},D.addRestoreFunction(function(){e.self.onEnter=a}),e}function k(e,t){var a=e.self.onEnter;return e.self.onEnter=function(){i.stateEntering(e,t,a,!0)},D.addRestoreFunction(function(){e.self.onEnter=a}),e}function y(e){var t=e.self.onExit;return e.self.onExit=function(){i.stateExiting(e,A,t)},D.addRestoreFunction(function(){e.self.onExit=t}),e}var w=d.debugMode();c.locals||(c.locals=u.locals);var F=f.length;l&&(l(),w&&o.debug("Restored paths from pending transition"));var R,T,I,_,O=a.$current,j=a.params,C=x&&x.relative||a.$current,M=a.get(t,C),q=[],A=[];$=$||{},arguments[1]=$;var B=function(){},D=function(){R&&(J.path=R,R=null),T&&(O.path=T,T=null),e.forEach(D.restoreFunctions,function(e){e()}),D=B,l=null,f.splice(F,1)};if(D.restoreFunctions=[],D.addRestoreFunction=function(e){this.restoreFunctions.push(e)},M){var J=h[M.name];if(J){R=J.path,T=O.path;var K=x&&x.reload||!1,L=K&&(K===!0?R[0].self:a.get(K,C));x&&K&&K!==!0&&delete x.reload;var U={toState:J,toParams:$||{},fromState:O,fromParams:j||{},options:x,reloadStateTree:L};if(f.push(U),l=D,L){U.toParams.$$uirouterextrasreload=Math.random();var V=L.$$state().params,z=L.$$state().ownParams;if(v.hasParamSet){var G=new m.Param("$$uirouterextrasreload");V.$$uirouterextrasreload=z.$$uirouterextrasreload=G,D.restoreFunctions.push(function(){delete V.$$uirouterextrasreload,delete z.$$uirouterextrasreload})}else V.push("$$uirouterextrasreload"),z.push("$$uirouterextrasreload"),D.restoreFunctions.push(function(){V.length=V.length-1,z.length=z.length-1})}I=i.processTransition(U),w&&r(o,U,I);var H=J.path.slice(0,I.keep),N=O.path.slice(0,I.keep);e.forEach(c.locals,function(e,t){-1!=t.indexOf("@")&&delete c.locals[t]});for(var Q=0;Q= 0) continue; + if (indexOf(inheritList, parentParams[j]) >= 0) continue; inheritList.push(parentParams[j]); inherited[parentParams[j]] = currentParams[parentParams[j]]; } @@ -159,6 +159,66 @@ function filterByKeys(keys, values) { }); return filtered; } + +// like _.indexBy +// when you know that your index values will be unique, or you want last-one-in to win +function indexBy(array, propName) { + var result = {}; + forEach(array, function(item) { + result[item[propName]] = item; + }); + return result; +} + +// extracted from underscore.js +// Return a copy of the object only containing the whitelisted properties. +function pick(obj) { + var copy = {}; + var keys = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1)); + forEach(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; +} + +// extracted from underscore.js +// Return a copy of the object omitting the blacklisted properties. +function omit(obj) { + var copy = {}; + var keys = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1)); + for (var key in obj) { + if (keys.indexOf(key) == -1) copy[key] = obj[key]; + } + return copy; +} + +function pluck(collection, key) { + var result = isArray(collection) ? [] : {}; + + forEach(collection, function(val, i) { + result[i] = isFunction(key) ? key(val) : val[key]; + }); + return result; +} + +function filter(collection, callback) { + var result = isArray(collection) ? [] : {}; + forEach(collection, function(val, i) { + if (callback(val, i)) + result[i] = val; + }); + return result; +} + +function map(collection, callback) { + var result = isArray(collection) ? [] : {}; + + forEach(collection, function(val, i) { + result[i] = callback(val, i); + }); + return result; +} + /** * @ngdoc overview * @name ui.router.util @@ -285,6 +345,7 @@ function $Resolve( $q, $injector) { */ this.study = function (invocables) { if (!isObject(invocables)) throw new Error("'invocables' must be an object"); + var invocableKeys = objectKeys(invocables || {}); // Perform a topological sort of invocables to build an ordered plan var plan = [], cycle = [], visited = {}; @@ -293,7 +354,7 @@ function $Resolve( $q, $injector) { cycle.push(key); if (visited[key] === VISIT_IN_PROGRESS) { - cycle.splice(0, cycle.indexOf(key)); + cycle.splice(0, indexOf(cycle, key)); throw new Error("Cyclic dependency: " + cycle.join(" -> ")); } visited[key] = VISIT_IN_PROGRESS; @@ -345,7 +406,7 @@ function $Resolve( $q, $injector) { if (!--wait) { if (!merged) merge(values, parent.$$values); result.$$values = values; - result.$$promises = true; // keep for isResolve() + result.$$promises = result.$$promises || true; // keep for isResolve() delete result.$$inheritedValues; resolution.resolve(values); } @@ -355,7 +416,7 @@ function $Resolve( $q, $injector) { result.$$failure = reason; resolution.reject(reason); } - + // Short-circuit if parent has already failed if (isDefined(parent.$$failure)) { fail(parent.$$failure); @@ -363,20 +424,20 @@ function $Resolve( $q, $injector) { } if (parent.$$inheritedValues) { - merge(values, parent.$$inheritedValues); + merge(values, omit(parent.$$inheritedValues, invocableKeys)); } // Merge parent values if the parent has already resolved, or merge // parent promises and wait if the parent resolve is still in progress. + extend(promises, parent.$$promises); if (parent.$$values) { - merged = merge(values, parent.$$values); - result.$$inheritedValues = parent.$$values; + merged = merge(values, omit(parent.$$values, invocableKeys)); + result.$$inheritedValues = omit(parent.$$values, invocableKeys); done(); } else { if (parent.$$inheritedValues) { - result.$$inheritedValues = parent.$$inheritedValues; + result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); } - extend(promises, parent.$$promises); parent.then(done, fail); } @@ -579,7 +640,7 @@ function $TemplateFactory( $http, $templateCache, $injector) { if (isFunction(url)) url = url(params); if (url == null) return null; else return $http - .get(url, { cache: $templateCache }) + .get(url, { cache: $templateCache, headers: { Accept: 'text/html' }}) .then(function(response) { return response.data; }); }; @@ -605,6 +666,8 @@ function $TemplateFactory( $http, $templateCache, $injector) { angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); +var $$UMFP; // reference to $UrlMatcherFactoryProvider + /** * @ngdoc object * @name ui.router.util.type:UrlMatcher @@ -622,8 +685,8 @@ angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); * * `':'` name - colon placeholder * * `'*'` name - catch-all placeholder * * `'{' name '}'` - curly placeholder - * * `'{' name ':' regexp '}'` - curly placeholder with regexp. Should the regexp itself contain - * curly braces, they must be in matched pairs or escaped with a backslash. + * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the + * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. * * Parameter names may contain only word characters (latin letters, digits, and underscore) and * must be unique within the pattern (across both path and search parameters). For colon @@ -644,9 +707,13 @@ angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the * path into the parameter 'path'. * * `'/files/*path'` - ditto. + * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined + * in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start * * @param {string} pattern The pattern to compile into a matcher. * @param {Object} config A configuration object hash: + * @param {Object=} parentMatcher Used to concatenate the pattern/config onto + * an existing UrlMatcher * * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. @@ -666,8 +733,8 @@ angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); * * @returns {Object} New `UrlMatcher` object */ -function UrlMatcher(pattern, config) { - config = angular.isObject(config) ? config : {}; +function UrlMatcher(pattern, config, parentMatcher) { + config = extend({ params: {} }, isObject(config) ? config : {}); // Find all placeholders and create a compiled pattern, using either classic or curly syntax: // '*' name @@ -676,63 +743,62 @@ function UrlMatcher(pattern, config) { // '{' name ':' regexp '}' // The regular expression is somewhat complicated due to the need to allow curly braces // inside the regular expression. The placeholder regexp breaks down as follows: - // ([:*])(\w+) classic placeholder ($1 / $2) - // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4) - // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either - // [^{}\\]+ - anything other than curly braces or backslash - // \\. - a backslash escape - // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms - var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case) + // \{([\w\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case + // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either + // [^{}\\]+ - anything other than curly braces or backslash + // \\. - a backslash escape + // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms + var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, compiled = '^', last = 0, m, segments = this.segments = [], - params = this.params = {}; - - /** - * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the - * default value, which may be the result of an injectable function. - */ - function $value(value) { - /*jshint validthis: true */ - return isDefined(value) ? this.type.decode(value) : $UrlMatcherFactory.$$getDefaultValue(this); - } + parentParams = parentMatcher ? parentMatcher.params : {}, + params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(); - function addParameter(id, type, config) { - if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + function addParameter(id, type, config, location) { + if (parentParams[id]) return parentParams[id]; + if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); - params[id] = extend({ type: type || new Type(), $value: $value }, config); + params[id] = new $$UMFP.Param(id, type, config, location); + return params[id]; } - function quoteRegExp(string, pattern, isOptional) { - var result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + function quoteRegExp(string, pattern, squash) { + var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); if (!pattern) return result; - var flag = isOptional ? '?' : ''; - return result + flag + '(' + pattern + ')' + flag; - } - - function paramConfig(param) { - if (!config.params || !config.params[param]) return {}; - var cfg = config.params[param]; - return isObject(cfg) ? cfg : { value: cfg }; + switch(squash) { + case false: surroundPattern = ['(', ')']; break; + case true: surroundPattern = ['?(', ')?']; break; + default: surroundPattern = ['(' + squash + "|", ')?']; break; + } + return result + surroundPattern[0] + pattern + surroundPattern[1]; } this.source = pattern; // Split into static segments separated by path parameter placeholders. // The number of segments is always 1 more than the number of parameters. - var id, regexp, segment, type, cfg; + function matchDetails(m, isSearch) { + var id, regexp, segment, type, cfg, arrayMode; + id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null + cfg = config.params[id]; + segment = pattern.substring(last, m.index); + regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); + type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp) }); + return { + id: id, regexp: regexp, segment: segment, type: type, cfg: cfg + }; + } + var p, param, segment; while ((m = placeholder.exec(pattern))) { - id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null - regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*'); - segment = pattern.substring(last, m.index); - type = this.$types[regexp] || new Type({ pattern: new RegExp(regexp) }); - cfg = paramConfig(id); + p = matchDetails(m, false); + if (p.segment.indexOf('?') >= 0) break; // we're into the search part - if (segment.indexOf('?') >= 0) break; // we're into the search part - - compiled += quoteRegExp(segment, type.$subPattern(), isDefined(cfg.value)); - addParameter(id, type, cfg); - segments.push(segment); + param = addParameter(p.id, p.type, p.cfg, "path"); + compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash); + segments.push(p.segment); last = placeholder.lastIndex; } segment = pattern.substring(last); @@ -745,10 +811,15 @@ function UrlMatcher(pattern, config) { segment = segment.substring(0, i); this.sourcePath = pattern.substring(0, last + i); - // Allow parameters to be separated by '?' as well as '&' to make concat() easier - forEach(search.substring(1).split(/[&?]/), function(key) { - addParameter(key, null, paramConfig(key)); - }); + if (search.length > 0) { + last = 0; + while ((m = searchPlaceholder.exec(search))) { + p = matchDetails(m, true); + param = addParameter(p.id, p.type, p.cfg, "search"); + last = placeholder.lastIndex; + // check if ?& + } + } } else { this.sourcePath = pattern; this.sourceSearch = ''; @@ -787,7 +858,12 @@ UrlMatcher.prototype.concat = function (pattern, config) { // Because order of search parameters is irrelevant, we can add our own search // parameters to the end of the new pattern. Parse the new pattern by itself // and then join the bits together, but it's much easier to do this on a string level. - return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, config); + var defaultConfig = { + caseInsensitive: $$UMFP.caseInsensitive(), + strict: $$UMFP.strictMode(), + squash: $$UMFP.defaultSquashPolicy() + }; + return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); }; UrlMatcher.prototype.toString = function () { @@ -823,21 +899,35 @@ UrlMatcher.prototype.exec = function (path, searchParams) { if (!m) return null; searchParams = searchParams || {}; - var params = this.parameters(), nTotal = params.length, + var paramNames = this.parameters(), nTotal = paramNames.length, nPath = this.segments.length - 1, - values = {}, i, cfg, param; + values = {}, i, j, cfg, paramName; if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); + function decodePathArray(string) { + function reverseString(str) { return str.split("").reverse().join(""); } + function unquoteDashes(str) { return str.replace(/\\-/, "-"); } + + var split = reverseString(string).split(/-(?!\\)/); + var allReversed = map(split, reverseString); + return map(allReversed, unquoteDashes).reverse(); + } + for (i = 0; i < nPath; i++) { - param = params[i]; - cfg = this.params[param]; - values[param] = cfg.$value(m[i + 1]); + paramName = paramNames[i]; + var param = this.params[paramName]; + var paramVal = m[i+1]; + // if the param value matches a pre-replace pair, replace the value before decoding. + for (j = 0; j < param.replace; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); + values[paramName] = param.value(paramVal); } for (/**/; i < nTotal; i++) { - param = params[i]; - cfg = this.params[param]; - values[param] = cfg.$value(searchParams[param]); + paramName = paramNames[i]; + values[paramName] = this.params[paramName].value(searchParams[paramName]); } return values; @@ -855,7 +945,7 @@ UrlMatcher.prototype.exec = function (path, searchParams) { * pattern has no parameters, an empty array is returned. */ UrlMatcher.prototype.parameters = function (param) { - if (!isDefined(param)) return objectKeys(this.params); + if (!isDefined(param)) return this.params.$$keys(); return this.params[param] || null; }; @@ -872,15 +962,7 @@ UrlMatcher.prototype.parameters = function (param) { * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. */ UrlMatcher.prototype.validates = function (params) { - var result = true, isOptional, cfg, self = this; - - forEach(params, function(val, key) { - if (!self.params[key]) return; - cfg = self.params[key]; - isOptional = !val && isDefined(cfg.value); - result = result && (isOptional || cfg.type.is(val)); - }); - return result; + return this.params.$$validates(params); }; /** @@ -903,42 +985,52 @@ UrlMatcher.prototype.validates = function (params) { * @returns {string} the formatted URL (path and optionally search part). */ UrlMatcher.prototype.format = function (values) { - var segments = this.segments, params = this.parameters(); - - if (!values) return segments.join('').replace('//', '/'); - - var nPath = segments.length - 1, nTotal = params.length, - result = segments[0], i, search, value, param, cfg, array; - + values = values || {}; + var segments = this.segments, params = this.parameters(), paramset = this.params; if (!this.validates(values)) return null; - for (i = 0; i < nPath; i++) { - param = params[i]; - value = values[param]; - cfg = this.params[param]; + var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; - if (!isDefined(value) && (segments[i] === '/' || segments[i + 1] === '/')) continue; - if (value != null) result += encodeURIComponent(cfg.type.encode(value)); - result += segments[i + 1]; + function encodeDashes(str) { // Replace dashes with encoded "\-" + return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); } - for (/**/; i < nTotal; i++) { - param = params[i]; - value = values[param]; - if (value == null) continue; - array = isArray(value); - - if (array) { - value = value.map(encodeURIComponent).join('&' + param + '='); + for (i = 0; i < nTotal; i++) { + var isPathParam = i < nPath; + var name = params[i], param = paramset[name], value = param.value(values[name]); + var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); + var squash = isDefaultValue ? param.squash : false; + var encoded = param.type.encode(value); + + if (isPathParam) { + var nextSegment = segments[i + 1]; + if (squash === false) { + if (encoded != null) { + if (isArray(encoded)) { + result += map(encoded, encodeDashes).join("-"); + } else { + result += encodeURIComponent(encoded); + } + } + result += nextSegment; + } else if (squash === true) { + var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/; + result += nextSegment.match(capture)[1]; + } else if (isString(squash)) { + result += squash + nextSegment; + } + } else { + if (encoded == null || (isDefaultValue && squash !== false)) continue; + if (!isArray(encoded)) encoded = [ encoded ]; + encoded = map(encoded, encodeURIComponent).join('&' + name + '='); + result += (search ? '&' : '?') + (name + '=' + encoded); + search = true; } - result += (search ? '&' : '?') + param + '=' + (array ? value : encodeURIComponent(value)); - search = true; } + return result; }; -UrlMatcher.prototype.$types = {}; - /** * @ngdoc object * @name ui.router.util.type:Type @@ -951,9 +1043,18 @@ UrlMatcher.prototype.$types = {}; * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more * information on registering custom types. * - * @param {Object} config A configuration object hash that includes any method in `Type`'s public - * interface, and/or `pattern`, which should contain a custom regular expression used to match - * string parameters originating from a URL. + * @param {Object} config A configuration object which contains the custom type definition. The object's + * properties will override the default methods and/or pattern in `Type`'s public interface. + * @example + *
+ * {
+ *   decode: function(val) { return parseInt(val, 10); },
+ *   encode: function(val) { return val && val.toString(); },
+ *   equals: function(a, b) { return this.is(a) && a === b; },
+ *   is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
+ *   pattern: /\d+/
+ * }
+ * 
* * @property {RegExp} pattern The regular expression pattern used to match values of this type when * coming from a substring of a URL. @@ -1008,7 +1109,7 @@ Type.prototype.encode = function(val, key) { * @methodOf ui.router.util.type:Type * * @description - * Converts a string URL parameter value to a custom/native value. + * Converts a parameter value (from URL string or transition param) to a custom/native value. * * @param {string} val The URL parameter value to decode. * @param {string} key The name of the parameter in which `val` is stored. Can be used for @@ -1042,6 +1143,53 @@ Type.prototype.$subPattern = function() { Type.prototype.pattern = /.*/; +Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; + +/* + * Wraps an existing custom Type as an array of Type, depending on 'mode'. + * e.g.: + * - urlmatcher pattern "/path?{queryParam[]:int}" + * - url: "/path?queryParam=1&queryParam=2 + * - $stateParams.queryParam will be [1, 2] + * if `mode` is "auto", then + * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 + * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] + */ +Type.prototype.$asArray = function(mode, isSearch) { + if (!mode) return this; + if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); + return new ArrayType(this, mode); + + function ArrayType(type, mode) { + function bindTo(thisObj, callback) { + return function() { + return callback.apply(thisObj, arguments); + }; + } + + function arrayHandler(callback, reducefn) { + // Wraps type functions to operate on each value of an array + return function handleArray(val) { + if (!isArray(val)) val = [ val ]; + var result = map(val, callback); + if (reducefn) + return result.reduce(reducefn, true); + return (result && result.length == 1 && mode === "auto") ? result[0] : result; + }; + } + + function alltruthy(val, memo) { return val && memo; } + this.encode = arrayHandler(bindTo(this, type.encode)); + this.decode = arrayHandler(bindTo(this, type.decode)); + this.equals = arrayHandler(bindTo(this, type.equals), alltruthy); + this.is = arrayHandler(bindTo(this, type.is), alltruthy); + this.pattern = type.pattern; + this.$arrayMode = mode; + } +}; + + + /** * @ngdoc object * @name ui.router.util.$urlMatcherFactory @@ -1051,49 +1199,46 @@ Type.prototype.pattern = /.*/; * is also available to providers under the name `$urlMatcherFactoryProvider`. */ function $UrlMatcherFactory() { + $$UMFP = this; - var isCaseInsensitive = false, isStrictMode = true; + var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; - var enqueue = true, typeQueue = [], injector, defaultTypes = { + function valToString(val) { return val != null ? val.toString().replace("/", "%2F") : val; } + function valFromString(val) { return val != null ? val.toString().replace("%2F", "/") : val; } + function angularEquals(left, right) { return angular.equals(left, right); } +// TODO: in 1.0, make string .is() return false if value is undefined by default. +// function regexpMatches(val) { /*jshint validthis:true */ return isDefined(val) && this.pattern.test(val); } + function regexpMatches(val) { /*jshint validthis:true */ return this.pattern.test(val); } + + var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { + string: { + encode: valToString, + decode: valFromString, + is: regexpMatches, + pattern: /[^/]*/ + }, int: { - decode: function(val) { - return parseInt(val, 10); - }, - is: function(val) { - if (!isDefined(val)) return false; - return this.decode(val.toString()) === val; - }, + encode: valToString, + decode: function(val) { return parseInt(val, 10); }, + is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, pattern: /\d+/ }, bool: { - encode: function(val) { - return val ? 1 : 0; - }, - decode: function(val) { - return parseInt(val, 10) === 0 ? false : true; - }, - is: function(val) { - return val === true || val === false; - }, + encode: function(val) { return val ? 1 : 0; }, + decode: function(val) { return parseInt(val, 10) !== 0; }, + is: function(val) { return val === true || val === false; }, pattern: /0|1/ }, - string: { - pattern: /[^\/]*/ - }, date: { - equals: function (a, b) { - return a.toISOString() === b.toISOString(); - }, - decode: function (val) { - return new Date(val); - }, - encode: function (val) { - return [ + encode: function (val) { return [ val.getFullYear(), ('0' + (val.getMonth() + 1)).slice(-2), ('0' + val.getDate()).slice(-2) ].join("-"); }, + decode: function (val) { return new Date(val); }, + is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, + equals: function (a, b) { return a.toISOString() === b.toISOString(); }, pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/ } }; @@ -1127,9 +1272,12 @@ function $UrlMatcherFactory() { * Defines whether URL matching should be case sensitive (the default behavior), or not. * * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; + * @returns {boolean} the current value of caseInsensitive */ this.caseInsensitive = function(value) { - isCaseInsensitive = value; + if (isDefined(value)) + isCaseInsensitive = value; + return isCaseInsensitive; }; /** @@ -1140,10 +1288,36 @@ function $UrlMatcherFactory() { * @description * Defines whether URLs should match trailing slashes, or not (the default behavior). * - * @param {boolean} value `false` to match trailing slashes in URLs, otherwise `true`. + * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. + * @returns {boolean} the current value of strictMode */ this.strictMode = function(value) { - isStrictMode = value; + if (isDefined(value)) + isStrictMode = value; + return isStrictMode; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Sets the default behavior when generating or matching URLs with default parameter values. + * + * @param {string} value A string that defines the default parameter URL squashing behavior. + * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL + * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the + * parameter is surrounded by slashes, squash (remove) one slash from the URL + * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) + * the parameter value from the URL and replace it with this string. + */ + this.defaultSquashPolicy = function(value) { + if (!isDefined(value)) return defaultSquashPolicy; + if (value !== true && value !== false && !isString(value)) + throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); + defaultSquashPolicy = value; + return value; }; /** @@ -1153,7 +1327,7 @@ function $UrlMatcherFactory() { * * @description * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. - * + * * @param {string} pattern The URL pattern. * @param {Object} config The config object hash. * @returns {UrlMatcher} The UrlMatcher. @@ -1196,8 +1370,11 @@ function $UrlMatcherFactory() { * generate URLs with typed parameters. * * @param {string} name The type name. - * @param {Object|Function} def The type definition. See + * @param {Object|Function} definition The type definition. See * {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * @param {Object|Function} definitionFn (optional) A function that is injected before the app + * runtime starts. The result of this function is merged into the existing `definition`. + * See {@link ui.router.util.type:Type `Type`} for information on the values accepted. * * @returns {Object} Returns `$urlMatcherFactoryProvider`. * @@ -1245,7 +1422,7 @@ function $UrlMatcherFactory() { * // Defines a custom type that gets a value from a service, * // where each service gets different types of values from * // a backend API: - * $urlMatcherFactoryProvider.type('dbObject', function(Users, Posts) { + * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) { * * // Matches up services to URL parameter names * var services = { @@ -1290,42 +1467,198 @@ function $UrlMatcherFactory() { * }); * */ - this.type = function (name, def) { - if (!isDefined(def)) return UrlMatcher.prototype.$types[name]; - typeQueue.push({ name: name, def: def }); - if (!enqueue) flushTypeQueue(); + this.type = function (name, definition, definitionFn) { + if (!isDefined(definition)) return $types[name]; + if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); + + $types[name] = new Type(extend({ name: name }, definition)); + if (definitionFn) { + typeQueue.push({ name: name, def: definitionFn }); + if (!enqueue) flushTypeQueue(); + } return this; }; + // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s + function flushTypeQueue() { + while(typeQueue.length) { + var type = typeQueue.shift(); + if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); + angular.extend($types[type.name], injector.invoke(type.def)); + } + } + + // Register default types. Store them in the prototype of $types. + forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); + $types = inherit($types, {}); + /* No need to document $get, since it returns this */ this.$get = ['$injector', function ($injector) { injector = $injector; enqueue = false; - UrlMatcher.prototype.$types = {}; flushTypeQueue(); forEach(defaultTypes, function(type, name) { - if (!UrlMatcher.prototype.$types[name]) UrlMatcher.prototype.$types[name] = new Type(type); + if (!$types[name]) $types[name] = new Type(type); }); return this; }]; - // To ensure proper order of operations in object configuration, and to allow internal - // types to be overridden, `flushTypeQueue()` waits until `$urlMatcherFactory` is injected - // before actually wiring up and assigning type definitions - function flushTypeQueue() { - forEach(typeQueue, function(type) { - if (UrlMatcher.prototype.$types[type.name]) { - throw new Error("A type named '" + type.name + "' has already been defined."); + this.Param = function Param(id, type, config, location) { + var self = this; + var defaultValueConfig = getDefaultValueConfig(config); + config = config || {}; + type = getType(config, type); + var arrayMode = getArrayMode(); + type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; + if (type.name === "string" && !arrayMode && location === "path" && defaultValueConfig.value === undefined) + defaultValueConfig.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" + var isOptional = defaultValueConfig.value !== undefined; + var squash = getSquashPolicy(config, isOptional); + var replace = getReplace(config, arrayMode, isOptional, squash); + + function getDefaultValueConfig(config) { + var keys = isObject(config) ? objectKeys(config) : []; + var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && + indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; + var configValue = isShorthand ? config : config.value; + var result = { + fn: isInjectable(configValue) ? configValue : function () { return result.value; }, + value: configValue + }; + return result; + } + + function getType(config, urlType) { + if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); + if (urlType) return urlType; + if (!config.type) return $types.string; + return config.type instanceof Type ? config.type : new Type(config.type); + } + + // array config: param name (param[]) overrides default settings. explicit config overrides param name. + function getArrayMode() { + var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; + return extend(arrayDefaults, arrayParamNomenclature, config).array; + } + + /** + * returns false, true, or the squash value to indicate the "default parameter url squash policy". + */ + function getSquashPolicy(config, isOptional) { + var squash = config.squash; + if (!isOptional || squash === false) return false; + if (!isDefined(squash) || squash == null) return defaultSquashPolicy; + if (squash === true || isString(squash)) return squash; + throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); + } + + function getReplace(config, arrayMode, isOptional, squash) { + var replace, configuredKeys, defaultPolicy = [ + { from: "", to: (isOptional || arrayMode ? undefined : "") }, + { from: null, to: (isOptional || arrayMode ? undefined : "") } + ]; + replace = isArray(config.replace) ? config.replace : []; + if (isString(squash)) + replace.push({ from: squash, to: undefined }); + configuredKeys = map(replace, function(item) { return item.from; } ); + return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + function $$getDefaultValue() { + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + return injector.invoke(defaultValueConfig.fn); + } + + /** + * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the + * default value, which may be the result of an injectable function. + */ + function $value(value) { + function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } + function $replace(value) { + var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); + return replacement.length ? replacement[0] : value; } - var def = new Type(isInjectable(type.def) ? injector.invoke(type.def) : type.def); - UrlMatcher.prototype.$types[type.name] = def; + value = $replace(value); + return isDefined(value) ? self.type.decode(value) : $$getDefaultValue(); + } + + function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } + + extend(this, { + id: id, + type: type, + array: arrayMode, + config: config, + squash: squash, + replace: replace, + isOptional: isOptional, + dynamic: undefined, + value: $value, + toString: toString }); + }; + + function ParamSet(params) { + extend(this, params || {}); } + + ParamSet.prototype = { + $$new: function() { + return inherit(this, extend(new ParamSet(), { $$parent: this})); + }, + $$keys: function () { + var keys = [], chain = [], parent = this, + ignore = objectKeys(ParamSet.prototype); + while (parent) { chain.push(parent); parent = parent.$$parent; } + chain.reverse(); + forEach(chain, function(paramset) { + forEach(objectKeys(paramset), function(key) { + if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); + }); + }); + return keys; + }, + $$values: function(paramValues) { + var values = {}, self = this; + forEach(self.$$keys(), function(key) { + values[key] = self[key].value(paramValues && paramValues[key]); + }); + return values; + }, + $$equals: function(paramValues1, paramValues2) { + var equal = true, self = this; + forEach(self.$$keys(), function(key) { + var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; + if (!self[key].type.equals(left, right)) equal = false; + }); + return equal; + }, + $$validates: function $$validate(paramValues) { + var result = true, isOptional, val, param, self = this; + + forEach(this.$$keys(), function(key) { + param = self[key]; + val = paramValues[key]; + isOptional = !val && param.isOptional; + result = result && (isOptional || param.type.is(val)); + }); + return result; + }, + $$parent: undefined + }; + + this.ParamSet = ParamSet; } // Register as a provider so it's available to other providers angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); +angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]); /** * @ngdoc object @@ -1710,6 +2043,10 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { if (!urlMatcher.validates(params)) return null; var isHtml5 = $locationProvider.html5Mode(); + if (angular.isObject(isHtml5)) { + isHtml5 = isHtml5.enabled; + } + var url = urlMatcher.format(params); options = options || {}; @@ -1799,12 +2136,18 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { return state.url ? state : (state.parent ? state.parent.navigable : null); }, + // Own parameters for this state. state.url.params is already built at this point. Create and add non-url params + ownParams: function(state) { + var params = state.url && state.url.params || new $$UMFP.ParamSet(); + forEach(state.params || {}, function(config, id) { + if (!params[id]) params[id] = new $$UMFP.Param(id, null, config); + }); + return params; + }, + // Derive parameters for this state and ensure they're a super-set of parent's parameters params: function(state) { - if (!state.params) { - return state.url ? state.url.params : state.parent.params; - } - return state.params; + return state.parent && state.parent.params ? extend(state.parent.params.$$new(), state.ownParams) : new $$UMFP.ParamSet(); }, // If there is no explicit multi-view configuration, make one up so we don't have @@ -1822,28 +2165,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { return views; }, - ownParams: function(state) { - state.params = state.params || {}; - - if (!state.parent) { - return objectKeys(state.params); - } - var paramNames = {}; forEach(state.params, function (v, k) { paramNames[k] = true; }); - - forEach(state.parent.params, function (v, k) { - if (!paramNames[k]) { - throw new Error("Missing required parameter '" + k + "' in state '" + state.name + "'"); - } - paramNames[k] = false; - }); - var ownParams = []; - - forEach(paramNames, function (own, p) { - if (own) ownParams.push(p); - }); - return ownParams; - }, - // Keep a full path from the root down to this state as this is needed for state activation. path: function(state) { return state.parent ? state.parent.path.concat(state) : []; // exclude root from path @@ -1872,6 +2193,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { if (path) { if (!base) throw new Error("No reference point given for path '" + name + "'"); + base = findState(base); + var rel = name.split("."), i = 0, pathLength = rel.length, current = base; for (; i < pathLength; i++) { @@ -1904,6 +2227,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { queue[parentName].push(state); } + function flushQueuedChildren(parentName) { + var queued = queue[parentName] || []; + while(queued.length) { + registerState(queued.shift()); + } + } + function registerState(state) { // Wrap a new object around the state so we can store our private details easily. state = inherit(state, { @@ -1919,6 +2249,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { // Get parent name var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) : (isString(state.parent)) ? state.parent + : (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name : ''; // If parent is not registered yet, add state to queue and register later @@ -1941,11 +2272,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } // Register any queued children - if (queue[name]) { - for (var i = 0; i < queue[name].length; i++) { - registerState(queue[name][i]); - } - } + flushQueuedChildren(name); return state; } @@ -1962,12 +2289,12 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { //match greedy starts if (globSegments[0] === '**') { - segments = segments.slice(segments.indexOf(globSegments[1])); + segments = segments.slice(indexOf(segments, globSegments[1])); segments.unshift('**'); } //match greedy ends if (globSegments[globSegments.length - 1] === '**') { - segments.splice(segments.indexOf(globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); + segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); segments.push('**'); } @@ -2112,9 +2439,12 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * Registers a state configuration under a given state name. The stateConfig object * has the following acceptable properties. * + * @param {string} name A unique state name, e.g. "home", "about", "contacts". + * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". + * @param {object} stateConfig State configuration object. + * @param {string|function=} stateConfig.template * - * - * - **`template`** - {string|function=} - html template as a string or a function that returns + * html template as a string or a function that returns * an html template as a string which should be used by the uiView directives. This property * takes precedence over templateUrl. * @@ -2123,9 +2453,17 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * - {array.<object>} - state parameters extracted from the current $location.path() by * applying the current state * + *
template:
+   *   "

inline template definition

" + + * "
"
+ *
template: function(params) {
+   *       return "

generated template

"; }
+ * + * + * @param {string|function=} stateConfig.templateUrl * * - * - **`templateUrl`** - {string|function=} - path or function that returns a path to an html + * path or function that returns a path to an html * template that should be used by uiView. * * If `templateUrl` is a function, it will be called with the following parameters: @@ -2133,84 +2471,250 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * - {array.<object>} - state parameters extracted from the current $location.path() by * applying the current state * - * + *
templateUrl: "home.html"
+ *
templateUrl: function(params) {
+   *     return myTemplates[params.pageId]; }
* - * - **`templateProvider`** - {function=} - Provider function that returns HTML content - * string. + * @param {function=} stateConfig.templateProvider + * + * Provider function that returns HTML content string. + *
 templateProvider:
+   *       function(MyTemplateService, params) {
+   *         return MyTemplateService.getTemplate(params.pageId);
+   *       }
* + * @param {string|function=} stateConfig.controller * * - * - **`controller`** - {string|function=} - Controller fn that should be associated with newly + * Controller fn that should be associated with newly * related scope or the name of a registered controller if passed as a string. - * + * Optionally, the ControllerAs may be declared here. + *
controller: "MyRegisteredController"
+ *
controller:
+   *     "MyRegisteredController as fooCtrl"}
+ *
controller: function($scope, MyService) {
+   *     $scope.data = MyService.getData(); }
+ * + * @param {function=} stateConfig.controllerProvider * * - * - **`controllerProvider`** - {function=} - Injectable provider function that returns - * the actual controller or string. + * Injectable provider function that returns the actual controller or string. + *
controllerProvider:
+   *   function(MyResolveData) {
+   *     if (MyResolveData.foo)
+   *       return "FooCtrl"
+   *     else if (MyResolveData.bar)
+   *       return "BarCtrl";
+   *     else return function($scope) {
+   *       $scope.baz = "Qux";
+   *     }
+   *   }
* + * @param {string=} stateConfig.controllerAs * * - * - **`controllerAs`** – {string=} – A controller alias name. If present the controller will be + * A controller alias name. If present the controller will be * published to scope under the controllerAs name. + *
controllerAs: "myCtrl"
* + * @param {object=} stateConfig.resolve * * - * - **`resolve`** - {object.<string, function>=} - An optional map of dependencies which + * An optional map<string, function> of dependencies which * should be injected into the controller. If any of these dependencies are promises, - * the router will wait for them all to be resolved or one to be rejected before the - * controller is instantiated. If all the promises are resolved successfully, the values - * of the resolved promises are injected and $stateChangeSuccess event is fired. If any - * of the promises are rejected the $stateChangeError event is fired. The map object is: + * the router will wait for them all to be resolved before the controller is instantiated. + * If all the promises are resolved successfully, the $stateChangeSuccess event is fired + * and the values of the resolved promises are injected into any controllers that reference them. + * If any of the promises are rejected the $stateChangeError event is fired. + * + * The map object is: * * - key - {string}: name of dependency to be injected into controller * - factory - {string|function}: If string then it is alias for service. Otherwise if function, * it is injected and return value it treated as dependency. If result is a promise, it is * resolved before its value is injected into controller. * + *
resolve: {
+   *     myResolve1:
+   *       function($http, $stateParams) {
+   *         return $http.get("/api/foos/"+stateParams.fooID);
+   *       }
+   *     }
+ * + * @param {string=} stateConfig.url * * - * - **`url`** - {string=} - A url with optional parameters. When a state is navigated or + * A url fragment with optional parameters. When a state is navigated or * transitioned to, the `$stateParams` service will be populated with any * parameters that were passed. * - * - * - * - **`params`** - {object=} - An array of parameter names or regular expressions. Only - * use this within a state if you are not using url. Otherwise you can specify your - * parameters within the url. When a state is navigated or transitioned to, the - * $stateParams service will be populated with any parameters that were passed. - * + * examples: + *
url: "/home"
+   * url: "/users/:userid"
+   * url: "/books/{bookid:[a-zA-Z_-]}"
+   * url: "/books/{categoryid:int}"
+   * url: "/books/{publishername:string}/{categoryid:int}"
+   * url: "/messages?before&after"
+   * url: "/messages?{before:date}&{after:date}"
+ * url: "/messages/:mailboxid?{before:date}&{after:date}" + * + * @param {object=} stateConfig.views * + * an optional map<string, object> which defined multiple views, or targets views + * manually/explicitly. + * + * Examples: + * + * Targets three named `ui-view`s in the parent state's template + *
views: {
+   *     header: {
+   *       controller: "headerCtrl",
+   *       templateUrl: "header.html"
+   *     }, body: {
+   *       controller: "bodyCtrl",
+   *       templateUrl: "body.html"
+   *     }, footer: {
+   *       controller: "footCtrl",
+   *       templateUrl: "footer.html"
+   *     }
+   *   }
+ * + * Targets named `ui-view="header"` from grandparent state 'top''s template, and named `ui-view="body" from parent state's template. + *
views: {
+   *     'header@top': {
+   *       controller: "msgHeaderCtrl",
+   *       templateUrl: "msgHeader.html"
+   *     }, 'body': {
+   *       controller: "messagesCtrl",
+   *       templateUrl: "messages.html"
+   *     }
+   *   }
* - * - **`views`** - {object=} - Use the views property to set up multiple views or to target views - * manually/explicitly. - * + * @param {boolean=} [stateConfig.abstract=false] * - * - * - **`abstract`** - {boolean=} - An abstract state will never be directly activated, + * An abstract state will never be directly activated, * but can provide inherited properties to its common children states. + *
abstract: true
* + * @param {function=} stateConfig.onEnter * * - * - **`onEnter`** - {object=} - Callback function for when a state is entered. Good way + * Callback function for when a state is entered. Good way * to trigger an action or dispatch an event, such as opening a dialog. - * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax. + * If minifying your scripts, make sure to explictly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
onEnter: function(MyService, $stateParams) {
+   *     MyService.foo($stateParams.myParam);
+   * }
* + * @param {function=} stateConfig.onExit * * - * - **`onExit`** - {object=} - Callback function for when a state is exited. Good way to + * Callback function for when a state is exited. Good way to * trigger an action or dispatch an event, such as opening a dialog. - * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax. + * If minifying your scripts, make sure to explictly annotate this function, + * because it won't be automatically annotated by your build tools. * + *
onExit: function(MyService, $stateParams) {
+   *     MyService.cleanup($stateParams.myParam);
+   * }
+ * + * @param {boolean=} [stateConfig.reloadOnSearch=true] * * - * - **`reloadOnSearch = true`** - {boolean=} - If `false`, will not retrigger the same state + * If `false`, will not retrigger the same state * just because a search/query parameter has changed (via $location.search() or $location.hash()). * Useful for when you'd like to modify $location.search() without triggering a reload. + *
reloadOnSearch: false
* + * @param {object=} stateConfig.data * * - * - **`data`** - {object=} - Arbitrary data object, useful for custom configuration. + * Arbitrary data object, useful for custom configuration. The parent state's `data` is + * prototypally inherited. In other words, adding a data property to a state adds it to + * the entire subtree via prototypal inheritance. + * + *
data: {
+   *     requiredRole: 'foo'
+   * } 
+ * + * @param {object=} stateConfig.params + * + * + * A map which optionally configures parameters declared in the `url`, or + * defines additional non-url parameters. For each parameter being + * configured, add a configuration object keyed to the name of the parameter. + * + * Each parameter configuration object may contain the following properties: + * + * - ** value ** - {object|function=}: specifies the default value for this + * parameter. This implicitly sets this parameter as optional. + * + * When UI-Router routes to a state and no value is + * specified for this parameter in the URL or transition, the + * default value will be used instead. If `value` is a function, + * it will be injected and invoked, and the return value used. + * + * *Note*: `undefined` is treated as "no default value" while `null` + * is treated as "the default value is `null`". + * + * *Shorthand*: If you only need to configure the default value of the + * parameter, you may use a shorthand syntax. In the **`params`** + * map, instead mapping the param name to a full parameter configuration + * object, simply set map it to the default parameter value, e.g.: + * + *
// define a parameter's default value
+   * params: {
+   *     param1: { value: "defaultValue" }
+   * }
+   * // shorthand default values
+   * params: {
+   *     param1: "defaultValue",
+   *     param2: "param2Default"
+   * }
+ * + * - ** array ** - {boolean=}: *(default: false)* If true, the param value will be + * treated as an array of values. If you specified a Type, the value will be + * treated as an array of the specified Type. Note: query parameter values + * default to a special `"auto"` mode. + * + * For query parameters in `"auto"` mode, if multiple values for a single parameter + * are present in the URL (e.g.: `/foo?bar=1&bar=2&bar=3`) then the values + * are mapped to an array (e.g.: `{ foo: [ '1', '2', '3' ] }`). However, if + * only one value is present (e.g.: `/foo?bar=1`) then the value is treated as single + * value (e.g.: `{ foo: '1' }`). + * + *
params: {
+   *     param1: { array: true }
+   * }
+ * + * - ** squash ** - {bool|string=}: `squash` configures how a default parameter value is represented in the URL when + * the current parameter value is the same as the default value. If `squash` is not set, it uses the + * configured default squash policy. + * (See {@link ui.router.util.$urlMatcherFactory#methods_defaultSquashPolicy `defaultSquashPolicy()`}) + * + * There are three squash settings: + * + * - false: The parameter's default value is not squashed. It is encoded and included in the URL + * - true: The parameter's default value is omitted from the URL. If the parameter is preceeded and followed + * by slashes in the state's `url` declaration, then one of those slashes are omitted. + * This can allow for cleaner looking URLs. + * - `""`: The parameter's default value is replaced with an arbitrary placeholder of your choice. + * + *
params: {
+   *     param1: {
+   *       value: "defaultId",
+   *       squash: true
+   * } }
+   * // squash "defaultValue" to "~"
+   * params: {
+   *     param1: {
+   *       value: "defaultValue",
+   *       squash: "~"
+   * } }
+   * 
+ * * * @example *
@@ -2219,7 +2723,7 @@ function $StateProvider(   $urlRouterProvider,   $urlMatcherFactory) {
    * // stateName can be a single top-level name (must be unique).
    * $stateProvider.state("home", {});
    *
-   * // Or it can be a nested state name. This state is a child of the 
+   * // Or it can be a nested state name. This state is a child of the
    * // above "home" state.
    * $stateProvider.state("home.newest", {});
    *
@@ -2233,9 +2737,6 @@ function $StateProvider(   $urlRouterProvider,   $urlMatcherFactory) {
    *   .state("contacts", {});
    * 
* - * @param {string} name A unique state name, e.g. "home", "about", "contacts". - * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". - * @param {object} definition State configuration object. */ this.state = state; function state(name, definition) { @@ -2271,8 +2772,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * you're coming from. */ this.$get = $get; - $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter']; - function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter) { + $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter', '$location', '$urlMatcherFactory']; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) { var TransitionSuperseded = $q.reject(new Error('transition superseded')); var TransitionPrevented = $q.reject(new Error('transition prevented')); @@ -2376,12 +2877,15 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * `reload()` is just an alias for: *
      * $state.transitionTo($state.current, $stateParams, { 
-     *   reload: true, inherit: false, notify: false 
+     *   reload: true, inherit: false, notify: true
      * });
      * 
+ * + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. */ $state.reload = function reload() { - $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: false }); + return $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: true }); }; /** @@ -2523,6 +3027,9 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + if (!toState.params.$$validates(toParams)) return TransitionFailed; + + toParams = toState.params.$$values(toParams); to = toState; var toPath = to.path; @@ -2531,7 +3038,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; if (!options.reload) { - while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) { + while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) { locals = toLocals[keep] = state.locals; keep++; state = toPath[keep]; @@ -2550,7 +3057,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } // Filter parameters before we pass them to event handlers etc. - toParams = filterByKeys(objectKeys(to.params), toParams || {}); + toParams = filterByKeys(to.params.$$keys(), toParams || {}); // Broadcast start event and cancel the transition if requested if (options.notify) { @@ -2598,7 +3105,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { for (var l = keep; l < toPath.length; l++, state = toPath[l]) { locals = toLocals[l] = inherit(locals); - resolved = resolveState(state, toParams, state === to, resolved, locals); + resolved = resolveState(state, toParams, state === to, resolved, locals, options); } // Once everything is resolved, we are ready to perform the actual transition @@ -2705,8 +3212,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * @description * Similar to {@link ui.router.state.$state#methods_includes $state.includes}, - * but only checks for the full state name. If params is supplied then it will be - * tested for strict equality against the current active params object, so all params + * but only checks for the full state name. If params is supplied then it will be + * tested for strict equality against the current active params object, so all params * must match with none missing and no extras. * * @example @@ -2722,13 +3229,19 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { *
Item
* * - * @param {string|object} stateName The state name (absolute or relative) or state object you'd like to check. - * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like + * @param {string|object} stateOrName The state name (absolute or relative) or state object you'd like to check. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like * to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object} - If `stateOrName` is a relative state name and `options.relative` is set, .is will + * test relative to `options.relative` state (or name). + * * @returns {boolean} Returns true if it is the state. */ - $state.is = function is(stateOrName, params) { - var state = findState(stateOrName); + $state.is = function is(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + var state = findState(stateOrName, options.relative); if (!isDefined(state)) { return undefined; @@ -2783,19 +3296,25 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * * @param {string} stateOrName A partial name, relative name, or glob pattern * to be searched for within the current state name. - * @param {object} params A param object, e.g. `{sectionId: section.id}`, + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, * that you'd like to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object=} - If `stateOrName` is a relative state reference and `options.relative` is set, + * .includes will test relative to `options.relative` state (or name). + * * @returns {boolean} Returns true if it does include the state */ - $state.includes = function includes(stateOrName, params) { + $state.includes = function includes(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); if (isString(stateOrName) && isGlob(stateOrName)) { if (!doesStateMatchGlob(stateOrName)) { return false; } stateOrName = $state.$current.name; } - var state = findState(stateOrName); + var state = findState(stateOrName, options.relative); if (!isDefined(state)) { return undefined; } @@ -2848,10 +3367,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { var nav = (state && options.lossy) ? state.navigable : state; - if (!nav || !nav.url) { + if (!nav || nav.url === undefined || nav.url === null) { return null; } - return $urlRouter.href(nav.url, filterByKeys(objectKeys(state.params), params || {}), { + return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys(), params || {}), { absolute: options.absolute }); }; @@ -2864,22 +3383,23 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { * @description * Returns the state configuration object for any specific state or all states. * - * @param {string|Sbject=} stateOrName (absolute or relative) If provided, will only get the config for + * @param {string|object=} stateOrName (absolute or relative) If provided, will only get the config for * the requested state. If not provided, returns an array of ALL state configs. + * @param {string|object=} context When stateOrName is a relative state reference, the state will be retrieved relative to context. * @returns {Object|Array} State configuration object or array of all objects. */ $state.get = function (stateOrName, context) { - if (arguments.length === 0) return objectKeys(states).map(function(name) { return states[name].self; }); - var state = findState(stateOrName, context); + if (arguments.length === 0) return map(objectKeys(states), function(name) { return states[name].self; }); + var state = findState(stateOrName, context || $state.$current); return (state && state.self) ? state.self : null; }; - function resolveState(state, params, paramsAreFiltered, inherited, dst) { + function resolveState(state, params, paramsAreFiltered, inherited, dst, options) { // Make a restricted $stateParams with only the parameters that apply to this state if // necessary. In addition to being available to the controller and onEnter/onExit callbacks, // we also need $stateParams to be available for any $injector calls we make during the // dependency resolution process. - var $stateParams = (paramsAreFiltered) ? params : filterByKeys(objectKeys(state.params), params); + var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params); var locals = { $stateParams: $stateParams }; // Resolve 'global' dependencies for the state, i.e. those not specific to a view. @@ -2896,7 +3416,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { forEach(state.views, function (view, name) { var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); injectables.$template = [ function () { - return $view.load(name, { view: view, locals: locals, params: $stateParams }) || ''; + return $view.load(name, { view: view, locals: locals, params: $stateParams, notify: options.notify }) || ''; }]; promises.push($resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) { @@ -3075,7 +3595,7 @@ angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider) * @description * The ui-view directive tells $state where to place your templates. * - * @param {string=} ui-view A view name. The name should be unique amongst the other views in the + * @param {string=} name A view name. The name should be unique amongst the other views in the * same state. You can have views of the same name that live in different states. * * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window @@ -3172,8 +3692,8 @@ angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider) * * */ -$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll']; -function $ViewDirective( $state, $injector, $uiViewScroll) { +$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll', '$interpolate']; +function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) { function getService() { return ($injector.has) ? function(service) { @@ -3203,8 +3723,14 @@ function $ViewDirective( $state, $injector, $uiViewScroll) { if ($animate) { return { - enter: function(element, target, cb) { $animate.enter(element, null, target, cb); }, - leave: function(element, cb) { $animate.leave(element, cb); } + enter: function(element, target, cb) { + var promise = $animate.enter(element, null, target, cb); + if (promise && promise.then) promise.then(cb); + }, + leave: function(element, cb) { + var promise = $animate.leave(element, cb); + if (promise && promise.then) promise.then(cb); + } }; } @@ -3264,7 +3790,7 @@ function $ViewDirective( $state, $injector, $uiViewScroll) { function updateView(firstTime) { var newScope, - name = getUiViewName(attrs, $element.inheritedData('$uiView')), + name = getUiViewName(scope, attrs, $element, $interpolate), previousLocals = name && $state.$current && $state.$current.locals[name]; if (!firstTime && previousLocals === latestLocals) return; // nothing to do @@ -3273,6 +3799,10 @@ function $ViewDirective( $state, $injector, $uiViewScroll) { var clone = $transclude(newScope, function(clone) { renderer.enter(clone, $element, function onUiViewEnter() { + if(currentScope) { + currentScope.$emit('$viewContentAnimationEnded'); + } + if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { $uiViewScroll(clone); } @@ -3302,8 +3832,8 @@ function $ViewDirective( $state, $injector, $uiViewScroll) { return directive; } -$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state']; -function $ViewDirectiveFill ($compile, $controller, $state) { +$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state', '$interpolate']; +function $ViewDirectiveFill ( $compile, $controller, $state, $interpolate) { return { restrict: 'ECA', priority: -400, @@ -3311,7 +3841,7 @@ function $ViewDirectiveFill ($compile, $controller, $state) { var initial = tElement.html(); return function (scope, $element, attrs) { var current = $state.$current, - name = getUiViewName(attrs, $element.inheritedData('$uiView')), + name = getUiViewName(scope, attrs, $element, $interpolate), locals = current && current.locals[name]; if (! locals) { @@ -3341,10 +3871,11 @@ function $ViewDirectiveFill ($compile, $controller, $state) { /** * Shared ui-view code for both directives: - * Given attributes and inherited $uiView data, return the view's name + * Given scope, element, and its attributes, return the view's name */ -function getUiViewName(attrs, inherited) { - var name = attrs.uiView || attrs.name || ''; +function getUiViewName(scope, attrs, element, $interpolate) { + var name = $interpolate(attrs.uiView || attrs.name || '')(scope); + var inherited = element.inheritedData('$uiView'); return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); } @@ -3439,6 +3970,7 @@ function $StateRefDirective($state, $timeout) { link: function(scope, element, attrs, uiSrefActive) { var ref = parseStateRef(attrs.uiSref, $state.current.name); var params = null, url = null, base = stateContext(element) || $state.$current; + var newHref = null, isAnchor = element.prop("tagName") === "A"; var isForm = element[0].nodeName === "FORM"; var attr = isForm ? "action" : "href", nav = true; @@ -3452,10 +3984,10 @@ function $StateRefDirective($state, $timeout) { }); var update = function(newVal) { - if (newVal) params = newVal; + if (newVal) params = angular.copy(newVal); if (!nav) return; - var newHref = $state.href(ref.state, params, options); + newHref = $state.href(ref.state, params, options); var activeDirective = uiSrefActive[1] || uiSrefActive[0]; if (activeDirective) { @@ -3465,14 +3997,14 @@ function $StateRefDirective($state, $timeout) { nav = false; return false; } - element[0][attr] = newHref; + attrs.$set(attr, newHref); }; if (ref.paramExpr) { scope.$watch(ref.paramExpr, function(newVal, oldVal) { if (newVal !== params) update(newVal); }, true); - params = scope.$eval(ref.paramExpr); + params = angular.copy(scope.$eval(ref.paramExpr)); } update(); @@ -3487,8 +4019,11 @@ function $StateRefDirective($state, $timeout) { }); e.preventDefault(); + // if the state has no URL, ignore one preventDefault from the directive. + var ignorePreventDefaultCount = isAnchor && !newHref ? 1: 0; e.preventDefault = function() { - $timeout.cancel(transition); + if (ignorePreventDefaultCount-- <= 0) + $timeout.cancel(transition); }; } }); @@ -3566,7 +4101,7 @@ function $StateRefDirective($state, $timeout) { * @restrict A * * @description - * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will will only activate + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will only activate * when the exact target state used in the `ui-sref` is active; no child states. * */ @@ -3604,7 +4139,7 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) { if (typeof $attrs.uiSrefActiveEq !== 'undefined') { return $state.$current.self === state && matchesParams(); } else { - return $state.includes(state.name) && matchesParams(); + return state && $state.includes(state.name) && matchesParams(); } } @@ -3631,9 +4166,11 @@ angular.module('ui.router.state') */ $IsStateFilter.$inject = ['$state']; function $IsStateFilter($state) { - return function(state) { + var isFilter = function (state) { return $state.is(state); }; + isFilter.$stateful = true; + return isFilter; } /** @@ -3647,12 +4184,14 @@ function $IsStateFilter($state) { */ $IncludedByStateFilter.$inject = ['$state']; function $IncludedByStateFilter($state) { - return function(state) { + var includesFilter = function (state) { return $state.includes(state); }; + includesFilter.$stateful = true; + return includesFilter; } angular.module('ui.router.state') .filter('isState', $IsStateFilter) .filter('includedByState', $IncludedByStateFilter); -})(window, window.angular); +})(window, window.angular); \ No newline at end of file