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
|
@@ -7,7 +7,6 @@ var _ScrollPositionSaver = _interopRequireDefault(require("./ScrollPositionSaver
|
|
|
7
7
|
var _ScrollPositionSetter = _interopRequireDefault(require("./ScrollPositionSetter"));
|
|
8
8
|
var _constants = require("./constants");
|
|
9
9
|
var _LocationDataStorage = _interopRequireDefault(require("../data-storage/LocationDataStorage"));
|
|
10
|
-
var _debug = _interopRequireDefault(require("../debug"));
|
|
11
10
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
11
|
/* eslint-disable no-underscore-dangle */
|
|
13
12
|
|
|
@@ -22,7 +21,7 @@ function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
|
|
|
22
21
|
return true;
|
|
23
22
|
}
|
|
24
23
|
class ScrollPositionRestoration {
|
|
25
|
-
constructor(session,
|
|
24
|
+
constructor(session, options) {
|
|
26
25
|
// Once configured, scroll restoration mode persists across page reloads.
|
|
27
26
|
// I.e. even if a user refreshes the page in a web browser, the custom
|
|
28
27
|
// `window.history.scrollRestoration` value will still remain.
|
|
@@ -54,10 +53,10 @@ class ScrollPositionRestoration {
|
|
|
54
53
|
running
|
|
55
54
|
}) => {
|
|
56
55
|
if (running) {
|
|
57
|
-
|
|
56
|
+
this._log.debug('▶ running');
|
|
58
57
|
this._disableAutomaticScrollRestoration();
|
|
59
58
|
} else {
|
|
60
|
-
|
|
59
|
+
this._log.debug('⏹ not running');
|
|
61
60
|
this._enableAutomaticScrollRestoration();
|
|
62
61
|
|
|
63
62
|
// There might be previous scroll position already saved in the data storage.
|
|
@@ -85,27 +84,30 @@ class ScrollPositionRestoration {
|
|
|
85
84
|
try {
|
|
86
85
|
this._scrollPosition.disableAutomaticScrollRestoration();
|
|
87
86
|
} catch (error) {
|
|
88
|
-
|
|
89
|
-
console.error('[navigation-stack] could not disable default scroll restoration mode');
|
|
87
|
+
this._log.error('[navigation-stack] could not disable default scroll restoration mode');
|
|
90
88
|
}
|
|
91
89
|
};
|
|
92
90
|
this._enableAutomaticScrollRestoration = () => {
|
|
93
91
|
try {
|
|
94
92
|
this._scrollPosition.enableAutomaticScrollRestoration();
|
|
95
93
|
} catch (error) {
|
|
96
|
-
|
|
97
|
-
console.error('[navigation-stack] could not enable default scroll restoration mode');
|
|
94
|
+
this._log.error('[navigation-stack] could not enable default scroll restoration mode');
|
|
98
95
|
}
|
|
99
96
|
};
|
|
100
97
|
this._saveScrollPositionForLocation = (location, scrollableContainerKey, scrollPosition) => {
|
|
101
98
|
this._locationDataStorage.set(location, scrollableContainerKey || _constants.PAGE_SCROLLABLE_CONTAINER_KEY, scrollPosition);
|
|
102
99
|
};
|
|
100
|
+
this._log = session.environment.log;
|
|
103
101
|
this._scrollPosition = session.environment.scrollPosition;
|
|
104
|
-
|
|
102
|
+
|
|
103
|
+
// Custom `ScrollPositionSetter`.
|
|
104
|
+
this._scrollPositionSetter = options.scrollPositionSetter;
|
|
105
|
+
this._sessionLifecycle = session.environment.lifecycle;
|
|
105
106
|
this._locationDataStorage = new _LocationDataStorage.default(session, {
|
|
106
|
-
namespace: 'navigation-stack
|
|
107
|
+
namespace: 'navigation-stack-scroll-position'
|
|
107
108
|
});
|
|
108
109
|
this._scrollPositionSaver = new _ScrollPositionSaver.default({
|
|
110
|
+
log: this._log,
|
|
109
111
|
scrollPosition: this._scrollPosition,
|
|
110
112
|
saveScrollPositionForLocation: this._saveScrollPositionForLocation,
|
|
111
113
|
getScrollableContainers: () => this._scrollableContainers,
|
|
@@ -127,20 +129,22 @@ class ScrollPositionRestoration {
|
|
|
127
129
|
// Using this option, a developer could theoretically provide their own implementation
|
|
128
130
|
// of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
|
|
129
131
|
// This could be part of the public API if anyone provided a sensible real-world use case for it.
|
|
130
|
-
scrollPositionSetter:
|
|
131
|
-
//
|
|
132
|
+
scrollPositionSetter: options && options._pageScrollPositionSetter ||
|
|
133
|
+
// eslint-disable-next-line new-cap
|
|
134
|
+
this._scrollPositionSetter && new this._scrollPositionSetter() ||
|
|
135
|
+
// A default `ScrollPositionSetter` for a page (sets page scroll position twice with a momentary delay).
|
|
132
136
|
new _PageScrollPositionSetter.default(),
|
|
133
137
|
// This function is only used in tests.
|
|
134
138
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
135
139
|
// It's only used in tests.
|
|
136
|
-
_getSavedScrollPositionOnLocationChange:
|
|
140
|
+
_getSavedScrollPositionOnLocationChange: options && options._getSavedPageScrollPositionOnLocationChange,
|
|
137
141
|
// This function is only used in tests.
|
|
138
142
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
139
143
|
// It's only used in tests.
|
|
140
|
-
|
|
144
|
+
shouldChangeScrollPositionOnLocationChange: options && options.shouldChangePageScrollPositionOnLocationChange
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
|
-
addScrollableContainer(scrollableContainerKey, scrollableContainer,
|
|
147
|
+
addScrollableContainer(scrollableContainerKey, scrollableContainer, options) {
|
|
144
148
|
// Originally, `scrollableContainerKey` was auto-generated,
|
|
145
149
|
// but then it didn't work with the concept of dynamically adding or removing
|
|
146
150
|
// scrollable containers after `ScrollPositionRestoration` has already started.
|
|
@@ -157,7 +161,7 @@ class ScrollPositionRestoration {
|
|
|
157
161
|
if (this._scrollableContainers[scrollableContainerKey]) {
|
|
158
162
|
throw new Error(`Scrollable container key "${scrollableContainerKey}" is already added`);
|
|
159
163
|
}
|
|
160
|
-
|
|
164
|
+
this._log.debug('add scrollable container', scrollableContainerKey);
|
|
161
165
|
|
|
162
166
|
// Add scrollable container entry.
|
|
163
167
|
this._scrollableContainers[scrollableContainerKey] = {
|
|
@@ -166,17 +170,17 @@ class ScrollPositionRestoration {
|
|
|
166
170
|
// Using this option, a developer could theoretically provide their own implementation
|
|
167
171
|
// of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
|
|
168
172
|
// This could be part of the public API if anyone provided a sensible real-world use case for it.
|
|
169
|
-
scrollPositionSetter:
|
|
173
|
+
scrollPositionSetter: options && options._scrollPositionSetter || this._scrollPositionSetter && new this._scrollPositionSetter() ||
|
|
170
174
|
// The default basic "immediate" scroll position setter.
|
|
171
175
|
new _ScrollPositionSetter.default(),
|
|
172
176
|
// This function is only used in tests.
|
|
173
177
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
174
178
|
// It's only used in tests.
|
|
175
|
-
|
|
179
|
+
shouldChangeScrollPositionOnLocationChange: options && options.shouldChangeScrollPositionOnLocationChange,
|
|
176
180
|
// This function is only used in tests.
|
|
177
181
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
178
182
|
// It's only used in tests.
|
|
179
|
-
_getSavedScrollPositionOnLocationChange:
|
|
183
|
+
_getSavedScrollPositionOnLocationChange: options && options._getSavedScrollPositionOnLocationChange
|
|
180
184
|
};
|
|
181
185
|
|
|
182
186
|
// Scrollable containers could be added at any time, including page mount.
|
|
@@ -188,10 +192,10 @@ class ScrollPositionRestoration {
|
|
|
188
192
|
if (this._location) {
|
|
189
193
|
const previouslySavedScrollPosition = this._getSavedScrollPositionForLocation(this._location, scrollableContainerKey);
|
|
190
194
|
if (previouslySavedScrollPosition) {
|
|
191
|
-
|
|
195
|
+
this._log.debug('restore scroll position on add scrollable container', this._location.pathname, scrollableContainerKey, previouslySavedScrollPosition);
|
|
192
196
|
this._scrollPosition.setScrollableContainerScrollPosition(scrollableContainer, previouslySavedScrollPosition);
|
|
193
197
|
} else {
|
|
194
|
-
|
|
198
|
+
this._log.debug('save scroll position on add scrollable container', this._location.pathname, scrollableContainerKey);
|
|
195
199
|
this._scrollPositionSaver.saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer);
|
|
196
200
|
}
|
|
197
201
|
}
|
|
@@ -201,7 +205,7 @@ class ScrollPositionRestoration {
|
|
|
201
205
|
|
|
202
206
|
// Removes the scrollable container.
|
|
203
207
|
return () => {
|
|
204
|
-
|
|
208
|
+
this._log.debug('remove scrollable container', scrollableContainerKey);
|
|
205
209
|
this._scrollPositionSaver._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(scrollableContainerKey);
|
|
206
210
|
this._scrollPositionSaver._scrollPositionAutoSaver.removeScrollableContainerScrollListener(scrollableContainerKey);
|
|
207
211
|
delete this._scrollableContainers[scrollableContainerKey];
|
|
@@ -273,7 +277,7 @@ class ScrollPositionRestoration {
|
|
|
273
277
|
if (!location.key) {
|
|
274
278
|
throw new Error('`location` must have a `key`');
|
|
275
279
|
}
|
|
276
|
-
|
|
280
|
+
this._log.debug('rendered location', location.pathname);
|
|
277
281
|
this._prevLocation = this._location;
|
|
278
282
|
this._location = location;
|
|
279
283
|
this._scrollPosition.init();
|
|
@@ -334,8 +338,8 @@ class ScrollPositionRestoration {
|
|
|
334
338
|
// This function is only used in tests.
|
|
335
339
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
336
340
|
// It's only used in tests.
|
|
337
|
-
if (scrollableContainerEntry.
|
|
338
|
-
if (!scrollableContainerEntry.
|
|
341
|
+
if (scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange) {
|
|
342
|
+
if (!scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange(this._prevLocation, this._location)) {
|
|
339
343
|
return Promise.resolve();
|
|
340
344
|
}
|
|
341
345
|
}
|
|
@@ -347,14 +351,14 @@ class ScrollPositionRestoration {
|
|
|
347
351
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
348
352
|
// It's only used in tests.
|
|
349
353
|
if (scrollableContainerEntry._getSavedScrollPositionOnLocationChange) {
|
|
350
|
-
scrollPositionOrAnchorToSet = scrollableContainerEntry._getSavedScrollPositionOnLocationChange(this.
|
|
354
|
+
scrollPositionOrAnchorToSet = scrollableContainerEntry._getSavedScrollPositionOnLocationChange(this._prevLocation, this._location);
|
|
351
355
|
}
|
|
352
356
|
|
|
353
357
|
// Get scroll position (or anchor) to set.
|
|
354
358
|
if (!scrollPositionOrAnchorToSet) {
|
|
355
359
|
scrollPositionOrAnchorToSet = scrollableContainerKey === _constants.PAGE_SCROLLABLE_CONTAINER_KEY ? this._getPageScrollPositionOrAnchorToSet(this._location) : this._getScrollableContainerScrollPositionToSet(this._location, scrollableContainerKey);
|
|
356
360
|
}
|
|
357
|
-
|
|
361
|
+
this._log.debug('restore scroll position', this._location.pathname, scrollableContainerKey, scrollPositionOrAnchorToSet);
|
|
358
362
|
|
|
359
363
|
// Set scroll position of scrollable container.
|
|
360
364
|
return scrollableContainerEntry.scrollPositionSetter.set(scrollableContainerEntry.scrollableContainer, scrollPositionOrAnchorToSet, this._scrollPosition);
|
|
@@ -4,24 +4,26 @@ exports.__esModule = true;
|
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
var _ScrollPositionAutoSaver = _interopRequireDefault(require("./ScrollPositionAutoSaver"));
|
|
6
6
|
var _constants = require("./constants");
|
|
7
|
-
var _debug = _interopRequireDefault(require("../debug"));
|
|
8
7
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
8
|
/* eslint-disable no-underscore-dangle */
|
|
10
9
|
|
|
11
10
|
class ScrollPositionSaver {
|
|
12
11
|
constructor({
|
|
12
|
+
log,
|
|
13
13
|
scrollPosition,
|
|
14
14
|
getLocation,
|
|
15
15
|
saveScrollPositionForLocation,
|
|
16
16
|
getScrollableContainers,
|
|
17
17
|
shouldSaveScrollPosition
|
|
18
18
|
}) {
|
|
19
|
+
this._log = log;
|
|
19
20
|
this._scrollPosition = scrollPosition;
|
|
20
21
|
this._getLocation = getLocation;
|
|
21
22
|
this._saveScrollPositionForLocation = saveScrollPositionForLocation;
|
|
22
23
|
this._getScrollableContainers = getScrollableContainers;
|
|
23
24
|
this._shouldSaveScrollPosition = shouldSaveScrollPosition;
|
|
24
25
|
this._scrollPositionAutoSaver = new _ScrollPositionAutoSaver.default({
|
|
26
|
+
log: this._log,
|
|
25
27
|
scrollPosition: this._scrollPosition,
|
|
26
28
|
scrollPositionSaver: this,
|
|
27
29
|
getScrollableContainers,
|
|
@@ -42,7 +44,7 @@ class ScrollPositionSaver {
|
|
|
42
44
|
if (!this._shouldSaveScrollPosition()) {
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
this._log.debug('save scroll position', this._getLocation().pathname);
|
|
46
48
|
|
|
47
49
|
// Get scrollable containers.
|
|
48
50
|
const scrollableContainers = this._getScrollableContainers();
|
|
@@ -57,7 +59,7 @@ class ScrollPositionSaver {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
savePageScrollPosition() {
|
|
60
|
-
|
|
62
|
+
this._log.debug('save scroll position', this._getLocation().pathname, _constants.PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
|
|
61
63
|
|
|
62
64
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
63
65
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
@@ -70,7 +72,7 @@ class ScrollPositionSaver {
|
|
|
70
72
|
this._saveScrollPositionForLocation(this._getLocation(), undefined, this._scrollPosition.getPageScrollPosition());
|
|
71
73
|
}
|
|
72
74
|
saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer) {
|
|
73
|
-
|
|
75
|
+
this._log.debug('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
|
|
74
76
|
|
|
75
77
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
76
78
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
@@ -2,20 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = void 0;
|
|
5
|
-
var _debug = _interopRequireDefault(require("../debug"));
|
|
6
5
|
var _parseInputLocation = _interopRequireDefault(require("../parseInputLocation"));
|
|
7
6
|
var _createSessionKey = _interopRequireDefault(require("./key/createSessionKey"));
|
|
8
|
-
var _NavigationOutOfBoundsError = _interopRequireDefault(require("./navigation/error/NavigationOutOfBoundsError"));
|
|
9
|
-
var _operations = _interopRequireDefault(require("./navigation/operation/operations"));
|
|
10
7
|
var _Subscription = _interopRequireDefault(require("./subscription/Subscription"));
|
|
8
|
+
var _NavigationOutOfBoundsError = _interopRequireDefault(require("../environment/navigation/error/NavigationOutOfBoundsError"));
|
|
9
|
+
var _operations = _interopRequireDefault(require("../environment/navigation/operation/operations"));
|
|
11
10
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
11
|
const INITIAL_KEY_INDEX = -1;
|
|
13
12
|
const INITIAL_INDEX = -1;
|
|
14
13
|
const INIT_LOCATION_DELTA = 0;
|
|
15
14
|
class Session {
|
|
16
|
-
constructor({
|
|
17
|
-
navigation
|
|
18
|
-
}) {
|
|
15
|
+
constructor(EnvironmentClass) {
|
|
19
16
|
// This function is used by navigation.
|
|
20
17
|
this._getCurrentLocationIndex = () => {
|
|
21
18
|
return this._currentLocationIndex;
|
|
@@ -25,6 +22,9 @@ class Session {
|
|
|
25
22
|
// under the hood, and `window.sessionStorage` is shared between different sessions.
|
|
26
23
|
this.key = (0, _createSessionKey.default)();
|
|
27
24
|
|
|
25
|
+
// Create an environment instance.
|
|
26
|
+
this.environment = new EnvironmentClass();
|
|
27
|
+
|
|
28
28
|
// `this._locationKeyIndex` is incremented every time the current location changes.
|
|
29
29
|
this._locationKeyIndex = INITIAL_KEY_INDEX;
|
|
30
30
|
|
|
@@ -36,18 +36,34 @@ class Session {
|
|
|
36
36
|
// In other words, this is the last location index that it can `.shift()` to.
|
|
37
37
|
this._terminalLocationIndex = this._currentLocationIndex;
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
this.
|
|
39
|
+
// Allows subscribing to location updates.
|
|
40
|
+
this._subscription = new _Subscription.default();
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// Subscribing to location changes means subscribing to both "synchronous"
|
|
43
|
+
// and "asynchronous" location changes. "synchronous" location changes
|
|
44
|
+
// happen immediately when the code triggers them."asynchronous" location changes
|
|
45
|
+
// either happen after an arbitrary delay or are even triggered from outside the code.
|
|
46
|
+
//
|
|
47
|
+
// Subscribing to "asynchronous" location changes is not necessary when
|
|
48
|
+
// there're no actual subscribers, in order to not unnecessarily "waste" any resources.
|
|
49
|
+
// Of course, this statement is rather far-fetched and in reality no one would ever tell any difference.
|
|
50
|
+
// Still, I felt like randomly introducing this seemingly unnecessary minor optimization.
|
|
51
|
+
//
|
|
52
|
+
// So it only subscribes to "asynchronous" location changes if there's at least one active subscriber.
|
|
53
|
+
// And in case all subscribers get unsubscribed, it will unsubscribe from "asynchronous" location changes too.
|
|
54
|
+
// One might think of it as some form of "mental masturbation", but what can I do — I already wrote the code.
|
|
55
|
+
//
|
|
56
|
+
this._subscription.onFirstSubscriber(() => {
|
|
57
|
+
return this.environment.navigation.subscribeToAsyncrhonousLocationUpdates(location => {
|
|
58
|
+
// Notify all subscribers about this "asynchronous" location change.
|
|
59
|
+
this._subscription.notifySubscribers(location);
|
|
60
|
+
});
|
|
47
61
|
});
|
|
48
62
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
63
|
+
// This subscription is triggered in two cases:
|
|
64
|
+
// * Set initial current location index at initial page load.
|
|
65
|
+
// * Update current location index whenever a location change is not initiated
|
|
66
|
+
// by this session but rather by the user clicking "Back" or "Forward" button.
|
|
51
67
|
this._unsubscribe = this.subscribe(location => {
|
|
52
68
|
// Update `this._currentLocationIndex` when the location change was not initiated
|
|
53
69
|
// by this session but rather by the user clicking "Back" or "Forward" button.
|
|
@@ -57,16 +73,20 @@ class Session {
|
|
|
57
73
|
// but if it was possible, this call would be required. It would also be required
|
|
58
74
|
// by `navigation` to call `session.getNextKey()` function to increment `locationKeyIndex`.
|
|
59
75
|
this._updateTerminalLocationIndex(location);
|
|
60
|
-
|
|
76
|
+
this.environment.log.debug('current location', location.pathname, 'index', this._currentLocationIndex);
|
|
61
77
|
});
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
// Subscribes to changes in location.
|
|
81
|
+
// The first subscriber is always the `Session` itself:
|
|
82
|
+
// its listener keeps the current location index up-to-date.
|
|
83
|
+
// Any additional application-specific listeners could be added, if required.
|
|
84
|
+
// Applications should prefer adding any such listeners by calling `NavigationStack.subscribe()`
|
|
85
|
+
// method instead of calling this method directly, in order to "normalize" the `location` argument.
|
|
65
86
|
subscribe(listener) {
|
|
66
87
|
return this._subscription.subscribe(location => {
|
|
67
88
|
if (!this._isStarted() && location.operation !== _operations.default.INIT) {
|
|
68
|
-
|
|
69
|
-
console.error('Unexpected location change', location);
|
|
89
|
+
this.environment.log.error('Unexpected location change', location);
|
|
70
90
|
throw new Error('Not started');
|
|
71
91
|
} else {
|
|
72
92
|
// Call the listener.
|
|
@@ -74,6 +94,18 @@ class Session {
|
|
|
74
94
|
}
|
|
75
95
|
});
|
|
76
96
|
}
|
|
97
|
+
|
|
98
|
+
// Starts a navigation session.
|
|
99
|
+
//
|
|
100
|
+
// When run in a web browser, it could not only "start" a new session
|
|
101
|
+
// but also "resume" a previously-started session. That could happen
|
|
102
|
+
// when the user refreshes a page in a web browser which still retains
|
|
103
|
+
// the previous session's data but at the same time restarts the javascript code
|
|
104
|
+
// from scratch.
|
|
105
|
+
//
|
|
106
|
+
// So this `start()` method handles both cases: when there's previous session's data
|
|
107
|
+
// that should be restored and when there's no previous session's data.
|
|
108
|
+
//
|
|
77
109
|
start(initialLocation) {
|
|
78
110
|
if (this._stopped) {
|
|
79
111
|
throw new Error('Can not be restarted');
|
|
@@ -86,7 +118,7 @@ class Session {
|
|
|
86
118
|
// the initial location by the time javascript code starts execution.
|
|
87
119
|
//
|
|
88
120
|
if (!initialLocation) {
|
|
89
|
-
initialLocation = this.
|
|
121
|
+
initialLocation = this.environment.navigation.getInitialLocation();
|
|
90
122
|
if (initialLocation) {
|
|
91
123
|
initialLocation = (0, _parseInputLocation.default)(initialLocation);
|
|
92
124
|
}
|
|
@@ -97,18 +129,19 @@ class Session {
|
|
|
97
129
|
if (this._currentLocationIndex !== INITIAL_INDEX) {
|
|
98
130
|
throw new Error('Already started');
|
|
99
131
|
}
|
|
100
|
-
|
|
132
|
+
this.environment.log.debug('▶ start session', initialLocation.pathname);
|
|
101
133
|
this._started = true;
|
|
102
134
|
const key = this._getNextLocationKey();
|
|
103
135
|
const index = INITIAL_INDEX + 1;
|
|
104
136
|
const delta = INIT_LOCATION_DELTA;
|
|
105
|
-
const locationResult = this.
|
|
137
|
+
const locationResult = this.environment.navigation.init(initialLocation, {
|
|
106
138
|
operation: _operations.default.INIT,
|
|
107
139
|
key,
|
|
108
140
|
index,
|
|
109
141
|
delta
|
|
110
142
|
});
|
|
111
143
|
if (locationResult) {
|
|
144
|
+
// Notify all subscribers about this "synchronous" location change.
|
|
112
145
|
this._subscription.notifySubscribers(locationResult);
|
|
113
146
|
}
|
|
114
147
|
}
|
|
@@ -116,7 +149,7 @@ class Session {
|
|
|
116
149
|
if (this._stopped) {
|
|
117
150
|
throw Error('Already stopped');
|
|
118
151
|
}
|
|
119
|
-
|
|
152
|
+
this.environment.log.debug('⏹ stop session');
|
|
120
153
|
|
|
121
154
|
// Once stopped, it won't be able to be restarted.
|
|
122
155
|
this._stopped = true;
|
|
@@ -142,16 +175,17 @@ class Session {
|
|
|
142
175
|
});
|
|
143
176
|
const key = this._getNextLocationKey();
|
|
144
177
|
const index = this._currentLocationIndex + delta;
|
|
145
|
-
|
|
178
|
+
this.environment.log.debug(operation === _operations.default.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
|
|
146
179
|
|
|
147
180
|
// Navigate to the location.
|
|
148
|
-
const locationResult = this.
|
|
181
|
+
const locationResult = this.environment.navigation.navigate(location, {
|
|
149
182
|
operation,
|
|
150
183
|
key,
|
|
151
184
|
index,
|
|
152
185
|
delta
|
|
153
186
|
});
|
|
154
187
|
if (locationResult) {
|
|
188
|
+
// Notify all subscribers about this "synchronous" location change.
|
|
155
189
|
this._subscription.notifySubscribers(locationResult);
|
|
156
190
|
}
|
|
157
191
|
}
|
|
@@ -165,7 +199,7 @@ class Session {
|
|
|
165
199
|
return;
|
|
166
200
|
}
|
|
167
201
|
const index = this._currentLocationIndex + delta;
|
|
168
|
-
|
|
202
|
+
this.environment.log.debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
|
|
169
203
|
|
|
170
204
|
// Validate that the new `index` is not out of bounds.
|
|
171
205
|
if (index < 0 || index > this._terminalLocationIndex) {
|
|
@@ -173,12 +207,13 @@ class Session {
|
|
|
173
207
|
}
|
|
174
208
|
|
|
175
209
|
// Navigate to the location.
|
|
176
|
-
const locationResult = this.
|
|
210
|
+
const locationResult = this.environment.navigation.shift({
|
|
177
211
|
operation: _operations.default.SHIFT,
|
|
178
212
|
index,
|
|
179
213
|
delta
|
|
180
214
|
});
|
|
181
215
|
if (locationResult) {
|
|
216
|
+
// Notify all subscribers about this "synchronous" location change.
|
|
182
217
|
this._subscription.notifySubscribers(locationResult);
|
|
183
218
|
}
|
|
184
219
|
}
|
|
@@ -3,22 +3,34 @@
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
class Subscription {
|
|
6
|
-
constructor({
|
|
7
|
-
activateSubscription
|
|
8
|
-
} = {}) {
|
|
9
|
-
this.notifySubscribers = argument => {
|
|
10
|
-
// `._latest` is only used in tests.
|
|
11
|
-
this._latest = argument;
|
|
12
|
-
for (const {
|
|
13
|
-
listener
|
|
14
|
-
} of this._listeners) {
|
|
15
|
-
listener(argument);
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
this._activateSubscription = activateSubscription;
|
|
19
|
-
|
|
6
|
+
constructor() {
|
|
20
7
|
// This property is accessed in tests.
|
|
21
8
|
this._listeners = [];
|
|
9
|
+
|
|
10
|
+
// These listeners will be called when the subscription enters "active" or "inactive" state.
|
|
11
|
+
// A subscription enters "active" state when it has at least one listener rather than zero.
|
|
12
|
+
// A subscription enters "inactive" state when it has no more listeners.
|
|
13
|
+
this._subscriptionActiveStateListeners = [];
|
|
14
|
+
this._subscriptionInactiveStateListeners = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Adds a subscription active state listener.
|
|
18
|
+
// Returns a function that removes the subscription active state listener.
|
|
19
|
+
onFirstSubscriber(activeStateListener) {
|
|
20
|
+
this._subscriptionActiveStateListeners.push(activeStateListener);
|
|
21
|
+
// Return a function that removes the subscription active state listener.
|
|
22
|
+
return () => {
|
|
23
|
+
this._subscriptionActiveStateListeners = this._subscriptionActiveStateListeners.filter(_ => _ !== activeStateListener);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
notifySubscribers(argument) {
|
|
27
|
+
// `._latest` is only used in tests.
|
|
28
|
+
this._latest = argument;
|
|
29
|
+
for (const {
|
|
30
|
+
listener
|
|
31
|
+
} of this._listeners) {
|
|
32
|
+
listener(argument);
|
|
33
|
+
}
|
|
22
34
|
}
|
|
23
35
|
subscribe(listener) {
|
|
24
36
|
// If subscriptions are stopped, i.e. no new subscriptions are to be added,
|
|
@@ -38,7 +50,9 @@ class Subscription {
|
|
|
38
50
|
|
|
39
51
|
// If it's the first listener, activate subscription.
|
|
40
52
|
if (this._listeners.length === 0) {
|
|
41
|
-
|
|
53
|
+
// Run all subscription active state listeners.
|
|
54
|
+
// The functions returned from those will become subscription inactive state listeners.
|
|
55
|
+
this._subscriptionInactiveStateListeners = this._subscriptionActiveStateListeners.map(activeStateListener => activeStateListener());
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
// Add the `listener` to the list.
|
|
@@ -70,10 +84,14 @@ class Subscription {
|
|
|
70
84
|
// Remove the `listener` from the list.
|
|
71
85
|
this._listeners = this._listeners.filter(_ => _ !== listenerEntry);
|
|
72
86
|
|
|
73
|
-
// If it was the last listener
|
|
87
|
+
// If it was the last listener.
|
|
74
88
|
if (this._listeners.length === 0) {
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
// Run any subscription inactive state listeners,
|
|
90
|
+
// after which clear the list of such listeners.
|
|
91
|
+
for (const inactiveStateListener of this._subscriptionInactiveStateListeners) {
|
|
92
|
+
inactiveStateListener();
|
|
93
|
+
}
|
|
94
|
+
this._subscriptionInactiveStateListeners = [];
|
|
77
95
|
}
|
|
78
96
|
}
|
|
79
97
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = stringifyQuery;
|
|
5
|
+
// "The more recent RFC3986 reserves !, ', (, ), and *,
|
|
6
|
+
// even though these characters have no formalized URI delimiting uses.
|
|
7
|
+
//
|
|
8
|
+
// https://datatracker.ietf.org/doc/html/rfc3986
|
|
9
|
+
//
|
|
10
|
+
// The following function encodes a string for RFC3986-compliant URL component format.
|
|
11
|
+
// It also encodes [ and ], which are part of the IPv6 URI syntax.
|
|
12
|
+
//
|
|
13
|
+
// An RFC3986-compliant encodeURI implementation should not escape them,
|
|
14
|
+
// which is demonstrated in the encodeURI() example.
|
|
15
|
+
//
|
|
16
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
|
|
17
|
+
//
|
|
18
|
+
// Can throw a `URIError` if the `string` contains a "lone surrogate".
|
|
19
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
|
|
20
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
|
|
21
|
+
// Example: "URIError: malformed URI sequence"
|
|
22
|
+
//
|
|
23
|
+
function encode(string) {
|
|
24
|
+
return encodeURIComponent(string).replace(/[!'()*]/g, character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
25
|
+
}
|
|
26
|
+
function stringifyQuery(query) {
|
|
27
|
+
let queryString = '';
|
|
28
|
+
if (!query) {
|
|
29
|
+
return queryString;
|
|
30
|
+
}
|
|
31
|
+
for (const key of Object.keys(query)) {
|
|
32
|
+
let value = query[key];
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
throw new Error('Array values are not supported');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Ignore `value: undefined`.
|
|
38
|
+
if (value === undefined) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Stringify `value`.
|
|
43
|
+
if (value === null) {
|
|
44
|
+
value = '';
|
|
45
|
+
} else {
|
|
46
|
+
value = String(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Can throw a `URIError` if the `string` contains a "lone surrogate".
|
|
50
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
|
|
51
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
|
|
52
|
+
// Example: "URIError: malformed URI sequence"
|
|
53
|
+
try {
|
|
54
|
+
const keyValuePair = `${encode(key)}${value ? '=' : ''}${encode(value)}`;
|
|
55
|
+
if (queryString.length > 1) {
|
|
56
|
+
queryString += '&';
|
|
57
|
+
}
|
|
58
|
+
queryString += keyValuePair;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Simply ignore an invalid query parameter.
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return queryString;
|
|
65
|
+
}
|
|
66
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = stringifyQueryAsSearch;
|
|
5
|
+
var _stringifyQuery = _interopRequireDefault(require("./stringifyQuery"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
function stringifyQueryAsSearch(query) {
|
|
8
|
+
const queryString = (0, _stringifyQuery.default)(query);
|
|
9
|
+
if (queryString) {
|
|
10
|
+
return `?${queryString}`;
|
|
11
|
+
}
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
module.exports = exports.default;
|