navigation-stack 0.5.2 → 0.6.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/CHANGELOG.md +16 -0
- package/README.md +144 -282
- package/karma.conf.cjs +1 -1
- package/lib/cjs/NavigationStack.js +138 -49
- package/lib/cjs/data-storage/DataStorage.js +7 -6
- package/lib/cjs/environment/InMemoryEnvironment.js +6 -0
- package/lib/cjs/{session/ServerSideRenderSession.js → environment/ServerSideRenderEnvironment.js} +5 -6
- package/lib/cjs/environment/WebBrowserEnvironment.js +6 -0
- package/lib/cjs/environment/log/InMemoryLog.js +23 -0
- package/lib/cjs/environment/log/WebBrowserLog.js +22 -0
- package/lib/cjs/{session → environment}/navigation/InMemoryNavigation.js +16 -5
- package/lib/cjs/{session → environment}/navigation/ServerSideNavigation.js +16 -7
- package/lib/cjs/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
- package/lib/cjs/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +2 -2
- package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
- package/lib/cjs/getLocationBaseFromLocation.js +14 -0
- package/lib/cjs/getLocationUrl.js +3 -5
- package/lib/cjs/index.js +10 -16
- package/lib/cjs/navigationBlockers.js +34 -32
- package/lib/cjs/navigationBlockersEvaluation.js +150 -0
- package/lib/cjs/parseInputLocation.js +10 -3
- package/lib/cjs/parseQueryFromSearch.js +3 -6
- package/lib/cjs/parseQueryString.js +77 -0
- package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +7 -6
- package/lib/cjs/scroll-position/ScrollPositionRestoration.js +31 -27
- package/lib/cjs/scroll-position/ScrollPositionSaver.js +6 -4
- package/lib/cjs/session/Session.js +61 -26
- package/lib/cjs/session/subscription/Subscription.js +36 -18
- package/lib/cjs/stringifyQuery.js +66 -0
- package/lib/cjs/stringifyQueryAsSearch.js +14 -0
- package/lib/esm/NavigationStack.js +138 -49
- package/lib/esm/data-storage/DataStorage.js +7 -6
- package/lib/esm/environment/InMemoryEnvironment.js +6 -0
- package/lib/esm/environment/ServerSideRenderEnvironment.js +10 -0
- package/lib/esm/environment/WebBrowserEnvironment.js +6 -0
- package/lib/esm/environment/log/InMemoryLog.js +17 -0
- package/lib/esm/environment/log/WebBrowserLog.js +16 -0
- package/lib/esm/{session → environment}/navigation/InMemoryNavigation.js +16 -5
- package/lib/esm/{session → environment}/navigation/ServerSideNavigation.js +16 -7
- package/lib/esm/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
- package/lib/esm/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
- package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
- package/lib/esm/getLocationBaseFromLocation.js +9 -0
- package/lib/esm/getLocationUrl.js +2 -5
- package/lib/esm/index.js +5 -8
- package/lib/esm/navigationBlockers.js +34 -32
- package/lib/esm/navigationBlockersEvaluation.js +145 -0
- package/lib/esm/parseInputLocation.js +9 -3
- package/lib/esm/parseQueryFromSearch.js +2 -6
- package/lib/esm/parseQueryString.js +72 -0
- package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +7 -6
- package/lib/esm/scroll-position/ScrollPositionRestoration.js +31 -27
- package/lib/esm/scroll-position/ScrollPositionSaver.js +6 -4
- package/lib/esm/session/Session.js +61 -26
- package/lib/esm/session/subscription/Subscription.js +36 -18
- package/lib/esm/stringifyQuery.js +61 -0
- package/lib/esm/stringifyQueryAsSearch.js +8 -0
- package/lib/index.d.ts +180 -34
- package/package.json +4 -7
- package/src/NavigationStack.js +166 -56
- package/src/data-storage/DataStorage.js +9 -6
- package/src/environment/InMemoryEnvironment.js +6 -0
- package/src/environment/ServerSideRenderEnvironment.js +10 -0
- package/src/environment/WebBrowserEnvironment.js +6 -0
- package/src/environment/log/InMemoryLog.js +20 -0
- package/src/environment/log/WebBrowserLog.js +18 -0
- package/src/{session → environment}/navigation/InMemoryNavigation.js +16 -5
- package/src/{session → environment}/navigation/ServerSideNavigation.js +16 -7
- package/src/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
- package/src/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
- package/src/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
- package/src/getLocationBaseFromLocation.js +7 -0
- package/src/getLocationUrl.js +2 -5
- package/src/index.js +10 -13
- package/src/navigationBlockers.js +55 -34
- package/src/navigationBlockersEvaluation.js +161 -0
- package/src/parseInputLocation.js +10 -3
- package/src/parseQueryFromSearch.js +2 -6
- package/src/parseQueryString.js +81 -0
- package/src/scroll-position/ScrollPositionAutoSaver.js +10 -6
- package/src/scroll-position/ScrollPositionRestoration.js +36 -30
- package/src/scroll-position/ScrollPositionSaver.js +6 -4
- package/src/scroll-position/index.js +1 -1
- package/src/session/Session.js +68 -24
- package/src/session/subscription/Subscription.js +36 -11
- package/src/stringifyQuery.js +71 -0
- package/src/stringifyQueryAsSearch.js +9 -0
- package/test/NavigationStack.addBasePath.test.js +50 -0
- package/test/{redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockNonProgrammaticNavigationIfRequired.test.js} +51 -63
- package/test/{redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockProgrammaticNavigationIfRequired.test.js} +98 -78
- package/test/NavigationStack.general.test.js +68 -0
- package/test/NavigationStack.parseInputLocation.test.js +52 -0
- package/test/NavigationStack.removeBasePath.test.js +69 -0
- package/test/NavigationStack.test.js +97 -29
- package/test/data-storage/LocationDataStorage.test.js +3 -2
- package/test/index.js +7 -31
- package/test/index.test.js +4 -5
- package/test/parseQueryFromSearch.test.js +19 -0
- package/test/parseQueryString.test.js +18 -0
- package/test/scroll-position/ScrollPositionRestoration.test.js +34 -13
- package/test/scroll-position/createApp.js +8 -8
- package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +4 -4
- package/test/session/{InMemorySession.test.js → Session.InMemoryEnvironment.test.js} +10 -9
- package/test/session/{ServerSession.test.js → Session.ServerSideRenderEnvironment.test.js} +5 -4
- package/test/session/{WebBrowserSession.test.js → Session.WebBrowserEnvironment.test.js} +63 -13
- package/test/shouldWarn.js +44 -0
- package/test/stringifyQuery.test.js +65 -0
- package/types/index.d.ts +180 -34
- package/types/tsconfig.json +0 -1
- package/data-storage/package.json +0 -7
- package/lib/cjs/createSearchFromQuery.js +0 -13
- package/lib/cjs/debug.js +0 -12
- package/lib/cjs/redux/ActionTypes.js +0 -14
- package/lib/cjs/redux/ActionTypesInternal.js +0 -8
- package/lib/cjs/redux/Actions.js +0 -28
- package/lib/cjs/redux/createMiddlewares.js +0 -60
- package/lib/cjs/redux/index.js +0 -13
- package/lib/cjs/redux/internalLocationReducer.js +0 -14
- package/lib/cjs/redux/locationReducer.js +0 -13
- package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -32
- package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -113
- package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
- package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -30
- package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -73
- package/lib/cjs/redux/middleware/navigationOperationMiddleware.js +0 -40
- package/lib/cjs/redux/middleware/parseInputLocationMiddleware.js +0 -29
- package/lib/cjs/redux/middleware/updateLocationMiddleware.js +0 -34
- package/lib/cjs/session/InMemorySession.js +0 -22
- package/lib/cjs/session/WebBrowserSession.js +0 -20
- package/lib/data-storage/index.d.ts +0 -35
- package/lib/esm/createSearchFromQuery.js +0 -8
- package/lib/esm/debug.js +0 -7
- package/lib/esm/redux/ActionTypes.js +0 -9
- package/lib/esm/redux/ActionTypesInternal.js +0 -3
- package/lib/esm/redux/Actions.js +0 -22
- package/lib/esm/redux/createMiddlewares.js +0 -54
- package/lib/esm/redux/index.js +0 -4
- package/lib/esm/redux/internalLocationReducer.js +0 -8
- package/lib/esm/redux/locationReducer.js +0 -7
- package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
- package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -108
- package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -88
- package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -25
- package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -68
- package/lib/esm/redux/middleware/navigationOperationMiddleware.js +0 -35
- package/lib/esm/redux/middleware/parseInputLocationMiddleware.js +0 -24
- package/lib/esm/redux/middleware/updateLocationMiddleware.js +0 -28
- package/lib/esm/session/InMemorySession.js +0 -15
- package/lib/esm/session/ServerSideRenderSession.js +0 -11
- package/lib/esm/session/WebBrowserSession.js +0 -13
- package/lib/redux/index.d.ts +0 -90
- package/lib/scroll-position/index.d.ts +0 -107
- package/redux/package.json +0 -7
- package/scroll-position/package.json +0 -7
- package/src/createSearchFromQuery.js +0 -9
- package/src/debug.js +0 -8
- package/src/redux/ActionTypes.js +0 -9
- package/src/redux/ActionTypesInternal.js +0 -3
- package/src/redux/Actions.js +0 -27
- package/src/redux/createMiddlewares.js +0 -65
- package/src/redux/index.js +0 -4
- package/src/redux/internalLocationReducer.js +0 -9
- package/src/redux/locationReducer.js +0 -8
- package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
- package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -119
- package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
- package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -26
- package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -72
- package/src/redux/middleware/navigationOperationMiddleware.js +0 -34
- package/src/redux/middleware/parseInputLocationMiddleware.js +0 -23
- package/src/redux/middleware/updateLocationMiddleware.js +0 -28
- package/src/session/InMemorySession.js +0 -13
- package/src/session/ServerSideRenderSession.js +0 -9
- package/src/session/WebBrowserSession.js +0 -13
- package/test/middlewareTestUtil.js +0 -31
- package/test/redux/Action.test.js +0 -73
- package/test/redux/ActionTypes.test.js +0 -13
- package/test/redux/createMiddlewares.test.js +0 -96
- package/test/redux/index.test.js +0 -10
- package/test/redux/locationReducer.test.js +0 -39
- package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +0 -40
- package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +0 -51
- package/test/redux/middleware/navigationOperationMiddleware.test.js +0 -78
- package/test/redux/middleware/parseInputLocationMiddleware.test.js +0 -62
- package/test/testUtil.js +0 -3
- package/types/data-storage/index.d.ts +0 -35
- package/types/redux/index.d.ts +0 -90
- package/types/scroll-position/index.d.ts +0 -107
- /package/lib/cjs/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
- /package/lib/cjs/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
- /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
- /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
- /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
- /package/lib/cjs/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
- /package/lib/cjs/{session → environment}/navigation/operation/operations.js +0 -0
- /package/lib/esm/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
- /package/lib/esm/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
- /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
- /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
- /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
- /package/lib/esm/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
- /package/lib/esm/{session → environment}/navigation/operation/operations.js +0 -0
- /package/src/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
- /package/src/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
- /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
- /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
- /package/src/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
- /package/src/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
- /package/src/{session → environment}/navigation/operation/operations.js +0 -0
- /package/test/{parseInputLocationMiddleware.test.js → parseInputLocation.test.js} +0 -0
|
@@ -1,54 +1,51 @@
|
|
|
1
1
|
/* eslint-disable no-underscore-dangle */
|
|
2
2
|
|
|
3
|
-
import debug from './debug';
|
|
4
3
|
import isPromise from './isPromise';
|
|
5
|
-
export function getNavigationBlockers(
|
|
6
|
-
return
|
|
4
|
+
export function getNavigationBlockers(container) {
|
|
5
|
+
return container._navigationBlockersList || [];
|
|
7
6
|
}
|
|
8
|
-
function addNavigationBlockerToTheList(blocker,
|
|
9
|
-
if (!
|
|
10
|
-
|
|
7
|
+
function addNavigationBlockerToTheList(blocker, container) {
|
|
8
|
+
if (!container._navigationBlockersList) {
|
|
9
|
+
container._navigationBlockersList = [];
|
|
11
10
|
}
|
|
12
|
-
|
|
11
|
+
container._navigationBlockersList.push(blocker);
|
|
13
12
|
}
|
|
14
|
-
function removeNavigationBlockerFromTheList(blocker,
|
|
15
|
-
if (
|
|
16
|
-
|
|
13
|
+
function removeNavigationBlockerFromTheList(blocker, container) {
|
|
14
|
+
if (container._navigationBlockersList) {
|
|
15
|
+
container._navigationBlockersList = container._navigationBlockersList.filter(_ => _ !== blocker);
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
export function removeAllNavigationBlockers(session) {
|
|
20
|
-
|
|
19
|
+
// `navigationBlockers` are stored in `session`.
|
|
20
|
+
const container = session;
|
|
21
|
+
if (getNavigationBlockers(container).some(blocker => blocker.beforeTermination)) {
|
|
21
22
|
if (!session._removeTerminationBlocker) {
|
|
22
23
|
throw new Error('`_removeTerminationBlocker` property not found in the `session`');
|
|
23
24
|
}
|
|
24
25
|
session._removeTerminationBlocker();
|
|
25
26
|
session._removeTerminationBlocker = undefined;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
container._navigationBlockersList = [];
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// Runs the `blocker` while ignoring any errors that might be thrown by it.
|
|
31
32
|
function runNavigationBlocker({
|
|
32
33
|
blocker
|
|
33
|
-
}, location) {
|
|
34
|
+
}, location, environment) {
|
|
34
35
|
let result;
|
|
35
36
|
try {
|
|
36
37
|
result = blocker(location);
|
|
37
38
|
} catch (error) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// eslint-disable-next-line no-console
|
|
41
|
-
console.error(error);
|
|
39
|
+
environment.log.warn(`Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`);
|
|
40
|
+
environment.log.error(error);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
// If the blocker returned a `Promise`, await for that `Promise`
|
|
45
44
|
// and then return the result.
|
|
46
45
|
if (isPromise(result)) {
|
|
47
46
|
return result.catch(error => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// eslint-disable-next-line no-console
|
|
51
|
-
console.error(error);
|
|
47
|
+
environment.log.warn(`Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`);
|
|
48
|
+
environment.log.error(error);
|
|
52
49
|
});
|
|
53
50
|
}
|
|
54
51
|
// The blocker didn't return a `Promise`.
|
|
@@ -59,28 +56,28 @@ function runNavigationBlocker({
|
|
|
59
56
|
// Runs all blockers in order.
|
|
60
57
|
// If any blocker returns `true`, it stops and returns the result.
|
|
61
58
|
// If there's no such blocker, returns `undefined`.
|
|
62
|
-
export function runNavigationBlockers(navigationBlockers, toLocation) {
|
|
59
|
+
export function runNavigationBlockers(navigationBlockers, toLocation, environment) {
|
|
63
60
|
if (navigationBlockers.length === 0) {
|
|
64
61
|
return undefined;
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
// Call the first blocker in the list.
|
|
68
|
-
const result = runNavigationBlocker(navigationBlockers[0], toLocation);
|
|
65
|
+
const result = runNavigationBlocker(navigationBlockers[0], toLocation, environment);
|
|
69
66
|
const next = () => {
|
|
70
67
|
// Proceed to the next blocker.
|
|
71
|
-
return runNavigationBlockers(navigationBlockers.slice(1), toLocation);
|
|
68
|
+
return runNavigationBlockers(navigationBlockers.slice(1), toLocation, environment);
|
|
72
69
|
};
|
|
73
70
|
if (isPromise(result)) {
|
|
74
71
|
return result.then(resultValue => {
|
|
75
72
|
if (resultValue) {
|
|
76
|
-
debug('Navigation blocked', toLocation.pathname);
|
|
73
|
+
environment.log.debug('Navigation blocked', toLocation.pathname);
|
|
77
74
|
return resultValue;
|
|
78
75
|
}
|
|
79
76
|
return next();
|
|
80
77
|
});
|
|
81
78
|
}
|
|
82
79
|
if (result) {
|
|
83
|
-
debug('Navigation blocked', toLocation.pathname);
|
|
80
|
+
environment.log.debug('Navigation blocked', toLocation.pathname);
|
|
84
81
|
return result;
|
|
85
82
|
}
|
|
86
83
|
return next();
|
|
@@ -88,7 +85,9 @@ export function runNavigationBlockers(navigationBlockers, toLocation) {
|
|
|
88
85
|
|
|
89
86
|
/* istanbul ignore next: not testable with Karma */
|
|
90
87
|
function terminationBlocker(session) {
|
|
91
|
-
|
|
88
|
+
// `navigationBlockers` are stored in `session`.
|
|
89
|
+
const container = session;
|
|
90
|
+
const result = runNavigationBlockers(getNavigationBlockers(container), null, session.environment);
|
|
92
91
|
|
|
93
92
|
// If no blocker returned anything, so don't prevent the navigation.
|
|
94
93
|
if (!result) {
|
|
@@ -107,6 +106,9 @@ function terminationBlocker(session) {
|
|
|
107
106
|
return true;
|
|
108
107
|
}
|
|
109
108
|
export function addNavigationBlocker(session, blocker) {
|
|
109
|
+
// `navigationBlockers` are stored in `session`.
|
|
110
|
+
const container = session;
|
|
111
|
+
|
|
110
112
|
// All navigation blockers also run on `beforeTermination` event.
|
|
111
113
|
// If required, this could be a parameter of this function.
|
|
112
114
|
// The rationale could be that adding a `beforeunload` listener
|
|
@@ -123,11 +125,11 @@ export function addNavigationBlocker(session, blocker) {
|
|
|
123
125
|
// and this is bad for performance."
|
|
124
126
|
//
|
|
125
127
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
|
126
|
-
if (beforeTermination && !getNavigationBlockers(
|
|
128
|
+
if (beforeTermination && !getNavigationBlockers(container).some(navigationBlocker => navigationBlocker.beforeTermination)) {
|
|
127
129
|
if (session._removeTerminationBlocker) {
|
|
128
130
|
throw new Error('Unexpected `_removeTerminationBlocker` property found in the `session`');
|
|
129
131
|
}
|
|
130
|
-
session._removeTerminationBlocker = session.lifecycle.addTerminationBlocker(() => {
|
|
132
|
+
session._removeTerminationBlocker = session.environment.lifecycle.addTerminationBlocker(() => {
|
|
131
133
|
return terminationBlocker(session);
|
|
132
134
|
});
|
|
133
135
|
}
|
|
@@ -135,12 +137,12 @@ export function addNavigationBlocker(session, blocker) {
|
|
|
135
137
|
blocker,
|
|
136
138
|
beforeTermination
|
|
137
139
|
};
|
|
138
|
-
addNavigationBlockerToTheList(newNavigationBlocker,
|
|
140
|
+
addNavigationBlockerToTheList(newNavigationBlocker, container);
|
|
139
141
|
return () => {
|
|
140
|
-
removeNavigationBlockerFromTheList(newNavigationBlocker,
|
|
142
|
+
removeNavigationBlockerFromTheList(newNavigationBlocker, container);
|
|
141
143
|
|
|
142
144
|
// If it was the last "beforeTermination" blocker, remove navigation blocker.
|
|
143
|
-
if (beforeTermination && !getNavigationBlockers(
|
|
145
|
+
if (beforeTermination && !getNavigationBlockers(container).some(navigationBlocker => navigationBlocker.beforeTermination)) {
|
|
144
146
|
if (!session._removeTerminationBlocker) {
|
|
145
147
|
throw new Error('`_removeTerminationBlocker` property not found in the `session`');
|
|
146
148
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import getLocationBaseFromLocation from './getLocationBaseFromLocation';
|
|
2
|
+
import getLocationFromInternalLocation from './getLocationFromInternalLocation';
|
|
3
|
+
import isPromise from './isPromise';
|
|
4
|
+
import { getNavigationBlockers, runNavigationBlockers } from './navigationBlockers';
|
|
5
|
+
|
|
6
|
+
// Creates "navigation blockers evaluation" status object.
|
|
7
|
+
// It tracks the "cancelled" status of the evaluation:
|
|
8
|
+
// when next navigation happens, the previous one is no longer relevant
|
|
9
|
+
// so the evaluation of navigation blockers for it can be cancelled.
|
|
10
|
+
function createNavigationBlockersEvaluationStatus(container) {
|
|
11
|
+
/* eslint-disable no-underscore-dangle */
|
|
12
|
+
if (container._navigationBlockersEvaluationStatus) {
|
|
13
|
+
container._navigationBlockersEvaluationStatus.cancelled = true;
|
|
14
|
+
}
|
|
15
|
+
container._navigationBlockersEvaluationStatus = {
|
|
16
|
+
cancelled: false
|
|
17
|
+
};
|
|
18
|
+
return container._navigationBlockersEvaluationStatus;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Prevents or allows navigation that was initiated by the application code
|
|
22
|
+
// by making a `.push()` or `.replace()` method call.
|
|
23
|
+
//
|
|
24
|
+
// It doesn't handle `.shift()` method calls because it doesn't yet know
|
|
25
|
+
// the `location` that it's gonna `shift` to. Instead, it waits for the web browser
|
|
26
|
+
// to "shift" to that `location` and then reads the `location` from the address bar
|
|
27
|
+
// and, if such "shift" should've been blocked, it "rewinds" the address bar back
|
|
28
|
+
// to the previous location. This part is handled by another function.
|
|
29
|
+
//
|
|
30
|
+
// Such type of "shifting" and then rewinding the "shift" doesn't really matter to the application code at all.
|
|
31
|
+
// From the application code's point of view, all web browser's address bar doesn't matter and even doesn't exist.
|
|
32
|
+
// All that exists from the application code's point of view is the `location` object in the `NavigationStack`'s state.
|
|
33
|
+
// Until the `location` object in the `NavigationStack`'s state is updated, the "old" page is still rendered.
|
|
34
|
+
// The appliation is only concerned with the updates of the `location` object in the `NavigationStack`'s state
|
|
35
|
+
// and completely ignores any updates to the URL in the web browser's address bar.
|
|
36
|
+
//
|
|
37
|
+
export function blockProgrammaticNavigationIfRequired(toLocationBase,
|
|
38
|
+
// `location` of type `LocationBase`
|
|
39
|
+
session) {
|
|
40
|
+
// `resultValue` variable name works around a stupid javascript error:
|
|
41
|
+
// "Cannot redeclare block-scoped variable 'result'".
|
|
42
|
+
const result = runNavigationBlockers(getNavigationBlockers(session),
|
|
43
|
+
// Here `payload.location` is `LocationBase`.
|
|
44
|
+
toLocationBase, session.environment);
|
|
45
|
+
if (isPromise(result)) {
|
|
46
|
+
const evaluationStatus = createNavigationBlockersEvaluationStatus(session);
|
|
47
|
+
// eslint-disable-next-line consistent-return
|
|
48
|
+
return result.then(promiseResult => {
|
|
49
|
+
if (evaluationStatus.cancelled) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return promiseResult;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Runs navigation blockers on internal location update and "undoes" the location update
|
|
59
|
+
// if it should've been blocked.
|
|
60
|
+
//
|
|
61
|
+
// One could ask: Why the hassle of running navigation blockers on internal location update
|
|
62
|
+
// and then rewinding back if the location change should've been blocked?
|
|
63
|
+
// Why not just run navigation blockers on `.push()`/`.replace()`/`.shift()`?
|
|
64
|
+
//
|
|
65
|
+
// The reason why it runs on internal location update here is because
|
|
66
|
+
// aside from programmatic `.shift()` that can be initiated from the application code,
|
|
67
|
+
// there's non-programmatic "shift" navigation when the user manually clicks "Back" or "Forward" button
|
|
68
|
+
// in a web browser. And even if a "shift" navigation is initiated programmatically in the application code,
|
|
69
|
+
// it still doesn't know yet what the new location is gonna be cause it only knows the numeric `delta`.
|
|
70
|
+
// Such cases could only be handled by reacting to internal location updates which,
|
|
71
|
+
// in case of "Back"/"Forward", only happen after the URL in the browser's address bar has changed.
|
|
72
|
+
//
|
|
73
|
+
// There's no real drawback in reacting to an internal location update "post factum" because
|
|
74
|
+
// from the application code's point of view, web browser's address bar doesn't matter and even doesn't exist.
|
|
75
|
+
// All that exists from the application code's point of view is the `navigationStack.current` location
|
|
76
|
+
// returned from the `NavigationStack`. Until that location is updated, the "old" page is still rendered.
|
|
77
|
+
// The appliation is only concerned with the updates of the internal `location` object in the `NavigationStack``
|
|
78
|
+
// and completely ignores any updates to the URL in the web browser's address bar.
|
|
79
|
+
//
|
|
80
|
+
// So here, the code attempts to prevent or allow navigation that has already happened
|
|
81
|
+
// in the web browser's address bar but hasn't yet happened in the `NavigationStack`'s state.
|
|
82
|
+
// For example, it could be a user clicking a "Back"/"Forward" button in a web browser.
|
|
83
|
+
// If such navigation should've been blocked, it will simply not update the `location` object
|
|
84
|
+
// in the `NavigationStack`'s state, and it will also "rewind" the change of the URL in the web browser's
|
|
85
|
+
// address bar so that it's consistent with the `location` in the `NavigationStack`'s state.
|
|
86
|
+
//
|
|
87
|
+
// Returns either a `boolean` value or a `Promise` that resolves to a `boolean` value:
|
|
88
|
+
// * `false` when navigation should not have been blocked and therefore was not "rewinded".
|
|
89
|
+
// * `true` when navigation should have been blocked and therefore was "rewinded".
|
|
90
|
+
// * `true` when it "rewinded" the navigation "just in case" and then started evaluating async blockers,
|
|
91
|
+
// but while doing that, next navigation already happened so this one is no longer relevant.
|
|
92
|
+
//
|
|
93
|
+
export function blockNonProgrammaticNavigationIfRequired(toLocationInternal,
|
|
94
|
+
// `location` of type `LocationInternal`
|
|
95
|
+
session, doAndIgnoreLocationUpdates) {
|
|
96
|
+
// If there're no navigation blockers to run, don't do anything.
|
|
97
|
+
if (getNavigationBlockers(session).length === 0) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If it was the initial page load or a redirect,
|
|
102
|
+
// it's not really a navigation that could be rolled back.
|
|
103
|
+
if (toLocationInternal.delta === 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const result = runNavigationBlockers(getNavigationBlockers(session), getLocationBaseFromLocation(getLocationFromInternalLocation(toLocationInternal)), session.environment);
|
|
107
|
+
|
|
108
|
+
// If some navigation blocker returned a `Promise`.
|
|
109
|
+
if (isPromise(result)) {
|
|
110
|
+
const evaluationStatus = createNavigationBlockersEvaluationStatus(session);
|
|
111
|
+
|
|
112
|
+
// While location blockers are running, rewind to the previous location.
|
|
113
|
+
doAndIgnoreLocationUpdates(() => {
|
|
114
|
+
session.shift(-toLocationInternal.delta);
|
|
115
|
+
});
|
|
116
|
+
return result.then(promiseResult => {
|
|
117
|
+
if (evaluationStatus.cancelled) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (promiseResult) {
|
|
121
|
+
// Navigation blocked.
|
|
122
|
+
// Already rewound to a previous location.
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
// Navigation not blocked.
|
|
126
|
+
// Rewind back to the new location.
|
|
127
|
+
doAndIgnoreLocationUpdates(() => {
|
|
128
|
+
session.shift(toLocationInternal.delta);
|
|
129
|
+
});
|
|
130
|
+
// Update the location.
|
|
131
|
+
return false;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Navigation blockers did not return a `Promise`.
|
|
136
|
+
if (result) {
|
|
137
|
+
// Prevent the navigation: rewind to the previous location.
|
|
138
|
+
doAndIgnoreLocationUpdates(() => {
|
|
139
|
+
session.shift(-toLocationInternal.delta);
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
// Update the location.
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import createSearchFromQuery from './createSearchFromQuery';
|
|
2
1
|
import parseLocationUrl from './parseLocationUrl';
|
|
3
2
|
import parseQueryFromSearch from './parseQueryFromSearch';
|
|
3
|
+
import stringifyQueryAsSearch from './stringifyQueryAsSearch';
|
|
4
|
+
function stringifyQueryParameterValue(value) {
|
|
5
|
+
if (value === null || value === undefined) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
4
10
|
|
|
5
11
|
// * If `location` is a string, it parses it into a `LocationBase`.
|
|
6
12
|
// * If `location` is an object, it ensures that `search` and `hash` properties aren't `undefined`,
|
|
@@ -16,7 +22,7 @@ export default function parseInputLocation(location) {
|
|
|
16
22
|
if (typeof location.query[key] !== 'string') {
|
|
17
23
|
location = Object.assign({}, location, {
|
|
18
24
|
query: Object.assign({}, location.query, {
|
|
19
|
-
[key]:
|
|
25
|
+
[key]: stringifyQueryParameterValue(location.query[key])
|
|
20
26
|
})
|
|
21
27
|
});
|
|
22
28
|
}
|
|
@@ -34,7 +40,7 @@ export default function parseInputLocation(location) {
|
|
|
34
40
|
// if `query` is present but `search` is not.
|
|
35
41
|
if (location.query && !location.search) {
|
|
36
42
|
location = Object.assign({}, location, {
|
|
37
|
-
search:
|
|
43
|
+
search: stringifyQueryAsSearch(location.query)
|
|
38
44
|
});
|
|
39
45
|
}
|
|
40
46
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import parseQueryString from './parseQueryString';
|
|
2
2
|
export default function parseQueryFromSearch(search) {
|
|
3
3
|
if (search.length > '?'.length) {
|
|
4
|
-
|
|
5
|
-
return parseQuery(search.slice(1));
|
|
6
|
-
} catch (error) {
|
|
7
|
-
// Ignore any query parsing errors.
|
|
8
|
-
}
|
|
4
|
+
return parseQueryString(search.slice('?'.length));
|
|
9
5
|
}
|
|
10
6
|
return {};
|
|
11
7
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function splitAtFirstOccurence(string, separator) {
|
|
2
|
+
const separatorIndex = string.indexOf(separator);
|
|
3
|
+
if (separatorIndex === -1) {
|
|
4
|
+
return [string, ''];
|
|
5
|
+
}
|
|
6
|
+
return [string.slice(0, separatorIndex), string.slice(separatorIndex + separator.length)];
|
|
7
|
+
}
|
|
8
|
+
function decode(value) {
|
|
9
|
+
// There's a convention that a space character could be encoded
|
|
10
|
+
// either as "%20" or as "+". Both of them are valid.
|
|
11
|
+
// The "+" character is unusally preferred because it results in a more
|
|
12
|
+
// human-readable URL.
|
|
13
|
+
//
|
|
14
|
+
// https://dev.to/lico/understanding-how-spaces-are-encoded-20-with-encodeuri-vs-with-url-2d6c
|
|
15
|
+
// https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding
|
|
16
|
+
//
|
|
17
|
+
// Those "+" characters don't get transformed to spaces by `decodeURIComponent()` function.
|
|
18
|
+
// This means that they should be transformed to spaces manually.
|
|
19
|
+
//
|
|
20
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
|
|
21
|
+
//
|
|
22
|
+
value = value.replaceAll('+', ' ');
|
|
23
|
+
|
|
24
|
+
// `decodeURIComponent()` could throw an error of class `URIError`.
|
|
25
|
+
//
|
|
26
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
|
|
27
|
+
//
|
|
28
|
+
// Example: "URIError: malformed URI sequence".
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(value);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.error(error);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export default function parseQueryString(queryString) {
|
|
38
|
+
// Create an object with no prototype
|
|
39
|
+
const query = Object.create(null);
|
|
40
|
+
|
|
41
|
+
// query parameter parsing is described in the specification:
|
|
42
|
+
// https://url.spec.whatwg.org/#urlencoded-parsing
|
|
43
|
+
for (const keyValuePair of queryString.split('&')) {
|
|
44
|
+
if (!keyValuePair) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let [key, value] = splitAtFirstOccurence(keyValuePair, '=');
|
|
48
|
+
|
|
49
|
+
// If `key` is empty, the specification considers this a valid case with `key: null`.
|
|
50
|
+
// But, there seems to be no practical use for a query parameter with `key: null`.
|
|
51
|
+
// So just skip it.
|
|
52
|
+
if (!key) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
key = decode(key);
|
|
56
|
+
|
|
57
|
+
// According to the specification, missing `=` should be treated as `value: null`.
|
|
58
|
+
if (value === '') {
|
|
59
|
+
value = null;
|
|
60
|
+
} else {
|
|
61
|
+
value = decode(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The handling of duplicate URL query parameters is not explicitly defined by a single,
|
|
65
|
+
// universally enforced specification. Hence, we just assume such query parameters invalid
|
|
66
|
+
// and only include the first occurrence of the query parameter in the query string.
|
|
67
|
+
if (query[key] === undefined) {
|
|
68
|
+
query[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return query;
|
|
72
|
+
}
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
|
|
4
4
|
import scheduleNextTick from './scheduleNextTick';
|
|
5
|
-
import debug from '../debug';
|
|
6
5
|
export default class ScrollPositionAutoSaver {
|
|
7
6
|
constructor({
|
|
7
|
+
log,
|
|
8
8
|
scrollPosition,
|
|
9
9
|
scrollPositionSaver,
|
|
10
10
|
getScrollableContainers,
|
|
11
11
|
shouldSaveScrollPosition
|
|
12
12
|
}) {
|
|
13
|
+
this._log = log;
|
|
13
14
|
this._scrollPosition = scrollPosition;
|
|
14
15
|
this._scrollPositionSaver = scrollPositionSaver;
|
|
15
16
|
this._shouldSaveScrollPosition = shouldSaveScrollPosition;
|
|
@@ -61,7 +62,7 @@ export default class ScrollPositionAutoSaver {
|
|
|
61
62
|
cancelSavePageScrollPosition(hasRun) {
|
|
62
63
|
if (this._cancelSavePageScrollPosition) {
|
|
63
64
|
if (!hasRun) {
|
|
64
|
-
debug('cancel delayed save scroll position', PAGE_SCROLLABLE_CONTAINER_KEY);
|
|
65
|
+
this._log.debug('cancel delayed save scroll position', PAGE_SCROLLABLE_CONTAINER_KEY);
|
|
65
66
|
}
|
|
66
67
|
this._cancelSavePageScrollPosition();
|
|
67
68
|
this._cancelSavePageScrollPosition = null;
|
|
@@ -71,7 +72,7 @@ export default class ScrollPositionAutoSaver {
|
|
|
71
72
|
const scrollableContainerEntry = this._getScrollableContainers()[scrollableContainerKey];
|
|
72
73
|
if (scrollableContainerEntry.cancelSaveScrollPosition) {
|
|
73
74
|
if (!hasRun) {
|
|
74
|
-
debug('cancel delayed save scroll position', scrollableContainerKey);
|
|
75
|
+
this._log.debug('cancel delayed save scroll position', scrollableContainerKey);
|
|
75
76
|
}
|
|
76
77
|
scrollableContainerEntry.cancelSaveScrollPosition();
|
|
77
78
|
scrollableContainerEntry.cancelSaveScrollPosition = null;
|
|
@@ -103,9 +104,9 @@ export default class ScrollPositionAutoSaver {
|
|
|
103
104
|
// because there might be too many in a given short period of time
|
|
104
105
|
// which could affect the performance of the application.
|
|
105
106
|
if (!scrollableContainerEntry.cancelSaveScrollPosition) {
|
|
106
|
-
debug('scroll detected', scrollableContainerKey);
|
|
107
|
+
this._log.debug('scroll detected', scrollableContainerKey);
|
|
107
108
|
scrollableContainerEntry.cancelSaveScrollPosition = scheduleNextTick(() => {
|
|
108
|
-
debug('auto-save scroll position after scroll', scrollableContainerKey);
|
|
109
|
+
this._log.debug('auto-save scroll position after scroll', scrollableContainerKey);
|
|
109
110
|
this._scrollPositionSaver.saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainerEntry.scrollableContainer);
|
|
110
111
|
});
|
|
111
112
|
}
|
|
@@ -114,7 +115,7 @@ export default class ScrollPositionAutoSaver {
|
|
|
114
115
|
addPageScrollListener() {
|
|
115
116
|
// Set up scroll listener on the page.
|
|
116
117
|
this._removePageScrollListener = this._scrollPosition.addPageScrollListener(() => {
|
|
117
|
-
debug('scroll detected', PAGE_SCROLLABLE_CONTAINER_KEY);
|
|
118
|
+
this._log.debug('scroll detected', PAGE_SCROLLABLE_CONTAINER_KEY);
|
|
118
119
|
|
|
119
120
|
// This flag is not used in real life and is only used in tests (for some reason).
|
|
120
121
|
if (!this._shouldSaveScrollPosition()) {
|
|
@@ -5,7 +5,6 @@ import ScrollPositionSaver from './ScrollPositionSaver';
|
|
|
5
5
|
import ScrollPositionSetter from './ScrollPositionSetter';
|
|
6
6
|
import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
|
|
7
7
|
import LocationDataStorage from '../data-storage/LocationDataStorage';
|
|
8
|
-
import debug from '../debug';
|
|
9
8
|
function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
|
|
10
9
|
let i = 0;
|
|
11
10
|
while (i < scrollPosition1.length) {
|
|
@@ -17,7 +16,7 @@ function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
|
|
|
17
16
|
return true;
|
|
18
17
|
}
|
|
19
18
|
export default class ScrollPositionRestoration {
|
|
20
|
-
constructor(session,
|
|
19
|
+
constructor(session, options) {
|
|
21
20
|
// Once configured, scroll restoration mode persists across page reloads.
|
|
22
21
|
// I.e. even if a user refreshes the page in a web browser, the custom
|
|
23
22
|
// `window.history.scrollRestoration` value will still remain.
|
|
@@ -49,10 +48,10 @@ export default class ScrollPositionRestoration {
|
|
|
49
48
|
running
|
|
50
49
|
}) => {
|
|
51
50
|
if (running) {
|
|
52
|
-
debug('▶ running');
|
|
51
|
+
this._log.debug('▶ running');
|
|
53
52
|
this._disableAutomaticScrollRestoration();
|
|
54
53
|
} else {
|
|
55
|
-
debug('⏹ not running');
|
|
54
|
+
this._log.debug('⏹ not running');
|
|
56
55
|
this._enableAutomaticScrollRestoration();
|
|
57
56
|
|
|
58
57
|
// There might be previous scroll position already saved in the data storage.
|
|
@@ -80,27 +79,30 @@ export default class ScrollPositionRestoration {
|
|
|
80
79
|
try {
|
|
81
80
|
this._scrollPosition.disableAutomaticScrollRestoration();
|
|
82
81
|
} catch (error) {
|
|
83
|
-
|
|
84
|
-
console.error('[navigation-stack] could not disable default scroll restoration mode');
|
|
82
|
+
this._log.error('[navigation-stack] could not disable default scroll restoration mode');
|
|
85
83
|
}
|
|
86
84
|
};
|
|
87
85
|
this._enableAutomaticScrollRestoration = () => {
|
|
88
86
|
try {
|
|
89
87
|
this._scrollPosition.enableAutomaticScrollRestoration();
|
|
90
88
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
console.error('[navigation-stack] could not enable default scroll restoration mode');
|
|
89
|
+
this._log.error('[navigation-stack] could not enable default scroll restoration mode');
|
|
93
90
|
}
|
|
94
91
|
};
|
|
95
92
|
this._saveScrollPositionForLocation = (location, scrollableContainerKey, scrollPosition) => {
|
|
96
93
|
this._locationDataStorage.set(location, scrollableContainerKey || PAGE_SCROLLABLE_CONTAINER_KEY, scrollPosition);
|
|
97
94
|
};
|
|
95
|
+
this._log = session.environment.log;
|
|
98
96
|
this._scrollPosition = session.environment.scrollPosition;
|
|
99
|
-
|
|
97
|
+
|
|
98
|
+
// Custom `ScrollPositionSetter`.
|
|
99
|
+
this._scrollPositionSetter = options.scrollPositionSetter;
|
|
100
|
+
this._sessionLifecycle = session.environment.lifecycle;
|
|
100
101
|
this._locationDataStorage = new LocationDataStorage(session, {
|
|
101
|
-
namespace: 'navigation-stack
|
|
102
|
+
namespace: 'navigation-stack-scroll-position'
|
|
102
103
|
});
|
|
103
104
|
this._scrollPositionSaver = new ScrollPositionSaver({
|
|
105
|
+
log: this._log,
|
|
104
106
|
scrollPosition: this._scrollPosition,
|
|
105
107
|
saveScrollPositionForLocation: this._saveScrollPositionForLocation,
|
|
106
108
|
getScrollableContainers: () => this._scrollableContainers,
|
|
@@ -122,20 +124,22 @@ export default class ScrollPositionRestoration {
|
|
|
122
124
|
// Using this option, a developer could theoretically provide their own implementation
|
|
123
125
|
// of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
|
|
124
126
|
// This could be part of the public API if anyone provided a sensible real-world use case for it.
|
|
125
|
-
scrollPositionSetter:
|
|
126
|
-
//
|
|
127
|
+
scrollPositionSetter: options && options._pageScrollPositionSetter ||
|
|
128
|
+
// eslint-disable-next-line new-cap
|
|
129
|
+
this._scrollPositionSetter && new this._scrollPositionSetter() ||
|
|
130
|
+
// A default `ScrollPositionSetter` for a page (sets page scroll position twice with a momentary delay).
|
|
127
131
|
new PageScrollPositionSetter(),
|
|
128
132
|
// This function is only used in tests.
|
|
129
133
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
130
134
|
// It's only used in tests.
|
|
131
|
-
_getSavedScrollPositionOnLocationChange:
|
|
135
|
+
_getSavedScrollPositionOnLocationChange: options && options._getSavedPageScrollPositionOnLocationChange,
|
|
132
136
|
// This function is only used in tests.
|
|
133
137
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
134
138
|
// It's only used in tests.
|
|
135
|
-
|
|
139
|
+
shouldChangeScrollPositionOnLocationChange: options && options.shouldChangePageScrollPositionOnLocationChange
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
|
-
addScrollableContainer(scrollableContainerKey, scrollableContainer,
|
|
142
|
+
addScrollableContainer(scrollableContainerKey, scrollableContainer, options) {
|
|
139
143
|
// Originally, `scrollableContainerKey` was auto-generated,
|
|
140
144
|
// but then it didn't work with the concept of dynamically adding or removing
|
|
141
145
|
// scrollable containers after `ScrollPositionRestoration` has already started.
|
|
@@ -152,7 +156,7 @@ export default class ScrollPositionRestoration {
|
|
|
152
156
|
if (this._scrollableContainers[scrollableContainerKey]) {
|
|
153
157
|
throw new Error(`Scrollable container key "${scrollableContainerKey}" is already added`);
|
|
154
158
|
}
|
|
155
|
-
debug('add scrollable container', scrollableContainerKey);
|
|
159
|
+
this._log.debug('add scrollable container', scrollableContainerKey);
|
|
156
160
|
|
|
157
161
|
// Add scrollable container entry.
|
|
158
162
|
this._scrollableContainers[scrollableContainerKey] = {
|
|
@@ -161,17 +165,17 @@ export default class ScrollPositionRestoration {
|
|
|
161
165
|
// Using this option, a developer could theoretically provide their own implementation
|
|
162
166
|
// of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
|
|
163
167
|
// This could be part of the public API if anyone provided a sensible real-world use case for it.
|
|
164
|
-
scrollPositionSetter:
|
|
168
|
+
scrollPositionSetter: options && options._scrollPositionSetter || this._scrollPositionSetter && new this._scrollPositionSetter() ||
|
|
165
169
|
// The default basic "immediate" scroll position setter.
|
|
166
170
|
new ScrollPositionSetter(),
|
|
167
171
|
// This function is only used in tests.
|
|
168
172
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
169
173
|
// It's only used in tests.
|
|
170
|
-
|
|
174
|
+
shouldChangeScrollPositionOnLocationChange: options && options.shouldChangeScrollPositionOnLocationChange,
|
|
171
175
|
// This function is only used in tests.
|
|
172
176
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
173
177
|
// It's only used in tests.
|
|
174
|
-
_getSavedScrollPositionOnLocationChange:
|
|
178
|
+
_getSavedScrollPositionOnLocationChange: options && options._getSavedScrollPositionOnLocationChange
|
|
175
179
|
};
|
|
176
180
|
|
|
177
181
|
// Scrollable containers could be added at any time, including page mount.
|
|
@@ -183,10 +187,10 @@ export default class ScrollPositionRestoration {
|
|
|
183
187
|
if (this._location) {
|
|
184
188
|
const previouslySavedScrollPosition = this._getSavedScrollPositionForLocation(this._location, scrollableContainerKey);
|
|
185
189
|
if (previouslySavedScrollPosition) {
|
|
186
|
-
debug('restore scroll position on add scrollable container', this._location.pathname, scrollableContainerKey, previouslySavedScrollPosition);
|
|
190
|
+
this._log.debug('restore scroll position on add scrollable container', this._location.pathname, scrollableContainerKey, previouslySavedScrollPosition);
|
|
187
191
|
this._scrollPosition.setScrollableContainerScrollPosition(scrollableContainer, previouslySavedScrollPosition);
|
|
188
192
|
} else {
|
|
189
|
-
debug('save scroll position on add scrollable container', this._location.pathname, scrollableContainerKey);
|
|
193
|
+
this._log.debug('save scroll position on add scrollable container', this._location.pathname, scrollableContainerKey);
|
|
190
194
|
this._scrollPositionSaver.saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer);
|
|
191
195
|
}
|
|
192
196
|
}
|
|
@@ -196,7 +200,7 @@ export default class ScrollPositionRestoration {
|
|
|
196
200
|
|
|
197
201
|
// Removes the scrollable container.
|
|
198
202
|
return () => {
|
|
199
|
-
debug('remove scrollable container', scrollableContainerKey);
|
|
203
|
+
this._log.debug('remove scrollable container', scrollableContainerKey);
|
|
200
204
|
this._scrollPositionSaver._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(scrollableContainerKey);
|
|
201
205
|
this._scrollPositionSaver._scrollPositionAutoSaver.removeScrollableContainerScrollListener(scrollableContainerKey);
|
|
202
206
|
delete this._scrollableContainers[scrollableContainerKey];
|
|
@@ -268,7 +272,7 @@ export default class ScrollPositionRestoration {
|
|
|
268
272
|
if (!location.key) {
|
|
269
273
|
throw new Error('`location` must have a `key`');
|
|
270
274
|
}
|
|
271
|
-
debug('rendered location', location.pathname);
|
|
275
|
+
this._log.debug('rendered location', location.pathname);
|
|
272
276
|
this._prevLocation = this._location;
|
|
273
277
|
this._location = location;
|
|
274
278
|
this._scrollPosition.init();
|
|
@@ -329,8 +333,8 @@ export default class ScrollPositionRestoration {
|
|
|
329
333
|
// This function is only used in tests.
|
|
330
334
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
331
335
|
// It's only used in tests.
|
|
332
|
-
if (scrollableContainerEntry.
|
|
333
|
-
if (!scrollableContainerEntry.
|
|
336
|
+
if (scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange) {
|
|
337
|
+
if (!scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange(this._prevLocation, this._location)) {
|
|
334
338
|
return Promise.resolve();
|
|
335
339
|
}
|
|
336
340
|
}
|
|
@@ -342,14 +346,14 @@ export default class ScrollPositionRestoration {
|
|
|
342
346
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
343
347
|
// It's only used in tests.
|
|
344
348
|
if (scrollableContainerEntry._getSavedScrollPositionOnLocationChange) {
|
|
345
|
-
scrollPositionOrAnchorToSet = scrollableContainerEntry._getSavedScrollPositionOnLocationChange(this.
|
|
349
|
+
scrollPositionOrAnchorToSet = scrollableContainerEntry._getSavedScrollPositionOnLocationChange(this._prevLocation, this._location);
|
|
346
350
|
}
|
|
347
351
|
|
|
348
352
|
// Get scroll position (or anchor) to set.
|
|
349
353
|
if (!scrollPositionOrAnchorToSet) {
|
|
350
354
|
scrollPositionOrAnchorToSet = scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY ? this._getPageScrollPositionOrAnchorToSet(this._location) : this._getScrollableContainerScrollPositionToSet(this._location, scrollableContainerKey);
|
|
351
355
|
}
|
|
352
|
-
debug('restore scroll position', this._location.pathname, scrollableContainerKey, scrollPositionOrAnchorToSet);
|
|
356
|
+
this._log.debug('restore scroll position', this._location.pathname, scrollableContainerKey, scrollPositionOrAnchorToSet);
|
|
353
357
|
|
|
354
358
|
// Set scroll position of scrollable container.
|
|
355
359
|
return scrollableContainerEntry.scrollPositionSetter.set(scrollableContainerEntry.scrollableContainer, scrollPositionOrAnchorToSet, this._scrollPosition);
|