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,161 @@
1
+ import getLocationBaseFromLocation from './getLocationBaseFromLocation';
2
+ import getLocationFromInternalLocation from './getLocationFromInternalLocation';
3
+ import isPromise from './isPromise';
4
+ import {
5
+ getNavigationBlockers,
6
+ runNavigationBlockers,
7
+ } from './navigationBlockers';
8
+
9
+ // Creates "navigation blockers evaluation" status object.
10
+ // It tracks the "cancelled" status of the evaluation:
11
+ // when next navigation happens, the previous one is no longer relevant
12
+ // so the evaluation of navigation blockers for it can be cancelled.
13
+ function createNavigationBlockersEvaluationStatus(container) {
14
+ /* eslint-disable no-underscore-dangle */
15
+ if (container._navigationBlockersEvaluationStatus) {
16
+ container._navigationBlockersEvaluationStatus.cancelled = true;
17
+ }
18
+ container._navigationBlockersEvaluationStatus = { cancelled: false };
19
+ return container._navigationBlockersEvaluationStatus;
20
+ }
21
+
22
+ // Prevents or allows navigation that was initiated by the application code
23
+ // by making a `.push()` or `.replace()` method call.
24
+ //
25
+ // It doesn't handle `.shift()` method calls because it doesn't yet know
26
+ // the `location` that it's gonna `shift` to. Instead, it waits for the web browser
27
+ // to "shift" to that `location` and then reads the `location` from the address bar
28
+ // and, if such "shift" should've been blocked, it "rewinds" the address bar back
29
+ // to the previous location. This part is handled by another function.
30
+ //
31
+ // Such type of "shifting" and then rewinding the "shift" doesn't really matter to the application code at all.
32
+ // From the application code's point of view, all web browser's address bar doesn't matter and even doesn't exist.
33
+ // All that exists from the application code's point of view is the `location` object in the `NavigationStack`'s state.
34
+ // Until the `location` object in the `NavigationStack`'s state is updated, the "old" page is still rendered.
35
+ // The appliation is only concerned with the updates of the `location` object in the `NavigationStack`'s state
36
+ // and completely ignores any updates to the URL in the web browser's address bar.
37
+ //
38
+ export function blockProgrammaticNavigationIfRequired(
39
+ toLocationBase, // `location` of type `LocationBase`
40
+ session,
41
+ ) {
42
+ // `resultValue` variable name works around a stupid javascript error:
43
+ // "Cannot redeclare block-scoped variable 'result'".
44
+ const result = runNavigationBlockers(
45
+ getNavigationBlockers(session),
46
+ // Here `payload.location` is `LocationBase`.
47
+ toLocationBase,
48
+ session.environment,
49
+ );
50
+
51
+ if (isPromise(result)) {
52
+ const evaluationStatus = createNavigationBlockersEvaluationStatus(session);
53
+ // eslint-disable-next-line consistent-return
54
+ return result.then((promiseResult) => {
55
+ if (evaluationStatus.cancelled) {
56
+ return true;
57
+ }
58
+ return promiseResult;
59
+ });
60
+ }
61
+ return result;
62
+ }
63
+
64
+ // Runs navigation blockers on internal location update and "undoes" the location update
65
+ // if it should've been blocked.
66
+ //
67
+ // One could ask: Why the hassle of running navigation blockers on internal location update
68
+ // and then rewinding back if the location change should've been blocked?
69
+ // Why not just run navigation blockers on `.push()`/`.replace()`/`.shift()`?
70
+ //
71
+ // The reason why it runs on internal location update here is because
72
+ // aside from programmatic `.shift()` that can be initiated from the application code,
73
+ // there's non-programmatic "shift" navigation when the user manually clicks "Back" or "Forward" button
74
+ // in a web browser. And even if a "shift" navigation is initiated programmatically in the application code,
75
+ // it still doesn't know yet what the new location is gonna be cause it only knows the numeric `delta`.
76
+ // Such cases could only be handled by reacting to internal location updates which,
77
+ // in case of "Back"/"Forward", only happen after the URL in the browser's address bar has changed.
78
+ //
79
+ // There's no real drawback in reacting to an internal location update "post factum" because
80
+ // from the application code's point of view, web browser's address bar doesn't matter and even doesn't exist.
81
+ // All that exists from the application code's point of view is the `navigationStack.current` location
82
+ // returned from the `NavigationStack`. Until that location is updated, the "old" page is still rendered.
83
+ // The appliation is only concerned with the updates of the internal `location` object in the `NavigationStack``
84
+ // and completely ignores any updates to the URL in the web browser's address bar.
85
+ //
86
+ // So here, the code attempts to prevent or allow navigation that has already happened
87
+ // in the web browser's address bar but hasn't yet happened in the `NavigationStack`'s state.
88
+ // For example, it could be a user clicking a "Back"/"Forward" button in a web browser.
89
+ // If such navigation should've been blocked, it will simply not update the `location` object
90
+ // in the `NavigationStack`'s state, and it will also "rewind" the change of the URL in the web browser's
91
+ // address bar so that it's consistent with the `location` in the `NavigationStack`'s state.
92
+ //
93
+ // Returns either a `boolean` value or a `Promise` that resolves to a `boolean` value:
94
+ // * `false` when navigation should not have been blocked and therefore was not "rewinded".
95
+ // * `true` when navigation should have been blocked and therefore was "rewinded".
96
+ // * `true` when it "rewinded" the navigation "just in case" and then started evaluating async blockers,
97
+ // but while doing that, next navigation already happened so this one is no longer relevant.
98
+ //
99
+ export function blockNonProgrammaticNavigationIfRequired(
100
+ toLocationInternal, // `location` of type `LocationInternal`
101
+ session,
102
+ doAndIgnoreLocationUpdates,
103
+ ) {
104
+ // If there're no navigation blockers to run, don't do anything.
105
+ if (getNavigationBlockers(session).length === 0) {
106
+ return false;
107
+ }
108
+
109
+ // If it was the initial page load or a redirect,
110
+ // it's not really a navigation that could be rolled back.
111
+ if (toLocationInternal.delta === 0) {
112
+ return false;
113
+ }
114
+
115
+ const result = runNavigationBlockers(
116
+ getNavigationBlockers(session),
117
+ getLocationBaseFromLocation(
118
+ getLocationFromInternalLocation(toLocationInternal),
119
+ ),
120
+ session.environment,
121
+ );
122
+
123
+ // If some navigation blocker returned a `Promise`.
124
+ if (isPromise(result)) {
125
+ const evaluationStatus = createNavigationBlockersEvaluationStatus(session);
126
+
127
+ // While location blockers are running, rewind to the previous location.
128
+ doAndIgnoreLocationUpdates(() => {
129
+ session.shift(-toLocationInternal.delta);
130
+ });
131
+
132
+ return result.then((promiseResult) => {
133
+ if (evaluationStatus.cancelled) {
134
+ return true;
135
+ }
136
+ if (promiseResult) {
137
+ // Navigation blocked.
138
+ // Already rewound to a previous location.
139
+ return true;
140
+ }
141
+ // Navigation not blocked.
142
+ // Rewind back to the new location.
143
+ doAndIgnoreLocationUpdates(() => {
144
+ session.shift(toLocationInternal.delta);
145
+ });
146
+ // Update the location.
147
+ return false;
148
+ });
149
+ }
150
+
151
+ // Navigation blockers did not return a `Promise`.
152
+ if (result) {
153
+ // Prevent the navigation: rewind to the previous location.
154
+ doAndIgnoreLocationUpdates(() => {
155
+ session.shift(-toLocationInternal.delta);
156
+ });
157
+ return true;
158
+ }
159
+ // Update the location.
160
+ return false;
161
+ }
@@ -1,6 +1,6 @@
1
- import createSearchFromQuery from './createSearchFromQuery';
2
1
  import parseLocationUrl from './parseLocationUrl';
3
2
  import parseQueryFromSearch from './parseQueryFromSearch';
3
+ import stringifyQueryAsSearch from './stringifyQueryAsSearch';
4
4
 
5
5
  function stringifyQueryParameterValue(value) {
6
6
  if (value === null || value === undefined) {
@@ -45,7 +45,7 @@ export default function parseInputLocation(location) {
45
45
  if (location.query && !location.search) {
46
46
  location = {
47
47
  ...location,
48
- search: createSearchFromQuery(location.query),
48
+ search: stringifyQueryAsSearch(location.query),
49
49
  };
50
50
  }
51
51
 
@@ -1,12 +1,8 @@
1
- import { parse as parseQuery } from 'query-string';
1
+ import parseQueryString from './parseQueryString';
2
2
 
3
3
  export default function parseQueryFromSearch(search) {
4
4
  if (search.length > '?'.length) {
5
- try {
6
- return parseQuery(search.slice(1));
7
- } catch (error) {
8
- // Ignore any query parsing errors.
9
- }
5
+ return parseQueryString(search.slice('?'.length));
10
6
  }
11
7
  return {};
12
8
  }
@@ -0,0 +1,81 @@
1
+ function splitAtFirstOccurence(string, separator) {
2
+ const separatorIndex = string.indexOf(separator);
3
+ if (separatorIndex === -1) {
4
+ return [string, ''];
5
+ }
6
+
7
+ return [
8
+ string.slice(0, separatorIndex),
9
+ string.slice(separatorIndex + separator.length),
10
+ ];
11
+ }
12
+
13
+ function decode(value) {
14
+ // There's a convention that a space character could be encoded
15
+ // either as "%20" or as "+". Both of them are valid.
16
+ // The "+" character is unusally preferred because it results in a more
17
+ // human-readable URL.
18
+ //
19
+ // https://dev.to/lico/understanding-how-spaces-are-encoded-20-with-encodeuri-vs-with-url-2d6c
20
+ // https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding
21
+ //
22
+ // Those "+" characters don't get transformed to spaces by `decodeURIComponent()` function.
23
+ // This means that they should be transformed to spaces manually.
24
+ //
25
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
26
+ //
27
+ value = value.replaceAll('+', ' ');
28
+
29
+ // `decodeURIComponent()` could throw an error of class `URIError`.
30
+ //
31
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
32
+ //
33
+ // Example: "URIError: malformed URI sequence".
34
+ try {
35
+ return decodeURIComponent(value);
36
+ } catch (error) {
37
+ // eslint-disable-next-line no-console
38
+ console.error(error);
39
+ return value;
40
+ }
41
+ }
42
+
43
+ export default function parseQueryString(queryString) {
44
+ // Create an object with no prototype
45
+ const query = Object.create(null);
46
+
47
+ // query parameter parsing is described in the specification:
48
+ // https://url.spec.whatwg.org/#urlencoded-parsing
49
+ for (const keyValuePair of queryString.split('&')) {
50
+ if (!keyValuePair) {
51
+ continue;
52
+ }
53
+
54
+ let [key, value] = splitAtFirstOccurence(keyValuePair, '=');
55
+
56
+ // If `key` is empty, the specification considers this a valid case with `key: null`.
57
+ // But, there seems to be no practical use for a query parameter with `key: null`.
58
+ // So just skip it.
59
+ if (!key) {
60
+ continue;
61
+ }
62
+
63
+ key = decode(key);
64
+
65
+ // According to the specification, missing `=` should be treated as `value: null`.
66
+ if (value === '') {
67
+ value = null;
68
+ } else {
69
+ value = decode(value);
70
+ }
71
+
72
+ // The handling of duplicate URL query parameters is not explicitly defined by a single,
73
+ // universally enforced specification. Hence, we just assume such query parameters invalid
74
+ // and only include the first occurrence of the query parameter in the query string.
75
+ if (query[key] === undefined) {
76
+ query[key] = value;
77
+ }
78
+ }
79
+
80
+ return query;
81
+ }
@@ -2,15 +2,16 @@
2
2
 
3
3
  import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
4
4
  import scheduleNextTick from './scheduleNextTick';
5
- import debug from '../debug';
6
5
 
7
6
  export default class ScrollPositionAutoSaver {
8
7
  constructor({
8
+ log,
9
9
  scrollPosition,
10
10
  scrollPositionSaver,
11
11
  getScrollableContainers,
12
12
  shouldSaveScrollPosition,
13
13
  }) {
14
+ this._log = log;
14
15
  this._scrollPosition = scrollPosition;
15
16
  this._scrollPositionSaver = scrollPositionSaver;
16
17
  this._shouldSaveScrollPosition = shouldSaveScrollPosition;
@@ -71,7 +72,7 @@ export default class ScrollPositionAutoSaver {
71
72
  cancelSavePageScrollPosition(hasRun) {
72
73
  if (this._cancelSavePageScrollPosition) {
73
74
  if (!hasRun) {
74
- debug(
75
+ this._log.debug(
75
76
  'cancel delayed save scroll position',
76
77
  PAGE_SCROLLABLE_CONTAINER_KEY,
77
78
  );
@@ -86,7 +87,10 @@ export default class ScrollPositionAutoSaver {
86
87
  this._getScrollableContainers()[scrollableContainerKey];
87
88
  if (scrollableContainerEntry.cancelSaveScrollPosition) {
88
89
  if (!hasRun) {
89
- debug('cancel delayed save scroll position', scrollableContainerKey);
90
+ this._log.debug(
91
+ 'cancel delayed save scroll position',
92
+ scrollableContainerKey,
93
+ );
90
94
  }
91
95
  scrollableContainerEntry.cancelSaveScrollPosition();
92
96
  scrollableContainerEntry.cancelSaveScrollPosition = null;
@@ -127,10 +131,10 @@ export default class ScrollPositionAutoSaver {
127
131
  // because there might be too many in a given short period of time
128
132
  // which could affect the performance of the application.
129
133
  if (!scrollableContainerEntry.cancelSaveScrollPosition) {
130
- debug('scroll detected', scrollableContainerKey);
134
+ this._log.debug('scroll detected', scrollableContainerKey);
131
135
  scrollableContainerEntry.cancelSaveScrollPosition =
132
136
  scheduleNextTick(() => {
133
- debug(
137
+ this._log.debug(
134
138
  'auto-save scroll position after scroll',
135
139
  scrollableContainerKey,
136
140
  );
@@ -148,7 +152,7 @@ export default class ScrollPositionAutoSaver {
148
152
  // Set up scroll listener on the page.
149
153
  this._removePageScrollListener =
150
154
  this._scrollPosition.addPageScrollListener(() => {
151
- debug('scroll detected', PAGE_SCROLLABLE_CONTAINER_KEY);
155
+ this._log.debug('scroll detected', PAGE_SCROLLABLE_CONTAINER_KEY);
152
156
 
153
157
  // This flag is not used in real life and is only used in tests (for some reason).
154
158
  if (!this._shouldSaveScrollPosition()) {
@@ -5,7 +5,6 @@ import ScrollPositionSaver from './ScrollPositionSaver';
5
5
  import ScrollPositionSetter from './ScrollPositionSetter';
6
6
  import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
7
7
  import LocationDataStorage from '../data-storage/LocationDataStorage';
8
- import debug from '../debug';
9
8
 
10
9
  function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
11
10
  let i = 0;
@@ -19,16 +18,22 @@ function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
19
18
  }
20
19
 
21
20
  export default class ScrollPositionRestoration {
22
- constructor(session, _options) {
21
+ constructor(session, options) {
22
+ this._log = session.environment.log;
23
+
23
24
  this._scrollPosition = session.environment.scrollPosition;
24
25
 
25
- this._sessionLifecycle = session.lifecycle;
26
+ // Custom `ScrollPositionSetter`.
27
+ this._scrollPositionSetter = options.scrollPositionSetter;
28
+
29
+ this._sessionLifecycle = session.environment.lifecycle;
26
30
 
27
31
  this._locationDataStorage = new LocationDataStorage(session, {
28
- namespace: 'navigation-stack/scroll-position',
32
+ namespace: 'navigation-stack-scroll-position',
29
33
  });
30
34
 
31
35
  this._scrollPositionSaver = new ScrollPositionSaver({
36
+ log: this._log,
32
37
  scrollPosition: this._scrollPosition,
33
38
  saveScrollPositionForLocation: this._saveScrollPositionForLocation,
34
39
  getScrollableContainers: () => this._scrollableContainers,
@@ -52,28 +57,30 @@ export default class ScrollPositionRestoration {
52
57
  // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
53
58
  // This could be part of the public API if anyone provided a sensible real-world use case for it.
54
59
  scrollPositionSetter:
55
- (_options && _options._pageScrollPositionSetter) ||
56
- // The default page scroll position setter.
60
+ (options && options._pageScrollPositionSetter) ||
61
+ // eslint-disable-next-line new-cap
62
+ (this._scrollPositionSetter && new this._scrollPositionSetter()) ||
63
+ // A default `ScrollPositionSetter` for a page (sets page scroll position twice with a momentary delay).
57
64
  new PageScrollPositionSetter(),
58
65
 
59
66
  // This function is only used in tests.
60
67
  // There seems to be no use of it in real life, hence it's not public API.
61
68
  // It's only used in tests.
62
69
  _getSavedScrollPositionOnLocationChange:
63
- _options && _options._getSavedPageScrollPositionOnLocationChange,
70
+ options && options._getSavedPageScrollPositionOnLocationChange,
64
71
 
65
72
  // This function is only used in tests.
66
73
  // There seems to be no use of it in real life, hence it's not public API.
67
74
  // It's only used in tests.
68
- _shouldSetScrollPositionOnLocationChange:
69
- _options && _options._shouldSetPageScrollPositionOnLocationChange,
75
+ shouldChangeScrollPositionOnLocationChange:
76
+ options && options.shouldChangePageScrollPositionOnLocationChange,
70
77
  };
71
78
  }
72
79
 
73
80
  addScrollableContainer(
74
81
  scrollableContainerKey,
75
82
  scrollableContainer,
76
- _options,
83
+ options,
77
84
  ) {
78
85
  // Originally, `scrollableContainerKey` was auto-generated,
79
86
  // but then it didn't work with the concept of dynamically adding or removing
@@ -96,7 +103,7 @@ export default class ScrollPositionRestoration {
96
103
  );
97
104
  }
98
105
 
99
- debug('add scrollable container', scrollableContainerKey);
106
+ this._log.debug('add scrollable container', scrollableContainerKey);
100
107
 
101
108
  // Add scrollable container entry.
102
109
  this._scrollableContainers[scrollableContainerKey] = {
@@ -107,21 +114,22 @@ export default class ScrollPositionRestoration {
107
114
  // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
108
115
  // This could be part of the public API if anyone provided a sensible real-world use case for it.
109
116
  scrollPositionSetter:
110
- (_options && _options._scrollPositionSetter) ||
117
+ (options && options._scrollPositionSetter) ||
118
+ (this._scrollPositionSetter && new this._scrollPositionSetter()) ||
111
119
  // The default basic "immediate" scroll position setter.
112
120
  new ScrollPositionSetter(),
113
121
 
114
122
  // This function is only used in tests.
115
123
  // There seems to be no use of it in real life, hence it's not public API.
116
124
  // It's only used in tests.
117
- _shouldSetScrollPositionOnLocationChange:
118
- _options && _options._shouldSetScrollPositionOnLocationChange,
125
+ shouldChangeScrollPositionOnLocationChange:
126
+ options && options.shouldChangeScrollPositionOnLocationChange,
119
127
 
120
128
  // This function is only used in tests.
121
129
  // There seems to be no use of it in real life, hence it's not public API.
122
130
  // It's only used in tests.
123
131
  _getSavedScrollPositionOnLocationChange:
124
- _options && _options._getSavedScrollPositionOnLocationChange,
132
+ options && options._getSavedScrollPositionOnLocationChange,
125
133
  };
126
134
 
127
135
  // Scrollable containers could be added at any time, including page mount.
@@ -137,7 +145,7 @@ export default class ScrollPositionRestoration {
137
145
  scrollableContainerKey,
138
146
  );
139
147
  if (previouslySavedScrollPosition) {
140
- debug(
148
+ this._log.debug(
141
149
  'restore scroll position on add scrollable container',
142
150
  this._location.pathname,
143
151
  scrollableContainerKey,
@@ -148,7 +156,7 @@ export default class ScrollPositionRestoration {
148
156
  previouslySavedScrollPosition,
149
157
  );
150
158
  } else {
151
- debug(
159
+ this._log.debug(
152
160
  'save scroll position on add scrollable container',
153
161
  this._location.pathname,
154
162
  scrollableContainerKey,
@@ -168,7 +176,7 @@ export default class ScrollPositionRestoration {
168
176
 
169
177
  // Removes the scrollable container.
170
178
  return () => {
171
- debug('remove scrollable container', scrollableContainerKey);
179
+ this._log.debug('remove scrollable container', scrollableContainerKey);
172
180
 
173
181
  this._scrollPositionSaver._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(
174
182
  scrollableContainerKey,
@@ -257,10 +265,10 @@ export default class ScrollPositionRestoration {
257
265
  //
258
266
  _sessionExecutionStatusListener = ({ running }) => {
259
267
  if (running) {
260
- debug('▶ running');
268
+ this._log.debug('▶ running');
261
269
  this._disableAutomaticScrollRestoration();
262
270
  } else {
263
- debug('⏹ not running');
271
+ this._log.debug('⏹ not running');
264
272
  this._enableAutomaticScrollRestoration();
265
273
 
266
274
  // There might be previous scroll position already saved in the data storage.
@@ -308,7 +316,7 @@ export default class ScrollPositionRestoration {
308
316
  throw new Error('`location` must have a `key`');
309
317
  }
310
318
 
311
- debug('rendered location', location.pathname);
319
+ this._log.debug('rendered location', location.pathname);
312
320
 
313
321
  this._prevLocation = this._location;
314
322
  this._location = location;
@@ -391,12 +399,12 @@ export default class ScrollPositionRestoration {
391
399
  // There seems to be no use of it in real life, hence it's not public API.
392
400
  // It's only used in tests.
393
401
  if (
394
- scrollableContainerEntry._shouldSetScrollPositionOnLocationChange
402
+ scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange
395
403
  ) {
396
404
  if (
397
- !scrollableContainerEntry._shouldSetScrollPositionOnLocationChange(
398
- this._location,
405
+ !scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange(
399
406
  this._prevLocation,
407
+ this._location,
400
408
  )
401
409
  ) {
402
410
  return Promise.resolve();
@@ -412,8 +420,8 @@ export default class ScrollPositionRestoration {
412
420
  if (scrollableContainerEntry._getSavedScrollPositionOnLocationChange) {
413
421
  scrollPositionOrAnchorToSet =
414
422
  scrollableContainerEntry._getSavedScrollPositionOnLocationChange(
415
- this._location,
416
423
  this._prevLocation,
424
+ this._location,
417
425
  );
418
426
  }
419
427
 
@@ -428,7 +436,7 @@ export default class ScrollPositionRestoration {
428
436
  );
429
437
  }
430
438
 
431
- debug(
439
+ this._log.debug(
432
440
  'restore scroll position',
433
441
  this._location.pathname,
434
442
  scrollableContainerKey,
@@ -462,8 +470,7 @@ export default class ScrollPositionRestoration {
462
470
  try {
463
471
  this._scrollPosition.disableAutomaticScrollRestoration();
464
472
  } catch (error) {
465
- // eslint-disable-next-line no-console
466
- console.error(
473
+ this._log.error(
467
474
  '[navigation-stack] could not disable default scroll restoration mode',
468
475
  );
469
476
  }
@@ -473,8 +480,7 @@ export default class ScrollPositionRestoration {
473
480
  try {
474
481
  this._scrollPosition.enableAutomaticScrollRestoration();
475
482
  } catch (error) {
476
- // eslint-disable-next-line no-console
477
- console.error(
483
+ this._log.error(
478
484
  '[navigation-stack] could not enable default scroll restoration mode',
479
485
  );
480
486
  }
@@ -2,16 +2,17 @@
2
2
 
3
3
  import ScrollPositionAutoSaver from './ScrollPositionAutoSaver';
4
4
  import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
5
- import debug from '../debug';
6
5
 
7
6
  export default class ScrollPositionSaver {
8
7
  constructor({
8
+ log,
9
9
  scrollPosition,
10
10
  getLocation,
11
11
  saveScrollPositionForLocation,
12
12
  getScrollableContainers,
13
13
  shouldSaveScrollPosition,
14
14
  }) {
15
+ this._log = log;
15
16
  this._scrollPosition = scrollPosition;
16
17
  this._getLocation = getLocation;
17
18
  this._saveScrollPositionForLocation = saveScrollPositionForLocation;
@@ -19,6 +20,7 @@ export default class ScrollPositionSaver {
19
20
  this._shouldSaveScrollPosition = shouldSaveScrollPosition;
20
21
 
21
22
  this._scrollPositionAutoSaver = new ScrollPositionAutoSaver({
23
+ log: this._log,
22
24
  scrollPosition: this._scrollPosition,
23
25
  scrollPositionSaver: this,
24
26
  getScrollableContainers,
@@ -44,7 +46,7 @@ export default class ScrollPositionSaver {
44
46
  return;
45
47
  }
46
48
 
47
- debug('save scroll position', this._getLocation().pathname);
49
+ this._log.debug('save scroll position', this._getLocation().pathname);
48
50
 
49
51
  // Get scrollable containers.
50
52
  const scrollableContainers = this._getScrollableContainers();
@@ -63,7 +65,7 @@ export default class ScrollPositionSaver {
63
65
  }
64
66
 
65
67
  savePageScrollPosition() {
66
- debug(
68
+ this._log.debug(
67
69
  'save scroll position',
68
70
  this._getLocation().pathname,
69
71
  PAGE_SCROLLABLE_CONTAINER_KEY,
@@ -89,7 +91,7 @@ export default class ScrollPositionSaver {
89
91
  scrollableContainerKey,
90
92
  scrollableContainer,
91
93
  ) {
92
- debug(
94
+ this._log.debug(
93
95
  'save scroll position',
94
96
  this._getLocation().pathname,
95
97
  scrollableContainerKey,
@@ -1 +1 @@
1
- export ScrollPositionRestoration from './ScrollPositionRestoration';
1
+ export { default as ScrollPositionRestoration } from './ScrollPositionRestoration';