navigation-stack 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +144 -282
  3. package/karma.conf.cjs +1 -1
  4. package/lib/cjs/NavigationStack.js +138 -49
  5. package/lib/cjs/data-storage/DataStorage.js +7 -6
  6. package/lib/cjs/environment/InMemoryEnvironment.js +6 -0
  7. package/lib/cjs/{session/ServerSideRenderSession.js → environment/ServerSideRenderEnvironment.js} +5 -6
  8. package/lib/cjs/environment/WebBrowserEnvironment.js +6 -0
  9. package/lib/cjs/environment/log/InMemoryLog.js +23 -0
  10. package/lib/cjs/environment/log/WebBrowserLog.js +22 -0
  11. package/lib/cjs/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  12. package/lib/cjs/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  13. package/lib/cjs/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  14. package/lib/cjs/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +2 -2
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  16. package/lib/cjs/getLocationBaseFromLocation.js +14 -0
  17. package/lib/cjs/getLocationUrl.js +3 -5
  18. package/lib/cjs/index.js +10 -16
  19. package/lib/cjs/navigationBlockers.js +34 -32
  20. package/lib/cjs/navigationBlockersEvaluation.js +150 -0
  21. package/lib/cjs/parseInputLocation.js +2 -2
  22. package/lib/cjs/parseQueryFromSearch.js +3 -6
  23. package/lib/cjs/parseQueryString.js +77 -0
  24. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +7 -6
  25. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +31 -27
  26. package/lib/cjs/scroll-position/ScrollPositionSaver.js +6 -4
  27. package/lib/cjs/session/Session.js +61 -26
  28. package/lib/cjs/session/subscription/Subscription.js +36 -18
  29. package/lib/cjs/stringifyQuery.js +66 -0
  30. package/lib/cjs/stringifyQueryAsSearch.js +14 -0
  31. package/lib/esm/NavigationStack.js +138 -49
  32. package/lib/esm/data-storage/DataStorage.js +7 -6
  33. package/lib/esm/environment/InMemoryEnvironment.js +6 -0
  34. package/lib/esm/environment/ServerSideRenderEnvironment.js +10 -0
  35. package/lib/esm/environment/WebBrowserEnvironment.js +6 -0
  36. package/lib/esm/environment/log/InMemoryLog.js +17 -0
  37. package/lib/esm/environment/log/WebBrowserLog.js +16 -0
  38. package/lib/esm/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  39. package/lib/esm/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  40. package/lib/esm/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  41. package/lib/esm/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  42. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  43. package/lib/esm/getLocationBaseFromLocation.js +9 -0
  44. package/lib/esm/getLocationUrl.js +2 -5
  45. package/lib/esm/index.js +5 -8
  46. package/lib/esm/navigationBlockers.js +34 -32
  47. package/lib/esm/navigationBlockersEvaluation.js +145 -0
  48. package/lib/esm/parseInputLocation.js +2 -2
  49. package/lib/esm/parseQueryFromSearch.js +2 -6
  50. package/lib/esm/parseQueryString.js +72 -0
  51. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +7 -6
  52. package/lib/esm/scroll-position/ScrollPositionRestoration.js +31 -27
  53. package/lib/esm/scroll-position/ScrollPositionSaver.js +6 -4
  54. package/lib/esm/session/Session.js +61 -26
  55. package/lib/esm/session/subscription/Subscription.js +36 -18
  56. package/lib/esm/stringifyQuery.js +61 -0
  57. package/lib/esm/stringifyQueryAsSearch.js +8 -0
  58. package/lib/index.d.ts +180 -34
  59. package/package.json +4 -7
  60. package/src/NavigationStack.js +166 -56
  61. package/src/data-storage/DataStorage.js +9 -6
  62. package/src/environment/InMemoryEnvironment.js +6 -0
  63. package/src/environment/ServerSideRenderEnvironment.js +10 -0
  64. package/src/environment/WebBrowserEnvironment.js +6 -0
  65. package/src/environment/log/InMemoryLog.js +20 -0
  66. package/src/environment/log/WebBrowserLog.js +18 -0
  67. package/src/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  68. package/src/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  69. package/src/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  70. package/src/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  71. package/src/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  72. package/src/getLocationBaseFromLocation.js +7 -0
  73. package/src/getLocationUrl.js +2 -5
  74. package/src/index.js +10 -13
  75. package/src/navigationBlockers.js +55 -34
  76. package/src/navigationBlockersEvaluation.js +161 -0
  77. package/src/parseInputLocation.js +2 -2
  78. package/src/parseQueryFromSearch.js +2 -6
  79. package/src/parseQueryString.js +81 -0
  80. package/src/scroll-position/ScrollPositionAutoSaver.js +10 -6
  81. package/src/scroll-position/ScrollPositionRestoration.js +36 -30
  82. package/src/scroll-position/ScrollPositionSaver.js +6 -4
  83. package/src/scroll-position/index.js +1 -1
  84. package/src/session/Session.js +68 -24
  85. package/src/session/subscription/Subscription.js +36 -11
  86. package/src/stringifyQuery.js +71 -0
  87. package/src/stringifyQueryAsSearch.js +9 -0
  88. package/test/NavigationStack.addBasePath.test.js +50 -0
  89. package/test/{redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockNonProgrammaticNavigationIfRequired.test.js} +51 -63
  90. package/test/{redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockProgrammaticNavigationIfRequired.test.js} +98 -78
  91. package/test/NavigationStack.general.test.js +68 -0
  92. package/test/NavigationStack.parseInputLocation.test.js +52 -0
  93. package/test/NavigationStack.removeBasePath.test.js +69 -0
  94. package/test/NavigationStack.test.js +97 -29
  95. package/test/data-storage/LocationDataStorage.test.js +3 -2
  96. package/test/index.js +7 -31
  97. package/test/index.test.js +4 -5
  98. package/test/parseQueryFromSearch.test.js +19 -0
  99. package/test/parseQueryString.test.js +18 -0
  100. package/test/scroll-position/ScrollPositionRestoration.test.js +34 -13
  101. package/test/scroll-position/createApp.js +8 -8
  102. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +4 -4
  103. package/test/session/{InMemorySession.test.js → Session.InMemoryEnvironment.test.js} +10 -9
  104. package/test/session/{ServerSession.test.js → Session.ServerSideRenderEnvironment.test.js} +5 -4
  105. package/test/session/{WebBrowserSession.test.js → Session.WebBrowserEnvironment.test.js} +63 -13
  106. package/test/shouldWarn.js +44 -0
  107. package/test/stringifyQuery.test.js +65 -0
  108. package/types/index.d.ts +180 -34
  109. package/types/tsconfig.json +0 -1
  110. package/data-storage/package.json +0 -7
  111. package/lib/cjs/createSearchFromQuery.js +0 -13
  112. package/lib/cjs/debug.js +0 -12
  113. package/lib/cjs/redux/ActionTypes.js +0 -14
  114. package/lib/cjs/redux/ActionTypesInternal.js +0 -8
  115. package/lib/cjs/redux/Actions.js +0 -28
  116. package/lib/cjs/redux/createMiddlewares.js +0 -60
  117. package/lib/cjs/redux/index.js +0 -13
  118. package/lib/cjs/redux/internalLocationReducer.js +0 -14
  119. package/lib/cjs/redux/locationReducer.js +0 -13
  120. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -32
  121. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -113
  122. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  123. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -30
  124. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -73
  125. package/lib/cjs/redux/middleware/navigationOperationMiddleware.js +0 -40
  126. package/lib/cjs/redux/middleware/parseInputLocationMiddleware.js +0 -29
  127. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +0 -34
  128. package/lib/cjs/session/InMemorySession.js +0 -22
  129. package/lib/cjs/session/WebBrowserSession.js +0 -20
  130. package/lib/data-storage/index.d.ts +0 -35
  131. package/lib/esm/createSearchFromQuery.js +0 -8
  132. package/lib/esm/debug.js +0 -7
  133. package/lib/esm/redux/ActionTypes.js +0 -9
  134. package/lib/esm/redux/ActionTypesInternal.js +0 -3
  135. package/lib/esm/redux/Actions.js +0 -22
  136. package/lib/esm/redux/createMiddlewares.js +0 -54
  137. package/lib/esm/redux/index.js +0 -4
  138. package/lib/esm/redux/internalLocationReducer.js +0 -8
  139. package/lib/esm/redux/locationReducer.js +0 -7
  140. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  141. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -108
  142. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -88
  143. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -25
  144. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -68
  145. package/lib/esm/redux/middleware/navigationOperationMiddleware.js +0 -35
  146. package/lib/esm/redux/middleware/parseInputLocationMiddleware.js +0 -24
  147. package/lib/esm/redux/middleware/updateLocationMiddleware.js +0 -28
  148. package/lib/esm/session/InMemorySession.js +0 -15
  149. package/lib/esm/session/ServerSideRenderSession.js +0 -11
  150. package/lib/esm/session/WebBrowserSession.js +0 -13
  151. package/lib/redux/index.d.ts +0 -90
  152. package/lib/scroll-position/index.d.ts +0 -107
  153. package/redux/package.json +0 -7
  154. package/scroll-position/package.json +0 -7
  155. package/src/createSearchFromQuery.js +0 -9
  156. package/src/debug.js +0 -8
  157. package/src/redux/ActionTypes.js +0 -9
  158. package/src/redux/ActionTypesInternal.js +0 -3
  159. package/src/redux/Actions.js +0 -27
  160. package/src/redux/createMiddlewares.js +0 -65
  161. package/src/redux/index.js +0 -4
  162. package/src/redux/internalLocationReducer.js +0 -9
  163. package/src/redux/locationReducer.js +0 -8
  164. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  165. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -119
  166. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  167. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -26
  168. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -72
  169. package/src/redux/middleware/navigationOperationMiddleware.js +0 -34
  170. package/src/redux/middleware/parseInputLocationMiddleware.js +0 -23
  171. package/src/redux/middleware/updateLocationMiddleware.js +0 -28
  172. package/src/session/InMemorySession.js +0 -13
  173. package/src/session/ServerSideRenderSession.js +0 -9
  174. package/src/session/WebBrowserSession.js +0 -13
  175. package/test/middlewareTestUtil.js +0 -31
  176. package/test/redux/Action.test.js +0 -73
  177. package/test/redux/ActionTypes.test.js +0 -13
  178. package/test/redux/createMiddlewares.test.js +0 -96
  179. package/test/redux/index.test.js +0 -10
  180. package/test/redux/locationReducer.test.js +0 -39
  181. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +0 -40
  182. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +0 -51
  183. package/test/redux/middleware/navigationOperationMiddleware.test.js +0 -78
  184. package/test/redux/middleware/parseInputLocationMiddleware.test.js +0 -62
  185. package/test/testUtil.js +0 -3
  186. package/types/data-storage/index.d.ts +0 -35
  187. package/types/redux/index.d.ts +0 -90
  188. package/types/scroll-position/index.d.ts +0 -107
  189. /package/lib/cjs/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  190. /package/lib/cjs/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  191. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  192. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  193. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  194. /package/lib/cjs/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  195. /package/lib/cjs/{session → environment}/navigation/operation/operations.js +0 -0
  196. /package/lib/esm/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  197. /package/lib/esm/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  198. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  199. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  200. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  201. /package/lib/esm/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  202. /package/lib/esm/{session → environment}/navigation/operation/operations.js +0 -0
  203. /package/src/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  204. /package/src/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  205. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  206. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  207. /package/src/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  208. /package/src/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  209. /package/src/{session → environment}/navigation/operation/operations.js +0 -0
  210. /package/test/{parseInputLocationMiddleware.test.js → parseInputLocation.test.js} +0 -0
@@ -0,0 +1,10 @@
1
+ import InMemoryEnvironment from './InMemoryEnvironment';
2
+ import ServerSideNavigation from './navigation/ServerSideNavigation';
3
+
4
+ // `ServerSideRenderSession` is just a `InMemorySession` that specifically prohibits any navigation.
5
+ export default class ServerSideRenderEnvironment extends InMemoryEnvironment {
6
+ constructor() {
7
+ super();
8
+ this.navigation = new ServerSideNavigation();
9
+ }
10
+ }
@@ -1,9 +1,15 @@
1
1
  import WebBrowserDataStorage from './data-storage/WebBrowserDataStorage';
2
+ import WebBrowserSessionLifecycle from './lifecycle/WebBrowserSessionLifecycle';
3
+ import WebBrowserLog from './log/WebBrowserLog';
4
+ import WebBrowserNavigation from './navigation/WebBrowserNavigation';
2
5
  import WebBrowserScrollPosition from './scroll-position/WebBrowserScrollPosition';
3
6
 
4
7
  export default class WebBrowserEnvironment {
5
8
  constructor() {
6
9
  this.dataStorage = new WebBrowserDataStorage();
7
10
  this.scrollPosition = new WebBrowserScrollPosition();
11
+ this.lifecycle = new WebBrowserSessionLifecycle();
12
+ this.log = new WebBrowserLog();
13
+ this.navigation = new WebBrowserNavigation();
8
14
  }
9
15
  }
@@ -0,0 +1,20 @@
1
+ const DEBUG_ENABLED = false;
2
+
3
+ export default class InMemoryLog {
4
+ debug(...args) {
5
+ if (DEBUG_ENABLED) {
6
+ // eslint-disable-next-line no-console
7
+ console.log(...args);
8
+ }
9
+ }
10
+
11
+ warn(...args) {
12
+ // eslint-disable-next-line no-console
13
+ console.warn(...args);
14
+ }
15
+
16
+ error(...args) {
17
+ // eslint-disable-next-line no-console
18
+ console.error(...args);
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ export default class WebBrowserLog {
2
+ debug(...args) {
3
+ if (window.NAVIGATION_STACK_DEBUG_ENABLED) {
4
+ // eslint-disable-next-line no-console
5
+ console.log(...args);
6
+ }
7
+ }
8
+
9
+ warn(...args) {
10
+ // eslint-disable-next-line no-console
11
+ console.warn(...args);
12
+ }
13
+
14
+ error(...args) {
15
+ // eslint-disable-next-line no-console
16
+ console.error(...args);
17
+ }
18
+ }
@@ -6,12 +6,23 @@ export default class InMemoryNavigation {
6
6
  this._stack = [];
7
7
  }
8
8
 
9
+ // Subscribes to any "asynchronous" changes of the current location,
10
+ // "asynchronous" changes being ones that happen out-of-sync with the code
11
+ // that might have potentially triggered those changes.
12
+ //
13
+ // For example, in a web browser, "Back"/"Forward" navigation happens out-of-sync
14
+ // with the code that calls `window.pushState()` or `window.replaceState()` function.
15
+ //
16
+ // Additionally, in a web browser, "Back"/"Forward" navigation could be triggered
17
+ // outside of the application code by user clicking those "Back"/"Forward" buttons manually
18
+ // in their web browser.
19
+ //
9
20
  // eslint-disable-next-line no-unused-vars
10
- subscribe(listener) {
11
- // `InMemoryNavigation` doesn't have any "asynchronycity" about it
12
- // and performs any navigation immediately at the time of the call.
13
- // Hence, no asynchronous listener would ever be called
14
- // due to no asynchronous events being dispatched.
21
+ subscribeToAsyncrhonousLocationUpdates(listener) {
22
+ // `InMemoryNavigation` location changes are always "synchronous"
23
+ // with the code that initiated such changes, i.e. it always performs
24
+ // any navigation immediately at the time such navigation is triggered in code.
25
+ // Hence, this function doesn't have to "subscribe" to anything, so it's a "no op".
15
26
  return () => {};
16
27
  }
17
28
 
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable no-underscore-dangle, max-classes-per-file */
2
2
 
3
- import ServerSideNavigationError from './error/ServerSideNavigationError';
3
+ import ServerSideRedirectError from './error/ServerSideRedirectError';
4
4
 
5
5
  export default class ServerSideNavigation {
6
6
  init(initialLocation, { operation, key, index, delta }) {
@@ -13,18 +13,27 @@ export default class ServerSideNavigation {
13
13
  };
14
14
  }
15
15
 
16
+ // Subscribes to any "asynchronous" changes of the current location,
17
+ // "asynchronous" changes being ones that happen out-of-sync with the code
18
+ // that might have potentially triggered those changes.
19
+ //
20
+ // For example, in a web browser, "Back"/"Forward" navigation happens out-of-sync
21
+ // with the code that calls `window.pushState()` or `window.replaceState()` function.
22
+ //
23
+ // Additionally, in a web browser, "Back"/"Forward" navigation could be triggered
24
+ // outside of the application code by user clicking those "Back"/"Forward" buttons manually
25
+ // in their web browser.
26
+ //
16
27
  // eslint-disable-next-line no-unused-vars
17
- subscribe(listener) {
18
- // `ServerSideNavigation` doesn't have any "asynchronycity" about it
19
- // and any navigation is prohibited and would result in an error.
20
- // So no asynchronous listener would ever be called
21
- // due to no asynchronous events being dispatched.
28
+ subscribeToAsyncrhonousLocationUpdates(listener) {
29
+ // `ServerSideNavigation` location changes are prohibited, so they couldn't happen.
30
+ // Hence, this function doesn't have to "subscribe" to anything, so it's a "no op".
22
31
  return () => {};
23
32
  }
24
33
 
25
34
  // eslint-disable-next-line no-unused-vars
26
35
  navigate(location, { operation, key, index, delta }) {
27
- throw new ServerSideNavigationError(location);
36
+ throw new ServerSideRedirectError(location);
28
37
  }
29
38
 
30
39
  // eslint-disable-next-line no-unused-vars
@@ -48,15 +48,30 @@ export default class WebBrowserNavigation {
48
48
  this._currentLocationIndex = NO_LOCATION_INDEX;
49
49
  }
50
50
 
51
- // Subscribes to changes in the current location.
51
+ // Subscribes to any "asynchronous" changes of the current location,
52
+ // "asynchronous" changes being ones that happen out-of-sync with the code
53
+ // that might have potentially triggered those changes.
54
+ //
55
+ // For example, in a web browser, "Back"/"Forward" navigation happens out-of-sync
56
+ // with the code that calls `window.pushState()` or `window.replaceState()` function.
57
+ //
58
+ // Additionally, in a web browser, "Back"/"Forward" navigation could be triggered
59
+ // outside of the application code by user clicking those "Back"/"Forward" buttons manually
60
+ // in their web browser.
61
+ //
52
62
  // Returns an `unsubscribe()` function which is "idempotent", i.e. it can be called multiple times.
53
- subscribe(listener) {
63
+ //
64
+ subscribeToAsyncrhonousLocationUpdates(listener) {
54
65
  const onPopState = () => {
55
66
  // If "popstate" event is received before navigation is initialized,
56
- // ignore such "popstate" event. This behavior is logical from the application code's view.
67
+ // ignore such "popstate" event. Such "ignore" behavior is logical from
68
+ // the application code's point of view: it doesn't expect any navigation events
69
+ // to be recorded before it has initialized the navigation.
57
70
  // And besides, `this._currentLocationIndex` is not defined in such conditions.
58
71
  if (this._currentLocationIndex === NO_LOCATION_INDEX) {
59
- throw new Error('Received a "popstate" event before initialized');
72
+ throw new Error(
73
+ 'Received a "popstate" event before finished initializing navigation',
74
+ );
60
75
  }
61
76
  const prevIndex = this._currentLocationIndex;
62
77
  const { index } = this._getCurrentLocationState();
@@ -93,6 +108,15 @@ export default class WebBrowserNavigation {
93
108
  };
94
109
  }
95
110
 
111
+ // When run in a web browser, it could not only "start" a new navigation session
112
+ // but also "resume" a previously-started navigation session. That could happen
113
+ // when the user refreshes a page in a web browser which still retains
114
+ // the previous navigation session's data but at the same time restarts
115
+ // the javascript code from scratch.
116
+ //
117
+ // So this `init()` method handles both cases: when there's previous navigation session's data
118
+ // that should be restored and when there's no previous navigation session's data.
119
+ //
96
120
  init(initialLocation, { operation, key, index, delta }) {
97
121
  // Validate that `initialLocation` is same as `window.location`.
98
122
  const isCurrentLocation =
@@ -109,7 +133,8 @@ export default class WebBrowserNavigation {
109
133
  // by calling `window.history.replaceState()` on page load.
110
134
  // Otherwise, `window.history.state` would be `null` for the initial location
111
135
  // and there'd be no place to store the additional properties of the initial location
112
- // such as `location.key`.
136
+ // such as `location.key`. Without `location.key` always being present the initial location object,
137
+ // there'd be a bug of incorrect scroll position being set on the initial location URL in some scenarios:
113
138
  // https://github.com/taion/scroll-behavior/issues/215
114
139
  //
115
140
  // If the user opens the initial page for the first time, `window.history.state` will be `null`.
@@ -120,7 +145,7 @@ export default class WebBrowserNavigation {
120
145
  // Create additional properties for the initial locaiton.
121
146
  const additionalProperties = { key, index };
122
147
  // Call `history.replaceState()`.
123
- this._storeAdditionalPropertiesForLocation(
148
+ this._navigateToLocationAndKeepItsAdditionalPropertiesInHistory(
124
149
  initialLocation,
125
150
  additionalProperties,
126
151
  delta,
@@ -136,7 +161,7 @@ export default class WebBrowserNavigation {
136
161
  navigate(location, { operation, key, index, delta }) {
137
162
  const additionalProperties = { key, index };
138
163
 
139
- this._storeAdditionalPropertiesForLocation(
164
+ this._navigateToLocationAndKeepItsAdditionalPropertiesInHistory(
140
165
  location,
141
166
  additionalProperties,
142
167
  delta,
@@ -205,7 +230,22 @@ export default class WebBrowserNavigation {
205
230
  };
206
231
  }
207
232
 
208
- _storeAdditionalPropertiesForLocation(
233
+ // Stores "additional" properties associated with `location` in web browser's history storage.
234
+ // Web browser's history storage is not intended for large datasets and should only be used
235
+ // to store small bits of data.
236
+ //
237
+ // "Some browsers save state objects to the user's disk so they can be restored after the user restarts
238
+ // the browser, and impose a size limit on the serialized representation of a state object, and will throw
239
+ // an exception if you pass a state object whose serialized representation is larger than that size limit.
240
+ // So in cases where you want to ensure you have more space than what some browsers might impose,
241
+ // you're encouraged to use sessionStorage and/or localStorage."
242
+ //
243
+ // Source: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
244
+ //
245
+ // To store large amounts of data, one could use `window.sessionStorage` instead.
246
+ // It is accessible via `DataStorage(session)` class.
247
+ //
248
+ _navigateToLocationAndKeepItsAdditionalPropertiesInHistory(
209
249
  location,
210
250
  additionalProperties,
211
251
  delta,
@@ -1,6 +1,6 @@
1
1
  import getLocationUrl from '../../../getLocationUrl';
2
2
 
3
- export default class ServerSideNavigationError extends Error {
3
+ export default class ServerSideRedirectError extends Error {
4
4
  constructor(location) {
5
5
  super(
6
6
  location
@@ -61,10 +61,25 @@ export default class WebBrowserScrollPosition {
61
61
  }
62
62
 
63
63
  enableAutomaticScrollRestoration() {
64
+ // The default "auto" behavior seems to work in the following way:
65
+ //
66
+ // * It doesn't scroll to top on `window.history.pushState()` or `window.history.replaceState()`.
67
+ //
68
+ // * It does restore scroll position on "popstate" event
69
+ // (they say, in Firefox it happens before the event is dispatched,
70
+ // while in Chrome it happens after the event is dispatched)
71
+ //
72
+ // https://v5.reactrouter.com/web/guides/scroll-restoration
73
+ //
64
74
  window.history.scrollRestoration = 'auto';
65
75
  }
66
76
 
67
77
  disableAutomaticScrollRestoration() {
78
+ // Setting `window.history.scrollRestoration` value updates it in the current history entry
79
+ // and any subsequent history entries.
80
+ // This means that it should be set at application initialization stage,
81
+ // that is before any navigation.
82
+ // https://majido.github.io/scroll-restoration-proposal/history-based-api.html
68
83
  window.history.scrollRestoration = 'manual';
69
84
  }
70
85
 
@@ -0,0 +1,7 @@
1
+ // Converts `Location` object to a `LocationBase` object.
2
+ // It hides properties of location such as `key` or `index`.
3
+ export default function getLocationBaseFromLocation(location) {
4
+ // eslint-disable-next-line no-unused-vars
5
+ const { key, index, ...locationBase } = location;
6
+ return locationBase;
7
+ }
@@ -1,11 +1,8 @@
1
- import { stringify as stringifyQuery } from 'query-string';
1
+ import stringifyQueryAsSearch from './stringifyQueryAsSearch';
2
2
 
3
3
  export default function getLocationUrl({ pathname, search, query, hash }) {
4
4
  if (!search && query) {
5
- const queryString = stringifyQuery(query);
6
- if (queryString) {
7
- search = `?${queryString}`;
8
- }
5
+ search = stringifyQueryAsSearch(query);
9
6
  }
10
7
 
11
8
  return `${pathname}${search || ''}${hash || ''}`;
package/src/index.js CHANGED
@@ -1,14 +1,11 @@
1
1
  export { addBasePath, removeBasePath } from './basePath';
2
- export addNavigationBlocker from './addNavigationBlocker';
3
- export getLocationUrl from './getLocationUrl';
4
- export parseLocationUrl from './parseLocationUrl';
5
- export parseInputLocation from './parseInputLocation';
6
- export NavigationStack from './NavigationStack';
7
- export DataStorage from './data-storage/DataStorage';
8
- export LocationDataStorage from './data-storage/LocationDataStorage';
9
- export Session from './session/Session';
10
- export InMemorySession from './session/InMemorySession';
11
- export WebBrowserSession from './session/WebBrowserSession';
12
- export ServerSideRenderSession from './session/ServerSideRenderSession';
13
- export ServerSideNavigationError from './session/navigation/error/ServerSideNavigationError';
14
- export NavigationOutOfBoundsError from './session/navigation/error/NavigationOutOfBoundsError';
2
+ export { default as addNavigationBlocker } from './addNavigationBlocker';
3
+ export { default as getLocationUrl } from './getLocationUrl';
4
+ export { default as parseLocationUrl } from './parseLocationUrl';
5
+ export { default as parseInputLocation } from './parseInputLocation';
6
+ export { default as NavigationStack } from './NavigationStack';
7
+ export { default as InMemoryEnvironment } from './environment/InMemoryEnvironment';
8
+ export { default as WebBrowserEnvironment } from './environment/WebBrowserEnvironment';
9
+ export { default as ServerSideRenderEnvironment } from './environment/ServerSideRenderEnvironment';
10
+ export { default as ServerSideRedirectError } from './environment/navigation/error/ServerSideRedirectError';
11
+ export { default as NavigationOutOfBoundsError } from './environment/navigation/error/NavigationOutOfBoundsError';
@@ -1,30 +1,33 @@
1
1
  /* eslint-disable no-underscore-dangle */
2
2
 
3
- import debug from './debug';
4
3
  import isPromise from './isPromise';
5
4
 
6
- export function getNavigationBlockers(session) {
7
- return session._navigationBlockersList || [];
5
+ export function getNavigationBlockers(container) {
6
+ return container._navigationBlockersList || [];
8
7
  }
9
8
 
10
- function addNavigationBlockerToTheList(blocker, session) {
11
- if (!session._navigationBlockersList) {
12
- session._navigationBlockersList = [];
9
+ function addNavigationBlockerToTheList(blocker, container) {
10
+ if (!container._navigationBlockersList) {
11
+ container._navigationBlockersList = [];
13
12
  }
14
- session._navigationBlockersList.push(blocker);
13
+ container._navigationBlockersList.push(blocker);
15
14
  }
16
15
 
17
- function removeNavigationBlockerFromTheList(blocker, session) {
18
- if (session._navigationBlockersList) {
19
- session._navigationBlockersList = session._navigationBlockersList.filter(
20
- (_) => _ !== blocker,
21
- );
16
+ function removeNavigationBlockerFromTheList(blocker, container) {
17
+ if (container._navigationBlockersList) {
18
+ container._navigationBlockersList =
19
+ container._navigationBlockersList.filter((_) => _ !== blocker);
22
20
  }
23
21
  }
24
22
 
25
23
  export function removeAllNavigationBlockers(session) {
24
+ // `navigationBlockers` are stored in `session`.
25
+ const container = session;
26
+
26
27
  if (
27
- getNavigationBlockers(session).some((blocker) => blocker.beforeTermination)
28
+ getNavigationBlockers(container).some(
29
+ (blocker) => blocker.beforeTermination,
30
+ )
28
31
  ) {
29
32
  if (!session._removeTerminationBlocker) {
30
33
  throw new Error(
@@ -34,33 +37,29 @@ export function removeAllNavigationBlockers(session) {
34
37
  session._removeTerminationBlocker();
35
38
  session._removeTerminationBlocker = undefined;
36
39
  }
37
- session._navigationBlockersList = [];
40
+ container._navigationBlockersList = [];
38
41
  }
39
42
 
40
43
  // Runs the `blocker` while ignoring any errors that might be thrown by it.
41
- function runNavigationBlocker({ blocker }, location) {
44
+ function runNavigationBlocker({ blocker }, location, environment) {
42
45
  let result;
43
46
  try {
44
47
  result = blocker(location);
45
48
  } catch (error) {
46
- // eslint-disable-next-line no-console
47
- console.warn(
49
+ environment.log.warn(
48
50
  `Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`,
49
51
  );
50
- // eslint-disable-next-line no-console
51
- console.error(error);
52
+ environment.log.error(error);
52
53
  }
53
54
 
54
55
  // If the blocker returned a `Promise`, await for that `Promise`
55
56
  // and then return the result.
56
57
  if (isPromise(result)) {
57
58
  return result.catch((error) => {
58
- // eslint-disable-next-line no-console
59
- console.warn(
59
+ environment.log.warn(
60
60
  `Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`,
61
61
  );
62
- // eslint-disable-next-line no-console
63
- console.error(error);
62
+ environment.log.error(error);
64
63
  });
65
64
  }
66
65
  // The blocker didn't return a `Promise`.
@@ -71,23 +70,35 @@ function runNavigationBlocker({ blocker }, location) {
71
70
  // Runs all blockers in order.
72
71
  // If any blocker returns `true`, it stops and returns the result.
73
72
  // If there's no such blocker, returns `undefined`.
74
- export function runNavigationBlockers(navigationBlockers, toLocation) {
73
+ export function runNavigationBlockers(
74
+ navigationBlockers,
75
+ toLocation,
76
+ environment,
77
+ ) {
75
78
  if (navigationBlockers.length === 0) {
76
79
  return undefined;
77
80
  }
78
81
 
79
82
  // Call the first blocker in the list.
80
- const result = runNavigationBlocker(navigationBlockers[0], toLocation);
83
+ const result = runNavigationBlocker(
84
+ navigationBlockers[0],
85
+ toLocation,
86
+ environment,
87
+ );
81
88
 
82
89
  const next = () => {
83
90
  // Proceed to the next blocker.
84
- return runNavigationBlockers(navigationBlockers.slice(1), toLocation);
91
+ return runNavigationBlockers(
92
+ navigationBlockers.slice(1),
93
+ toLocation,
94
+ environment,
95
+ );
85
96
  };
86
97
 
87
98
  if (isPromise(result)) {
88
99
  return result.then((resultValue) => {
89
100
  if (resultValue) {
90
- debug('Navigation blocked', toLocation.pathname);
101
+ environment.log.debug('Navigation blocked', toLocation.pathname);
91
102
  return resultValue;
92
103
  }
93
104
  return next();
@@ -95,7 +106,7 @@ export function runNavigationBlockers(navigationBlockers, toLocation) {
95
106
  }
96
107
 
97
108
  if (result) {
98
- debug('Navigation blocked', toLocation.pathname);
109
+ environment.log.debug('Navigation blocked', toLocation.pathname);
99
110
  return result;
100
111
  }
101
112
  return next();
@@ -103,7 +114,14 @@ export function runNavigationBlockers(navigationBlockers, toLocation) {
103
114
 
104
115
  /* istanbul ignore next: not testable with Karma */
105
116
  function terminationBlocker(session) {
106
- const result = runNavigationBlockers(getNavigationBlockers(session), null);
117
+ // `navigationBlockers` are stored in `session`.
118
+ const container = session;
119
+
120
+ const result = runNavigationBlockers(
121
+ getNavigationBlockers(container),
122
+ null,
123
+ session.environment,
124
+ );
107
125
 
108
126
  // If no blocker returned anything, so don't prevent the navigation.
109
127
  if (!result) {
@@ -123,6 +141,9 @@ function terminationBlocker(session) {
123
141
  }
124
142
 
125
143
  export function addNavigationBlocker(session, blocker) {
144
+ // `navigationBlockers` are stored in `session`.
145
+ const container = session;
146
+
126
147
  // All navigation blockers also run on `beforeTermination` event.
127
148
  // If required, this could be a parameter of this function.
128
149
  // The rationale could be that adding a `beforeunload` listener
@@ -141,7 +162,7 @@ export function addNavigationBlocker(session, blocker) {
141
162
  // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
142
163
  if (
143
164
  beforeTermination &&
144
- !getNavigationBlockers(session).some(
165
+ !getNavigationBlockers(container).some(
145
166
  (navigationBlocker) => navigationBlocker.beforeTermination,
146
167
  )
147
168
  ) {
@@ -151,21 +172,21 @@ export function addNavigationBlocker(session, blocker) {
151
172
  );
152
173
  }
153
174
  session._removeTerminationBlocker =
154
- session.lifecycle.addTerminationBlocker(() => {
175
+ session.environment.lifecycle.addTerminationBlocker(() => {
155
176
  return terminationBlocker(session);
156
177
  });
157
178
  }
158
179
 
159
180
  const newNavigationBlocker = { blocker, beforeTermination };
160
- addNavigationBlockerToTheList(newNavigationBlocker, session);
181
+ addNavigationBlockerToTheList(newNavigationBlocker, container);
161
182
 
162
183
  return () => {
163
- removeNavigationBlockerFromTheList(newNavigationBlocker, session);
184
+ removeNavigationBlockerFromTheList(newNavigationBlocker, container);
164
185
 
165
186
  // If it was the last "beforeTermination" blocker, remove navigation blocker.
166
187
  if (
167
188
  beforeTermination &&
168
- !getNavigationBlockers(session).some(
189
+ !getNavigationBlockers(container).some(
169
190
  (navigationBlocker) => navigationBlocker.beforeTermination,
170
191
  )
171
192
  ) {