navigation-stack 0.5.3 → 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- 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
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import ScrollPositionAutoSaver from './ScrollPositionAutoSaver';
|
|
4
4
|
import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
|
|
5
|
-
import debug from '../debug';
|
|
6
5
|
export default class ScrollPositionSaver {
|
|
7
6
|
constructor({
|
|
7
|
+
log,
|
|
8
8
|
scrollPosition,
|
|
9
9
|
getLocation,
|
|
10
10
|
saveScrollPositionForLocation,
|
|
11
11
|
getScrollableContainers,
|
|
12
12
|
shouldSaveScrollPosition
|
|
13
13
|
}) {
|
|
14
|
+
this._log = log;
|
|
14
15
|
this._scrollPosition = scrollPosition;
|
|
15
16
|
this._getLocation = getLocation;
|
|
16
17
|
this._saveScrollPositionForLocation = saveScrollPositionForLocation;
|
|
17
18
|
this._getScrollableContainers = getScrollableContainers;
|
|
18
19
|
this._shouldSaveScrollPosition = shouldSaveScrollPosition;
|
|
19
20
|
this._scrollPositionAutoSaver = new ScrollPositionAutoSaver({
|
|
21
|
+
log: this._log,
|
|
20
22
|
scrollPosition: this._scrollPosition,
|
|
21
23
|
scrollPositionSaver: this,
|
|
22
24
|
getScrollableContainers,
|
|
@@ -37,7 +39,7 @@ export default class ScrollPositionSaver {
|
|
|
37
39
|
if (!this._shouldSaveScrollPosition()) {
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
|
-
debug('save scroll position', this._getLocation().pathname);
|
|
42
|
+
this._log.debug('save scroll position', this._getLocation().pathname);
|
|
41
43
|
|
|
42
44
|
// Get scrollable containers.
|
|
43
45
|
const scrollableContainers = this._getScrollableContainers();
|
|
@@ -52,7 +54,7 @@ export default class ScrollPositionSaver {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
savePageScrollPosition() {
|
|
55
|
-
debug('save scroll position', this._getLocation().pathname, PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
|
|
57
|
+
this._log.debug('save scroll position', this._getLocation().pathname, PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
|
|
56
58
|
|
|
57
59
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
58
60
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
@@ -65,7 +67,7 @@ export default class ScrollPositionSaver {
|
|
|
65
67
|
this._saveScrollPositionForLocation(this._getLocation(), undefined, this._scrollPosition.getPageScrollPosition());
|
|
66
68
|
}
|
|
67
69
|
saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer) {
|
|
68
|
-
debug('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
|
|
70
|
+
this._log.debug('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
|
|
69
71
|
|
|
70
72
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
71
73
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import debug from '../debug';
|
|
2
1
|
import parseInputLocation from '../parseInputLocation';
|
|
3
2
|
import createSessionKey from './key/createSessionKey';
|
|
4
|
-
import NavigationOutOfBoundsError from './navigation/error/NavigationOutOfBoundsError';
|
|
5
|
-
import NavigationOperations from './navigation/operation/operations';
|
|
6
3
|
import Subscription from './subscription/Subscription';
|
|
4
|
+
import NavigationOutOfBoundsError from '../environment/navigation/error/NavigationOutOfBoundsError';
|
|
5
|
+
import NavigationOperations from '../environment/navigation/operation/operations';
|
|
7
6
|
const INITIAL_KEY_INDEX = -1;
|
|
8
7
|
const INITIAL_INDEX = -1;
|
|
9
8
|
const INIT_LOCATION_DELTA = 0;
|
|
10
9
|
export default class Session {
|
|
11
|
-
constructor({
|
|
12
|
-
navigation
|
|
13
|
-
}) {
|
|
10
|
+
constructor(EnvironmentClass) {
|
|
14
11
|
// This function is used by navigation.
|
|
15
12
|
this._getCurrentLocationIndex = () => {
|
|
16
13
|
return this._currentLocationIndex;
|
|
@@ -20,6 +17,9 @@ export default class Session {
|
|
|
20
17
|
// under the hood, and `window.sessionStorage` is shared between different sessions.
|
|
21
18
|
this.key = createSessionKey();
|
|
22
19
|
|
|
20
|
+
// Create an environment instance.
|
|
21
|
+
this.environment = new EnvironmentClass();
|
|
22
|
+
|
|
23
23
|
// `this._locationKeyIndex` is incremented every time the current location changes.
|
|
24
24
|
this._locationKeyIndex = INITIAL_KEY_INDEX;
|
|
25
25
|
|
|
@@ -31,18 +31,34 @@ export default class Session {
|
|
|
31
31
|
// In other words, this is the last location index that it can `.shift()` to.
|
|
32
32
|
this._terminalLocationIndex = this._currentLocationIndex;
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
this.
|
|
34
|
+
// Allows subscribing to location updates.
|
|
35
|
+
this._subscription = new Subscription();
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
// Subscribing to location changes means subscribing to both "synchronous"
|
|
38
|
+
// and "asynchronous" location changes. "synchronous" location changes
|
|
39
|
+
// happen immediately when the code triggers them."asynchronous" location changes
|
|
40
|
+
// either happen after an arbitrary delay or are even triggered from outside the code.
|
|
41
|
+
//
|
|
42
|
+
// Subscribing to "asynchronous" location changes is not necessary when
|
|
43
|
+
// there're no actual subscribers, in order to not unnecessarily "waste" any resources.
|
|
44
|
+
// Of course, this statement is rather far-fetched and in reality no one would ever tell any difference.
|
|
45
|
+
// Still, I felt like randomly introducing this seemingly unnecessary minor optimization.
|
|
46
|
+
//
|
|
47
|
+
// So it only subscribes to "asynchronous" location changes if there's at least one active subscriber.
|
|
48
|
+
// And in case all subscribers get unsubscribed, it will unsubscribe from "asynchronous" location changes too.
|
|
49
|
+
// One might think of it as some form of "mental masturbation", but what can I do — I already wrote the code.
|
|
50
|
+
//
|
|
51
|
+
this._subscription.onFirstSubscriber(() => {
|
|
52
|
+
return this.environment.navigation.subscribeToAsyncrhonousLocationUpdates(location => {
|
|
53
|
+
// Notify all subscribers about this "asynchronous" location change.
|
|
54
|
+
this._subscription.notifySubscribers(location);
|
|
55
|
+
});
|
|
42
56
|
});
|
|
43
57
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
58
|
+
// This subscription is triggered in two cases:
|
|
59
|
+
// * Set initial current location index at initial page load.
|
|
60
|
+
// * Update current location index whenever a location change is not initiated
|
|
61
|
+
// by this session but rather by the user clicking "Back" or "Forward" button.
|
|
46
62
|
this._unsubscribe = this.subscribe(location => {
|
|
47
63
|
// Update `this._currentLocationIndex` when the location change was not initiated
|
|
48
64
|
// by this session but rather by the user clicking "Back" or "Forward" button.
|
|
@@ -52,16 +68,20 @@ export default class Session {
|
|
|
52
68
|
// but if it was possible, this call would be required. It would also be required
|
|
53
69
|
// by `navigation` to call `session.getNextKey()` function to increment `locationKeyIndex`.
|
|
54
70
|
this._updateTerminalLocationIndex(location);
|
|
55
|
-
debug('current location', location.pathname, 'index', this._currentLocationIndex);
|
|
71
|
+
this.environment.log.debug('current location', location.pathname, 'index', this._currentLocationIndex);
|
|
56
72
|
});
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
// Subscribes to changes in location.
|
|
76
|
+
// The first subscriber is always the `Session` itself:
|
|
77
|
+
// its listener keeps the current location index up-to-date.
|
|
78
|
+
// Any additional application-specific listeners could be added, if required.
|
|
79
|
+
// Applications should prefer adding any such listeners by calling `NavigationStack.subscribe()`
|
|
80
|
+
// method instead of calling this method directly, in order to "normalize" the `location` argument.
|
|
60
81
|
subscribe(listener) {
|
|
61
82
|
return this._subscription.subscribe(location => {
|
|
62
83
|
if (!this._isStarted() && location.operation !== NavigationOperations.INIT) {
|
|
63
|
-
|
|
64
|
-
console.error('Unexpected location change', location);
|
|
84
|
+
this.environment.log.error('Unexpected location change', location);
|
|
65
85
|
throw new Error('Not started');
|
|
66
86
|
} else {
|
|
67
87
|
// Call the listener.
|
|
@@ -69,6 +89,18 @@ export default class Session {
|
|
|
69
89
|
}
|
|
70
90
|
});
|
|
71
91
|
}
|
|
92
|
+
|
|
93
|
+
// Starts a navigation session.
|
|
94
|
+
//
|
|
95
|
+
// When run in a web browser, it could not only "start" a new session
|
|
96
|
+
// but also "resume" a previously-started session. That could happen
|
|
97
|
+
// when the user refreshes a page in a web browser which still retains
|
|
98
|
+
// the previous session's data but at the same time restarts the javascript code
|
|
99
|
+
// from scratch.
|
|
100
|
+
//
|
|
101
|
+
// So this `start()` method handles both cases: when there's previous session's data
|
|
102
|
+
// that should be restored and when there's no previous session's data.
|
|
103
|
+
//
|
|
72
104
|
start(initialLocation) {
|
|
73
105
|
if (this._stopped) {
|
|
74
106
|
throw new Error('Can not be restarted');
|
|
@@ -81,7 +113,7 @@ export default class Session {
|
|
|
81
113
|
// the initial location by the time javascript code starts execution.
|
|
82
114
|
//
|
|
83
115
|
if (!initialLocation) {
|
|
84
|
-
initialLocation = this.
|
|
116
|
+
initialLocation = this.environment.navigation.getInitialLocation();
|
|
85
117
|
if (initialLocation) {
|
|
86
118
|
initialLocation = parseInputLocation(initialLocation);
|
|
87
119
|
}
|
|
@@ -92,18 +124,19 @@ export default class Session {
|
|
|
92
124
|
if (this._currentLocationIndex !== INITIAL_INDEX) {
|
|
93
125
|
throw new Error('Already started');
|
|
94
126
|
}
|
|
95
|
-
debug('▶ start session', initialLocation.pathname);
|
|
127
|
+
this.environment.log.debug('▶ start session', initialLocation.pathname);
|
|
96
128
|
this._started = true;
|
|
97
129
|
const key = this._getNextLocationKey();
|
|
98
130
|
const index = INITIAL_INDEX + 1;
|
|
99
131
|
const delta = INIT_LOCATION_DELTA;
|
|
100
|
-
const locationResult = this.
|
|
132
|
+
const locationResult = this.environment.navigation.init(initialLocation, {
|
|
101
133
|
operation: NavigationOperations.INIT,
|
|
102
134
|
key,
|
|
103
135
|
index,
|
|
104
136
|
delta
|
|
105
137
|
});
|
|
106
138
|
if (locationResult) {
|
|
139
|
+
// Notify all subscribers about this "synchronous" location change.
|
|
107
140
|
this._subscription.notifySubscribers(locationResult);
|
|
108
141
|
}
|
|
109
142
|
}
|
|
@@ -111,7 +144,7 @@ export default class Session {
|
|
|
111
144
|
if (this._stopped) {
|
|
112
145
|
throw Error('Already stopped');
|
|
113
146
|
}
|
|
114
|
-
debug('⏹ stop session');
|
|
147
|
+
this.environment.log.debug('⏹ stop session');
|
|
115
148
|
|
|
116
149
|
// Once stopped, it won't be able to be restarted.
|
|
117
150
|
this._stopped = true;
|
|
@@ -137,16 +170,17 @@ export default class Session {
|
|
|
137
170
|
});
|
|
138
171
|
const key = this._getNextLocationKey();
|
|
139
172
|
const index = this._currentLocationIndex + delta;
|
|
140
|
-
debug(operation === NavigationOperations.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
|
|
173
|
+
this.environment.log.debug(operation === NavigationOperations.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
|
|
141
174
|
|
|
142
175
|
// Navigate to the location.
|
|
143
|
-
const locationResult = this.
|
|
176
|
+
const locationResult = this.environment.navigation.navigate(location, {
|
|
144
177
|
operation,
|
|
145
178
|
key,
|
|
146
179
|
index,
|
|
147
180
|
delta
|
|
148
181
|
});
|
|
149
182
|
if (locationResult) {
|
|
183
|
+
// Notify all subscribers about this "synchronous" location change.
|
|
150
184
|
this._subscription.notifySubscribers(locationResult);
|
|
151
185
|
}
|
|
152
186
|
}
|
|
@@ -160,7 +194,7 @@ export default class Session {
|
|
|
160
194
|
return;
|
|
161
195
|
}
|
|
162
196
|
const index = this._currentLocationIndex + delta;
|
|
163
|
-
debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
|
|
197
|
+
this.environment.log.debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
|
|
164
198
|
|
|
165
199
|
// Validate that the new `index` is not out of bounds.
|
|
166
200
|
if (index < 0 || index > this._terminalLocationIndex) {
|
|
@@ -168,12 +202,13 @@ export default class Session {
|
|
|
168
202
|
}
|
|
169
203
|
|
|
170
204
|
// Navigate to the location.
|
|
171
|
-
const locationResult = this.
|
|
205
|
+
const locationResult = this.environment.navigation.shift({
|
|
172
206
|
operation: NavigationOperations.SHIFT,
|
|
173
207
|
index,
|
|
174
208
|
delta
|
|
175
209
|
});
|
|
176
210
|
if (locationResult) {
|
|
211
|
+
// Notify all subscribers about this "synchronous" location change.
|
|
177
212
|
this._subscription.notifySubscribers(locationResult);
|
|
178
213
|
}
|
|
179
214
|
}
|
|
@@ -1,20 +1,32 @@
|
|
|
1
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
|
-
|
|
2
|
+
constructor() {
|
|
16
3
|
// This property is accessed in tests.
|
|
17
4
|
this._listeners = [];
|
|
5
|
+
|
|
6
|
+
// These listeners will be called when the subscription enters "active" or "inactive" state.
|
|
7
|
+
// A subscription enters "active" state when it has at least one listener rather than zero.
|
|
8
|
+
// A subscription enters "inactive" state when it has no more listeners.
|
|
9
|
+
this._subscriptionActiveStateListeners = [];
|
|
10
|
+
this._subscriptionInactiveStateListeners = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Adds a subscription active state listener.
|
|
14
|
+
// Returns a function that removes the subscription active state listener.
|
|
15
|
+
onFirstSubscriber(activeStateListener) {
|
|
16
|
+
this._subscriptionActiveStateListeners.push(activeStateListener);
|
|
17
|
+
// Return a function that removes the subscription active state listener.
|
|
18
|
+
return () => {
|
|
19
|
+
this._subscriptionActiveStateListeners = this._subscriptionActiveStateListeners.filter(_ => _ !== activeStateListener);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
notifySubscribers(argument) {
|
|
23
|
+
// `._latest` is only used in tests.
|
|
24
|
+
this._latest = argument;
|
|
25
|
+
for (const {
|
|
26
|
+
listener
|
|
27
|
+
} of this._listeners) {
|
|
28
|
+
listener(argument);
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
subscribe(listener) {
|
|
20
32
|
// If subscriptions are stopped, i.e. no new subscriptions are to be added,
|
|
@@ -34,7 +46,9 @@ export default class Subscription {
|
|
|
34
46
|
|
|
35
47
|
// If it's the first listener, activate subscription.
|
|
36
48
|
if (this._listeners.length === 0) {
|
|
37
|
-
|
|
49
|
+
// Run all subscription active state listeners.
|
|
50
|
+
// The functions returned from those will become subscription inactive state listeners.
|
|
51
|
+
this._subscriptionInactiveStateListeners = this._subscriptionActiveStateListeners.map(activeStateListener => activeStateListener());
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
// Add the `listener` to the list.
|
|
@@ -66,10 +80,14 @@ export default class Subscription {
|
|
|
66
80
|
// Remove the `listener` from the list.
|
|
67
81
|
this._listeners = this._listeners.filter(_ => _ !== listenerEntry);
|
|
68
82
|
|
|
69
|
-
// If it was the last listener
|
|
83
|
+
// If it was the last listener.
|
|
70
84
|
if (this._listeners.length === 0) {
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
// Run any subscription inactive state listeners,
|
|
86
|
+
// after which clear the list of such listeners.
|
|
87
|
+
for (const inactiveStateListener of this._subscriptionInactiveStateListeners) {
|
|
88
|
+
inactiveStateListener();
|
|
89
|
+
}
|
|
90
|
+
this._subscriptionInactiveStateListeners = [];
|
|
73
91
|
}
|
|
74
92
|
}
|
|
75
93
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// "The more recent RFC3986 reserves !, ', (, ), and *,
|
|
2
|
+
// even though these characters have no formalized URI delimiting uses.
|
|
3
|
+
//
|
|
4
|
+
// https://datatracker.ietf.org/doc/html/rfc3986
|
|
5
|
+
//
|
|
6
|
+
// The following function encodes a string for RFC3986-compliant URL component format.
|
|
7
|
+
// It also encodes [ and ], which are part of the IPv6 URI syntax.
|
|
8
|
+
//
|
|
9
|
+
// An RFC3986-compliant encodeURI implementation should not escape them,
|
|
10
|
+
// which is demonstrated in the encodeURI() example.
|
|
11
|
+
//
|
|
12
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
|
|
13
|
+
//
|
|
14
|
+
// Can throw a `URIError` if the `string` contains a "lone surrogate".
|
|
15
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
|
|
16
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
|
|
17
|
+
// Example: "URIError: malformed URI sequence"
|
|
18
|
+
//
|
|
19
|
+
function encode(string) {
|
|
20
|
+
return encodeURIComponent(string).replace(/[!'()*]/g, character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
21
|
+
}
|
|
22
|
+
export default function stringifyQuery(query) {
|
|
23
|
+
let queryString = '';
|
|
24
|
+
if (!query) {
|
|
25
|
+
return queryString;
|
|
26
|
+
}
|
|
27
|
+
for (const key of Object.keys(query)) {
|
|
28
|
+
let value = query[key];
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
throw new Error('Array values are not supported');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ignore `value: undefined`.
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Stringify `value`.
|
|
39
|
+
if (value === null) {
|
|
40
|
+
value = '';
|
|
41
|
+
} else {
|
|
42
|
+
value = String(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Can throw a `URIError` if the `string` contains a "lone surrogate".
|
|
46
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
|
|
47
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
|
|
48
|
+
// Example: "URIError: malformed URI sequence"
|
|
49
|
+
try {
|
|
50
|
+
const keyValuePair = `${encode(key)}${value ? '=' : ''}${encode(value)}`;
|
|
51
|
+
if (queryString.length > 1) {
|
|
52
|
+
queryString += '&';
|
|
53
|
+
}
|
|
54
|
+
queryString += keyValuePair;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Simply ignore an invalid query parameter.
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return queryString;
|
|
61
|
+
}
|