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,97 @@
|
|
|
1
|
+
import parseInputLocation from '../../parseInputLocation';
|
|
2
|
+
export default class InMemoryNavigation {
|
|
3
|
+
constructor() {
|
|
4
|
+
// A stack of `LocationBase` objects.
|
|
5
|
+
this._stack = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line no-unused-vars
|
|
9
|
+
subscribe(listener) {
|
|
10
|
+
// `InMemoryNavigation` doesn't have any "asynchronycity" about it
|
|
11
|
+
// and performs any navigation immediately at the time of the call.
|
|
12
|
+
// Hence, no asynchronous listener would ever be called
|
|
13
|
+
// due to no asynchronous events being dispatched.
|
|
14
|
+
return () => {};
|
|
15
|
+
}
|
|
16
|
+
init(initialLocation, {
|
|
17
|
+
operation,
|
|
18
|
+
key,
|
|
19
|
+
index,
|
|
20
|
+
delta
|
|
21
|
+
}) {
|
|
22
|
+
this._stack.push({
|
|
23
|
+
location: parseInputLocation(initialLocation),
|
|
24
|
+
key
|
|
25
|
+
});
|
|
26
|
+
return this._createLocationObject({
|
|
27
|
+
operation,
|
|
28
|
+
index,
|
|
29
|
+
delta
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
navigate(location, {
|
|
33
|
+
operation,
|
|
34
|
+
key,
|
|
35
|
+
index,
|
|
36
|
+
delta
|
|
37
|
+
}) {
|
|
38
|
+
const {
|
|
39
|
+
pathname,
|
|
40
|
+
search,
|
|
41
|
+
query,
|
|
42
|
+
hash
|
|
43
|
+
} = location;
|
|
44
|
+
this._stack[index] = {
|
|
45
|
+
location: {
|
|
46
|
+
pathname,
|
|
47
|
+
search,
|
|
48
|
+
query,
|
|
49
|
+
hash
|
|
50
|
+
},
|
|
51
|
+
key
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// A `PUSH` navigation sets a new terminal (rightmost) location.
|
|
55
|
+
if (delta === 1) {
|
|
56
|
+
// Trim the location stack by removing any previous location history
|
|
57
|
+
// that might've existed after the current index.
|
|
58
|
+
this._stack.length = index + 1;
|
|
59
|
+
}
|
|
60
|
+
return Object.assign({}, location, {
|
|
61
|
+
key,
|
|
62
|
+
operation,
|
|
63
|
+
index,
|
|
64
|
+
delta
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
shift({
|
|
68
|
+
operation,
|
|
69
|
+
index,
|
|
70
|
+
delta
|
|
71
|
+
}) {
|
|
72
|
+
return this._createLocationObject({
|
|
73
|
+
operation,
|
|
74
|
+
index,
|
|
75
|
+
delta
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
getInitialLocation() {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
_createLocationObject({
|
|
82
|
+
operation,
|
|
83
|
+
index,
|
|
84
|
+
delta
|
|
85
|
+
}) {
|
|
86
|
+
const {
|
|
87
|
+
location,
|
|
88
|
+
key
|
|
89
|
+
} = this._stack[index];
|
|
90
|
+
return Object.assign({}, location, {
|
|
91
|
+
key,
|
|
92
|
+
operation,
|
|
93
|
+
index,
|
|
94
|
+
delta
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle, max-classes-per-file */
|
|
2
|
+
|
|
3
|
+
import ServerSideNavigationError from './error/ServerSideNavigationError';
|
|
4
|
+
export default class ServerSideNavigation {
|
|
5
|
+
init(initialLocation, {
|
|
6
|
+
operation,
|
|
7
|
+
key,
|
|
8
|
+
index,
|
|
9
|
+
delta
|
|
10
|
+
}) {
|
|
11
|
+
return Object.assign({}, initialLocation, {
|
|
12
|
+
operation,
|
|
13
|
+
key,
|
|
14
|
+
index,
|
|
15
|
+
delta
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line no-unused-vars
|
|
20
|
+
subscribe(listener) {
|
|
21
|
+
// `ServerSideNavigation` doesn't have any "asynchronycity" about it
|
|
22
|
+
// and any navigation is prohibited and would result in an error.
|
|
23
|
+
// So no asynchronous listener would ever be called
|
|
24
|
+
// due to no asynchronous events being dispatched.
|
|
25
|
+
return () => {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line no-unused-vars
|
|
29
|
+
navigate(location, {
|
|
30
|
+
operation,
|
|
31
|
+
key,
|
|
32
|
+
index,
|
|
33
|
+
delta
|
|
34
|
+
}) {
|
|
35
|
+
throw new ServerSideNavigationError(location);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line no-unused-vars
|
|
39
|
+
shift({
|
|
40
|
+
operation,
|
|
41
|
+
index,
|
|
42
|
+
delta
|
|
43
|
+
}) {
|
|
44
|
+
// It's not supposed to ever get to this code.
|
|
45
|
+
// * If `delta` is `0` then it's a "do nothing" scenario and `Session` won't even call this code.
|
|
46
|
+
// * If `delta` is not `0` and is out of bounds then a `NavigationOutOfBoundsError` will be thrown.
|
|
47
|
+
// * If `delta` is not `0` and is not out of bounds then it implies that a valid navigation has happened before
|
|
48
|
+
// which can't be the case because no navigation is possible on server side without throwing an error.
|
|
49
|
+
throw new Error('Server side has no navigation history');
|
|
50
|
+
}
|
|
51
|
+
getInitialLocation() {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import getLocationUrl from '../../getLocationUrl';
|
|
2
|
+
// import parseInputLocation from '../../parseInputLocation';
|
|
3
|
+
import parseQueryFromSearch from '../../parseQueryFromSearch';
|
|
4
|
+
import Operations from './operation/operations';
|
|
5
|
+
const NO_LOCATION_INDEX = -1;
|
|
6
|
+
|
|
7
|
+
// A web browser has a notion of a "navigation history".
|
|
8
|
+
// A "navigation history" exists within a given web browser's tab.
|
|
9
|
+
// The user can click "Back" or "Forward" buttons in the web browser and it will automatically load
|
|
10
|
+
// "previous" or "next" page from scratch.
|
|
11
|
+
//
|
|
12
|
+
// Later, web browsers added a `window.history` object that the application can,
|
|
13
|
+
// but isn't required to, interact with. That `window.history` object allows the application
|
|
14
|
+
// to programmatically control the URL in the address bar of the web browser, as well as
|
|
15
|
+
// the "navigation history" by programmatically adding new entries to it or reading the current entry,
|
|
16
|
+
// and it also allows the application to override the default web browser's behavior
|
|
17
|
+
// when the user clicks "Back" or "Forward" buttons in the web browser.
|
|
18
|
+
//
|
|
19
|
+
// Specifically, the `window.history` object has a method called `.pushState()` which programmatically adds
|
|
20
|
+
// a new entry in the "navigation history" and updates the URL in the address bar and also
|
|
21
|
+
// tells the web browser that starting from the entry before this new entry in the "navigation history",
|
|
22
|
+
// the application would prefer to manually handle any "Back"/"Forward" transition when the user clicks
|
|
23
|
+
// those "Back" or "Forward" buttons in the web browser, and this behavior should persist for any future
|
|
24
|
+
// "navigation history" entries programmatically added by the application via `window.history.pushState()`,
|
|
25
|
+
// and will only stop if the user navigates from the page by the means of conventional navigation,
|
|
26
|
+
// that is by clicking a standard hyperlink, at which point the current page gets "destroyed".
|
|
27
|
+
//
|
|
28
|
+
// So for manually "pushed" entries of the "navigation history", the web browser won't load those pages
|
|
29
|
+
// from scratch after a user-initiated "Back" or "Forward" transition. In fact, it won't do anything and
|
|
30
|
+
// it will just step aside and let the application itself do those transitions. The web browser will only
|
|
31
|
+
// update the URL in the address bar and that's it.
|
|
32
|
+
//
|
|
33
|
+
// This whole thing allows the application to:
|
|
34
|
+
//
|
|
35
|
+
// * Load the "previous" or "next" page much faster than when using the default "from scratch" approach
|
|
36
|
+
// because it doesn't have to destroy the current page, then send a new HTTP request to the server,
|
|
37
|
+
// then parse the HTML response and initialize a new page, re-download all those images, etc.
|
|
38
|
+
//
|
|
39
|
+
// * Optionally render a snapshotted verison of the "previous" page thereby "restoring" the "previous" page
|
|
40
|
+
// rather than reloading it from scratch, i.e. the state of the "previous" page could be fully restored.
|
|
41
|
+
//
|
|
42
|
+
export default class WebBrowserNavigation {
|
|
43
|
+
constructor() {
|
|
44
|
+
// `_currentLocationIndex` is used when receiving a "popstate" event
|
|
45
|
+
// that wasn't initiated by the application code but rather by the user
|
|
46
|
+
// clicking "Back" or "Forward" button in their web browser.
|
|
47
|
+
this._currentLocationIndex = NO_LOCATION_INDEX;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Subscribes to changes in the current location.
|
|
51
|
+
// Returns an `unsubscribe()` function which is "idempotent", i.e. it can be called multiple times.
|
|
52
|
+
subscribe(listener) {
|
|
53
|
+
const onPopState = () => {
|
|
54
|
+
// If "popstate" event is received before navigation is initialized,
|
|
55
|
+
// ignore such "popstate" event. This behavior is logical from the application code's view.
|
|
56
|
+
// And besides, `this._currentLocationIndex` is not defined in such conditions.
|
|
57
|
+
if (this._currentLocationIndex === NO_LOCATION_INDEX) {
|
|
58
|
+
throw new Error('Received a "popstate" event before initialized');
|
|
59
|
+
}
|
|
60
|
+
const prevIndex = this._currentLocationIndex;
|
|
61
|
+
const {
|
|
62
|
+
index
|
|
63
|
+
} = this._getCurrentLocationState();
|
|
64
|
+
this._currentLocationIndex = index;
|
|
65
|
+
listener(this._createEntryFromCurrentLocation({
|
|
66
|
+
operation: Operations.SHIFT,
|
|
67
|
+
delta: index - prevIndex
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Due to how `popstate` event listener works, there should only be one listener at a time,
|
|
72
|
+
// otherwise two different `Session`s would react to the same `popstate` event,
|
|
73
|
+
// each interpreting it as its own, while in reality it only belongs to one of them
|
|
74
|
+
// and the other one should completely ignore it.
|
|
75
|
+
// In other words, there can't exist two navigation sessions simultaneously by design.
|
|
76
|
+
// There can only be one active navigation session at a given time.
|
|
77
|
+
// Another one could only start after the previous one ends,
|
|
78
|
+
// not both of them being active simultaneously.
|
|
79
|
+
if (this._subscribed) {
|
|
80
|
+
throw new Error('There already is an active subscription. Only one subscription is allowed at a time.');
|
|
81
|
+
}
|
|
82
|
+
window.addEventListener('popstate', onPopState);
|
|
83
|
+
this._subscribed = true;
|
|
84
|
+
return () => {
|
|
85
|
+
window.removeEventListener('popstate', onPopState);
|
|
86
|
+
this._subscribed = false;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
init(initialLocation, {
|
|
90
|
+
operation,
|
|
91
|
+
key,
|
|
92
|
+
index,
|
|
93
|
+
delta
|
|
94
|
+
}) {
|
|
95
|
+
// Validate that `initialLocation` is same as `window.location`.
|
|
96
|
+
const isCurrentLocation = initialLocation === this._getCurrentLocation() || this._isSameAsCurrentLocation(initialLocation);
|
|
97
|
+
if (!isCurrentLocation) {
|
|
98
|
+
throw new Error('`initialLocation` argument should be same as `window.location`');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Set `window.history.state` of the initial location
|
|
102
|
+
// by calling `window.history.replaceState()` on page load.
|
|
103
|
+
// Otherwise, `window.history.state` would be `null` for the initial location
|
|
104
|
+
// and there'd be no place to store the additional properties of the initial location
|
|
105
|
+
// such as `location.key`.
|
|
106
|
+
// https://github.com/taion/scroll-behavior/issues/215
|
|
107
|
+
//
|
|
108
|
+
// If the user opens the initial page for the first time, `window.history.state` will be `null`.
|
|
109
|
+
// If the user refreshes the initial page, `window.history.state` will not be cleared
|
|
110
|
+
// and therefore will not be `null` and will instead have the previously-set value.
|
|
111
|
+
//
|
|
112
|
+
if (!this._getCurrentLocationState()) {
|
|
113
|
+
// Create additional properties for the initial locaiton.
|
|
114
|
+
const additionalProperties = {
|
|
115
|
+
key,
|
|
116
|
+
index
|
|
117
|
+
};
|
|
118
|
+
// Call `history.replaceState()`.
|
|
119
|
+
this._storeAdditionalPropertiesForLocation(initialLocation, additionalProperties, delta);
|
|
120
|
+
}
|
|
121
|
+
this._currentLocationIndex = index;
|
|
122
|
+
|
|
123
|
+
// Call the listeners.
|
|
124
|
+
return this._createEntryFromCurrentLocation({
|
|
125
|
+
operation,
|
|
126
|
+
delta
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
navigate(location, {
|
|
130
|
+
operation,
|
|
131
|
+
key,
|
|
132
|
+
index,
|
|
133
|
+
delta
|
|
134
|
+
}) {
|
|
135
|
+
const additionalProperties = {
|
|
136
|
+
key,
|
|
137
|
+
index
|
|
138
|
+
};
|
|
139
|
+
this._storeAdditionalPropertiesForLocation(location, additionalProperties, delta);
|
|
140
|
+
this._currentLocationIndex = index;
|
|
141
|
+
|
|
142
|
+
// Call the listeners.
|
|
143
|
+
return Object.assign({
|
|
144
|
+
operation,
|
|
145
|
+
delta
|
|
146
|
+
}, location, additionalProperties);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// shift({ operation, index, delta }) {
|
|
150
|
+
shift({
|
|
151
|
+
delta
|
|
152
|
+
}) {
|
|
153
|
+
// Web browser `history` is extremely non-strict when it comes to `history.go(delta)` navigation.
|
|
154
|
+
// It will allow any number as `delta`, regardless of whether such history entry exists or not.
|
|
155
|
+
// To introduce strict validation of the `delta` argument, `Session` class code explicitly checks
|
|
156
|
+
// the new `index` on whether it's out of bounds of the navigation history stack, and after it verifies
|
|
157
|
+
// that the new `index` is valid, it calls the `.shift(delta)` method of `WebBrowserNavigation` class.
|
|
158
|
+
//
|
|
159
|
+
// Calling `window.history.go()` will trigger a "popstate" event which will trigger the listeners.
|
|
160
|
+
//
|
|
161
|
+
window.history.go(delta);
|
|
162
|
+
}
|
|
163
|
+
getInitialLocation() {
|
|
164
|
+
// Web browser environment already knows the initial location
|
|
165
|
+
// by the time javascript code starts execution.
|
|
166
|
+
return this._getCurrentLocation();
|
|
167
|
+
}
|
|
168
|
+
_getCurrentLocation() {
|
|
169
|
+
return window.location;
|
|
170
|
+
}
|
|
171
|
+
_getCurrentLocationState() {
|
|
172
|
+
return window.history.state;
|
|
173
|
+
}
|
|
174
|
+
_isSameAsCurrentLocation(inputLocation) {
|
|
175
|
+
return typeof inputLocation === 'string' ? inputLocation === getLocationUrl(this._getCurrentLocation()) : inputLocation === this._getCurrentLocation() || getLocationUrl(inputLocation) === getLocationUrl(this._getCurrentLocation());
|
|
176
|
+
}
|
|
177
|
+
_createEntryFromCurrentLocation({
|
|
178
|
+
operation,
|
|
179
|
+
delta
|
|
180
|
+
}) {
|
|
181
|
+
const {
|
|
182
|
+
pathname,
|
|
183
|
+
search,
|
|
184
|
+
hash
|
|
185
|
+
} = this._getCurrentLocation();
|
|
186
|
+
const {
|
|
187
|
+
key,
|
|
188
|
+
index
|
|
189
|
+
} = this._getCurrentLocationState();
|
|
190
|
+
return {
|
|
191
|
+
operation,
|
|
192
|
+
pathname,
|
|
193
|
+
search,
|
|
194
|
+
query: parseQueryFromSearch(search),
|
|
195
|
+
hash,
|
|
196
|
+
key,
|
|
197
|
+
index,
|
|
198
|
+
delta
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
_storeAdditionalPropertiesForLocation(location, additionalProperties, delta) {
|
|
202
|
+
const url = getLocationUrl(location);
|
|
203
|
+
// `delta` property is not stored in `window.history.state`
|
|
204
|
+
// because it is supposed to be recalculated every time when reading from `window.history.state`.
|
|
205
|
+
if (delta === 1) {
|
|
206
|
+
window.history.pushState(additionalProperties, null, url);
|
|
207
|
+
} else if (delta === 0) {
|
|
208
|
+
window.history.replaceState(additionalProperties, null, url);
|
|
209
|
+
} else {
|
|
210
|
+
throw new Error(`Unsupported \`delta\`: ${delta}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const _excluded = ["operation"];
|
|
2
|
+
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
3
|
+
import getLocationUrl from '../../../getLocationUrl';
|
|
4
|
+
export default class ServerSideNavigationError extends Error {
|
|
5
|
+
constructor(location) {
|
|
6
|
+
super(location ? `Navigate to ${getLocationUrl(location)}` : 'Navigate to previous or next location');
|
|
7
|
+
if (location) {
|
|
8
|
+
// Remove `operation` property from `location`.
|
|
9
|
+
// eslint-disable-next-line no-unused-vars
|
|
10
|
+
const locationBase = _objectWithoutPropertiesLoose(location, _excluded);
|
|
11
|
+
this.location = locationBase;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export default class Subscription {
|
|
2
|
+
constructor({
|
|
3
|
+
activateSubscription
|
|
4
|
+
} = {}) {
|
|
5
|
+
this.notifySubscribers = argument => {
|
|
6
|
+
// `._latest` is only used in tests.
|
|
7
|
+
this._latest = argument;
|
|
8
|
+
for (const {
|
|
9
|
+
listener
|
|
10
|
+
} of this._listeners) {
|
|
11
|
+
listener(argument);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
this._activateSubscription = activateSubscription;
|
|
15
|
+
|
|
16
|
+
// This property is accessed in tests.
|
|
17
|
+
this._listeners = [];
|
|
18
|
+
}
|
|
19
|
+
subscribe(listener) {
|
|
20
|
+
// If subscriptions are stopped, i.e. no new subscriptions are to be added,
|
|
21
|
+
// then don't add any listeners and return a "do nothing" function.
|
|
22
|
+
if (this._stopped) {
|
|
23
|
+
return () => {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Creating a `listenerEntry` object ensures that the `.filter()` function
|
|
27
|
+
// during "unsubscribe" step doesn't accidentally remove another listeners
|
|
28
|
+
// having the same `listener` function.
|
|
29
|
+
// I.e. it's not illegal to call `.subscribe(listener)` multiple times
|
|
30
|
+
// with the same argument, and those would be considered different subscriptions.
|
|
31
|
+
const listenerEntry = {
|
|
32
|
+
listener
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// If it's the first listener, activate subscription.
|
|
36
|
+
if (this._listeners.length === 0) {
|
|
37
|
+
this._deactivateSubscription = this._activateSubscription(this.notifySubscribers);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add the `listener` to the list.
|
|
41
|
+
this._listeners.push(listenerEntry);
|
|
42
|
+
|
|
43
|
+
// The returned `unsubscribe()` function is "idempotent", i.e. it can be called multiple times.
|
|
44
|
+
return () => {
|
|
45
|
+
// Remove the listener, if not already removed.
|
|
46
|
+
this._removeListener(listenerEntry);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
stop() {
|
|
50
|
+
if (this._stopped) {
|
|
51
|
+
throw new Error('Already stopped');
|
|
52
|
+
}
|
|
53
|
+
this._stopped = true;
|
|
54
|
+
|
|
55
|
+
// Clear any remaining listeners.
|
|
56
|
+
for (const listener of this._listeners.slice()) {
|
|
57
|
+
this._removeListener(listener);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
_removeListener(listenerEntry) {
|
|
61
|
+
// If no listeners are left, no need to do anything.
|
|
62
|
+
if (this._listeners.length === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remove the `listener` from the list.
|
|
67
|
+
this._listeners = this._listeners.filter(_ => _ !== listenerEntry);
|
|
68
|
+
|
|
69
|
+
// If it was the last listener, deactivate subscription.
|
|
70
|
+
if (this._listeners.length === 0) {
|
|
71
|
+
this._deactivateSubscription();
|
|
72
|
+
this._deactivateSubscription = undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|