navigation-stack 0.4.0 → 0.5.1
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 +12 -0
- package/README.md +20 -12
- package/data-storage/package.json +2 -1
- package/lib/cjs/NavigationStack.js +17 -2
- package/lib/cjs/debug.js +12 -0
- package/lib/cjs/getLocationFromInternalLocation.js +2 -2
- package/lib/cjs/navigationBlockers.js +3 -0
- package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +13 -2
- package/lib/cjs/scroll-position/ScrollPositionRestoration.js +37 -13
- package/lib/cjs/scroll-position/ScrollPositionSaver.js +8 -2
- package/lib/cjs/session/Session.js +6 -0
- package/lib/cjs/session/navigation/operation/operations.js +4 -4
- package/lib/esm/NavigationStack.js +17 -2
- package/lib/esm/debug.js +7 -0
- package/lib/esm/getLocationFromInternalLocation.js +2 -2
- package/lib/esm/navigationBlockers.js +3 -0
- package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +13 -2
- package/lib/esm/scroll-position/ScrollPositionRestoration.js +37 -13
- package/lib/esm/scroll-position/ScrollPositionSaver.js +8 -2
- package/lib/esm/session/Session.js +6 -0
- package/lib/esm/session/navigation/operation/operations.js +4 -4
- package/lib/index.d.ts +10 -9
- package/lib/scroll-position/index.d.ts +11 -11
- package/package.json +1 -1
- package/redux/package.json +2 -1
- package/scroll-position/package.json +2 -1
- package/src/NavigationStack.js +18 -2
- package/src/debug.js +8 -0
- package/src/getLocationFromInternalLocation.js +2 -2
- package/src/navigationBlockers.js +3 -0
- package/src/scroll-position/ScrollPositionAutoSaver.js +19 -2
- package/src/scroll-position/ScrollPositionRestoration.js +100 -53
- package/src/scroll-position/ScrollPositionSaver.js +21 -1
- package/src/session/Session.js +22 -0
- package/src/session/navigation/operation/operations.js +4 -4
- package/test/NavigationStack.test.js +130 -27
- package/test/middlewareTestUtil.js +1 -1
- package/test/redux/locationReducer.test.js +1 -1
- package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +5 -5
- package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +2 -2
- package/test/redux/middleware/navigationOperationMiddleware.test.js +2 -2
- package/test/scroll-position/ScrollPositionRestoration.test.js +78 -61
- package/test/scroll-position/addScrollableContainer.js +5 -2
- package/test/scroll-position/{addScrollableContainerWithHyperlink.js → addScrollableContainerWithAnchors.js} +8 -2
- package/test/scroll-position/createApp.js +28 -7
- package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
- package/test/session/InMemorySession.test.js +28 -28
- package/test/session/ServerSession.test.js +1 -1
- package/test/session/WebBrowserSession.test.js +17 -17
- package/types/index.d.ts +10 -9
- package/types/scroll-position/index.d.ts +11 -11
- package/test/scroll-position/withScrollableContainerAtIndexPage.js +0 -62
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 0.5.0 / 07.09.2025
|
|
2
|
+
|
|
3
|
+
- Added `location.index` property.
|
|
4
|
+
- `location.operation` is now lowercase:
|
|
5
|
+
- `"INIT"` → `"init"`
|
|
6
|
+
- `"PUSH"` → `"push"`
|
|
7
|
+
- `"REPLACE"` → `"replace"`
|
|
8
|
+
- `"SHIFT"` → `"shift"`
|
|
9
|
+
|
|
10
|
+
# 0.4.0 / 06.09.2025
|
|
11
|
+
|
|
12
|
+
- Initial release after a refactoring.
|
package/README.md
CHANGED
|
@@ -65,7 +65,8 @@ navigationStack.push('/new-location')
|
|
|
65
65
|
navigationStack.replace('/new-location')
|
|
66
66
|
|
|
67
67
|
// Sets the `location` to be a previous one (if there is one).
|
|
68
|
-
// If there's no such `location` in the navigation history,
|
|
68
|
+
// If there's no such `location` in the navigation history,
|
|
69
|
+
// throws a `NavigationOutOfBoundsError` error that has an `index` property.
|
|
69
70
|
//
|
|
70
71
|
// One could think of it as an equivalent of clicking a "Back" button in a web browser.
|
|
71
72
|
//
|
|
@@ -76,7 +77,8 @@ navigationStack.replace('/new-location')
|
|
|
76
77
|
navigationStack.shift(-1)
|
|
77
78
|
|
|
78
79
|
// Sets the `location` to be a next one (if there is one).
|
|
79
|
-
// If there's no such `location` in the navigation history,
|
|
80
|
+
// If there's no such `location` in the navigation history,
|
|
81
|
+
// throws a `NavigationOutOfBoundsError` error that has an `index` property.
|
|
80
82
|
//
|
|
81
83
|
// One could think of it as an equivalent of clicking a "Forward" button in a web browser.
|
|
82
84
|
//
|
|
@@ -134,7 +136,8 @@ To get the current location, use `navigationStack.current()`.
|
|
|
134
136
|
|
|
135
137
|
Current `location` object has all the properties of a [standard web browser location](https://developer.mozilla.org/en-US/docs/Web/API/Window/location) with the addition of:
|
|
136
138
|
* `query: object` — URL query parameters.
|
|
137
|
-
* `key: string` —
|
|
139
|
+
* `key: string` — A string ID of the location that is guaranteed to be unique within the session's limits and could be used as a "key" to store any supplementary data associated to this location.
|
|
140
|
+
* `index: number` — The index of the location in the navigation stack, starting with `0` for the initial location.
|
|
138
141
|
|
|
139
142
|
<!-- ## Subscribe to Location Changes -->
|
|
140
143
|
|
|
@@ -224,7 +227,7 @@ navigationStack.init()
|
|
|
224
227
|
// Render the initial location.
|
|
225
228
|
document.body.innerHTML = '<div> Initial Location </div>'
|
|
226
229
|
|
|
227
|
-
//
|
|
230
|
+
// As soon as a page has been rendered, without any delay, tell `NavigationStack` to restore
|
|
228
231
|
// a previously-saved scroll position, if there's any.
|
|
229
232
|
//
|
|
230
233
|
// This method must be called both for the initial location and any subsequent location.
|
|
@@ -244,6 +247,7 @@ navigationStack.push('/new-location')
|
|
|
244
247
|
document.body.innerHTML = '<div> New Location </div>'
|
|
245
248
|
|
|
246
249
|
// The new location is now rendered.
|
|
250
|
+
// Immediately after it has been rendered, call `.locationRenered()`.
|
|
247
251
|
// There's no scroll position to restore because it's not a previously-visited location.
|
|
248
252
|
navigationStack.locationRendered()
|
|
249
253
|
|
|
@@ -260,6 +264,7 @@ navigationStack.shift(-1)
|
|
|
260
264
|
document.body.innerHTML = '<div> Initial Location </div>'
|
|
261
265
|
|
|
262
266
|
// The initial location is now rendered.
|
|
267
|
+
// Immediately after it has been rendered, call `.locationRenered()`.
|
|
263
268
|
// Restores the scroll position at the initial location.
|
|
264
269
|
navigationStack.locationRendered()
|
|
265
270
|
|
|
@@ -276,7 +281,7 @@ navigationStack.stop()
|
|
|
276
281
|
`NavigationStack` provides methods:
|
|
277
282
|
|
|
278
283
|
* `addScrollableContainer(key: string, element: Element)` — Use it in cases when it should restore not only the page scroll position but also the scroll position(s) of any other scrollable container(s). Returns a "remove scrollable container" function.
|
|
279
|
-
* `locationRendered()` — Call it every time a different location has been rendered, i.e. immediately after a different location has been rendered
|
|
284
|
+
* `locationRendered()` — Call it every time a different location has been rendered, including the initial location, without any delay, i.e. immediately after a different location has been rendered.
|
|
280
285
|
|
|
281
286
|
<details>
|
|
282
287
|
<summary>Using scroll position restoration feature without <code>NavigationStack</code></summary>
|
|
@@ -309,8 +314,8 @@ window.history.replaceState({ key: '123' }, '', '/initial-location')
|
|
|
309
314
|
// Render the initial location.
|
|
310
315
|
document.body.innerHTML = '<div> Initial Location </div>'
|
|
311
316
|
|
|
312
|
-
//
|
|
313
|
-
// with the "current location" object as the argument.
|
|
317
|
+
// Immediately after a page has been rendered, without any delay,
|
|
318
|
+
// call `.locationRendered()` method with the "current location" object as the argument.
|
|
314
319
|
scrollPositionRestoration.locationRendered({ key: '123', pathname: '/initial-location' })
|
|
315
320
|
|
|
316
321
|
//----------------------------------------------------------------------------------------
|
|
@@ -329,6 +334,7 @@ window.history.pushState({ key: '456' }, '', '/new-location')
|
|
|
329
334
|
document.body.innerHTML = '<div> New Location </div>'
|
|
330
335
|
|
|
331
336
|
// The new location is now rendered.
|
|
337
|
+
// Call `.locationRendered()` immediately after it has been rendered, i.e. without any delay.
|
|
332
338
|
// There's no scroll position to restore because it's not a previously-visited location.
|
|
333
339
|
// The "current location" object must have a `key`.
|
|
334
340
|
scrollPositionRestoration.locationRendered({ key: '456', pathname: '/new-location' })
|
|
@@ -349,7 +355,8 @@ window.history.go(-1)
|
|
|
349
355
|
document.body.innerHTML = '<div> Initial Location </div>'
|
|
350
356
|
|
|
351
357
|
// The initial location is now rendered.
|
|
352
|
-
//
|
|
358
|
+
// Call `.locationRendered()` immediately after it has been rendered, i.e. without any delay.
|
|
359
|
+
// It will restore the scroll position at the initial location.
|
|
353
360
|
// The "current location" object must have a `key`.
|
|
354
361
|
scrollPositionRestoration.locationRendered({ key: '123', pathname: '/initial-location' })
|
|
355
362
|
|
|
@@ -372,7 +379,7 @@ scrollPositionRestoration.stop()
|
|
|
372
379
|
`ScrollPositionRestoration` provides methods:
|
|
373
380
|
|
|
374
381
|
* `addScrollableContainer(key: string, element: Element)` — Use it in cases when it should restore not only the page scroll position but also the scroll position(s) of any other scrollable container(s). Returns a "remove scrollable container" function.
|
|
375
|
-
* `locationRendered(location)` — Call it every time a different location has been rendered, i.e. immediately after a different location has been rendered
|
|
382
|
+
* `locationRendered(location)` — Call it every time a different location has been rendered, including the initial location, without any delay, i.e. immediately after a different location has been rendered. The location argument must have a `key`.
|
|
376
383
|
* `stop()` — Stops scroll position restoration and clears any listeners or timers.
|
|
377
384
|
</details>
|
|
378
385
|
|
|
@@ -449,6 +456,8 @@ navigationStack.push('/new-location')
|
|
|
449
456
|
|
|
450
457
|
######
|
|
451
458
|
|
|
459
|
+
Every "session" has a unique `key`.
|
|
460
|
+
|
|
452
461
|
Once created, a "session" is simply passed to the `NavigationStack` constructor and then you don't have to deal with it anymore — `NavigationStack` will pull all the strings for you.
|
|
453
462
|
|
|
454
463
|
However, if someone prefers to completely bypass `NavigationStack` and interact with a "session" object directly, they could do so.
|
|
@@ -460,7 +469,6 @@ However, if someone prefers to completely bypass `NavigationStack` and interact
|
|
|
460
469
|
|
|
461
470
|
* `key: string` — A unique ID of the session.
|
|
462
471
|
* `subscribe(listener: (location) => {}): () => {}` — Subscribes to location changes, including setting the initial location. Returns an "unsubscribe" function. The `location` argument of the listener function is an "extended" location object having additional properties:
|
|
463
|
-
* `index: number` — The index of the location in the session's navigation history, starting with `0` for the initial location.
|
|
464
472
|
* `operation: string` — The type of navigation that led to the location.
|
|
465
473
|
* `INIT` in case of the initial location before any navigation has taken place.
|
|
466
474
|
* `SHIFT` when the user performs a "Back" or "Forward" navigation, or after a `.shift()` navigation which is essentially a "back or forward navigation".
|
|
@@ -474,7 +482,7 @@ However, if someone prefers to completely bypass `NavigationStack` and interact
|
|
|
474
482
|
* `-1` after the user clicks a "Back" button in their web browser.
|
|
475
483
|
* `1` after the user clicks a "Forward" button in their web browser.
|
|
476
484
|
<!-- * `getInitialLocation(): object?` — Returns the initial location, if the session can get it from somewhere. For example, in a web browser, the initial location can be read from `window.location`. In other environments, such as server side, the initial location can't be read from anywhere. -->
|
|
477
|
-
* `start(initialLocation?: object)` — Starts the session.
|
|
485
|
+
* `start(initialLocation?: object)` — Starts the session. The `initialLocation` argument is optional when the session can read it from somewhere. For example, `WebBrowserSession` can read `initialLocation` from `window.location`.
|
|
478
486
|
* `stop()` — Stops the session. Cleans up any listeners, etc.
|
|
479
487
|
* `navigate(operation: string, location: object)` — Navigates to a `location` using either `"PUSH"` or `"REPLACE"` operation. The `location` argument should be a result of calling `parseInputLocation()` function.
|
|
480
488
|
* `shift(delta: number)` — Navigates "back" or "forward" by skipping a specified count of pages. Negative `delta` skips backwards, positive `delta` skips forward.
|
|
@@ -586,7 +594,7 @@ navigationStack.push('/new-location')
|
|
|
586
594
|
|
|
587
595
|
Navigation blocker should be a function that receives a `newLocation` argument and could be "synchronous" or "asynchronous" (i.e. return a `Promise`, aka `async`/`await`).
|
|
588
596
|
|
|
589
|
-
The `newLocation` argument of a blocker function
|
|
597
|
+
The `newLocation` argument of a blocker function might not necessarily have a `key` or `index` property but other properties are present.
|
|
590
598
|
|
|
591
599
|
Navigation blockers fire both when navigating from one page to another and when closing the current browser tab. In the latter case, `newLocation` argument will be `null`, and also the blocker function can't return a `Promise` (because it won't wait), and returning `true` from it will cause the web browser will to show a confirmation modal with a non-customizable generic browser-specific text like "Leave site? Changes you made might not be saved".
|
|
592
600
|
|
|
@@ -7,13 +7,23 @@ var _Actions = _interopRequireDefault(require("./redux/Actions"));
|
|
|
7
7
|
var _createMiddlewares = _interopRequireDefault(require("./redux/createMiddlewares"));
|
|
8
8
|
var _locationReducer = _interopRequireDefault(require("./redux/locationReducer"));
|
|
9
9
|
var _ScrollPositionRestoration = _interopRequireDefault(require("./scroll-position/ScrollPositionRestoration"));
|
|
10
|
+
const _excluded = ["maintainScrollPosition"];
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
13
|
+
function getCreateMiddlewaresOptions(navigationStackOptions) {
|
|
14
|
+
if (!navigationStackOptions) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
// eslint-disable-next-line no-unused-vars
|
|
18
|
+
const restOptions = _objectWithoutPropertiesLoose(navigationStackOptions, _excluded);
|
|
19
|
+
return restOptions;
|
|
20
|
+
}
|
|
11
21
|
class NavigationStack {
|
|
12
22
|
constructor(session, options) {
|
|
13
23
|
this._session = session;
|
|
14
24
|
|
|
15
25
|
// Create a Redux store.
|
|
16
|
-
this._store = (0, _redux.createStore)(_locationReducer.default, (0, _redux.applyMiddleware)(...(0, _createMiddlewares.default)(session, options)));
|
|
26
|
+
this._store = (0, _redux.createStore)(_locationReducer.default, (0, _redux.applyMiddleware)(...(0, _createMiddlewares.default)(session, getCreateMiddlewaresOptions(options))));
|
|
17
27
|
|
|
18
28
|
// Create `ScrollPositionRestoration`.
|
|
19
29
|
if (options && options.maintainScrollPosition) {
|
|
@@ -65,8 +75,13 @@ class NavigationStack {
|
|
|
65
75
|
}
|
|
66
76
|
locationRendered() {
|
|
67
77
|
if (this._scrollPositionRestoration) {
|
|
68
|
-
this.
|
|
78
|
+
const location = this.current();
|
|
79
|
+
if (!location) {
|
|
80
|
+
throw new Error('Not initialized');
|
|
81
|
+
}
|
|
82
|
+
return this._scrollPositionRestoration.locationRendered(location);
|
|
69
83
|
}
|
|
84
|
+
return Promise.resolve();
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
exports.default = NavigationStack;
|
package/lib/cjs/debug.js
ADDED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = getLocationFromInternalLocation;
|
|
5
|
-
const _excluded = ["operation", "
|
|
5
|
+
const _excluded = ["operation", "delta"];
|
|
6
6
|
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
7
7
|
// Converts `LocationInternal` object to a publicly-visible `Location` object.
|
|
8
|
-
// It hides non-essential properties of location such as `operation
|
|
8
|
+
// It hides non-essential properties of location such as `operation` and `delta`.
|
|
9
9
|
function getLocationFromInternalLocation(internalLocation) {
|
|
10
10
|
// eslint-disable-next-line no-unused-vars
|
|
11
11
|
const location = _objectWithoutPropertiesLoose(internalLocation, _excluded);
|
|
@@ -5,6 +5,7 @@ exports.addNavigationBlocker = addNavigationBlocker;
|
|
|
5
5
|
exports.getNavigationBlockers = getNavigationBlockers;
|
|
6
6
|
exports.removeAllNavigationBlockers = removeAllNavigationBlockers;
|
|
7
7
|
exports.runNavigationBlockers = runNavigationBlockers;
|
|
8
|
+
var _debug = _interopRequireDefault(require("./debug"));
|
|
8
9
|
var _isPromise = _interopRequireDefault(require("./isPromise"));
|
|
9
10
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
11
|
/* eslint-disable no-underscore-dangle */
|
|
@@ -80,12 +81,14 @@ function runNavigationBlockers(navigationBlockers, toLocation) {
|
|
|
80
81
|
if ((0, _isPromise.default)(result)) {
|
|
81
82
|
return result.then(resultValue => {
|
|
82
83
|
if (resultValue) {
|
|
84
|
+
(0, _debug.default)('Navigation blocked', toLocation.pathname);
|
|
83
85
|
return resultValue;
|
|
84
86
|
}
|
|
85
87
|
return next();
|
|
86
88
|
});
|
|
87
89
|
}
|
|
88
90
|
if (result) {
|
|
91
|
+
(0, _debug.default)('Navigation blocked', toLocation.pathname);
|
|
89
92
|
return result;
|
|
90
93
|
}
|
|
91
94
|
return next();
|
|
@@ -4,6 +4,7 @@ exports.__esModule = true;
|
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
var _constants = require("./constants");
|
|
6
6
|
var _scheduleNextTick = _interopRequireDefault(require("./scheduleNextTick"));
|
|
7
|
+
var _debug = _interopRequireDefault(require("../debug"));
|
|
7
8
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
9
|
/* eslint-disable no-underscore-dangle */
|
|
9
10
|
|
|
@@ -62,15 +63,21 @@ class ScrollPositionAutoSaver {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
|
-
cancelSavePageScrollPosition() {
|
|
66
|
+
cancelSavePageScrollPosition(hasRun) {
|
|
66
67
|
if (this._cancelSavePageScrollPosition) {
|
|
68
|
+
if (!hasRun) {
|
|
69
|
+
(0, _debug.default)('cancel delayed save scroll position', _constants.PAGE_SCROLLABLE_CONTAINER_KEY);
|
|
70
|
+
}
|
|
67
71
|
this._cancelSavePageScrollPosition();
|
|
68
72
|
this._cancelSavePageScrollPosition = null;
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
|
-
cancelSaveScrollableContainerScrollPosition(scrollableContainerKey) {
|
|
75
|
+
cancelSaveScrollableContainerScrollPosition(scrollableContainerKey, hasRun) {
|
|
72
76
|
const scrollableContainerEntry = this._getScrollableContainers()[scrollableContainerKey];
|
|
73
77
|
if (scrollableContainerEntry.cancelSaveScrollPosition) {
|
|
78
|
+
if (!hasRun) {
|
|
79
|
+
(0, _debug.default)('cancel delayed save scroll position', scrollableContainerKey);
|
|
80
|
+
}
|
|
74
81
|
scrollableContainerEntry.cancelSaveScrollPosition();
|
|
75
82
|
scrollableContainerEntry.cancelSaveScrollPosition = null;
|
|
76
83
|
}
|
|
@@ -101,7 +108,9 @@ class ScrollPositionAutoSaver {
|
|
|
101
108
|
// because there might be too many in a given short period of time
|
|
102
109
|
// which could affect the performance of the application.
|
|
103
110
|
if (!scrollableContainerEntry.cancelSaveScrollPosition) {
|
|
111
|
+
(0, _debug.default)('scroll detected', scrollableContainerKey);
|
|
104
112
|
scrollableContainerEntry.cancelSaveScrollPosition = (0, _scheduleNextTick.default)(() => {
|
|
113
|
+
(0, _debug.default)('auto-save scroll position after scroll', scrollableContainerKey);
|
|
105
114
|
this._scrollPositionSaver.saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainerEntry.scrollableContainer);
|
|
106
115
|
});
|
|
107
116
|
}
|
|
@@ -110,6 +119,8 @@ class ScrollPositionAutoSaver {
|
|
|
110
119
|
addPageScrollListener() {
|
|
111
120
|
// Set up scroll listener on the page.
|
|
112
121
|
this._removePageScrollListener = this._scrollPosition.addPageScrollListener(() => {
|
|
122
|
+
(0, _debug.default)('scroll detected', _constants.PAGE_SCROLLABLE_CONTAINER_KEY);
|
|
123
|
+
|
|
113
124
|
// This flag is not used in real life and is only used in tests (for some reason).
|
|
114
125
|
if (!this._shouldSaveScrollPosition()) {
|
|
115
126
|
return;
|
|
@@ -7,6 +7,7 @@ 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"));
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
/* eslint-disable no-underscore-dangle */
|
|
12
13
|
|
|
@@ -53,8 +54,10 @@ class ScrollPositionRestoration {
|
|
|
53
54
|
running
|
|
54
55
|
}) => {
|
|
55
56
|
if (running) {
|
|
57
|
+
(0, _debug.default)('▶ running');
|
|
56
58
|
this._disableAutomaticScrollRestoration();
|
|
57
59
|
} else {
|
|
60
|
+
(0, _debug.default)('⏹ not running');
|
|
58
61
|
this._enableAutomaticScrollRestoration();
|
|
59
62
|
|
|
60
63
|
// There might be previous scroll position already saved in the data storage.
|
|
@@ -130,11 +133,11 @@ class ScrollPositionRestoration {
|
|
|
130
133
|
// This function is only used in tests.
|
|
131
134
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
132
135
|
// It's only used in tests.
|
|
133
|
-
|
|
136
|
+
_getSavedScrollPositionOnLocationChange: _options && _options._getSavedPageScrollPositionOnLocationChange,
|
|
134
137
|
// This function is only used in tests.
|
|
135
138
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
136
139
|
// It's only used in tests.
|
|
137
|
-
|
|
140
|
+
_shouldSetScrollPositionOnLocationChange: _options && _options._shouldSetPageScrollPositionOnLocationChange
|
|
138
141
|
};
|
|
139
142
|
}
|
|
140
143
|
addScrollableContainer(scrollableContainerKey, scrollableContainer, _options) {
|
|
@@ -145,10 +148,17 @@ class ScrollPositionRestoration {
|
|
|
145
148
|
// this._scrollableContainerKeyCounter++;
|
|
146
149
|
// const scrollableContainerKey = String(this._scrollableContainerKeyCounter);
|
|
147
150
|
|
|
151
|
+
// Validate `scrollableContainerKey`.
|
|
148
152
|
if (scrollableContainerKey === _constants.PAGE_SCROLLABLE_CONTAINER_KEY) {
|
|
149
153
|
throw new Error(`Scrollable container key "${scrollableContainerKey}" is not allowed`);
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
// Check that it hasn't already been added.
|
|
157
|
+
if (this._scrollableContainers[scrollableContainerKey]) {
|
|
158
|
+
throw new Error(`Scrollable container key "${scrollableContainerKey}" is already added`);
|
|
159
|
+
}
|
|
160
|
+
(0, _debug.default)('add scrollable container', scrollableContainerKey);
|
|
161
|
+
|
|
152
162
|
// Add scrollable container entry.
|
|
153
163
|
this._scrollableContainers[scrollableContainerKey] = {
|
|
154
164
|
// Scrollable container element.
|
|
@@ -162,11 +172,11 @@ class ScrollPositionRestoration {
|
|
|
162
172
|
// This function is only used in tests.
|
|
163
173
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
164
174
|
// It's only used in tests.
|
|
165
|
-
|
|
175
|
+
_shouldSetScrollPositionOnLocationChange: _options && _options._shouldSetScrollPositionOnLocationChange,
|
|
166
176
|
// This function is only used in tests.
|
|
167
177
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
168
178
|
// It's only used in tests.
|
|
169
|
-
|
|
179
|
+
_getSavedScrollPositionOnLocationChange: _options && _options._getSavedScrollPositionOnLocationChange
|
|
170
180
|
};
|
|
171
181
|
|
|
172
182
|
// Scrollable containers could be added at any time, including page mount.
|
|
@@ -178,8 +188,10 @@ class ScrollPositionRestoration {
|
|
|
178
188
|
if (this._location) {
|
|
179
189
|
const previouslySavedScrollPosition = this._getSavedScrollPositionForLocation(this._location, scrollableContainerKey);
|
|
180
190
|
if (previouslySavedScrollPosition) {
|
|
191
|
+
(0, _debug.default)('restore scroll position on add scrollable container', this._location.pathname, scrollableContainerKey, previouslySavedScrollPosition);
|
|
181
192
|
this._scrollPosition.setScrollableContainerScrollPosition(scrollableContainer, previouslySavedScrollPosition);
|
|
182
193
|
} else {
|
|
194
|
+
(0, _debug.default)('save scroll position on add scrollable container', this._location.pathname, scrollableContainerKey);
|
|
183
195
|
this._scrollPositionSaver.saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer);
|
|
184
196
|
}
|
|
185
197
|
}
|
|
@@ -189,6 +201,7 @@ class ScrollPositionRestoration {
|
|
|
189
201
|
|
|
190
202
|
// Removes the scrollable container.
|
|
191
203
|
return () => {
|
|
204
|
+
(0, _debug.default)('remove scrollable container', scrollableContainerKey);
|
|
192
205
|
this._scrollPositionSaver._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(scrollableContainerKey);
|
|
193
206
|
this._scrollPositionSaver._scrollPositionAutoSaver.removeScrollableContainerScrollListener(scrollableContainerKey);
|
|
194
207
|
delete this._scrollableContainers[scrollableContainerKey];
|
|
@@ -247,14 +260,20 @@ class ScrollPositionRestoration {
|
|
|
247
260
|
//
|
|
248
261
|
// // Save the current scroll position on the current page while it's still rendered.
|
|
249
262
|
// // This saved scroll position could later be restored in case of returing to this page.
|
|
263
|
+
// // Even if the current scroll position is a default one (scrolled to top), it should still
|
|
264
|
+
// // be saved in order to overwrite any potential previously-saved non-default scroll position.
|
|
250
265
|
// this._scrollPositionSaver.saveScrollPosition();
|
|
251
266
|
// };
|
|
252
267
|
|
|
268
|
+
// Should be called whenever a different location has been rendered (i.e. immediately after).
|
|
269
|
+
// Returns a Promise that resolves when finished restoring scroll position.
|
|
270
|
+
// There's no need to await for that Promise. It's just there because it exists.
|
|
253
271
|
locationRendered(location) {
|
|
254
272
|
// Validate that `location` has a `key`.
|
|
255
273
|
if (!location.key) {
|
|
256
274
|
throw new Error('`location` must have a `key`');
|
|
257
275
|
}
|
|
276
|
+
(0, _debug.default)('rendered location', location.pathname);
|
|
258
277
|
this._prevLocation = this._location;
|
|
259
278
|
this._location = location;
|
|
260
279
|
this._scrollPosition.init();
|
|
@@ -285,7 +304,7 @@ class ScrollPositionRestoration {
|
|
|
285
304
|
|
|
286
305
|
// Set the scroll position for the new page:
|
|
287
306
|
// either restore a previously-saved one or set it to a default scroll position.
|
|
288
|
-
this._setScrollPosition();
|
|
307
|
+
return this._setScrollPosition();
|
|
289
308
|
}
|
|
290
309
|
|
|
291
310
|
// Tells if the current scroll position is the default one.
|
|
@@ -304,16 +323,20 @@ class ScrollPositionRestoration {
|
|
|
304
323
|
}
|
|
305
324
|
return true;
|
|
306
325
|
}
|
|
326
|
+
|
|
327
|
+
// Restores scroll position.
|
|
328
|
+
// Returns a Promise that resolves when finished setting scroll position.
|
|
329
|
+
// There's no need to await for this Promise. It just exists.
|
|
307
330
|
_setScrollPosition() {
|
|
308
|
-
|
|
331
|
+
return Promise.all(Object.keys(this._scrollableContainers).map(scrollableContainerKey => {
|
|
309
332
|
const scrollableContainerEntry = this._scrollableContainers[scrollableContainerKey];
|
|
310
333
|
|
|
311
334
|
// This function is only used in tests.
|
|
312
335
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
313
336
|
// It's only used in tests.
|
|
314
|
-
if (scrollableContainerEntry.
|
|
315
|
-
if (!scrollableContainerEntry.
|
|
316
|
-
|
|
337
|
+
if (scrollableContainerEntry._shouldSetScrollPositionOnLocationChange) {
|
|
338
|
+
if (!scrollableContainerEntry._shouldSetScrollPositionOnLocationChange(this._location, this._prevLocation)) {
|
|
339
|
+
return Promise.resolve();
|
|
317
340
|
}
|
|
318
341
|
}
|
|
319
342
|
|
|
@@ -323,18 +346,19 @@ class ScrollPositionRestoration {
|
|
|
323
346
|
// This function is only used in tests.
|
|
324
347
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
325
348
|
// It's only used in tests.
|
|
326
|
-
if (scrollableContainerEntry.
|
|
327
|
-
scrollPositionOrAnchorToSet = scrollableContainerEntry.
|
|
349
|
+
if (scrollableContainerEntry._getSavedScrollPositionOnLocationChange) {
|
|
350
|
+
scrollPositionOrAnchorToSet = scrollableContainerEntry._getSavedScrollPositionOnLocationChange(this._location, this._prevLocation);
|
|
328
351
|
}
|
|
329
352
|
|
|
330
353
|
// Get scroll position (or anchor) to set.
|
|
331
354
|
if (!scrollPositionOrAnchorToSet) {
|
|
332
355
|
scrollPositionOrAnchorToSet = scrollableContainerKey === _constants.PAGE_SCROLLABLE_CONTAINER_KEY ? this._getPageScrollPositionOrAnchorToSet(this._location) : this._getScrollableContainerScrollPositionToSet(this._location, scrollableContainerKey);
|
|
333
356
|
}
|
|
357
|
+
(0, _debug.default)('restore scroll position', this._location.pathname, scrollableContainerKey, scrollPositionOrAnchorToSet);
|
|
334
358
|
|
|
335
359
|
// Set scroll position of scrollable container.
|
|
336
|
-
scrollableContainerEntry.scrollPositionSetter.set(scrollableContainerEntry.scrollableContainer, scrollPositionOrAnchorToSet, this._scrollPosition);
|
|
337
|
-
}
|
|
360
|
+
return scrollableContainerEntry.scrollPositionSetter.set(scrollableContainerEntry.scrollableContainer, scrollPositionOrAnchorToSet, this._scrollPosition);
|
|
361
|
+
}));
|
|
338
362
|
}
|
|
339
363
|
_getSavedScrollPositionForLocation(location, scrollableContainerKey = _constants.PAGE_SCROLLABLE_CONTAINER_KEY) {
|
|
340
364
|
return this._locationDataStorage.get(location, scrollableContainerKey);
|
|
@@ -4,6 +4,7 @@ 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"));
|
|
7
8
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
9
|
/* eslint-disable no-underscore-dangle */
|
|
9
10
|
|
|
@@ -41,6 +42,7 @@ class ScrollPositionSaver {
|
|
|
41
42
|
if (!this._shouldSaveScrollPosition()) {
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
45
|
+
(0, _debug.default)('save scroll position', this._getLocation().pathname);
|
|
44
46
|
|
|
45
47
|
// Get scrollable containers.
|
|
46
48
|
const scrollableContainers = this._getScrollableContainers();
|
|
@@ -55,23 +57,27 @@ class ScrollPositionSaver {
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
savePageScrollPosition() {
|
|
60
|
+
(0, _debug.default)('save scroll position', this._getLocation().pathname, _constants.PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
|
|
61
|
+
|
|
58
62
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
59
63
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
60
64
|
// cancel it and save scroll position right now instead.
|
|
61
65
|
// * If this is a scheduled "auto-save" of scroll position,
|
|
62
66
|
// clear the "cancel" function because it's no longer of use.
|
|
63
|
-
this._scrollPositionAutoSaver.cancelSavePageScrollPosition();
|
|
67
|
+
this._scrollPositionAutoSaver.cancelSavePageScrollPosition(true);
|
|
64
68
|
|
|
65
69
|
// Save scroll position.
|
|
66
70
|
this._saveScrollPositionForLocation(this._getLocation(), undefined, this._scrollPosition.getPageScrollPosition());
|
|
67
71
|
}
|
|
68
72
|
saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer) {
|
|
73
|
+
(0, _debug.default)('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
|
|
74
|
+
|
|
69
75
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
70
76
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
71
77
|
// cancel it and save scroll position right now instead.
|
|
72
78
|
// * If this is a scheduled "auto-save" of scroll position,
|
|
73
79
|
// clear the "cancel" function because it's no longer of use.
|
|
74
|
-
this._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(scrollableContainerKey);
|
|
80
|
+
this._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(scrollableContainerKey, true);
|
|
75
81
|
|
|
76
82
|
// Save scroll position.
|
|
77
83
|
this._saveScrollPositionForLocation(this._getLocation(), scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = void 0;
|
|
5
|
+
var _debug = _interopRequireDefault(require("../debug"));
|
|
5
6
|
var _parseInputLocation = _interopRequireDefault(require("../parseInputLocation"));
|
|
6
7
|
var _createSessionKey = _interopRequireDefault(require("./key/createSessionKey"));
|
|
7
8
|
var _NavigationOutOfBoundsError = _interopRequireDefault(require("./navigation/error/NavigationOutOfBoundsError"));
|
|
@@ -56,6 +57,7 @@ class Session {
|
|
|
56
57
|
// but if it was possible, this call would be required. It would also be required
|
|
57
58
|
// by `navigation` to call `session.getNextKey()` function to increment `locationKeyIndex`.
|
|
58
59
|
this._updateTerminalLocationIndex(location);
|
|
60
|
+
(0, _debug.default)('current location', location.pathname, 'index', this._currentLocationIndex);
|
|
59
61
|
});
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -95,6 +97,7 @@ class Session {
|
|
|
95
97
|
if (this._currentLocationIndex !== INITIAL_INDEX) {
|
|
96
98
|
throw new Error('Already started');
|
|
97
99
|
}
|
|
100
|
+
(0, _debug.default)('▶ start session', initialLocation.pathname);
|
|
98
101
|
this._started = true;
|
|
99
102
|
const key = this._getNextLocationKey();
|
|
100
103
|
const index = INITIAL_INDEX + 1;
|
|
@@ -113,6 +116,7 @@ class Session {
|
|
|
113
116
|
if (this._stopped) {
|
|
114
117
|
throw Error('Already stopped');
|
|
115
118
|
}
|
|
119
|
+
(0, _debug.default)('⏹ stop session');
|
|
116
120
|
|
|
117
121
|
// Once stopped, it won't be able to be restarted.
|
|
118
122
|
this._stopped = true;
|
|
@@ -138,6 +142,7 @@ class Session {
|
|
|
138
142
|
});
|
|
139
143
|
const key = this._getNextLocationKey();
|
|
140
144
|
const index = this._currentLocationIndex + delta;
|
|
145
|
+
(0, _debug.default)(operation === _operations.default.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
|
|
141
146
|
|
|
142
147
|
// Navigate to the location.
|
|
143
148
|
const locationResult = this._navigation.navigate(location, {
|
|
@@ -160,6 +165,7 @@ class Session {
|
|
|
160
165
|
return;
|
|
161
166
|
}
|
|
162
167
|
const index = this._currentLocationIndex + delta;
|
|
168
|
+
(0, _debug.default)(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
|
|
163
169
|
|
|
164
170
|
// Validate that the new `index` is not out of bounds.
|
|
165
171
|
if (index < 0 || index > this._terminalLocationIndex) {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
var _default = exports.default = {
|
|
6
|
-
INIT: '
|
|
7
|
-
PUSH: '
|
|
8
|
-
REPLACE: '
|
|
9
|
-
SHIFT: '
|
|
6
|
+
INIT: 'init',
|
|
7
|
+
PUSH: 'push',
|
|
8
|
+
REPLACE: 'replace',
|
|
9
|
+
SHIFT: 'shift'
|
|
10
10
|
};
|
|
11
11
|
module.exports = exports.default;
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
const _excluded = ["maintainScrollPosition"];
|
|
2
|
+
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
1
3
|
import { applyMiddleware, createStore } from 'redux';
|
|
2
4
|
import Actions from './redux/Actions';
|
|
3
5
|
import createMiddlewares from './redux/createMiddlewares';
|
|
4
6
|
import locationReducer from './redux/locationReducer';
|
|
5
7
|
import ScrollPositionRestoration from './scroll-position/ScrollPositionRestoration';
|
|
8
|
+
function getCreateMiddlewaresOptions(navigationStackOptions) {
|
|
9
|
+
if (!navigationStackOptions) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
// eslint-disable-next-line no-unused-vars
|
|
13
|
+
const restOptions = _objectWithoutPropertiesLoose(navigationStackOptions, _excluded);
|
|
14
|
+
return restOptions;
|
|
15
|
+
}
|
|
6
16
|
export default class NavigationStack {
|
|
7
17
|
constructor(session, options) {
|
|
8
18
|
this._session = session;
|
|
9
19
|
|
|
10
20
|
// Create a Redux store.
|
|
11
|
-
this._store = createStore(locationReducer, applyMiddleware(...createMiddlewares(session, options)));
|
|
21
|
+
this._store = createStore(locationReducer, applyMiddleware(...createMiddlewares(session, getCreateMiddlewaresOptions(options))));
|
|
12
22
|
|
|
13
23
|
// Create `ScrollPositionRestoration`.
|
|
14
24
|
if (options && options.maintainScrollPosition) {
|
|
@@ -60,7 +70,12 @@ export default class NavigationStack {
|
|
|
60
70
|
}
|
|
61
71
|
locationRendered() {
|
|
62
72
|
if (this._scrollPositionRestoration) {
|
|
63
|
-
this.
|
|
73
|
+
const location = this.current();
|
|
74
|
+
if (!location) {
|
|
75
|
+
throw new Error('Not initialized');
|
|
76
|
+
}
|
|
77
|
+
return this._scrollPositionRestoration.locationRendered(location);
|
|
64
78
|
}
|
|
79
|
+
return Promise.resolve();
|
|
65
80
|
}
|
|
66
81
|
}
|
package/lib/esm/debug.js
ADDED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const _excluded = ["operation", "
|
|
1
|
+
const _excluded = ["operation", "delta"];
|
|
2
2
|
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
3
3
|
// Converts `LocationInternal` object to a publicly-visible `Location` object.
|
|
4
|
-
// It hides non-essential properties of location such as `operation
|
|
4
|
+
// It hides non-essential properties of location such as `operation` and `delta`.
|
|
5
5
|
export default function getLocationFromInternalLocation(internalLocation) {
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
7
|
const location = _objectWithoutPropertiesLoose(internalLocation, _excluded);
|