navigation-stack 0.3.0 → 0.4.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 (253) hide show
  1. package/README.md +603 -163
  2. package/data-storage/package.json +6 -0
  3. package/karma.conf.cjs +21 -4
  4. package/lib/cjs/NavigationStack.js +73 -0
  5. package/lib/cjs/data-storage/DataStorage.js +71 -0
  6. package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
  7. package/lib/cjs/data-storage/index.js +9 -0
  8. package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
  9. package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
  10. package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
  11. package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
  12. package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
  13. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
  14. package/lib/cjs/getLocationFromInternalLocation.js +14 -0
  15. package/lib/cjs/index.js +20 -16
  16. package/lib/cjs/navigationBlockers.js +25 -23
  17. package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
  18. package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  19. package/lib/cjs/redux/ActionTypesInternal.js +8 -0
  20. package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
  21. package/lib/cjs/redux/createMiddlewares.js +60 -0
  22. package/lib/cjs/redux/index.js +13 -0
  23. package/lib/cjs/redux/internalLocationReducer.js +14 -0
  24. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
  25. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
  26. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  27. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
  28. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
  29. package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
  30. package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
  31. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
  32. package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
  33. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +130 -0
  34. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +383 -0
  35. package/lib/cjs/scroll-position/ScrollPositionSaver.js +81 -0
  36. package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
  37. package/lib/cjs/scroll-position/constants.js +5 -0
  38. package/lib/cjs/scroll-position/index.js +7 -0
  39. package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
  40. package/lib/cjs/session/InMemorySession.js +22 -0
  41. package/lib/cjs/session/ServerSideRenderSession.js +17 -0
  42. package/lib/cjs/session/Session.js +196 -0
  43. package/lib/cjs/session/WebBrowserSession.js +20 -0
  44. package/lib/cjs/session/key/createSessionKey.js +23 -0
  45. package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
  46. package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
  47. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
  48. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
  49. package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
  50. package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
  51. package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
  52. package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
  53. package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
  54. package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
  55. package/lib/cjs/session/navigation/operation/operations.js +11 -0
  56. package/lib/cjs/session/subscription/Subscription.js +81 -0
  57. package/lib/data-storage/index.d.ts +35 -0
  58. package/lib/esm/NavigationStack.js +66 -0
  59. package/lib/esm/data-storage/DataStorage.js +65 -0
  60. package/lib/esm/data-storage/LocationDataStorage.js +22 -0
  61. package/lib/esm/data-storage/index.js +2 -0
  62. package/lib/esm/environment/InMemoryEnvironment.js +8 -0
  63. package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
  64. package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
  65. package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
  66. package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
  67. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
  68. package/lib/esm/getLocationFromInternalLocation.js +9 -0
  69. package/lib/esm/index.js +10 -8
  70. package/lib/esm/navigationBlockers.js +25 -23
  71. package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
  72. package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  73. package/lib/esm/redux/ActionTypesInternal.js +3 -0
  74. package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
  75. package/lib/esm/redux/createMiddlewares.js +54 -0
  76. package/lib/esm/redux/index.js +4 -0
  77. package/lib/esm/redux/internalLocationReducer.js +8 -0
  78. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  79. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
  80. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
  81. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
  82. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
  83. package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
  84. package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  85. package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
  86. package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
  87. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +123 -0
  88. package/lib/esm/scroll-position/ScrollPositionRestoration.js +376 -0
  89. package/lib/esm/scroll-position/ScrollPositionSaver.js +74 -0
  90. package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
  91. package/lib/esm/scroll-position/constants.js +1 -0
  92. package/lib/esm/scroll-position/index.js +1 -0
  93. package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
  94. package/lib/esm/session/InMemorySession.js +15 -0
  95. package/lib/esm/session/ServerSideRenderSession.js +11 -0
  96. package/lib/esm/session/Session.js +189 -0
  97. package/lib/esm/session/WebBrowserSession.js +13 -0
  98. package/lib/esm/session/key/createSessionKey.js +18 -0
  99. package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  100. package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
  101. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
  102. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
  103. package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
  104. package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
  105. package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
  106. package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
  107. package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
  108. package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
  109. package/lib/esm/session/navigation/operation/operations.js +6 -0
  110. package/lib/esm/session/subscription/Subscription.js +75 -0
  111. package/lib/index.d.ts +178 -157
  112. package/lib/redux/index.d.ts +90 -0
  113. package/lib/scroll-position/index.d.ts +107 -0
  114. package/package.json +9 -5
  115. package/redux/package.json +6 -0
  116. package/scroll-position/package.json +6 -0
  117. package/src/NavigationStack.js +84 -0
  118. package/src/data-storage/DataStorage.js +69 -0
  119. package/src/data-storage/LocationDataStorage.js +23 -0
  120. package/src/data-storage/index.js +2 -0
  121. package/src/environment/InMemoryEnvironment.js +9 -0
  122. package/src/environment/WebBrowserEnvironment.js +9 -0
  123. package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
  124. package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
  125. package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
  126. package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
  127. package/src/getLocationFromInternalLocation.js +7 -0
  128. package/src/index.js +10 -8
  129. package/src/navigationBlockers.js +28 -27
  130. package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
  131. package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  132. package/src/redux/ActionTypesInternal.js +3 -0
  133. package/src/{Actions.js → redux/Actions.js} +4 -3
  134. package/src/redux/createMiddlewares.js +65 -0
  135. package/src/redux/index.js +4 -0
  136. package/src/redux/internalLocationReducer.js +9 -0
  137. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  138. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
  139. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  140. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
  141. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
  142. package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
  143. package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  144. package/src/redux/middleware/updateLocationMiddleware.js +28 -0
  145. package/src/scroll-position/PageScrollPositionSetter.js +110 -0
  146. package/src/scroll-position/ScrollPositionAutoSaver.js +151 -0
  147. package/src/scroll-position/ScrollPositionRestoration.js +506 -0
  148. package/src/scroll-position/ScrollPositionSaver.js +100 -0
  149. package/src/scroll-position/ScrollPositionSetter.js +16 -0
  150. package/src/scroll-position/constants.js +1 -0
  151. package/src/scroll-position/index.js +1 -0
  152. package/src/scroll-position/scheduleNextTick.js +6 -0
  153. package/src/session/InMemorySession.js +13 -0
  154. package/src/session/ServerSideRenderSession.js +9 -0
  155. package/src/session/Session.js +216 -0
  156. package/src/session/WebBrowserSession.js +13 -0
  157. package/src/session/key/createSessionKey.js +18 -0
  158. package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  159. package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
  160. package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
  161. package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
  162. package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
  163. package/src/session/navigation/InMemoryNavigation.js +78 -0
  164. package/src/session/navigation/ServerSideNavigation.js +43 -0
  165. package/src/session/navigation/WebBrowserNavigation.js +224 -0
  166. package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
  167. package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
  168. package/src/session/navigation/operation/operations.js +6 -0
  169. package/src/session/subscription/Subscription.js +76 -0
  170. package/test/NavigationStack.test.js +296 -0
  171. package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
  172. package/test/data-storage/index.test.js +8 -0
  173. package/test/index.js +12 -0
  174. package/test/index.test.js +8 -7
  175. package/test/{helpers.js → middlewareTestUtil.js} +9 -12
  176. package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
  177. package/test/{Action.test.js → redux/Action.test.js} +7 -6
  178. package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
  179. package/test/redux/createMiddlewares.test.js +96 -0
  180. package/test/redux/index.test.js +10 -0
  181. package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
  182. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
  183. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
  184. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
  185. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
  186. package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
  187. package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
  188. package/test/scroll-position/ScrollPositionRestoration.test.js +418 -0
  189. package/test/scroll-position/addScrollableContainer.js +36 -0
  190. package/test/scroll-position/addScrollableContainerWithHyperlink.js +50 -0
  191. package/test/scroll-position/createApp.js +112 -0
  192. package/test/scroll-position/delay.js +9 -0
  193. package/test/scroll-position/mockPageLifecycle.js +17 -0
  194. package/test/scroll-position/runApp.js +24 -0
  195. package/test/scroll-position/withScrollableContainerAtIndexPage.js +62 -0
  196. package/test/session/InMemorySession.test.js +348 -0
  197. package/test/session/ServerSession.test.js +17 -9
  198. package/test/session/WebBrowserSession.test.js +265 -0
  199. package/test/testUtil.js +3 -0
  200. package/types/data-storage/index.d.ts +35 -0
  201. package/types/index.d.ts +178 -157
  202. package/types/redux/index.d.ts +90 -0
  203. package/types/scroll-position/index.d.ts +107 -0
  204. package/types/tsconfig.json +1 -1
  205. package/lib/cjs/LocationDataStorage.js +0 -60
  206. package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
  207. package/lib/cjs/beforeLocationChangeListeners.js +0 -51
  208. package/lib/cjs/createMiddlewares.js +0 -47
  209. package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
  210. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
  211. package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
  212. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
  213. package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
  214. package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
  215. package/lib/cjs/session/BrowserSession.js +0 -235
  216. package/lib/cjs/session/MemorySession.js +0 -223
  217. package/lib/cjs/session/ServerSession.js +0 -65
  218. package/lib/esm/LocationDataStorage.js +0 -53
  219. package/lib/esm/addBeforeLocationChangeListener.js +0 -2
  220. package/lib/esm/beforeLocationChangeListeners.js +0 -44
  221. package/lib/esm/createMiddlewares.js +0 -41
  222. package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
  223. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
  224. package/lib/esm/middleware/createLocationMiddleware.js +0 -50
  225. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
  226. package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
  227. package/lib/esm/onlyAllowedOnClientSide.js +0 -5
  228. package/lib/esm/session/BrowserSession.js +0 -229
  229. package/lib/esm/session/MemorySession.js +0 -217
  230. package/lib/esm/session/ServerSession.js +0 -58
  231. package/src/LocationDataStorage.js +0 -59
  232. package/src/addBeforeLocationChangeListener.js +0 -2
  233. package/src/beforeLocationChangeListeners.js +0 -54
  234. package/src/createMiddlewares.js +0 -45
  235. package/src/middleware/createBasePathMiddleware.js +0 -20
  236. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
  237. package/src/middleware/createLocationMiddleware.js +0 -55
  238. package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
  239. package/src/middleware/createTransformLocationMiddleware.js +0 -29
  240. package/src/onlyAllowedOnClientSide.js +0 -5
  241. package/src/session/BrowserSession.js +0 -235
  242. package/src/session/MemorySession.js +0 -219
  243. package/src/session/ServerSession.js +0 -67
  244. package/test/createMiddlewares.test.js +0 -62
  245. package/test/middleware/createBasePathMiddleware.test.js +0 -67
  246. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
  247. package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
  248. package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
  249. package/test/session/BrowserSession.test.js +0 -182
  250. package/test/session/MemorySession.test.js +0 -244
  251. /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
  252. /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
  253. /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
@@ -0,0 +1,97 @@
1
+ import parseInputLocation from '../../parseInputLocation';
2
+ export default class InMemoryNavigation {
3
+ constructor() {
4
+ // A stack of `LocationBase` objects.
5
+ this._stack = [];
6
+ }
7
+
8
+ // eslint-disable-next-line no-unused-vars
9
+ subscribe(listener) {
10
+ // `InMemoryNavigation` doesn't have any "asynchronycity" about it
11
+ // and performs any navigation immediately at the time of the call.
12
+ // Hence, no asynchronous listener would ever be called
13
+ // due to no asynchronous events being dispatched.
14
+ return () => {};
15
+ }
16
+ init(initialLocation, {
17
+ operation,
18
+ key,
19
+ index,
20
+ delta
21
+ }) {
22
+ this._stack.push({
23
+ location: parseInputLocation(initialLocation),
24
+ key
25
+ });
26
+ return this._createLocationObject({
27
+ operation,
28
+ index,
29
+ delta
30
+ });
31
+ }
32
+ navigate(location, {
33
+ operation,
34
+ key,
35
+ index,
36
+ delta
37
+ }) {
38
+ const {
39
+ pathname,
40
+ search,
41
+ query,
42
+ hash
43
+ } = location;
44
+ this._stack[index] = {
45
+ location: {
46
+ pathname,
47
+ search,
48
+ query,
49
+ hash
50
+ },
51
+ key
52
+ };
53
+
54
+ // A `PUSH` navigation sets a new terminal (rightmost) location.
55
+ if (delta === 1) {
56
+ // Trim the location stack by removing any previous location history
57
+ // that might've existed after the current index.
58
+ this._stack.length = index + 1;
59
+ }
60
+ return Object.assign({}, location, {
61
+ key,
62
+ operation,
63
+ index,
64
+ delta
65
+ });
66
+ }
67
+ shift({
68
+ operation,
69
+ index,
70
+ delta
71
+ }) {
72
+ return this._createLocationObject({
73
+ operation,
74
+ index,
75
+ delta
76
+ });
77
+ }
78
+ getInitialLocation() {
79
+ return undefined;
80
+ }
81
+ _createLocationObject({
82
+ operation,
83
+ index,
84
+ delta
85
+ }) {
86
+ const {
87
+ location,
88
+ key
89
+ } = this._stack[index];
90
+ return Object.assign({}, location, {
91
+ key,
92
+ operation,
93
+ index,
94
+ delta
95
+ });
96
+ }
97
+ }
@@ -0,0 +1,54 @@
1
+ /* eslint-disable no-underscore-dangle, max-classes-per-file */
2
+
3
+ import ServerSideNavigationError from './error/ServerSideNavigationError';
4
+ export default class ServerSideNavigation {
5
+ init(initialLocation, {
6
+ operation,
7
+ key,
8
+ index,
9
+ delta
10
+ }) {
11
+ return Object.assign({}, initialLocation, {
12
+ operation,
13
+ key,
14
+ index,
15
+ delta
16
+ });
17
+ }
18
+
19
+ // eslint-disable-next-line no-unused-vars
20
+ subscribe(listener) {
21
+ // `ServerSideNavigation` doesn't have any "asynchronycity" about it
22
+ // and any navigation is prohibited and would result in an error.
23
+ // So no asynchronous listener would ever be called
24
+ // due to no asynchronous events being dispatched.
25
+ return () => {};
26
+ }
27
+
28
+ // eslint-disable-next-line no-unused-vars
29
+ navigate(location, {
30
+ operation,
31
+ key,
32
+ index,
33
+ delta
34
+ }) {
35
+ throw new ServerSideNavigationError(location);
36
+ }
37
+
38
+ // eslint-disable-next-line no-unused-vars
39
+ shift({
40
+ operation,
41
+ index,
42
+ delta
43
+ }) {
44
+ // It's not supposed to ever get to this code.
45
+ // * If `delta` is `0` then it's a "do nothing" scenario and `Session` won't even call this code.
46
+ // * If `delta` is not `0` and is out of bounds then a `NavigationOutOfBoundsError` will be thrown.
47
+ // * If `delta` is not `0` and is not out of bounds then it implies that a valid navigation has happened before
48
+ // which can't be the case because no navigation is possible on server side without throwing an error.
49
+ throw new Error('Server side has no navigation history');
50
+ }
51
+ getInitialLocation() {
52
+ return undefined;
53
+ }
54
+ }
@@ -0,0 +1,213 @@
1
+ import getLocationUrl from '../../getLocationUrl';
2
+ // import parseInputLocation from '../../parseInputLocation';
3
+ import parseQueryFromSearch from '../../parseQueryFromSearch';
4
+ import Operations from './operation/operations';
5
+ const NO_LOCATION_INDEX = -1;
6
+
7
+ // A web browser has a notion of a "navigation history".
8
+ // A "navigation history" exists within a given web browser's tab.
9
+ // The user can click "Back" or "Forward" buttons in the web browser and it will automatically load
10
+ // "previous" or "next" page from scratch.
11
+ //
12
+ // Later, web browsers added a `window.history` object that the application can,
13
+ // but isn't required to, interact with. That `window.history` object allows the application
14
+ // to programmatically control the URL in the address bar of the web browser, as well as
15
+ // the "navigation history" by programmatically adding new entries to it or reading the current entry,
16
+ // and it also allows the application to override the default web browser's behavior
17
+ // when the user clicks "Back" or "Forward" buttons in the web browser.
18
+ //
19
+ // Specifically, the `window.history` object has a method called `.pushState()` which programmatically adds
20
+ // a new entry in the "navigation history" and updates the URL in the address bar and also
21
+ // tells the web browser that starting from the entry before this new entry in the "navigation history",
22
+ // the application would prefer to manually handle any "Back"/"Forward" transition when the user clicks
23
+ // those "Back" or "Forward" buttons in the web browser, and this behavior should persist for any future
24
+ // "navigation history" entries programmatically added by the application via `window.history.pushState()`,
25
+ // and will only stop if the user navigates from the page by the means of conventional navigation,
26
+ // that is by clicking a standard hyperlink, at which point the current page gets "destroyed".
27
+ //
28
+ // So for manually "pushed" entries of the "navigation history", the web browser won't load those pages
29
+ // from scratch after a user-initiated "Back" or "Forward" transition. In fact, it won't do anything and
30
+ // it will just step aside and let the application itself do those transitions. The web browser will only
31
+ // update the URL in the address bar and that's it.
32
+ //
33
+ // This whole thing allows the application to:
34
+ //
35
+ // * Load the "previous" or "next" page much faster than when using the default "from scratch" approach
36
+ // because it doesn't have to destroy the current page, then send a new HTTP request to the server,
37
+ // then parse the HTML response and initialize a new page, re-download all those images, etc.
38
+ //
39
+ // * Optionally render a snapshotted verison of the "previous" page thereby "restoring" the "previous" page
40
+ // rather than reloading it from scratch, i.e. the state of the "previous" page could be fully restored.
41
+ //
42
+ export default class WebBrowserNavigation {
43
+ constructor() {
44
+ // `_currentLocationIndex` is used when receiving a "popstate" event
45
+ // that wasn't initiated by the application code but rather by the user
46
+ // clicking "Back" or "Forward" button in their web browser.
47
+ this._currentLocationIndex = NO_LOCATION_INDEX;
48
+ }
49
+
50
+ // Subscribes to changes in the current location.
51
+ // Returns an `unsubscribe()` function which is "idempotent", i.e. it can be called multiple times.
52
+ subscribe(listener) {
53
+ const onPopState = () => {
54
+ // If "popstate" event is received before navigation is initialized,
55
+ // ignore such "popstate" event. This behavior is logical from the application code's view.
56
+ // And besides, `this._currentLocationIndex` is not defined in such conditions.
57
+ if (this._currentLocationIndex === NO_LOCATION_INDEX) {
58
+ throw new Error('Received a "popstate" event before initialized');
59
+ }
60
+ const prevIndex = this._currentLocationIndex;
61
+ const {
62
+ index
63
+ } = this._getCurrentLocationState();
64
+ this._currentLocationIndex = index;
65
+ listener(this._createEntryFromCurrentLocation({
66
+ operation: Operations.SHIFT,
67
+ delta: index - prevIndex
68
+ }));
69
+ };
70
+
71
+ // Due to how `popstate` event listener works, there should only be one listener at a time,
72
+ // otherwise two different `Session`s would react to the same `popstate` event,
73
+ // each interpreting it as its own, while in reality it only belongs to one of them
74
+ // and the other one should completely ignore it.
75
+ // In other words, there can't exist two navigation sessions simultaneously by design.
76
+ // There can only be one active navigation session at a given time.
77
+ // Another one could only start after the previous one ends,
78
+ // not both of them being active simultaneously.
79
+ if (this._subscribed) {
80
+ throw new Error('There already is an active subscription. Only one subscription is allowed at a time.');
81
+ }
82
+ window.addEventListener('popstate', onPopState);
83
+ this._subscribed = true;
84
+ return () => {
85
+ window.removeEventListener('popstate', onPopState);
86
+ this._subscribed = false;
87
+ };
88
+ }
89
+ init(initialLocation, {
90
+ operation,
91
+ key,
92
+ index,
93
+ delta
94
+ }) {
95
+ // Validate that `initialLocation` is same as `window.location`.
96
+ const isCurrentLocation = initialLocation === this._getCurrentLocation() || this._isSameAsCurrentLocation(initialLocation);
97
+ if (!isCurrentLocation) {
98
+ throw new Error('`initialLocation` argument should be same as `window.location`');
99
+ }
100
+
101
+ // Set `window.history.state` of the initial location
102
+ // by calling `window.history.replaceState()` on page load.
103
+ // Otherwise, `window.history.state` would be `null` for the initial location
104
+ // and there'd be no place to store the additional properties of the initial location
105
+ // such as `location.key`.
106
+ // https://github.com/taion/scroll-behavior/issues/215
107
+ //
108
+ // If the user opens the initial page for the first time, `window.history.state` will be `null`.
109
+ // If the user refreshes the initial page, `window.history.state` will not be cleared
110
+ // and therefore will not be `null` and will instead have the previously-set value.
111
+ //
112
+ if (!this._getCurrentLocationState()) {
113
+ // Create additional properties for the initial locaiton.
114
+ const additionalProperties = {
115
+ key,
116
+ index
117
+ };
118
+ // Call `history.replaceState()`.
119
+ this._storeAdditionalPropertiesForLocation(initialLocation, additionalProperties, delta);
120
+ }
121
+ this._currentLocationIndex = index;
122
+
123
+ // Call the listeners.
124
+ return this._createEntryFromCurrentLocation({
125
+ operation,
126
+ delta
127
+ });
128
+ }
129
+ navigate(location, {
130
+ operation,
131
+ key,
132
+ index,
133
+ delta
134
+ }) {
135
+ const additionalProperties = {
136
+ key,
137
+ index
138
+ };
139
+ this._storeAdditionalPropertiesForLocation(location, additionalProperties, delta);
140
+ this._currentLocationIndex = index;
141
+
142
+ // Call the listeners.
143
+ return Object.assign({
144
+ operation,
145
+ delta
146
+ }, location, additionalProperties);
147
+ }
148
+
149
+ // shift({ operation, index, delta }) {
150
+ shift({
151
+ delta
152
+ }) {
153
+ // Web browser `history` is extremely non-strict when it comes to `history.go(delta)` navigation.
154
+ // It will allow any number as `delta`, regardless of whether such history entry exists or not.
155
+ // To introduce strict validation of the `delta` argument, `Session` class code explicitly checks
156
+ // the new `index` on whether it's out of bounds of the navigation history stack, and after it verifies
157
+ // that the new `index` is valid, it calls the `.shift(delta)` method of `WebBrowserNavigation` class.
158
+ //
159
+ // Calling `window.history.go()` will trigger a "popstate" event which will trigger the listeners.
160
+ //
161
+ window.history.go(delta);
162
+ }
163
+ getInitialLocation() {
164
+ // Web browser environment already knows the initial location
165
+ // by the time javascript code starts execution.
166
+ return this._getCurrentLocation();
167
+ }
168
+ _getCurrentLocation() {
169
+ return window.location;
170
+ }
171
+ _getCurrentLocationState() {
172
+ return window.history.state;
173
+ }
174
+ _isSameAsCurrentLocation(inputLocation) {
175
+ return typeof inputLocation === 'string' ? inputLocation === getLocationUrl(this._getCurrentLocation()) : inputLocation === this._getCurrentLocation() || getLocationUrl(inputLocation) === getLocationUrl(this._getCurrentLocation());
176
+ }
177
+ _createEntryFromCurrentLocation({
178
+ operation,
179
+ delta
180
+ }) {
181
+ const {
182
+ pathname,
183
+ search,
184
+ hash
185
+ } = this._getCurrentLocation();
186
+ const {
187
+ key,
188
+ index
189
+ } = this._getCurrentLocationState();
190
+ return {
191
+ operation,
192
+ pathname,
193
+ search,
194
+ query: parseQueryFromSearch(search),
195
+ hash,
196
+ key,
197
+ index,
198
+ delta
199
+ };
200
+ }
201
+ _storeAdditionalPropertiesForLocation(location, additionalProperties, delta) {
202
+ const url = getLocationUrl(location);
203
+ // `delta` property is not stored in `window.history.state`
204
+ // because it is supposed to be recalculated every time when reading from `window.history.state`.
205
+ if (delta === 1) {
206
+ window.history.pushState(additionalProperties, null, url);
207
+ } else if (delta === 0) {
208
+ window.history.replaceState(additionalProperties, null, url);
209
+ } else {
210
+ throw new Error(`Unsupported \`delta\`: ${delta}`);
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,6 @@
1
+ export default class NavigationOutOfBoundsError extends Error {
2
+ constructor(index) {
3
+ super(`Location index ${index} is out of navigation history bounds`);
4
+ this.index = index;
5
+ }
6
+ }
@@ -0,0 +1,14 @@
1
+ const _excluded = ["operation"];
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
+ import getLocationUrl from '../../../getLocationUrl';
4
+ export default class ServerSideNavigationError extends Error {
5
+ constructor(location) {
6
+ super(location ? `Navigate to ${getLocationUrl(location)}` : 'Navigate to previous or next location');
7
+ if (location) {
8
+ // Remove `operation` property from `location`.
9
+ // eslint-disable-next-line no-unused-vars
10
+ const locationBase = _objectWithoutPropertiesLoose(location, _excluded);
11
+ this.location = locationBase;
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ INIT: 'INIT',
3
+ PUSH: 'PUSH',
4
+ REPLACE: 'REPLACE',
5
+ SHIFT: 'SHIFT'
6
+ };
@@ -0,0 +1,75 @@
1
+ export default class Subscription {
2
+ constructor({
3
+ activateSubscription
4
+ } = {}) {
5
+ this.notifySubscribers = argument => {
6
+ // `._latest` is only used in tests.
7
+ this._latest = argument;
8
+ for (const {
9
+ listener
10
+ } of this._listeners) {
11
+ listener(argument);
12
+ }
13
+ };
14
+ this._activateSubscription = activateSubscription;
15
+
16
+ // This property is accessed in tests.
17
+ this._listeners = [];
18
+ }
19
+ subscribe(listener) {
20
+ // If subscriptions are stopped, i.e. no new subscriptions are to be added,
21
+ // then don't add any listeners and return a "do nothing" function.
22
+ if (this._stopped) {
23
+ return () => {};
24
+ }
25
+
26
+ // Creating a `listenerEntry` object ensures that the `.filter()` function
27
+ // during "unsubscribe" step doesn't accidentally remove another listeners
28
+ // having the same `listener` function.
29
+ // I.e. it's not illegal to call `.subscribe(listener)` multiple times
30
+ // with the same argument, and those would be considered different subscriptions.
31
+ const listenerEntry = {
32
+ listener
33
+ };
34
+
35
+ // If it's the first listener, activate subscription.
36
+ if (this._listeners.length === 0) {
37
+ this._deactivateSubscription = this._activateSubscription(this.notifySubscribers);
38
+ }
39
+
40
+ // Add the `listener` to the list.
41
+ this._listeners.push(listenerEntry);
42
+
43
+ // The returned `unsubscribe()` function is "idempotent", i.e. it can be called multiple times.
44
+ return () => {
45
+ // Remove the listener, if not already removed.
46
+ this._removeListener(listenerEntry);
47
+ };
48
+ }
49
+ stop() {
50
+ if (this._stopped) {
51
+ throw new Error('Already stopped');
52
+ }
53
+ this._stopped = true;
54
+
55
+ // Clear any remaining listeners.
56
+ for (const listener of this._listeners.slice()) {
57
+ this._removeListener(listener);
58
+ }
59
+ }
60
+ _removeListener(listenerEntry) {
61
+ // If no listeners are left, no need to do anything.
62
+ if (this._listeners.length === 0) {
63
+ return;
64
+ }
65
+
66
+ // Remove the `listener` from the list.
67
+ this._listeners = this._listeners.filter(_ => _ !== listenerEntry);
68
+
69
+ // If it was the last listener, deactivate subscription.
70
+ if (this._listeners.length === 0) {
71
+ this._deactivateSubscription();
72
+ this._deactivateSubscription = undefined;
73
+ }
74
+ }
75
+ }