navigation-stack 0.3.1 → 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 (257) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +611 -163
  3. package/data-storage/package.json +6 -0
  4. package/karma.conf.cjs +21 -4
  5. package/lib/cjs/NavigationStack.js +88 -0
  6. package/lib/cjs/data-storage/DataStorage.js +71 -0
  7. package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
  8. package/lib/cjs/data-storage/index.js +9 -0
  9. package/lib/cjs/debug.js +12 -0
  10. package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
  11. package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
  12. package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
  13. package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
  14. package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
  16. package/lib/cjs/getLocationFromInternalLocation.js +14 -0
  17. package/lib/cjs/index.js +20 -16
  18. package/lib/cjs/navigationBlockers.js +28 -23
  19. package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
  20. package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  21. package/lib/cjs/redux/ActionTypesInternal.js +8 -0
  22. package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
  23. package/lib/cjs/redux/createMiddlewares.js +60 -0
  24. package/lib/cjs/redux/index.js +13 -0
  25. package/lib/cjs/redux/internalLocationReducer.js +14 -0
  26. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
  27. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
  28. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  29. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
  30. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
  31. package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
  32. package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
  33. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
  34. package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
  35. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +141 -0
  36. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +407 -0
  37. package/lib/cjs/scroll-position/ScrollPositionSaver.js +87 -0
  38. package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
  39. package/lib/cjs/scroll-position/constants.js +5 -0
  40. package/lib/cjs/scroll-position/index.js +7 -0
  41. package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
  42. package/lib/cjs/session/InMemorySession.js +22 -0
  43. package/lib/cjs/session/ServerSideRenderSession.js +17 -0
  44. package/lib/cjs/session/Session.js +202 -0
  45. package/lib/cjs/session/WebBrowserSession.js +20 -0
  46. package/lib/cjs/session/key/createSessionKey.js +23 -0
  47. package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
  48. package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
  49. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
  50. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
  51. package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
  52. package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
  53. package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
  54. package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
  55. package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
  56. package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
  57. package/lib/cjs/session/navigation/operation/operations.js +11 -0
  58. package/lib/cjs/session/subscription/Subscription.js +81 -0
  59. package/lib/data-storage/index.d.ts +35 -0
  60. package/lib/esm/NavigationStack.js +81 -0
  61. package/lib/esm/data-storage/DataStorage.js +65 -0
  62. package/lib/esm/data-storage/LocationDataStorage.js +22 -0
  63. package/lib/esm/data-storage/index.js +2 -0
  64. package/lib/esm/debug.js +7 -0
  65. package/lib/esm/environment/InMemoryEnvironment.js +8 -0
  66. package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
  67. package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
  68. package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
  69. package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
  70. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
  71. package/lib/esm/getLocationFromInternalLocation.js +9 -0
  72. package/lib/esm/index.js +10 -8
  73. package/lib/esm/navigationBlockers.js +28 -23
  74. package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
  75. package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  76. package/lib/esm/redux/ActionTypesInternal.js +3 -0
  77. package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
  78. package/lib/esm/redux/createMiddlewares.js +54 -0
  79. package/lib/esm/redux/index.js +4 -0
  80. package/lib/esm/redux/internalLocationReducer.js +8 -0
  81. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  82. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
  83. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
  84. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
  85. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
  86. package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
  87. package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  88. package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
  89. package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
  90. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +134 -0
  91. package/lib/esm/scroll-position/ScrollPositionRestoration.js +400 -0
  92. package/lib/esm/scroll-position/ScrollPositionSaver.js +80 -0
  93. package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
  94. package/lib/esm/scroll-position/constants.js +1 -0
  95. package/lib/esm/scroll-position/index.js +1 -0
  96. package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
  97. package/lib/esm/session/InMemorySession.js +15 -0
  98. package/lib/esm/session/ServerSideRenderSession.js +11 -0
  99. package/lib/esm/session/Session.js +195 -0
  100. package/lib/esm/session/WebBrowserSession.js +13 -0
  101. package/lib/esm/session/key/createSessionKey.js +18 -0
  102. package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  103. package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
  104. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
  105. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
  106. package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
  107. package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
  108. package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
  109. package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
  110. package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
  111. package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
  112. package/lib/esm/session/navigation/operation/operations.js +6 -0
  113. package/lib/esm/session/subscription/Subscription.js +75 -0
  114. package/lib/index.d.ts +179 -157
  115. package/lib/redux/index.d.ts +90 -0
  116. package/lib/scroll-position/index.d.ts +107 -0
  117. package/package.json +9 -5
  118. package/redux/package.json +6 -0
  119. package/scroll-position/package.json +6 -0
  120. package/src/NavigationStack.js +100 -0
  121. package/src/data-storage/DataStorage.js +69 -0
  122. package/src/data-storage/LocationDataStorage.js +23 -0
  123. package/src/data-storage/index.js +2 -0
  124. package/src/debug.js +8 -0
  125. package/src/environment/InMemoryEnvironment.js +9 -0
  126. package/src/environment/WebBrowserEnvironment.js +9 -0
  127. package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
  128. package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
  129. package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
  130. package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
  131. package/src/getLocationFromInternalLocation.js +7 -0
  132. package/src/index.js +10 -8
  133. package/src/navigationBlockers.js +31 -27
  134. package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
  135. package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  136. package/src/redux/ActionTypesInternal.js +3 -0
  137. package/src/{Actions.js → redux/Actions.js} +4 -3
  138. package/src/redux/createMiddlewares.js +65 -0
  139. package/src/redux/index.js +4 -0
  140. package/src/redux/internalLocationReducer.js +9 -0
  141. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  142. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
  143. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  144. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
  145. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
  146. package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
  147. package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  148. package/src/redux/middleware/updateLocationMiddleware.js +28 -0
  149. package/src/scroll-position/PageScrollPositionSetter.js +110 -0
  150. package/src/scroll-position/ScrollPositionAutoSaver.js +168 -0
  151. package/src/scroll-position/ScrollPositionRestoration.js +551 -0
  152. package/src/scroll-position/ScrollPositionSaver.js +120 -0
  153. package/src/scroll-position/ScrollPositionSetter.js +16 -0
  154. package/src/scroll-position/constants.js +1 -0
  155. package/src/scroll-position/index.js +1 -0
  156. package/src/scroll-position/scheduleNextTick.js +6 -0
  157. package/src/session/InMemorySession.js +13 -0
  158. package/src/session/ServerSideRenderSession.js +9 -0
  159. package/src/session/Session.js +238 -0
  160. package/src/session/WebBrowserSession.js +13 -0
  161. package/src/session/key/createSessionKey.js +18 -0
  162. package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  163. package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
  164. package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
  165. package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
  166. package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
  167. package/src/session/navigation/InMemoryNavigation.js +78 -0
  168. package/src/session/navigation/ServerSideNavigation.js +43 -0
  169. package/src/session/navigation/WebBrowserNavigation.js +224 -0
  170. package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
  171. package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
  172. package/src/session/navigation/operation/operations.js +6 -0
  173. package/src/session/subscription/Subscription.js +76 -0
  174. package/test/NavigationStack.test.js +296 -0
  175. package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
  176. package/test/data-storage/index.test.js +8 -0
  177. package/test/index.js +12 -0
  178. package/test/index.test.js +8 -7
  179. package/test/{helpers.js → middlewareTestUtil.js} +9 -12
  180. package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
  181. package/test/{Action.test.js → redux/Action.test.js} +7 -6
  182. package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
  183. package/test/redux/createMiddlewares.test.js +96 -0
  184. package/test/redux/index.test.js +10 -0
  185. package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
  186. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
  187. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
  188. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
  189. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
  190. package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
  191. package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
  192. package/test/scroll-position/ScrollPositionRestoration.test.js +435 -0
  193. package/test/scroll-position/addScrollableContainer.js +39 -0
  194. package/test/scroll-position/addScrollableContainerWithAnchors.js +56 -0
  195. package/test/scroll-position/createApp.js +132 -0
  196. package/test/scroll-position/delay.js +9 -0
  197. package/test/scroll-position/mockPageLifecycle.js +17 -0
  198. package/test/scroll-position/runApp.js +24 -0
  199. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
  200. package/test/session/InMemorySession.test.js +348 -0
  201. package/test/session/ServerSession.test.js +17 -9
  202. package/test/session/WebBrowserSession.test.js +265 -0
  203. package/test/testUtil.js +3 -0
  204. package/types/data-storage/index.d.ts +35 -0
  205. package/types/index.d.ts +179 -157
  206. package/types/redux/index.d.ts +90 -0
  207. package/types/scroll-position/index.d.ts +107 -0
  208. package/types/tsconfig.json +1 -1
  209. package/lib/cjs/LocationDataStorage.js +0 -61
  210. package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
  211. package/lib/cjs/beforeLocationChangeListeners.js +0 -51
  212. package/lib/cjs/createMiddlewares.js +0 -47
  213. package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
  214. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
  215. package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
  216. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
  217. package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
  218. package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
  219. package/lib/cjs/session/BrowserSession.js +0 -235
  220. package/lib/cjs/session/MemorySession.js +0 -223
  221. package/lib/cjs/session/ServerSession.js +0 -65
  222. package/lib/esm/LocationDataStorage.js +0 -54
  223. package/lib/esm/addBeforeLocationChangeListener.js +0 -2
  224. package/lib/esm/beforeLocationChangeListeners.js +0 -44
  225. package/lib/esm/createMiddlewares.js +0 -41
  226. package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
  227. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
  228. package/lib/esm/middleware/createLocationMiddleware.js +0 -50
  229. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
  230. package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
  231. package/lib/esm/onlyAllowedOnClientSide.js +0 -5
  232. package/lib/esm/session/BrowserSession.js +0 -229
  233. package/lib/esm/session/MemorySession.js +0 -217
  234. package/lib/esm/session/ServerSession.js +0 -58
  235. package/src/LocationDataStorage.js +0 -60
  236. package/src/addBeforeLocationChangeListener.js +0 -2
  237. package/src/beforeLocationChangeListeners.js +0 -54
  238. package/src/createMiddlewares.js +0 -45
  239. package/src/middleware/createBasePathMiddleware.js +0 -20
  240. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
  241. package/src/middleware/createLocationMiddleware.js +0 -55
  242. package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
  243. package/src/middleware/createTransformLocationMiddleware.js +0 -29
  244. package/src/onlyAllowedOnClientSide.js +0 -5
  245. package/src/session/BrowserSession.js +0 -235
  246. package/src/session/MemorySession.js +0 -219
  247. package/src/session/ServerSession.js +0 -67
  248. package/test/createMiddlewares.test.js +0 -62
  249. package/test/middleware/createBasePathMiddleware.test.js +0 -67
  250. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
  251. package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
  252. package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
  253. package/test/session/BrowserSession.test.js +0 -182
  254. package/test/session/MemorySession.test.js +0 -244
  255. /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
  256. /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
  257. /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
@@ -0,0 +1,551 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+
3
+ import PageScrollPositionSetter from './PageScrollPositionSetter';
4
+ import ScrollPositionSaver from './ScrollPositionSaver';
5
+ import ScrollPositionSetter from './ScrollPositionSetter';
6
+ import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
7
+ import LocationDataStorage from '../data-storage/LocationDataStorage';
8
+ import debug from '../debug';
9
+
10
+ function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
11
+ let i = 0;
12
+ while (i < scrollPosition1.length) {
13
+ if (scrollPosition1[i] !== scrollPosition2[i]) {
14
+ return false;
15
+ }
16
+ i++;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ export default class ScrollPositionRestoration {
22
+ constructor(session, _options) {
23
+ this._scrollPosition = session.environment.scrollPosition;
24
+
25
+ this._sessionLifecycle = session.lifecycle;
26
+
27
+ this._locationDataStorage = new LocationDataStorage(session, {
28
+ namespace: 'navigation-stack/scroll-position',
29
+ });
30
+
31
+ this._scrollPositionSaver = new ScrollPositionSaver({
32
+ scrollPosition: this._scrollPosition,
33
+ saveScrollPositionForLocation: this._saveScrollPositionForLocation,
34
+ getScrollableContainers: () => this._scrollableContainers,
35
+ getLocation: () => this._location,
36
+ shouldSaveScrollPosition: () => !this._doNotSaveScrollPosition,
37
+ });
38
+
39
+ // Originally, `scrollableContainerKey` was auto-generated,
40
+ // but then it didn't work with the concept of dynamically adding or removing
41
+ // scrollable containers after `ScrollPositionRestoration` has already started.
42
+ //
43
+ // this._scrollableContainerKeyCounter = 0;
44
+
45
+ this._scrollableContainers = {};
46
+
47
+ // Add page scrollable container.
48
+ this._scrollableContainers[PAGE_SCROLLABLE_CONTAINER_KEY] = {
49
+ scrollableContainer: undefined,
50
+
51
+ // Using this option, a developer could theoretically provide their own implementation
52
+ // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
53
+ // This could be part of the public API if anyone provided a sensible real-world use case for it.
54
+ scrollPositionSetter:
55
+ (_options && _options._pageScrollPositionSetter) ||
56
+ // The default page scroll position setter.
57
+ new PageScrollPositionSetter(),
58
+
59
+ // This function is only used in tests.
60
+ // There seems to be no use of it in real life, hence it's not public API.
61
+ // It's only used in tests.
62
+ _getScrollPositionForLocation:
63
+ _options && _options._getPageScrollPositionForLocation,
64
+
65
+ // This function is only used in tests.
66
+ // There seems to be no use of it in real life, hence it's not public API.
67
+ // It's only used in tests.
68
+ _shouldUpdateScrollPositionForLocation:
69
+ _options && _options._shouldUpdatePageScrollPositionForLocation,
70
+ };
71
+ }
72
+
73
+ addScrollableContainer(
74
+ scrollableContainerKey,
75
+ scrollableContainer,
76
+ _options,
77
+ ) {
78
+ // Originally, `scrollableContainerKey` was auto-generated,
79
+ // but then it didn't work with the concept of dynamically adding or removing
80
+ // scrollable containers after `ScrollPositionRestoration` has already started.
81
+ //
82
+ // this._scrollableContainerKeyCounter++;
83
+ // const scrollableContainerKey = String(this._scrollableContainerKeyCounter);
84
+
85
+ // Validate `scrollableContainerKey`.
86
+ if (scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY) {
87
+ throw new Error(
88
+ `Scrollable container key "${scrollableContainerKey}" is not allowed`,
89
+ );
90
+ }
91
+
92
+ // Check that it hasn't already been added.
93
+ if (this._scrollableContainers[scrollableContainerKey]) {
94
+ throw new Error(
95
+ `Scrollable container key "${scrollableContainerKey}" is already added`,
96
+ );
97
+ }
98
+
99
+ debug('add scrollable container', scrollableContainerKey);
100
+
101
+ // Add scrollable container entry.
102
+ this._scrollableContainers[scrollableContainerKey] = {
103
+ // Scrollable container element.
104
+ scrollableContainer,
105
+
106
+ // Using this option, a developer could theoretically provide their own implementation
107
+ // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
108
+ // This could be part of the public API if anyone provided a sensible real-world use case for it.
109
+ scrollPositionSetter:
110
+ (_options && _options._scrollPositionSetter) ||
111
+ // The default basic "immediate" scroll position setter.
112
+ new ScrollPositionSetter(),
113
+
114
+ // This function is only used in tests.
115
+ // There seems to be no use of it in real life, hence it's not public API.
116
+ // It's only used in tests.
117
+ _shouldUpdateScrollPositionForLocation:
118
+ _options && _options._shouldUpdateScrollPositionForLocation,
119
+
120
+ // This function is only used in tests.
121
+ // There seems to be no use of it in real life, hence it's not public API.
122
+ // It's only used in tests.
123
+ _getScrollPositionForLocation:
124
+ _options && _options._getScrollPositionForLocation,
125
+ };
126
+
127
+ // Scrollable containers could be added at any time, including page mount.
128
+ // For example, a user navigates "Back" to a previous page where there's
129
+ // a "unique" scrollable container that's only present on that page.
130
+ // In that case, the previously-saved scroll position inside the scrollable container
131
+ // should be restored, if it pre-exists. Otherwise, if it doesn't pre-exist,
132
+ // the initial scroll position should be saved for the scrollable container.
133
+ if (this._location) {
134
+ const previouslySavedScrollPosition =
135
+ this._getSavedScrollPositionForLocation(
136
+ this._location,
137
+ scrollableContainerKey,
138
+ );
139
+ if (previouslySavedScrollPosition) {
140
+ debug(
141
+ 'restore scroll position on add scrollable container',
142
+ this._location.pathname,
143
+ scrollableContainerKey,
144
+ previouslySavedScrollPosition,
145
+ );
146
+ this._scrollPosition.setScrollableContainerScrollPosition(
147
+ scrollableContainer,
148
+ previouslySavedScrollPosition,
149
+ );
150
+ } else {
151
+ debug(
152
+ 'save scroll position on add scrollable container',
153
+ this._location.pathname,
154
+ scrollableContainerKey,
155
+ );
156
+ this._scrollPositionSaver.saveScrollableContainerScrollPosition(
157
+ scrollableContainerKey,
158
+ scrollableContainer,
159
+ );
160
+ }
161
+ }
162
+
163
+ if (this._started) {
164
+ this._scrollPositionSaver._scrollPositionAutoSaver.addScrollableContainerScrollListener(
165
+ scrollableContainerKey,
166
+ );
167
+ }
168
+
169
+ // Removes the scrollable container.
170
+ return () => {
171
+ debug('remove scrollable container', scrollableContainerKey);
172
+
173
+ this._scrollPositionSaver._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(
174
+ scrollableContainerKey,
175
+ );
176
+
177
+ this._scrollPositionSaver._scrollPositionAutoSaver.removeScrollableContainerScrollListener(
178
+ scrollableContainerKey,
179
+ );
180
+
181
+ delete this._scrollableContainers[scrollableContainerKey];
182
+ };
183
+ }
184
+
185
+ start() {
186
+ // "Foolproof" check.
187
+ if (this._started) {
188
+ throw new Error('Already started');
189
+ }
190
+
191
+ this._started = true;
192
+
193
+ this._disableAutomaticScrollRestoration();
194
+
195
+ this._scrollPositionSaver.start();
196
+
197
+ this._removePageStatusListener =
198
+ this._sessionLifecycle.addExecutionStatusListener(
199
+ this._sessionExecutionStatusListener,
200
+ );
201
+ }
202
+
203
+ // This method is "idempotent", i.e. it can be called multiple times.
204
+ stop() {
205
+ // "Foolproof" check.
206
+ if (!this._started) {
207
+ return;
208
+ // throw new Error('Not started');
209
+ }
210
+
211
+ this._started = false;
212
+
213
+ this._enableAutomaticScrollRestoration();
214
+
215
+ // If there's any scroll position still scheduled to be set, cancel it.
216
+ this._cancelAnyPendingSettingOfScrollPosition();
217
+
218
+ this._scrollPositionSaver.stop();
219
+
220
+ this._removePageStatusListener();
221
+ }
222
+
223
+ _cancelAnyPendingSettingOfScrollPosition() {
224
+ for (const scrollableContainerEntry of Object.values(
225
+ this._scrollableContainers,
226
+ )) {
227
+ scrollableContainerEntry.scrollPositionSetter.cancel();
228
+ }
229
+ }
230
+
231
+ // Once configured, scroll restoration mode persists across page reloads.
232
+ // I.e. even if a user refreshes the page in a web browser, the custom
233
+ // `window.history.scrollRestoration` value will still remain.
234
+ //
235
+ // And since it's set to a custom value of "manual", the web browser
236
+ // won't attempt to restore the scroll position on page load
237
+ // which it would otherwise normally do.
238
+ //
239
+ // So what happens if the website is fully server-side rendered?
240
+ // It will wait for the javascript code to be downloaded an executed first
241
+ // and only then that javascript code will programmatically restore the
242
+ // previously-saved scroll position.
243
+ //
244
+ // That would work but it also wouldn't be the most efficient way to do that.
245
+ // Instead, `window.history.scrollRestoration` value could be reset to default beforehand
246
+ // so that when the page finishes refreshing, the web browser could automatically
247
+ // restore the scroll position without waiting for the javascript code to download and run.
248
+ //
249
+ // To reset `window.history.scrollRestoration` value to default beforehand,
250
+ // the code should be notified when the browser tab is about to be terminated or suspended.
251
+ // Terminating could happen for various reasons such as not enough memory, code crash, etc.
252
+ // Suspending is treated equally to terminating because once suspended, it could potentially be
253
+ // terminated afterwards without the code being able to do its stuff while it's suspended.
254
+ //
255
+ // One could consider this feature a minor user experience optimization that relies on the web browser
256
+ // to correctly restore the page scroll every time on page refresh, which it normally does.
257
+ //
258
+ _sessionExecutionStatusListener = ({ running }) => {
259
+ if (running) {
260
+ debug('▶ running');
261
+ this._disableAutomaticScrollRestoration();
262
+ } else {
263
+ debug('⏹ not running');
264
+ this._enableAutomaticScrollRestoration();
265
+
266
+ // There might be previous scroll position already saved in the data storage.
267
+ // Overwrite that previously-saved scroll position with the most up-to-date one
268
+ // just so that there's no stale scroll position left over in the data storage.
269
+ // Alternatively, it could just clear any saved scroll position for this page,
270
+ // since the web browser's automatic scroll restoration is now enabled.
271
+ this._scrollPositionSaver.saveScrollPosition();
272
+ }
273
+ };
274
+
275
+ // willRenderLocation = (location) => {
276
+ // // "Foolproof" check.
277
+ // if (!this._started) {
278
+ // throw new Error('`ScrollPositionRestoration` not started');
279
+ // }
280
+ //
281
+ // // For the initial location, it doesn't do anything.
282
+ // if (location.operation === Operations.INIT) {
283
+ // return;
284
+ // }
285
+ //
286
+ // // Since the current page will no longer be rendered,
287
+ // // cancel any scheduled setting of scroll position on it.
288
+ // this._cancelAnyPendingSettingOfScrollPosition();
289
+ //
290
+ // // The previous page may have scheduled an auto-save of scroll position.
291
+ // // Since the previous page is no longer rendered, its scroll position can no longer be obtained,
292
+ // // so any scheduled scroll position auto-save produres are irrelevant now.
293
+ // this._scrollPositionSaver.cancelPreviouslyScheduledAutoSave();
294
+ //
295
+ // // Save the current scroll position on the current page while it's still rendered.
296
+ // // This saved scroll position could later be restored in case of returing to this page.
297
+ // // Even if the current scroll position is a default one (scrolled to top), it should still
298
+ // // be saved in order to overwrite any potential previously-saved non-default scroll position.
299
+ // this._scrollPositionSaver.saveScrollPosition();
300
+ // };
301
+
302
+ // Should be called whenever a different location has been rendered (i.e. immediately after).
303
+ // Returns a Promise that resolves when finished restoring scroll position.
304
+ // There's no need to await for that Promise. It's just there because it exists.
305
+ locationRendered(location) {
306
+ // Validate that `location` has a `key`.
307
+ if (!location.key) {
308
+ throw new Error('`location` must have a `key`');
309
+ }
310
+
311
+ debug('rendered location', location.pathname);
312
+
313
+ this._prevLocation = this._location;
314
+ this._location = location;
315
+
316
+ this._scrollPosition.init();
317
+
318
+ if (!this._started) {
319
+ // `this.start()` requires `this._location` to be set.
320
+ this.start();
321
+
322
+ // The initial page might've been server-side rendered which means that
323
+ // by the time this javascript code is downloaded and executed by the web browser,
324
+ // the user might've already scrolled the page to some position,
325
+ // and all those pre-javascript scroll events won't be registered by `ScrollPositionSaver`.
326
+ // If the user doesn't scroll after javascript is loaded and just navigates to a new page,
327
+ // the initial page won't have any saved scroll position to restore on "Back" navigation.
328
+ // Hence, it should explicitly save the current scroll position at the start of operation.
329
+ if (!this._isDefaultScrollPosition()) {
330
+ // `this._scrollPositionSaver.saveScrollPosition()` requires `this._location` to be set.
331
+ this._scrollPositionSaver.saveScrollPosition();
332
+ }
333
+ }
334
+
335
+ // The previous page may have scheduled an auto-save of scroll position.
336
+ // Since the previous page is no longer rendered, its scroll position can no longer be obtained,
337
+ // so any scheduled scroll position auto-save produres are irrelevant now.
338
+ this._scrollPositionSaver.cancelPreviouslyScheduledAutoSave();
339
+
340
+ // If it was in the middle of setting scroll position for a previous location, cancel it.
341
+ this._cancelAnyPendingSettingOfScrollPosition();
342
+
343
+ // Set the scroll position for the new page:
344
+ // either restore a previously-saved one or set it to a default scroll position.
345
+ return this._setScrollPosition();
346
+ }
347
+
348
+ // Tells if the current scroll position is the default one.
349
+ _isDefaultScrollPosition() {
350
+ for (const scrollableContainerKey of Object.keys(
351
+ this._scrollableContainers,
352
+ )) {
353
+ if (scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY) {
354
+ if (
355
+ !areEqualScrollPositions(
356
+ this._scrollPosition.getPageScrollPosition(),
357
+ this._getDefaultScrollPosition(),
358
+ )
359
+ ) {
360
+ return false;
361
+ }
362
+ } else {
363
+ const scrollableContainerEntry =
364
+ this._scrollableContainers[scrollableContainerKey];
365
+ if (
366
+ !areEqualScrollPositions(
367
+ this._scrollPosition.getScrollableContainerScrollPosition(
368
+ scrollableContainerEntry.scrollableContainer,
369
+ ),
370
+ this._getDefaultScrollPosition(),
371
+ )
372
+ ) {
373
+ return false;
374
+ }
375
+ }
376
+ }
377
+
378
+ return true;
379
+ }
380
+
381
+ // Restores scroll position.
382
+ // Returns a Promise that resolves when finished setting scroll position.
383
+ // There's no need to await for this Promise. It just exists.
384
+ _setScrollPosition() {
385
+ return Promise.all(
386
+ Object.keys(this._scrollableContainers).map((scrollableContainerKey) => {
387
+ const scrollableContainerEntry =
388
+ this._scrollableContainers[scrollableContainerKey];
389
+
390
+ // This function is only used in tests.
391
+ // There seems to be no use of it in real life, hence it's not public API.
392
+ // It's only used in tests.
393
+ if (scrollableContainerEntry._shouldUpdateScrollPositionForLocation) {
394
+ if (
395
+ !scrollableContainerEntry._shouldUpdateScrollPositionForLocation(
396
+ this._location,
397
+ this._prevLocation,
398
+ )
399
+ ) {
400
+ return Promise.resolve();
401
+ }
402
+ }
403
+
404
+ // Scroll position (or anchor) to set.
405
+ let scrollPositionOrAnchorToSet;
406
+
407
+ // This function is only used in tests.
408
+ // There seems to be no use of it in real life, hence it's not public API.
409
+ // It's only used in tests.
410
+ if (scrollableContainerEntry._getScrollPositionForLocation) {
411
+ scrollPositionOrAnchorToSet =
412
+ scrollableContainerEntry._getScrollPositionForLocation(
413
+ this._location,
414
+ this._prevLocation,
415
+ );
416
+ }
417
+
418
+ // Get scroll position (or anchor) to set.
419
+ if (!scrollPositionOrAnchorToSet) {
420
+ scrollPositionOrAnchorToSet =
421
+ scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY
422
+ ? this._getPageScrollPositionOrAnchorToSet(this._location)
423
+ : this._getScrollableContainerScrollPositionToSet(
424
+ this._location,
425
+ scrollableContainerKey,
426
+ );
427
+ }
428
+
429
+ debug(
430
+ 'restore scroll position',
431
+ this._location.pathname,
432
+ scrollableContainerKey,
433
+ scrollPositionOrAnchorToSet,
434
+ );
435
+
436
+ // Set scroll position of scrollable container.
437
+ return scrollableContainerEntry.scrollPositionSetter.set(
438
+ scrollableContainerEntry.scrollableContainer,
439
+ scrollPositionOrAnchorToSet,
440
+ this._scrollPosition,
441
+ );
442
+ }),
443
+ );
444
+ }
445
+
446
+ // Overrides the default `window.history.scrollRestoration` value.
447
+ // This prevents the web browser from interfering by disabling its
448
+ // automatic scroll position restoration on "Back"/"Forward" navigation.
449
+ // Instead, the application will have to do it manually.
450
+ // The reason is that when the web browser performs "Back" or "Forward" navigation,
451
+ // it updates the URL in the address bar immediately, and it also attempts to
452
+ // automatically restore scroll position immediately, but the thing is that
453
+ // the application might have delayed rendering of the page due to various reasons
454
+ // such as performance considerations or the architecture of the rendering framework.
455
+ // For example, React framework by design renders pages in "asynchronous" fashion.
456
+ // Hence, by the time the web browser attempts to restore the scroll position,
457
+ // the page might not yet be rendered which would result in incorrect scroll position restoration.
458
+ // That's why the application has to take over this functionality from the web browser.
459
+ _disableAutomaticScrollRestoration = () => {
460
+ try {
461
+ this._scrollPosition.disableAutomaticScrollRestoration();
462
+ } catch (error) {
463
+ // eslint-disable-next-line no-console
464
+ console.error(
465
+ '[navigation-stack] could not disable default scroll restoration mode',
466
+ );
467
+ }
468
+ };
469
+
470
+ _enableAutomaticScrollRestoration = () => {
471
+ try {
472
+ this._scrollPosition.enableAutomaticScrollRestoration();
473
+ } catch (error) {
474
+ // eslint-disable-next-line no-console
475
+ console.error(
476
+ '[navigation-stack] could not enable default scroll restoration mode',
477
+ );
478
+ }
479
+ };
480
+
481
+ _getSavedScrollPositionForLocation(
482
+ location,
483
+ scrollableContainerKey = PAGE_SCROLLABLE_CONTAINER_KEY,
484
+ ) {
485
+ return this._locationDataStorage.get(location, scrollableContainerKey);
486
+ }
487
+
488
+ _saveScrollPositionForLocation = (
489
+ location,
490
+ scrollableContainerKey,
491
+ scrollPosition,
492
+ ) => {
493
+ this._locationDataStorage.set(
494
+ location,
495
+ scrollableContainerKey || PAGE_SCROLLABLE_CONTAINER_KEY,
496
+ scrollPosition,
497
+ );
498
+ };
499
+
500
+ // Returns scroll position coordinates or an anchor name.
501
+ _getPageScrollPositionOrAnchorToSet(location) {
502
+ // If it's a return to a previously-visited location,
503
+ // read the saved scroll position from session data store.
504
+ return (
505
+ this._getSavedScrollPositionForLocation(location) ||
506
+ this._getAnchor(location) ||
507
+ this._getDefaultScrollPosition()
508
+ );
509
+ }
510
+
511
+ // Returns scroll position coordinates.
512
+ _getScrollableContainerScrollPositionToSet(
513
+ location,
514
+ scrollableContainerKey,
515
+ ) {
516
+ // If it's a return to a previously-visited location,
517
+ // read the saved scroll position from session data store.
518
+ return (
519
+ this._getSavedScrollPositionForLocation(
520
+ location,
521
+ scrollableContainerKey,
522
+ ) || this._getDefaultScrollPosition()
523
+ );
524
+ }
525
+
526
+ _getAnchor(location) {
527
+ const { hash } = location;
528
+ if (hash && hash !== '#') {
529
+ return hash.slice('#'.length);
530
+ }
531
+ return undefined;
532
+ }
533
+
534
+ _getDefaultScrollPosition() {
535
+ return [0, 0];
536
+ }
537
+
538
+ // `_enableSavingScrollPosition()` and `_disableSavingScrollPosition()`
539
+ // aren't used in real life and are not part of the public API.
540
+ // They're only used in tests.
541
+ _enableSavingScrollPosition() {
542
+ this._doNotSaveScrollPosition = undefined;
543
+ }
544
+
545
+ // `_enableSavingScrollPosition()` and `_disableSavingScrollPosition()`
546
+ // aren't used in real life and are not part of the public API.
547
+ // They're only used in tests.
548
+ _disableSavingScrollPosition() {
549
+ this._doNotSaveScrollPosition = true;
550
+ }
551
+ }
@@ -0,0 +1,120 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+
3
+ import ScrollPositionAutoSaver from './ScrollPositionAutoSaver';
4
+ import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
5
+ import debug from '../debug';
6
+
7
+ export default class ScrollPositionSaver {
8
+ constructor({
9
+ scrollPosition,
10
+ getLocation,
11
+ saveScrollPositionForLocation,
12
+ getScrollableContainers,
13
+ shouldSaveScrollPosition,
14
+ }) {
15
+ this._scrollPosition = scrollPosition;
16
+ this._getLocation = getLocation;
17
+ this._saveScrollPositionForLocation = saveScrollPositionForLocation;
18
+ this._getScrollableContainers = getScrollableContainers;
19
+ this._shouldSaveScrollPosition = shouldSaveScrollPosition;
20
+
21
+ this._scrollPositionAutoSaver = new ScrollPositionAutoSaver({
22
+ scrollPosition: this._scrollPosition,
23
+ scrollPositionSaver: this,
24
+ getScrollableContainers,
25
+ shouldSaveScrollPosition,
26
+ });
27
+ }
28
+
29
+ start() {
30
+ this._scrollPositionAutoSaver.start();
31
+ }
32
+
33
+ stop() {
34
+ this._scrollPositionAutoSaver.stop();
35
+ }
36
+
37
+ cancelPreviouslyScheduledAutoSave() {
38
+ this._scrollPositionAutoSaver.cancelScheduledAutoSave();
39
+ }
40
+
41
+ saveScrollPosition() {
42
+ // This flag is not used in real life and is only used in tests (for some reason).
43
+ if (!this._shouldSaveScrollPosition()) {
44
+ return;
45
+ }
46
+
47
+ debug('save scroll position', this._getLocation().pathname);
48
+
49
+ // Get scrollable containers.
50
+ const scrollableContainers = this._getScrollableContainers();
51
+
52
+ // Save scroll position of each scrollable container.
53
+ for (const scrollableContainerKey of Object.keys(scrollableContainers)) {
54
+ if (scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY) {
55
+ this.savePageScrollPosition();
56
+ } else {
57
+ this.saveScrollableContainerScrollPosition(
58
+ scrollableContainerKey,
59
+ scrollableContainers[scrollableContainerKey].scrollableContainer,
60
+ );
61
+ }
62
+ }
63
+ }
64
+
65
+ savePageScrollPosition() {
66
+ debug(
67
+ 'save scroll position',
68
+ this._getLocation().pathname,
69
+ PAGE_SCROLLABLE_CONTAINER_KEY,
70
+ this._scrollPosition.getPageScrollPosition(),
71
+ );
72
+
73
+ // * If this is not a scheduled "auto-save" of scroll position
74
+ // and there already exists any scheduled "auto-save" of scroll position,
75
+ // cancel it and save scroll position right now instead.
76
+ // * If this is a scheduled "auto-save" of scroll position,
77
+ // clear the "cancel" function because it's no longer of use.
78
+ this._scrollPositionAutoSaver.cancelSavePageScrollPosition(true);
79
+
80
+ // Save scroll position.
81
+ this._saveScrollPositionForLocation(
82
+ this._getLocation(),
83
+ undefined,
84
+ this._scrollPosition.getPageScrollPosition(),
85
+ );
86
+ }
87
+
88
+ saveScrollableContainerScrollPosition(
89
+ scrollableContainerKey,
90
+ scrollableContainer,
91
+ ) {
92
+ debug(
93
+ 'save scroll position',
94
+ this._getLocation().pathname,
95
+ scrollableContainerKey,
96
+ this._scrollPosition.getScrollableContainerScrollPosition(
97
+ scrollableContainer,
98
+ ),
99
+ );
100
+
101
+ // * If this is not a scheduled "auto-save" of scroll position
102
+ // and there already exists any scheduled "auto-save" of scroll position,
103
+ // cancel it and save scroll position right now instead.
104
+ // * If this is a scheduled "auto-save" of scroll position,
105
+ // clear the "cancel" function because it's no longer of use.
106
+ this._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(
107
+ scrollableContainerKey,
108
+ true,
109
+ );
110
+
111
+ // Save scroll position.
112
+ this._saveScrollPositionForLocation(
113
+ this._getLocation(),
114
+ scrollableContainerKey,
115
+ this._scrollPosition.getScrollableContainerScrollPosition(
116
+ scrollableContainer,
117
+ ),
118
+ );
119
+ }
120
+ }
@@ -0,0 +1,16 @@
1
+ export default class ScrollPositionSetter {
2
+ set(scrollableContainer, scrollPositionOrAnchor, environmentScrollPosition) {
3
+ if (typeof scrollPositionOrAnchor === 'string') {
4
+ throw new Error(
5
+ '`ScrollPositionSetter` only allows setting numeric scroll position, not an anchor string',
6
+ );
7
+ }
8
+ environmentScrollPosition.setScrollableContainerScrollPosition(
9
+ scrollableContainer,
10
+ scrollPositionOrAnchor,
11
+ );
12
+ return Promise.resolve();
13
+ }
14
+
15
+ cancel() {}
16
+ }
@@ -0,0 +1 @@
1
+ export const PAGE_SCROLLABLE_CONTAINER_KEY = 'page';
@@ -0,0 +1 @@
1
+ export ScrollPositionRestoration from './ScrollPositionRestoration';