navigation-stack 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +603 -163
- package/data-storage/package.json +6 -0
- package/karma.conf.cjs +21 -4
- package/lib/cjs/NavigationStack.js +73 -0
- package/lib/cjs/data-storage/DataStorage.js +71 -0
- package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
- package/lib/cjs/data-storage/index.js +9 -0
- package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
- package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
- package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
- package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
- package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
- package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
- package/lib/cjs/getLocationFromInternalLocation.js +14 -0
- package/lib/cjs/index.js +20 -16
- package/lib/cjs/navigationBlockers.js +25 -23
- package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
- package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/lib/cjs/redux/ActionTypesInternal.js +8 -0
- package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
- package/lib/cjs/redux/createMiddlewares.js +60 -0
- package/lib/cjs/redux/index.js +13 -0
- package/lib/cjs/redux/internalLocationReducer.js +14 -0
- package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
- package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
- package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
- package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
- package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
- package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
- package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
- package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
- package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
- package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +130 -0
- package/lib/cjs/scroll-position/ScrollPositionRestoration.js +383 -0
- package/lib/cjs/scroll-position/ScrollPositionSaver.js +81 -0
- package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
- package/lib/cjs/scroll-position/constants.js +5 -0
- package/lib/cjs/scroll-position/index.js +7 -0
- package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
- package/lib/cjs/session/InMemorySession.js +22 -0
- package/lib/cjs/session/ServerSideRenderSession.js +17 -0
- package/lib/cjs/session/Session.js +196 -0
- package/lib/cjs/session/WebBrowserSession.js +20 -0
- package/lib/cjs/session/key/createSessionKey.js +23 -0
- package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
- package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
- package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
- package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
- package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
- package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
- package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
- package/lib/cjs/session/navigation/operation/operations.js +11 -0
- package/lib/cjs/session/subscription/Subscription.js +81 -0
- package/lib/data-storage/index.d.ts +35 -0
- package/lib/esm/NavigationStack.js +66 -0
- package/lib/esm/data-storage/DataStorage.js +65 -0
- package/lib/esm/data-storage/LocationDataStorage.js +22 -0
- package/lib/esm/data-storage/index.js +2 -0
- package/lib/esm/environment/InMemoryEnvironment.js +8 -0
- package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
- package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
- package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
- package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
- package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
- package/lib/esm/getLocationFromInternalLocation.js +9 -0
- package/lib/esm/index.js +10 -8
- package/lib/esm/navigationBlockers.js +25 -23
- package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
- package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/lib/esm/redux/ActionTypesInternal.js +3 -0
- package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
- package/lib/esm/redux/createMiddlewares.js +54 -0
- package/lib/esm/redux/index.js +4 -0
- package/lib/esm/redux/internalLocationReducer.js +8 -0
- package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
- package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
- package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
- package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
- package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
- package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
- package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
- package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
- package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
- package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +123 -0
- package/lib/esm/scroll-position/ScrollPositionRestoration.js +376 -0
- package/lib/esm/scroll-position/ScrollPositionSaver.js +74 -0
- package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
- package/lib/esm/scroll-position/constants.js +1 -0
- package/lib/esm/scroll-position/index.js +1 -0
- package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
- package/lib/esm/session/InMemorySession.js +15 -0
- package/lib/esm/session/ServerSideRenderSession.js +11 -0
- package/lib/esm/session/Session.js +189 -0
- package/lib/esm/session/WebBrowserSession.js +13 -0
- package/lib/esm/session/key/createSessionKey.js +18 -0
- package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
- package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
- package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
- package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
- package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
- package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
- package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
- package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
- package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
- package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
- package/lib/esm/session/navigation/operation/operations.js +6 -0
- package/lib/esm/session/subscription/Subscription.js +75 -0
- package/lib/index.d.ts +178 -157
- package/lib/redux/index.d.ts +90 -0
- package/lib/scroll-position/index.d.ts +107 -0
- package/package.json +9 -5
- package/redux/package.json +6 -0
- package/scroll-position/package.json +6 -0
- package/src/NavigationStack.js +84 -0
- package/src/data-storage/DataStorage.js +69 -0
- package/src/data-storage/LocationDataStorage.js +23 -0
- package/src/data-storage/index.js +2 -0
- package/src/environment/InMemoryEnvironment.js +9 -0
- package/src/environment/WebBrowserEnvironment.js +9 -0
- package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
- package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
- package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
- package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
- package/src/getLocationFromInternalLocation.js +7 -0
- package/src/index.js +10 -8
- package/src/navigationBlockers.js +28 -27
- package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
- package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/src/redux/ActionTypesInternal.js +3 -0
- package/src/{Actions.js → redux/Actions.js} +4 -3
- package/src/redux/createMiddlewares.js +65 -0
- package/src/redux/index.js +4 -0
- package/src/redux/internalLocationReducer.js +9 -0
- package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
- package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
- package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
- package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
- package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
- package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
- package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
- package/src/redux/middleware/updateLocationMiddleware.js +28 -0
- package/src/scroll-position/PageScrollPositionSetter.js +110 -0
- package/src/scroll-position/ScrollPositionAutoSaver.js +151 -0
- package/src/scroll-position/ScrollPositionRestoration.js +506 -0
- package/src/scroll-position/ScrollPositionSaver.js +100 -0
- package/src/scroll-position/ScrollPositionSetter.js +16 -0
- package/src/scroll-position/constants.js +1 -0
- package/src/scroll-position/index.js +1 -0
- package/src/scroll-position/scheduleNextTick.js +6 -0
- package/src/session/InMemorySession.js +13 -0
- package/src/session/ServerSideRenderSession.js +9 -0
- package/src/session/Session.js +216 -0
- package/src/session/WebBrowserSession.js +13 -0
- package/src/session/key/createSessionKey.js +18 -0
- package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
- package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
- package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
- package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
- package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
- package/src/session/navigation/InMemoryNavigation.js +78 -0
- package/src/session/navigation/ServerSideNavigation.js +43 -0
- package/src/session/navigation/WebBrowserNavigation.js +224 -0
- package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
- package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
- package/src/session/navigation/operation/operations.js +6 -0
- package/src/session/subscription/Subscription.js +76 -0
- package/test/NavigationStack.test.js +296 -0
- package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
- package/test/data-storage/index.test.js +8 -0
- package/test/index.js +12 -0
- package/test/index.test.js +8 -7
- package/test/{helpers.js → middlewareTestUtil.js} +9 -12
- package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
- package/test/{Action.test.js → redux/Action.test.js} +7 -6
- package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
- package/test/redux/createMiddlewares.test.js +96 -0
- package/test/redux/index.test.js +10 -0
- package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
- package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
- package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
- package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
- package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
- package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
- package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
- package/test/scroll-position/ScrollPositionRestoration.test.js +418 -0
- package/test/scroll-position/addScrollableContainer.js +36 -0
- package/test/scroll-position/addScrollableContainerWithHyperlink.js +50 -0
- package/test/scroll-position/createApp.js +112 -0
- package/test/scroll-position/delay.js +9 -0
- package/test/scroll-position/mockPageLifecycle.js +17 -0
- package/test/scroll-position/runApp.js +24 -0
- package/test/scroll-position/withScrollableContainerAtIndexPage.js +62 -0
- package/test/session/InMemorySession.test.js +348 -0
- package/test/session/ServerSession.test.js +17 -9
- package/test/session/WebBrowserSession.test.js +265 -0
- package/test/testUtil.js +3 -0
- package/types/data-storage/index.d.ts +35 -0
- package/types/index.d.ts +178 -157
- package/types/redux/index.d.ts +90 -0
- package/types/scroll-position/index.d.ts +107 -0
- package/types/tsconfig.json +1 -1
- package/lib/cjs/LocationDataStorage.js +0 -60
- package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
- package/lib/cjs/beforeLocationChangeListeners.js +0 -51
- package/lib/cjs/createMiddlewares.js +0 -47
- package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
- package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
- package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
- package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
- package/lib/cjs/session/BrowserSession.js +0 -235
- package/lib/cjs/session/MemorySession.js +0 -223
- package/lib/cjs/session/ServerSession.js +0 -65
- package/lib/esm/LocationDataStorage.js +0 -53
- package/lib/esm/addBeforeLocationChangeListener.js +0 -2
- package/lib/esm/beforeLocationChangeListeners.js +0 -44
- package/lib/esm/createMiddlewares.js +0 -41
- package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
- package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
- package/lib/esm/middleware/createLocationMiddleware.js +0 -50
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
- package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
- package/lib/esm/onlyAllowedOnClientSide.js +0 -5
- package/lib/esm/session/BrowserSession.js +0 -229
- package/lib/esm/session/MemorySession.js +0 -217
- package/lib/esm/session/ServerSession.js +0 -58
- package/src/LocationDataStorage.js +0 -59
- package/src/addBeforeLocationChangeListener.js +0 -2
- package/src/beforeLocationChangeListeners.js +0 -54
- package/src/createMiddlewares.js +0 -45
- package/src/middleware/createBasePathMiddleware.js +0 -20
- package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
- package/src/middleware/createLocationMiddleware.js +0 -55
- package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
- package/src/middleware/createTransformLocationMiddleware.js +0 -29
- package/src/onlyAllowedOnClientSide.js +0 -5
- package/src/session/BrowserSession.js +0 -235
- package/src/session/MemorySession.js +0 -219
- package/src/session/ServerSession.js +0 -67
- package/test/createMiddlewares.test.js +0 -62
- package/test/middleware/createBasePathMiddleware.test.js +0 -67
- package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
- package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
- package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
- package/test/session/BrowserSession.test.js +0 -182
- package/test/session/MemorySession.test.js +0 -244
- /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
- /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
- /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
|
|
3
|
+
// This code was copy-pasted from the final read-only version of `page-lifecycle` repo:
|
|
4
|
+
// https://github.com/GoogleChromeLabs/page-lifecycle/blob/master/src/Lifecycle.mjs
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Copyright 2018 Google Inc. All Rights Reserved.
|
|
8
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
you may not use this file except in compliance with the License.
|
|
10
|
+
You may obtain a copy of the License at
|
|
11
|
+
|
|
12
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
|
|
14
|
+
Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
See the License for the specific language governing permissions and
|
|
18
|
+
limitations under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const ACTIVE = 'active';
|
|
22
|
+
const PASSIVE = 'passive';
|
|
23
|
+
const HIDDEN = 'hidden';
|
|
24
|
+
const FROZEN = 'frozen';
|
|
25
|
+
// const DISCARDED = 'discarded'; Not used but show to completeness.
|
|
26
|
+
const TERMINATED = 'terminated';
|
|
27
|
+
|
|
28
|
+
const EVENTS = [
|
|
29
|
+
'focus',
|
|
30
|
+
'blur',
|
|
31
|
+
'visibilitychange',
|
|
32
|
+
'freeze',
|
|
33
|
+
'resume',
|
|
34
|
+
'pageshow',
|
|
35
|
+
// IE9-10 do not support the "pagehide" event, so the code could fall back to "unload" event here.
|
|
36
|
+
// Note: using "unload" event instead of "pagehide" event will
|
|
37
|
+
// prevent page navigation caching (a.k.a bfcache).
|
|
38
|
+
'pagehide',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {!Event} evt
|
|
43
|
+
* @return {string}
|
|
44
|
+
*/
|
|
45
|
+
const onbeforeunload = (evt) => {
|
|
46
|
+
evt.preventDefault();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Converts an array of states into an object where the state is the key
|
|
51
|
+
* and the value is the index.
|
|
52
|
+
* @param {!Array<string>} arr
|
|
53
|
+
* @return {!Object}
|
|
54
|
+
*/
|
|
55
|
+
const toIndexedObject = (arr) =>
|
|
56
|
+
arr.reduce((acc, val, idx) => {
|
|
57
|
+
acc[val] = idx;
|
|
58
|
+
return acc;
|
|
59
|
+
}, {});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @type {!Array<!Object>}
|
|
63
|
+
*/
|
|
64
|
+
const LEGAL_STATE_TRANSITIONS = [
|
|
65
|
+
// The normal unload process (bfcache process is addressed above).
|
|
66
|
+
[ACTIVE, PASSIVE, HIDDEN, TERMINATED],
|
|
67
|
+
|
|
68
|
+
// An active page transitioning to frozen,
|
|
69
|
+
// or an unloading page going into the bfcache.
|
|
70
|
+
[ACTIVE, PASSIVE, HIDDEN, FROZEN],
|
|
71
|
+
|
|
72
|
+
// A hidden page transitioning back to active.
|
|
73
|
+
[HIDDEN, PASSIVE, ACTIVE],
|
|
74
|
+
|
|
75
|
+
// A frozen page being resumed
|
|
76
|
+
[FROZEN, HIDDEN],
|
|
77
|
+
|
|
78
|
+
// A frozen (bfcached) page navigated back to
|
|
79
|
+
// Note: [FROZEN, HIDDEN] can happen here, but it's already covered above.
|
|
80
|
+
[FROZEN, ACTIVE],
|
|
81
|
+
[FROZEN, PASSIVE],
|
|
82
|
+
].map(toIndexedObject);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Accepts a current state and a future state and returns an array of legal
|
|
86
|
+
* state transition paths. This is needed to normalize behavior across browsers
|
|
87
|
+
* since some browsers do not fire events in certain cases and thus skip
|
|
88
|
+
* states.
|
|
89
|
+
* @param {string} oldState
|
|
90
|
+
* @param {string} newState
|
|
91
|
+
* @return {!Array<string>}
|
|
92
|
+
*/
|
|
93
|
+
const getLegalStateTransitionPath = (oldState, newState) => {
|
|
94
|
+
for (const order of LEGAL_STATE_TRANSITIONS) {
|
|
95
|
+
const oldIndex = order[oldState];
|
|
96
|
+
const newIndex = order[newState];
|
|
97
|
+
|
|
98
|
+
if (oldIndex >= 0 && newIndex >= 0 && newIndex > oldIndex) {
|
|
99
|
+
// Differences greater than one should be reported
|
|
100
|
+
// because it means a state was skipped.
|
|
101
|
+
return Object.keys(order).slice(oldIndex, newIndex + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
// TODO(philipwalton): it shouldn't be possible to get here, but
|
|
106
|
+
// consider some kind of warning or call to action if it happens.
|
|
107
|
+
// console.warn(`Invalid state change detected: ${oldState} > ${newState}`);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns the current state based on the document's visibility and
|
|
112
|
+
* in input focus states. Note this method is only used to determine
|
|
113
|
+
* active vs passive vs hidden states, as other states require listening
|
|
114
|
+
* for events.
|
|
115
|
+
* @return {string}
|
|
116
|
+
*/
|
|
117
|
+
const getCurrentState = () => {
|
|
118
|
+
if (document.visibilityState === HIDDEN) {
|
|
119
|
+
return HIDDEN;
|
|
120
|
+
}
|
|
121
|
+
if (document.hasFocus()) {
|
|
122
|
+
return ACTIVE;
|
|
123
|
+
}
|
|
124
|
+
return PASSIVE;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
class StateChangeEvent extends Event {
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} type
|
|
130
|
+
* @param {!Object} initDict
|
|
131
|
+
*/
|
|
132
|
+
constructor(type, initDict) {
|
|
133
|
+
super(type);
|
|
134
|
+
this.newState = initDict.newState;
|
|
135
|
+
this.oldState = initDict.oldState;
|
|
136
|
+
this.originalEvent = initDict.originalEvent;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Class definition for the exported, singleton lifecycle instance.
|
|
142
|
+
*/
|
|
143
|
+
export default class Lifecycle extends EventTarget {
|
|
144
|
+
/**
|
|
145
|
+
* Initializes state, state history, and adds event listeners to monitor
|
|
146
|
+
* state changes.
|
|
147
|
+
*/
|
|
148
|
+
constructor() {
|
|
149
|
+
super();
|
|
150
|
+
|
|
151
|
+
const state = getCurrentState();
|
|
152
|
+
|
|
153
|
+
this._state = state;
|
|
154
|
+
this._unsavedChanges = [];
|
|
155
|
+
|
|
156
|
+
// Bind the callback and add event listeners.
|
|
157
|
+
this._handleEvents = this._handleEvents.bind(this);
|
|
158
|
+
|
|
159
|
+
// Add capturing events on window so they run immediately.
|
|
160
|
+
EVENTS.forEach((evt) =>
|
|
161
|
+
window.addEventListener(evt, this._handleEvents, true),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @return {string}
|
|
167
|
+
*/
|
|
168
|
+
get state() {
|
|
169
|
+
return this._state;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Returns the value of document.wasDiscarded. This is arguably unnecessary
|
|
174
|
+
* but I think there's value in having the entire API in one place and
|
|
175
|
+
* consistent across browsers.
|
|
176
|
+
* @return {boolean}
|
|
177
|
+
*/
|
|
178
|
+
get pageWasDiscarded() {
|
|
179
|
+
return document.wasDiscarded || false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {Symbol|Object} id A unique symbol or object identifying the
|
|
184
|
+
*. pending state. This ID is required when removing the state later.
|
|
185
|
+
*/
|
|
186
|
+
addUnsavedChanges(id) {
|
|
187
|
+
// Don't add duplicate state. Note: ideall this would be a set, but for
|
|
188
|
+
// better browser compatibility we're using an array.
|
|
189
|
+
if (!this._unsavedChanges.indexOf(id) > -1) {
|
|
190
|
+
// If this is the first state being added,
|
|
191
|
+
// also add a beforeunload listener.
|
|
192
|
+
if (this._unsavedChanges.length === 0) {
|
|
193
|
+
window.addEventListener('beforeunload', onbeforeunload);
|
|
194
|
+
}
|
|
195
|
+
this._unsavedChanges.push(id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {Symbol|Object} id A unique symbol or object identifying the
|
|
201
|
+
*. pending state. This ID is required when removing the state later.
|
|
202
|
+
*/
|
|
203
|
+
removeUnsavedChanges(id) {
|
|
204
|
+
const idIndex = this._unsavedChanges.indexOf(id);
|
|
205
|
+
|
|
206
|
+
if (idIndex > -1) {
|
|
207
|
+
this._unsavedChanges.splice(idIndex, 1);
|
|
208
|
+
|
|
209
|
+
// If there's no more pending state, remove the event listener.
|
|
210
|
+
if (this._unsavedChanges.length === 0) {
|
|
211
|
+
window.removeEventListener('beforeunload', onbeforeunload);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Transitions from `this._state` to `newState`
|
|
218
|
+
* going through any intermediate states if required.
|
|
219
|
+
* @private
|
|
220
|
+
* @param {!Event} originalEvent
|
|
221
|
+
* @param {string} newState
|
|
222
|
+
*/
|
|
223
|
+
_dispatchChangesIfNeeded(originalEvent, newState) {
|
|
224
|
+
if (newState !== this._state) {
|
|
225
|
+
const oldState = this._state;
|
|
226
|
+
// Get the full chain of states for properly transitioning
|
|
227
|
+
// from `oldState` to `newState`.
|
|
228
|
+
const path = getLegalStateTransitionPath(oldState, newState);
|
|
229
|
+
|
|
230
|
+
// Start from `oldState` and transition to `newState`
|
|
231
|
+
// going through any intermediate states if required.
|
|
232
|
+
// Go through each intermediate state if required.
|
|
233
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
234
|
+
const prevState = path[i];
|
|
235
|
+
const nextState = path[i + 1];
|
|
236
|
+
|
|
237
|
+
this._state = nextState;
|
|
238
|
+
this.dispatchEvent(
|
|
239
|
+
new StateChangeEvent('statechange', {
|
|
240
|
+
oldState: prevState,
|
|
241
|
+
newState: nextState,
|
|
242
|
+
originalEvent,
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @private
|
|
251
|
+
* @param {!Event} evt
|
|
252
|
+
*/
|
|
253
|
+
_handleEvents(evt) {
|
|
254
|
+
switch (evt.type) {
|
|
255
|
+
case 'pageshow':
|
|
256
|
+
case 'resume':
|
|
257
|
+
this._dispatchChangesIfNeeded(evt, getCurrentState());
|
|
258
|
+
break;
|
|
259
|
+
case 'focus':
|
|
260
|
+
this._dispatchChangesIfNeeded(evt, ACTIVE);
|
|
261
|
+
break;
|
|
262
|
+
case 'blur':
|
|
263
|
+
// The `blur` event can fire while the page is being unloaded, so we
|
|
264
|
+
// only need to update the state if the current state is "active".
|
|
265
|
+
if (this._state === ACTIVE) {
|
|
266
|
+
this._dispatchChangesIfNeeded(evt, getCurrentState());
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case 'pagehide':
|
|
270
|
+
case 'unload':
|
|
271
|
+
this._dispatchChangesIfNeeded(
|
|
272
|
+
evt,
|
|
273
|
+
evt.persisted ? FROZEN : TERMINATED,
|
|
274
|
+
);
|
|
275
|
+
break;
|
|
276
|
+
case 'visibilitychange':
|
|
277
|
+
// The document's `visibilityState` will change to hidden as the page
|
|
278
|
+
// is being unloaded, but in such cases the lifecycle state shouldn't
|
|
279
|
+
// change.
|
|
280
|
+
if (this._state !== FROZEN && this._state !== TERMINATED) {
|
|
281
|
+
this._dispatchChangesIfNeeded(evt, getCurrentState());
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
case 'freeze':
|
|
285
|
+
this._dispatchChangesIfNeeded(evt, FROZEN);
|
|
286
|
+
break;
|
|
287
|
+
default:
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2018 Google Inc. All Rights Reserved.
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function doesSupportConstructableEventTarget() {
|
|
17
|
+
try {
|
|
18
|
+
// eslint-disable-next-line no-unused-vars
|
|
19
|
+
const eventTarget = new EventTarget();
|
|
20
|
+
|
|
21
|
+
// When transpiled with babel and rollup, the `IS_CODE_TRANSPILED` constant
|
|
22
|
+
// is replaced with the boolean `true`, so this statement will always
|
|
23
|
+
// evaluate to `false`. When not transpiled, it will be true.
|
|
24
|
+
return typeof IS_CODE_TRANSPILED === 'undefined';
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const supportsConstructableEventTarget = doesSupportConstructableEventTarget();
|
|
31
|
+
|
|
32
|
+
export { supportsConstructableEventTarget };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import parseInputLocation from '../../parseInputLocation';
|
|
2
|
+
|
|
3
|
+
export default class InMemoryNavigation {
|
|
4
|
+
constructor() {
|
|
5
|
+
// A stack of `LocationBase` objects.
|
|
6
|
+
this._stack = [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line no-unused-vars
|
|
10
|
+
subscribe(listener) {
|
|
11
|
+
// `InMemoryNavigation` doesn't have any "asynchronycity" about it
|
|
12
|
+
// and performs any navigation immediately at the time of the call.
|
|
13
|
+
// Hence, no asynchronous listener would ever be called
|
|
14
|
+
// due to no asynchronous events being dispatched.
|
|
15
|
+
return () => {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
init(initialLocation, { operation, key, index, delta }) {
|
|
19
|
+
this._stack.push({
|
|
20
|
+
location: parseInputLocation(initialLocation),
|
|
21
|
+
key,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return this._createLocationObject({
|
|
25
|
+
operation,
|
|
26
|
+
index,
|
|
27
|
+
delta,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
navigate(location, { operation, key, index, delta }) {
|
|
32
|
+
const { pathname, search, query, hash } = location;
|
|
33
|
+
|
|
34
|
+
this._stack[index] = {
|
|
35
|
+
location: { pathname, search, query, hash },
|
|
36
|
+
key,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// A `PUSH` navigation sets a new terminal (rightmost) location.
|
|
40
|
+
if (delta === 1) {
|
|
41
|
+
// Trim the location stack by removing any previous location history
|
|
42
|
+
// that might've existed after the current index.
|
|
43
|
+
this._stack.length = index + 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...location,
|
|
48
|
+
key,
|
|
49
|
+
operation,
|
|
50
|
+
index,
|
|
51
|
+
delta,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
shift({ operation, index, delta }) {
|
|
56
|
+
return this._createLocationObject({
|
|
57
|
+
operation,
|
|
58
|
+
index,
|
|
59
|
+
delta,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getInitialLocation() {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_createLocationObject({ operation, index, delta }) {
|
|
68
|
+
const { location, key } = this._stack[index];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...location,
|
|
72
|
+
key,
|
|
73
|
+
operation,
|
|
74
|
+
index,
|
|
75
|
+
delta,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle, max-classes-per-file */
|
|
2
|
+
|
|
3
|
+
import ServerSideNavigationError from './error/ServerSideNavigationError';
|
|
4
|
+
|
|
5
|
+
export default class ServerSideNavigation {
|
|
6
|
+
init(initialLocation, { operation, key, index, delta }) {
|
|
7
|
+
return {
|
|
8
|
+
...initialLocation,
|
|
9
|
+
operation,
|
|
10
|
+
key,
|
|
11
|
+
index,
|
|
12
|
+
delta,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line no-unused-vars
|
|
17
|
+
subscribe(listener) {
|
|
18
|
+
// `ServerSideNavigation` doesn't have any "asynchronycity" about it
|
|
19
|
+
// and any navigation is prohibited and would result in an error.
|
|
20
|
+
// So no asynchronous listener would ever be called
|
|
21
|
+
// due to no asynchronous events being dispatched.
|
|
22
|
+
return () => {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line no-unused-vars
|
|
26
|
+
navigate(location, { operation, key, index, delta }) {
|
|
27
|
+
throw new ServerSideNavigationError(location);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line no-unused-vars
|
|
31
|
+
shift({ operation, index, delta }) {
|
|
32
|
+
// It's not supposed to ever get to this code.
|
|
33
|
+
// * If `delta` is `0` then it's a "do nothing" scenario and `Session` won't even call this code.
|
|
34
|
+
// * If `delta` is not `0` and is out of bounds then a `NavigationOutOfBoundsError` will be thrown.
|
|
35
|
+
// * If `delta` is not `0` and is not out of bounds then it implies that a valid navigation has happened before
|
|
36
|
+
// which can't be the case because no navigation is possible on server side without throwing an error.
|
|
37
|
+
throw new Error('Server side has no navigation history');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getInitialLocation() {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import getLocationUrl from '../../getLocationUrl';
|
|
2
|
+
// import parseInputLocation from '../../parseInputLocation';
|
|
3
|
+
import parseQueryFromSearch from '../../parseQueryFromSearch';
|
|
4
|
+
import Operations from './operation/operations';
|
|
5
|
+
|
|
6
|
+
const NO_LOCATION_INDEX = -1;
|
|
7
|
+
|
|
8
|
+
// A web browser has a notion of a "navigation history".
|
|
9
|
+
// A "navigation history" exists within a given web browser's tab.
|
|
10
|
+
// The user can click "Back" or "Forward" buttons in the web browser and it will automatically load
|
|
11
|
+
// "previous" or "next" page from scratch.
|
|
12
|
+
//
|
|
13
|
+
// Later, web browsers added a `window.history` object that the application can,
|
|
14
|
+
// but isn't required to, interact with. That `window.history` object allows the application
|
|
15
|
+
// to programmatically control the URL in the address bar of the web browser, as well as
|
|
16
|
+
// the "navigation history" by programmatically adding new entries to it or reading the current entry,
|
|
17
|
+
// and it also allows the application to override the default web browser's behavior
|
|
18
|
+
// when the user clicks "Back" or "Forward" buttons in the web browser.
|
|
19
|
+
//
|
|
20
|
+
// Specifically, the `window.history` object has a method called `.pushState()` which programmatically adds
|
|
21
|
+
// a new entry in the "navigation history" and updates the URL in the address bar and also
|
|
22
|
+
// tells the web browser that starting from the entry before this new entry in the "navigation history",
|
|
23
|
+
// the application would prefer to manually handle any "Back"/"Forward" transition when the user clicks
|
|
24
|
+
// those "Back" or "Forward" buttons in the web browser, and this behavior should persist for any future
|
|
25
|
+
// "navigation history" entries programmatically added by the application via `window.history.pushState()`,
|
|
26
|
+
// and will only stop if the user navigates from the page by the means of conventional navigation,
|
|
27
|
+
// that is by clicking a standard hyperlink, at which point the current page gets "destroyed".
|
|
28
|
+
//
|
|
29
|
+
// So for manually "pushed" entries of the "navigation history", the web browser won't load those pages
|
|
30
|
+
// from scratch after a user-initiated "Back" or "Forward" transition. In fact, it won't do anything and
|
|
31
|
+
// it will just step aside and let the application itself do those transitions. The web browser will only
|
|
32
|
+
// update the URL in the address bar and that's it.
|
|
33
|
+
//
|
|
34
|
+
// This whole thing allows the application to:
|
|
35
|
+
//
|
|
36
|
+
// * Load the "previous" or "next" page much faster than when using the default "from scratch" approach
|
|
37
|
+
// because it doesn't have to destroy the current page, then send a new HTTP request to the server,
|
|
38
|
+
// then parse the HTML response and initialize a new page, re-download all those images, etc.
|
|
39
|
+
//
|
|
40
|
+
// * Optionally render a snapshotted verison of the "previous" page thereby "restoring" the "previous" page
|
|
41
|
+
// rather than reloading it from scratch, i.e. the state of the "previous" page could be fully restored.
|
|
42
|
+
//
|
|
43
|
+
export default class WebBrowserNavigation {
|
|
44
|
+
constructor() {
|
|
45
|
+
// `_currentLocationIndex` is used when receiving a "popstate" event
|
|
46
|
+
// that wasn't initiated by the application code but rather by the user
|
|
47
|
+
// clicking "Back" or "Forward" button in their web browser.
|
|
48
|
+
this._currentLocationIndex = NO_LOCATION_INDEX;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Subscribes to changes in the current location.
|
|
52
|
+
// Returns an `unsubscribe()` function which is "idempotent", i.e. it can be called multiple times.
|
|
53
|
+
subscribe(listener) {
|
|
54
|
+
const onPopState = () => {
|
|
55
|
+
// If "popstate" event is received before navigation is initialized,
|
|
56
|
+
// ignore such "popstate" event. This behavior is logical from the application code's view.
|
|
57
|
+
// And besides, `this._currentLocationIndex` is not defined in such conditions.
|
|
58
|
+
if (this._currentLocationIndex === NO_LOCATION_INDEX) {
|
|
59
|
+
throw new Error('Received a "popstate" event before initialized');
|
|
60
|
+
}
|
|
61
|
+
const prevIndex = this._currentLocationIndex;
|
|
62
|
+
const { index } = this._getCurrentLocationState();
|
|
63
|
+
this._currentLocationIndex = index;
|
|
64
|
+
|
|
65
|
+
listener(
|
|
66
|
+
this._createEntryFromCurrentLocation({
|
|
67
|
+
operation: Operations.SHIFT,
|
|
68
|
+
delta: index - prevIndex,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Due to how `popstate` event listener works, there should only be one listener at a time,
|
|
74
|
+
// otherwise two different `Session`s would react to the same `popstate` event,
|
|
75
|
+
// each interpreting it as its own, while in reality it only belongs to one of them
|
|
76
|
+
// and the other one should completely ignore it.
|
|
77
|
+
// In other words, there can't exist two navigation sessions simultaneously by design.
|
|
78
|
+
// There can only be one active navigation session at a given time.
|
|
79
|
+
// Another one could only start after the previous one ends,
|
|
80
|
+
// not both of them being active simultaneously.
|
|
81
|
+
if (this._subscribed) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'There already is an active subscription. Only one subscription is allowed at a time.',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
window.addEventListener('popstate', onPopState);
|
|
88
|
+
this._subscribed = true;
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener('popstate', onPopState);
|
|
92
|
+
this._subscribed = false;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
init(initialLocation, { operation, key, index, delta }) {
|
|
97
|
+
// Validate that `initialLocation` is same as `window.location`.
|
|
98
|
+
const isCurrentLocation =
|
|
99
|
+
initialLocation === this._getCurrentLocation() ||
|
|
100
|
+
this._isSameAsCurrentLocation(initialLocation);
|
|
101
|
+
|
|
102
|
+
if (!isCurrentLocation) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'`initialLocation` argument should be same as `window.location`',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Set `window.history.state` of the initial location
|
|
109
|
+
// by calling `window.history.replaceState()` on page load.
|
|
110
|
+
// Otherwise, `window.history.state` would be `null` for the initial location
|
|
111
|
+
// and there'd be no place to store the additional properties of the initial location
|
|
112
|
+
// such as `location.key`.
|
|
113
|
+
// https://github.com/taion/scroll-behavior/issues/215
|
|
114
|
+
//
|
|
115
|
+
// If the user opens the initial page for the first time, `window.history.state` will be `null`.
|
|
116
|
+
// If the user refreshes the initial page, `window.history.state` will not be cleared
|
|
117
|
+
// and therefore will not be `null` and will instead have the previously-set value.
|
|
118
|
+
//
|
|
119
|
+
if (!this._getCurrentLocationState()) {
|
|
120
|
+
// Create additional properties for the initial locaiton.
|
|
121
|
+
const additionalProperties = { key, index };
|
|
122
|
+
// Call `history.replaceState()`.
|
|
123
|
+
this._storeAdditionalPropertiesForLocation(
|
|
124
|
+
initialLocation,
|
|
125
|
+
additionalProperties,
|
|
126
|
+
delta,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this._currentLocationIndex = index;
|
|
131
|
+
|
|
132
|
+
// Call the listeners.
|
|
133
|
+
return this._createEntryFromCurrentLocation({ operation, delta });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
navigate(location, { operation, key, index, delta }) {
|
|
137
|
+
const additionalProperties = { key, index };
|
|
138
|
+
|
|
139
|
+
this._storeAdditionalPropertiesForLocation(
|
|
140
|
+
location,
|
|
141
|
+
additionalProperties,
|
|
142
|
+
delta,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
this._currentLocationIndex = index;
|
|
146
|
+
|
|
147
|
+
// Call the listeners.
|
|
148
|
+
return {
|
|
149
|
+
operation,
|
|
150
|
+
delta,
|
|
151
|
+
...location,
|
|
152
|
+
...additionalProperties,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// shift({ operation, index, delta }) {
|
|
157
|
+
shift({ delta }) {
|
|
158
|
+
// Web browser `history` is extremely non-strict when it comes to `history.go(delta)` navigation.
|
|
159
|
+
// It will allow any number as `delta`, regardless of whether such history entry exists or not.
|
|
160
|
+
// To introduce strict validation of the `delta` argument, `Session` class code explicitly checks
|
|
161
|
+
// the new `index` on whether it's out of bounds of the navigation history stack, and after it verifies
|
|
162
|
+
// that the new `index` is valid, it calls the `.shift(delta)` method of `WebBrowserNavigation` class.
|
|
163
|
+
//
|
|
164
|
+
// Calling `window.history.go()` will trigger a "popstate" event which will trigger the listeners.
|
|
165
|
+
//
|
|
166
|
+
window.history.go(delta);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getInitialLocation() {
|
|
170
|
+
// Web browser environment already knows the initial location
|
|
171
|
+
// by the time javascript code starts execution.
|
|
172
|
+
return this._getCurrentLocation();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_getCurrentLocation() {
|
|
176
|
+
return window.location;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_getCurrentLocationState() {
|
|
180
|
+
return window.history.state;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_isSameAsCurrentLocation(inputLocation) {
|
|
184
|
+
return typeof inputLocation === 'string'
|
|
185
|
+
? inputLocation === getLocationUrl(this._getCurrentLocation())
|
|
186
|
+
: inputLocation === this._getCurrentLocation() ||
|
|
187
|
+
getLocationUrl(inputLocation) ===
|
|
188
|
+
getLocationUrl(this._getCurrentLocation());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_createEntryFromCurrentLocation({ operation, delta }) {
|
|
192
|
+
const { pathname, search, hash } = this._getCurrentLocation();
|
|
193
|
+
|
|
194
|
+
const { key, index } = this._getCurrentLocationState();
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
operation,
|
|
198
|
+
pathname,
|
|
199
|
+
search,
|
|
200
|
+
query: parseQueryFromSearch(search),
|
|
201
|
+
hash,
|
|
202
|
+
key,
|
|
203
|
+
index,
|
|
204
|
+
delta,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
_storeAdditionalPropertiesForLocation(
|
|
209
|
+
location,
|
|
210
|
+
additionalProperties,
|
|
211
|
+
delta,
|
|
212
|
+
) {
|
|
213
|
+
const url = getLocationUrl(location);
|
|
214
|
+
// `delta` property is not stored in `window.history.state`
|
|
215
|
+
// because it is supposed to be recalculated every time when reading from `window.history.state`.
|
|
216
|
+
if (delta === 1) {
|
|
217
|
+
window.history.pushState(additionalProperties, null, url);
|
|
218
|
+
} else if (delta === 0) {
|
|
219
|
+
window.history.replaceState(additionalProperties, null, url);
|
|
220
|
+
} else {
|
|
221
|
+
throw new Error(`Unsupported \`delta\`: ${delta}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|