forked from christopherthielen/ui-router-extras
/
ct-ui-router-extras.js
1819 lines (1584 loc) · 76.7 KB
/
ct-ui-router-extras.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* UI-Router Extras: Sticky states, Future States, Deep State Redirect, Transition promise
* Monolithic build (all modules)
* @version 0.0.13
* @link http://christopherthielen.github.io/ui-router-extras/
* @license MIT License, http://www.opensource.org/licenses/MIT
*/
(function(angular, undefined){
"use strict";
var mod_core = angular.module("ct.ui.router.extras.core", [ "ui.router" ]);
var internalStates = {}, stateRegisteredCallbacks = [];
mod_core.config([ '$stateProvider', '$injector', function ($stateProvider, $injector) {
// Decorate any state attribute in order to get access to the internal state representation.
$stateProvider.decorator('parent', function (state, parentFn) {
// Capture each internal UI-Router state representations as opposed to the user-defined state object.
// The internal state is, e.g., the state returned by $state.$current as opposed to $state.current
internalStates[state.self.name] = state;
// Add an accessor for the internal state from the user defined state
state.self.$$state = function () {
return internalStates[state.self.name];
};
angular.forEach(stateRegisteredCallbacks, function(callback) { callback(state); });
return parentFn(state);
});
}]);
var DEBUG = false;
var forEach = angular.forEach;
var extend = angular.extend;
var isArray = angular.isArray;
var map = function (collection, callback) {
"use strict";
var result = [];
forEach(collection, function (item, index) {
result.push(callback(item, index));
});
return result;
};
var keys = function (collection) {
"use strict";
return map(collection, function (collection, key) {
return key;
});
};
var filter = function (collection, callback) {
"use strict";
var result = [];
forEach(collection, function (item, index) {
if (callback(item, index)) {
result.push(item);
}
});
return result;
};
var filterObj = function (collection, callback) {
"use strict";
var result = {};
forEach(collection, function (item, index) {
if (callback(item, index)) {
result[index] = item;
}
});
return result;
};
// Duplicates code in UI-Router common.js
function ancestors(first, second) {
var path = [];
for (var n in first.path) {
if (first.path[n] !== second.path[n]) break;
path.push(first.path[n]);
}
return path;
}
// Duplicates code in UI-Router common.js
function objectKeys(object) {
if (Object.keys) {
return Object.keys(object);
}
var result = [];
angular.forEach(object, function (val, key) {
result.push(key);
});
return result;
}
/**
* like objectKeys, but includes keys from prototype chain.
* @param object the object whose prototypal keys will be returned
* @param ignoreKeys an array of keys to ignore
*/
// Duplicates code in UI-Router common.js
function protoKeys(object, ignoreKeys) {
var result = [];
for (var key in object) {
if (!ignoreKeys || ignoreKeys.indexOf(key) === -1)
result.push(key);
}
return result;
}
// Duplicates code in UI-Router common.js
function arraySearch(array, value) {
if (Array.prototype.indexOf) {
return array.indexOf(value, Number(arguments[2]) || 0);
}
var len = array.length >>> 0, from = Number(arguments[2]) || 0;
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
if (from < 0) from += len;
for (; from < len; from++) {
if (from in array && array[from] === value) return from;
}
return -1;
}
// Duplicates code in UI-Router common.js
// Added compatibility code (isArray check) to support both 0.2.x and 0.3.x series of UI-Router.
function inheritParams(currentParams, newParams, $current, $to) {
var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];
for (var i in parents) {
if (!parents[i].params) continue;
// This test allows compatibility with 0.2.x and 0.3.x (optional and object params)
parentParams = isArray(parents[i].params) ? parents[i].params : objectKeys(parents[i].params);
if (!parentParams.length) continue;
for (var j in parentParams) {
if (arraySearch(inheritList, parentParams[j]) >= 0) continue;
inheritList.push(parentParams[j]);
inherited[parentParams[j]] = currentParams[parentParams[j]];
}
}
return extend({}, inherited, newParams);
}
function inherit(parent, extra) {
return extend(new (extend(function () { }, {prototype: parent}))(), extra);
}
function onStateRegistered(callback) { stateRegisteredCallbacks.push(callback); }
mod_core.provider("uirextras_core", function() {
var core = {
internalStates: internalStates,
onStateRegistered: onStateRegistered,
forEach: forEach,
extend: extend,
isArray: isArray,
map: map,
keys: keys,
filter: filter,
filterObj: filterObj,
ancestors: ancestors,
objectKeys: objectKeys,
protoKeys: protoKeys,
arraySearch: arraySearch,
inheritParams: inheritParams,
inherit: inherit
};
angular.extend(this, core);
this.$get = function() {
return core;
};
});
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.module("ct.ui.router.extras.sticky", [ 'ct.ui.router.extras.core' ]);
var mod_sticky = angular.module("ct.ui.router.extras.sticky");
$StickyStateProvider.$inject = [ '$stateProvider', 'uirextras_coreProvider' ];
function $StickyStateProvider($stateProvider, uirextras_coreProvider) {
var core = uirextras_coreProvider;
var inheritParams = core.inheritParams;
var protoKeys = core.protoKeys;
var map = core.map;
// Holds all the states which are inactivated. Inactivated states can be either sticky states, or descendants of sticky states.
var inactiveStates = {}; // state.name -> (state)
var stickyStates = {}; // state.name -> true
var $state;
var DEBUG = false;
// Called by $stateProvider.registerState();
// registers a sticky state with $stickyStateProvider
this.registerStickyState = function (state) {
stickyStates[state.name] = state;
// console.log("Registered sticky state: ", state);
};
this.enableDebug = this.debugMode = function (enabled) {
if (angular.isDefined(enabled))
DEBUG = enabled;
return DEBUG;
};
this.$get = [ '$rootScope', '$state', '$stateParams', '$injector', '$log',
function ($rootScope, $state, $stateParams, $injector, $log) {
// Each inactive states is either a sticky state, or a child of a sticky state.
// This function finds the closest ancestor sticky state, then find that state's parent.
// Map all inactive states to their closest parent-to-sticky state.
function mapInactives() {
var mappedStates = {};
angular.forEach(inactiveStates, function (state, name) {
var stickyAncestors = getStickyStateStack(state);
for (var i = 0; i < stickyAncestors.length; i++) {
var parent = stickyAncestors[i].parent;
mappedStates[parent.name] = mappedStates[parent.name] || [];
mappedStates[parent.name].push(state);
}
if (mappedStates['']) {
// This is necessary to compute Transition.inactives when there are sticky states are children to root state.
mappedStates['__inactives'] = mappedStates['']; // jshint ignore:line
}
});
return mappedStates;
}
// 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 core = uirextras_coreProvider;
var internalStates = core.internalStates;
var inherit = core.inherit;
var inheritParams = core.inheritParams;
var map = core.map;
var filterObj = core.filterObj;
versionHeuristics.hasParamSet = !!$urlMatcherFactoryProvider.ParamSet;
// inactivePseudoState (__inactives) holds all the inactive locals which includes resolved states data, i.e., views, scope, etc
inactivePseudoState = angular.extend(new SurrogateState("__inactives"), { self: { name: '__inactives' } });
// Reset other module scoped variables. This is to primarily to flush any previous state during karma runs.
root = pendingRestore = undefined;
pendingTransitions = [];
uirextras_coreProvider.onStateRegistered(function(state) {
// Register the ones marked as "sticky"
if (state.self.sticky === true) {
$stickyStateProvider.registerStickyState(state.self);
}
});
var $state_transitionTo; // internal reference to the real $state.transitionTo function
// Decorate the $state service, so we can decorate the $state.transitionTo() function with sticky state stuff.
$provide.decorator("$state", ['$delegate', '$log', '$q', function ($state, $log, $q) {
// Note: this code gets run only on the first state that is decorated
root = $state.$current;
internalStates[""] = root;
root.parent = inactivePseudoState; // Make inactivePsuedoState the parent of root. "wat"
inactivePseudoState.parent = undefined; // Make inactivePsuedoState the real root.
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.