navigation-stack 0.4.0 → 0.5.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +20 -12
  3. package/lib/cjs/NavigationStack.js +17 -2
  4. package/lib/cjs/debug.js +12 -0
  5. package/lib/cjs/getLocationFromInternalLocation.js +2 -2
  6. package/lib/cjs/navigationBlockers.js +3 -0
  7. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +13 -2
  8. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +29 -5
  9. package/lib/cjs/scroll-position/ScrollPositionSaver.js +8 -2
  10. package/lib/cjs/session/Session.js +6 -0
  11. package/lib/cjs/session/navigation/operation/operations.js +4 -4
  12. package/lib/esm/NavigationStack.js +17 -2
  13. package/lib/esm/debug.js +7 -0
  14. package/lib/esm/getLocationFromInternalLocation.js +2 -2
  15. package/lib/esm/navigationBlockers.js +3 -0
  16. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +13 -2
  17. package/lib/esm/scroll-position/ScrollPositionRestoration.js +29 -5
  18. package/lib/esm/scroll-position/ScrollPositionSaver.js +8 -2
  19. package/lib/esm/session/Session.js +6 -0
  20. package/lib/esm/session/navigation/operation/operations.js +4 -4
  21. package/lib/index.d.ts +10 -9
  22. package/lib/scroll-position/index.d.ts +3 -3
  23. package/package.json +1 -1
  24. package/src/NavigationStack.js +18 -2
  25. package/src/debug.js +8 -0
  26. package/src/getLocationFromInternalLocation.js +2 -2
  27. package/src/navigationBlockers.js +3 -0
  28. package/src/scroll-position/ScrollPositionAutoSaver.js +19 -2
  29. package/src/scroll-position/ScrollPositionRestoration.js +92 -47
  30. package/src/scroll-position/ScrollPositionSaver.js +21 -1
  31. package/src/session/Session.js +22 -0
  32. package/src/session/navigation/operation/operations.js +4 -4
  33. package/test/NavigationStack.test.js +14 -14
  34. package/test/middlewareTestUtil.js +1 -1
  35. package/test/redux/locationReducer.test.js +1 -1
  36. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +5 -5
  37. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +2 -2
  38. package/test/redux/middleware/navigationOperationMiddleware.test.js +2 -2
  39. package/test/scroll-position/ScrollPositionRestoration.test.js +73 -56
  40. package/test/scroll-position/addScrollableContainer.js +5 -2
  41. package/test/scroll-position/{addScrollableContainerWithHyperlink.js → addScrollableContainerWithAnchors.js} +8 -2
  42. package/test/scroll-position/createApp.js +20 -0
  43. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
  44. package/test/session/InMemorySession.test.js +28 -28
  45. package/test/session/ServerSession.test.js +1 -1
  46. package/test/session/WebBrowserSession.test.js +17 -17
  47. package/types/index.d.ts +10 -9
  48. package/types/scroll-position/index.d.ts +3 -3
  49. 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, throws a `NavigationOutOfBoundsError`.
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, throws a `NavigationOutOfBoundsError`.
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` — a unique ID of the `location` object within the session.
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
- // When a page has been rendered, tell `NavigationStack` to restore
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, including the initial location.
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
- // When a page has been rendered, call "did render location" listener
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
- // Restores the scroll position at the initial location.
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, including the initial location. The location argument should be a `navigation-stack` location.
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 won't necessarily have a `key` property but other properties are present.
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._scrollPositionRestoration.locationRendered(this.current());
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;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = debug;
5
+ const DEBUG = false;
6
+ function debug(...args) {
7
+ if (DEBUG) {
8
+ // eslint-disable-next-line no-console
9
+ console.log(...args);
10
+ }
11
+ }
12
+ module.exports = exports.default;
@@ -2,10 +2,10 @@
2
2
 
3
3
  exports.__esModule = true;
4
4
  exports.default = getLocationFromInternalLocation;
5
- const _excluded = ["operation", "index", "delta"];
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`, `index`, `delta`.
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.
@@ -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.
@@ -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,8 +323,12 @@ 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
- for (const scrollableContainerKey of Object.keys(this._scrollableContainers)) {
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.
@@ -313,7 +336,7 @@ class ScrollPositionRestoration {
313
336
  // It's only used in tests.
314
337
  if (scrollableContainerEntry._shouldUpdateScrollPositionForLocation) {
315
338
  if (!scrollableContainerEntry._shouldUpdateScrollPositionForLocation(this._location, this._prevLocation)) {
316
- continue;
339
+ return Promise.resolve();
317
340
  }
318
341
  }
319
342
 
@@ -331,10 +354,11 @@ class ScrollPositionRestoration {
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: 'INIT',
7
- PUSH: 'PUSH',
8
- REPLACE: 'REPLACE',
9
- SHIFT: '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._scrollPositionRestoration.locationRendered(this.current());
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
  }
@@ -0,0 +1,7 @@
1
+ const DEBUG = false;
2
+ export default function debug(...args) {
3
+ if (DEBUG) {
4
+ // eslint-disable-next-line no-console
5
+ console.log(...args);
6
+ }
7
+ }
@@ -1,7 +1,7 @@
1
- const _excluded = ["operation", "index", "delta"];
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`, `index`, `delta`.
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);
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-underscore-dangle */
2
2
 
3
+ import debug from './debug';
3
4
  import isPromise from './isPromise';
4
5
  export function getNavigationBlockers(session) {
5
6
  return session._navigationBlockersList || [];
@@ -72,12 +73,14 @@ export function runNavigationBlockers(navigationBlockers, toLocation) {
72
73
  if (isPromise(result)) {
73
74
  return result.then(resultValue => {
74
75
  if (resultValue) {
76
+ debug('Navigation blocked', toLocation.pathname);
75
77
  return resultValue;
76
78
  }
77
79
  return next();
78
80
  });
79
81
  }
80
82
  if (result) {
83
+ debug('Navigation blocked', toLocation.pathname);
81
84
  return result;
82
85
  }
83
86
  return next();
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
4
4
  import scheduleNextTick from './scheduleNextTick';
5
+ import debug from '../debug';
5
6
  export default class ScrollPositionAutoSaver {
6
7
  constructor({
7
8
  scrollPosition,
@@ -57,15 +58,21 @@ export default class ScrollPositionAutoSaver {
57
58
  }
58
59
  }
59
60
  }
60
- cancelSavePageScrollPosition() {
61
+ cancelSavePageScrollPosition(hasRun) {
61
62
  if (this._cancelSavePageScrollPosition) {
63
+ if (!hasRun) {
64
+ debug('cancel delayed save scroll position', PAGE_SCROLLABLE_CONTAINER_KEY);
65
+ }
62
66
  this._cancelSavePageScrollPosition();
63
67
  this._cancelSavePageScrollPosition = null;
64
68
  }
65
69
  }
66
- cancelSaveScrollableContainerScrollPosition(scrollableContainerKey) {
70
+ cancelSaveScrollableContainerScrollPosition(scrollableContainerKey, hasRun) {
67
71
  const scrollableContainerEntry = this._getScrollableContainers()[scrollableContainerKey];
68
72
  if (scrollableContainerEntry.cancelSaveScrollPosition) {
73
+ if (!hasRun) {
74
+ debug('cancel delayed save scroll position', scrollableContainerKey);
75
+ }
69
76
  scrollableContainerEntry.cancelSaveScrollPosition();
70
77
  scrollableContainerEntry.cancelSaveScrollPosition = null;
71
78
  }
@@ -96,7 +103,9 @@ export default class ScrollPositionAutoSaver {
96
103
  // because there might be too many in a given short period of time
97
104
  // which could affect the performance of the application.
98
105
  if (!scrollableContainerEntry.cancelSaveScrollPosition) {
106
+ debug('scroll detected', scrollableContainerKey);
99
107
  scrollableContainerEntry.cancelSaveScrollPosition = scheduleNextTick(() => {
108
+ debug('auto-save scroll position after scroll', scrollableContainerKey);
100
109
  this._scrollPositionSaver.saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainerEntry.scrollableContainer);
101
110
  });
102
111
  }
@@ -105,6 +114,8 @@ export default class ScrollPositionAutoSaver {
105
114
  addPageScrollListener() {
106
115
  // Set up scroll listener on the page.
107
116
  this._removePageScrollListener = this._scrollPosition.addPageScrollListener(() => {
117
+ debug('scroll detected', PAGE_SCROLLABLE_CONTAINER_KEY);
118
+
108
119
  // This flag is not used in real life and is only used in tests (for some reason).
109
120
  if (!this._shouldSaveScrollPosition()) {
110
121
  return;