navigation-stack 0.5.2 → 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 +10 -3
  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 +9 -3
  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 +10 -3
  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
@@ -7,7 +7,6 @@ var _ScrollPositionSaver = _interopRequireDefault(require("./ScrollPositionSaver
7
7
  var _ScrollPositionSetter = _interopRequireDefault(require("./ScrollPositionSetter"));
8
8
  var _constants = require("./constants");
9
9
  var _LocationDataStorage = _interopRequireDefault(require("../data-storage/LocationDataStorage"));
10
- var _debug = _interopRequireDefault(require("../debug"));
11
10
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
11
  /* eslint-disable no-underscore-dangle */
13
12
 
@@ -22,7 +21,7 @@ function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
22
21
  return true;
23
22
  }
24
23
  class ScrollPositionRestoration {
25
- constructor(session, _options) {
24
+ constructor(session, options) {
26
25
  // Once configured, scroll restoration mode persists across page reloads.
27
26
  // I.e. even if a user refreshes the page in a web browser, the custom
28
27
  // `window.history.scrollRestoration` value will still remain.
@@ -54,10 +53,10 @@ class ScrollPositionRestoration {
54
53
  running
55
54
  }) => {
56
55
  if (running) {
57
- (0, _debug.default)('▶ running');
56
+ this._log.debug('▶ running');
58
57
  this._disableAutomaticScrollRestoration();
59
58
  } else {
60
- (0, _debug.default)('⏹ not running');
59
+ this._log.debug('⏹ not running');
61
60
  this._enableAutomaticScrollRestoration();
62
61
 
63
62
  // There might be previous scroll position already saved in the data storage.
@@ -85,27 +84,30 @@ class ScrollPositionRestoration {
85
84
  try {
86
85
  this._scrollPosition.disableAutomaticScrollRestoration();
87
86
  } catch (error) {
88
- // eslint-disable-next-line no-console
89
- console.error('[navigation-stack] could not disable default scroll restoration mode');
87
+ this._log.error('[navigation-stack] could not disable default scroll restoration mode');
90
88
  }
91
89
  };
92
90
  this._enableAutomaticScrollRestoration = () => {
93
91
  try {
94
92
  this._scrollPosition.enableAutomaticScrollRestoration();
95
93
  } catch (error) {
96
- // eslint-disable-next-line no-console
97
- console.error('[navigation-stack] could not enable default scroll restoration mode');
94
+ this._log.error('[navigation-stack] could not enable default scroll restoration mode');
98
95
  }
99
96
  };
100
97
  this._saveScrollPositionForLocation = (location, scrollableContainerKey, scrollPosition) => {
101
98
  this._locationDataStorage.set(location, scrollableContainerKey || _constants.PAGE_SCROLLABLE_CONTAINER_KEY, scrollPosition);
102
99
  };
100
+ this._log = session.environment.log;
103
101
  this._scrollPosition = session.environment.scrollPosition;
104
- this._sessionLifecycle = session.lifecycle;
102
+
103
+ // Custom `ScrollPositionSetter`.
104
+ this._scrollPositionSetter = options.scrollPositionSetter;
105
+ this._sessionLifecycle = session.environment.lifecycle;
105
106
  this._locationDataStorage = new _LocationDataStorage.default(session, {
106
- namespace: 'navigation-stack/scroll-position'
107
+ namespace: 'navigation-stack-scroll-position'
107
108
  });
108
109
  this._scrollPositionSaver = new _ScrollPositionSaver.default({
110
+ log: this._log,
109
111
  scrollPosition: this._scrollPosition,
110
112
  saveScrollPositionForLocation: this._saveScrollPositionForLocation,
111
113
  getScrollableContainers: () => this._scrollableContainers,
@@ -127,20 +129,22 @@ class ScrollPositionRestoration {
127
129
  // Using this option, a developer could theoretically provide their own implementation
128
130
  // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
129
131
  // This could be part of the public API if anyone provided a sensible real-world use case for it.
130
- scrollPositionSetter: _options && _options._pageScrollPositionSetter ||
131
- // The default page scroll position setter.
132
+ scrollPositionSetter: options && options._pageScrollPositionSetter ||
133
+ // eslint-disable-next-line new-cap
134
+ this._scrollPositionSetter && new this._scrollPositionSetter() ||
135
+ // A default `ScrollPositionSetter` for a page (sets page scroll position twice with a momentary delay).
132
136
  new _PageScrollPositionSetter.default(),
133
137
  // This function is only used in tests.
134
138
  // There seems to be no use of it in real life, hence it's not public API.
135
139
  // It's only used in tests.
136
- _getSavedScrollPositionOnLocationChange: _options && _options._getSavedPageScrollPositionOnLocationChange,
140
+ _getSavedScrollPositionOnLocationChange: options && options._getSavedPageScrollPositionOnLocationChange,
137
141
  // This function is only used in tests.
138
142
  // There seems to be no use of it in real life, hence it's not public API.
139
143
  // It's only used in tests.
140
- _shouldSetScrollPositionOnLocationChange: _options && _options._shouldSetPageScrollPositionOnLocationChange
144
+ shouldChangeScrollPositionOnLocationChange: options && options.shouldChangePageScrollPositionOnLocationChange
141
145
  };
142
146
  }
143
- addScrollableContainer(scrollableContainerKey, scrollableContainer, _options) {
147
+ addScrollableContainer(scrollableContainerKey, scrollableContainer, options) {
144
148
  // Originally, `scrollableContainerKey` was auto-generated,
145
149
  // but then it didn't work with the concept of dynamically adding or removing
146
150
  // scrollable containers after `ScrollPositionRestoration` has already started.
@@ -157,7 +161,7 @@ class ScrollPositionRestoration {
157
161
  if (this._scrollableContainers[scrollableContainerKey]) {
158
162
  throw new Error(`Scrollable container key "${scrollableContainerKey}" is already added`);
159
163
  }
160
- (0, _debug.default)('add scrollable container', scrollableContainerKey);
164
+ this._log.debug('add scrollable container', scrollableContainerKey);
161
165
 
162
166
  // Add scrollable container entry.
163
167
  this._scrollableContainers[scrollableContainerKey] = {
@@ -166,17 +170,17 @@ class ScrollPositionRestoration {
166
170
  // Using this option, a developer could theoretically provide their own implementation
167
171
  // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
168
172
  // This could be part of the public API if anyone provided a sensible real-world use case for it.
169
- scrollPositionSetter: _options && _options._scrollPositionSetter ||
173
+ scrollPositionSetter: options && options._scrollPositionSetter || this._scrollPositionSetter && new this._scrollPositionSetter() ||
170
174
  // The default basic "immediate" scroll position setter.
171
175
  new _ScrollPositionSetter.default(),
172
176
  // This function is only used in tests.
173
177
  // There seems to be no use of it in real life, hence it's not public API.
174
178
  // It's only used in tests.
175
- _shouldSetScrollPositionOnLocationChange: _options && _options._shouldSetScrollPositionOnLocationChange,
179
+ shouldChangeScrollPositionOnLocationChange: options && options.shouldChangeScrollPositionOnLocationChange,
176
180
  // This function is only used in tests.
177
181
  // There seems to be no use of it in real life, hence it's not public API.
178
182
  // It's only used in tests.
179
- _getSavedScrollPositionOnLocationChange: _options && _options._getSavedScrollPositionOnLocationChange
183
+ _getSavedScrollPositionOnLocationChange: options && options._getSavedScrollPositionOnLocationChange
180
184
  };
181
185
 
182
186
  // Scrollable containers could be added at any time, including page mount.
@@ -188,10 +192,10 @@ class ScrollPositionRestoration {
188
192
  if (this._location) {
189
193
  const previouslySavedScrollPosition = this._getSavedScrollPositionForLocation(this._location, scrollableContainerKey);
190
194
  if (previouslySavedScrollPosition) {
191
- (0, _debug.default)('restore scroll position on add scrollable container', this._location.pathname, scrollableContainerKey, previouslySavedScrollPosition);
195
+ this._log.debug('restore scroll position on add scrollable container', this._location.pathname, scrollableContainerKey, previouslySavedScrollPosition);
192
196
  this._scrollPosition.setScrollableContainerScrollPosition(scrollableContainer, previouslySavedScrollPosition);
193
197
  } else {
194
- (0, _debug.default)('save scroll position on add scrollable container', this._location.pathname, scrollableContainerKey);
198
+ this._log.debug('save scroll position on add scrollable container', this._location.pathname, scrollableContainerKey);
195
199
  this._scrollPositionSaver.saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer);
196
200
  }
197
201
  }
@@ -201,7 +205,7 @@ class ScrollPositionRestoration {
201
205
 
202
206
  // Removes the scrollable container.
203
207
  return () => {
204
- (0, _debug.default)('remove scrollable container', scrollableContainerKey);
208
+ this._log.debug('remove scrollable container', scrollableContainerKey);
205
209
  this._scrollPositionSaver._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(scrollableContainerKey);
206
210
  this._scrollPositionSaver._scrollPositionAutoSaver.removeScrollableContainerScrollListener(scrollableContainerKey);
207
211
  delete this._scrollableContainers[scrollableContainerKey];
@@ -273,7 +277,7 @@ class ScrollPositionRestoration {
273
277
  if (!location.key) {
274
278
  throw new Error('`location` must have a `key`');
275
279
  }
276
- (0, _debug.default)('rendered location', location.pathname);
280
+ this._log.debug('rendered location', location.pathname);
277
281
  this._prevLocation = this._location;
278
282
  this._location = location;
279
283
  this._scrollPosition.init();
@@ -334,8 +338,8 @@ class ScrollPositionRestoration {
334
338
  // This function is only used in tests.
335
339
  // There seems to be no use of it in real life, hence it's not public API.
336
340
  // It's only used in tests.
337
- if (scrollableContainerEntry._shouldSetScrollPositionOnLocationChange) {
338
- if (!scrollableContainerEntry._shouldSetScrollPositionOnLocationChange(this._location, this._prevLocation)) {
341
+ if (scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange) {
342
+ if (!scrollableContainerEntry.shouldChangeScrollPositionOnLocationChange(this._prevLocation, this._location)) {
339
343
  return Promise.resolve();
340
344
  }
341
345
  }
@@ -347,14 +351,14 @@ class ScrollPositionRestoration {
347
351
  // There seems to be no use of it in real life, hence it's not public API.
348
352
  // It's only used in tests.
349
353
  if (scrollableContainerEntry._getSavedScrollPositionOnLocationChange) {
350
- scrollPositionOrAnchorToSet = scrollableContainerEntry._getSavedScrollPositionOnLocationChange(this._location, this._prevLocation);
354
+ scrollPositionOrAnchorToSet = scrollableContainerEntry._getSavedScrollPositionOnLocationChange(this._prevLocation, this._location);
351
355
  }
352
356
 
353
357
  // Get scroll position (or anchor) to set.
354
358
  if (!scrollPositionOrAnchorToSet) {
355
359
  scrollPositionOrAnchorToSet = scrollableContainerKey === _constants.PAGE_SCROLLABLE_CONTAINER_KEY ? this._getPageScrollPositionOrAnchorToSet(this._location) : this._getScrollableContainerScrollPositionToSet(this._location, scrollableContainerKey);
356
360
  }
357
- (0, _debug.default)('restore scroll position', this._location.pathname, scrollableContainerKey, scrollPositionOrAnchorToSet);
361
+ this._log.debug('restore scroll position', this._location.pathname, scrollableContainerKey, scrollPositionOrAnchorToSet);
358
362
 
359
363
  // Set scroll position of scrollable container.
360
364
  return scrollableContainerEntry.scrollPositionSetter.set(scrollableContainerEntry.scrollableContainer, scrollPositionOrAnchorToSet, this._scrollPosition);
@@ -4,24 +4,26 @@ exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _ScrollPositionAutoSaver = _interopRequireDefault(require("./ScrollPositionAutoSaver"));
6
6
  var _constants = require("./constants");
7
- var _debug = _interopRequireDefault(require("../debug"));
8
7
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
8
  /* eslint-disable no-underscore-dangle */
10
9
 
11
10
  class ScrollPositionSaver {
12
11
  constructor({
12
+ log,
13
13
  scrollPosition,
14
14
  getLocation,
15
15
  saveScrollPositionForLocation,
16
16
  getScrollableContainers,
17
17
  shouldSaveScrollPosition
18
18
  }) {
19
+ this._log = log;
19
20
  this._scrollPosition = scrollPosition;
20
21
  this._getLocation = getLocation;
21
22
  this._saveScrollPositionForLocation = saveScrollPositionForLocation;
22
23
  this._getScrollableContainers = getScrollableContainers;
23
24
  this._shouldSaveScrollPosition = shouldSaveScrollPosition;
24
25
  this._scrollPositionAutoSaver = new _ScrollPositionAutoSaver.default({
26
+ log: this._log,
25
27
  scrollPosition: this._scrollPosition,
26
28
  scrollPositionSaver: this,
27
29
  getScrollableContainers,
@@ -42,7 +44,7 @@ class ScrollPositionSaver {
42
44
  if (!this._shouldSaveScrollPosition()) {
43
45
  return;
44
46
  }
45
- (0, _debug.default)('save scroll position', this._getLocation().pathname);
47
+ this._log.debug('save scroll position', this._getLocation().pathname);
46
48
 
47
49
  // Get scrollable containers.
48
50
  const scrollableContainers = this._getScrollableContainers();
@@ -57,7 +59,7 @@ class ScrollPositionSaver {
57
59
  }
58
60
  }
59
61
  savePageScrollPosition() {
60
- (0, _debug.default)('save scroll position', this._getLocation().pathname, _constants.PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
62
+ this._log.debug('save scroll position', this._getLocation().pathname, _constants.PAGE_SCROLLABLE_CONTAINER_KEY, this._scrollPosition.getPageScrollPosition());
61
63
 
62
64
  // * If this is not a scheduled "auto-save" of scroll position
63
65
  // and there already exists any scheduled "auto-save" of scroll position,
@@ -70,7 +72,7 @@ class ScrollPositionSaver {
70
72
  this._saveScrollPositionForLocation(this._getLocation(), undefined, this._scrollPosition.getPageScrollPosition());
71
73
  }
72
74
  saveScrollableContainerScrollPosition(scrollableContainerKey, scrollableContainer) {
73
- (0, _debug.default)('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
75
+ this._log.debug('save scroll position', this._getLocation().pathname, scrollableContainerKey, this._scrollPosition.getScrollableContainerScrollPosition(scrollableContainer));
74
76
 
75
77
  // * If this is not a scheduled "auto-save" of scroll position
76
78
  // and there already exists any scheduled "auto-save" of scroll position,
@@ -2,20 +2,17 @@
2
2
 
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
- var _debug = _interopRequireDefault(require("../debug"));
6
5
  var _parseInputLocation = _interopRequireDefault(require("../parseInputLocation"));
7
6
  var _createSessionKey = _interopRequireDefault(require("./key/createSessionKey"));
8
- var _NavigationOutOfBoundsError = _interopRequireDefault(require("./navigation/error/NavigationOutOfBoundsError"));
9
- var _operations = _interopRequireDefault(require("./navigation/operation/operations"));
10
7
  var _Subscription = _interopRequireDefault(require("./subscription/Subscription"));
8
+ var _NavigationOutOfBoundsError = _interopRequireDefault(require("../environment/navigation/error/NavigationOutOfBoundsError"));
9
+ var _operations = _interopRequireDefault(require("../environment/navigation/operation/operations"));
11
10
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
11
  const INITIAL_KEY_INDEX = -1;
13
12
  const INITIAL_INDEX = -1;
14
13
  const INIT_LOCATION_DELTA = 0;
15
14
  class Session {
16
- constructor({
17
- navigation
18
- }) {
15
+ constructor(EnvironmentClass) {
19
16
  // This function is used by navigation.
20
17
  this._getCurrentLocationIndex = () => {
21
18
  return this._currentLocationIndex;
@@ -25,6 +22,9 @@ class Session {
25
22
  // under the hood, and `window.sessionStorage` is shared between different sessions.
26
23
  this.key = (0, _createSessionKey.default)();
27
24
 
25
+ // Create an environment instance.
26
+ this.environment = new EnvironmentClass();
27
+
28
28
  // `this._locationKeyIndex` is incremented every time the current location changes.
29
29
  this._locationKeyIndex = INITIAL_KEY_INDEX;
30
30
 
@@ -36,18 +36,34 @@ class Session {
36
36
  // In other words, this is the last location index that it can `.shift()` to.
37
37
  this._terminalLocationIndex = this._currentLocationIndex;
38
38
 
39
- // Create `navigation`.
40
- this._navigation = navigation;
39
+ // Allows subscribing to location updates.
40
+ this._subscription = new _Subscription.default();
41
41
 
42
- // Manages subscriptions.
43
- this._subscription = new _Subscription.default({
44
- activateSubscription: listener => {
45
- return this._navigation.subscribe(listener);
46
- }
42
+ // Subscribing to location changes means subscribing to both "synchronous"
43
+ // and "asynchronous" location changes. "synchronous" location changes
44
+ // happen immediately when the code triggers them."asynchronous" location changes
45
+ // either happen after an arbitrary delay or are even triggered from outside the code.
46
+ //
47
+ // Subscribing to "asynchronous" location changes is not necessary when
48
+ // there're no actual subscribers, in order to not unnecessarily "waste" any resources.
49
+ // Of course, this statement is rather far-fetched and in reality no one would ever tell any difference.
50
+ // Still, I felt like randomly introducing this seemingly unnecessary minor optimization.
51
+ //
52
+ // So it only subscribes to "asynchronous" location changes if there's at least one active subscriber.
53
+ // And in case all subscribers get unsubscribed, it will unsubscribe from "asynchronous" location changes too.
54
+ // One might think of it as some form of "mental masturbation", but what can I do — I already wrote the code.
55
+ //
56
+ this._subscription.onFirstSubscriber(() => {
57
+ return this.environment.navigation.subscribeToAsyncrhonousLocationUpdates(location => {
58
+ // Notify all subscribers about this "asynchronous" location change.
59
+ this._subscription.notifySubscribers(location);
60
+ });
47
61
  });
48
62
 
49
- // Update current location index when a location change was not initiated
50
- // by this session but rather by the user clicking "Back" or "Forward" button.
63
+ // This subscription is triggered in two cases:
64
+ // * Set initial current location index at initial page load.
65
+ // * Update current location index whenever a location change is not initiated
66
+ // by this session but rather by the user clicking "Back" or "Forward" button.
51
67
  this._unsubscribe = this.subscribe(location => {
52
68
  // Update `this._currentLocationIndex` when the location change was not initiated
53
69
  // by this session but rather by the user clicking "Back" or "Forward" button.
@@ -57,16 +73,20 @@ class Session {
57
73
  // but if it was possible, this call would be required. It would also be required
58
74
  // by `navigation` to call `session.getNextKey()` function to increment `locationKeyIndex`.
59
75
  this._updateTerminalLocationIndex(location);
60
- (0, _debug.default)('current location', location.pathname, 'index', this._currentLocationIndex);
76
+ this.environment.log.debug('current location', location.pathname, 'index', this._currentLocationIndex);
61
77
  });
62
78
  }
63
79
 
64
80
  // Subscribes to changes in location.
81
+ // The first subscriber is always the `Session` itself:
82
+ // its listener keeps the current location index up-to-date.
83
+ // Any additional application-specific listeners could be added, if required.
84
+ // Applications should prefer adding any such listeners by calling `NavigationStack.subscribe()`
85
+ // method instead of calling this method directly, in order to "normalize" the `location` argument.
65
86
  subscribe(listener) {
66
87
  return this._subscription.subscribe(location => {
67
88
  if (!this._isStarted() && location.operation !== _operations.default.INIT) {
68
- // eslint-disable-next-line no-console
69
- console.error('Unexpected location change', location);
89
+ this.environment.log.error('Unexpected location change', location);
70
90
  throw new Error('Not started');
71
91
  } else {
72
92
  // Call the listener.
@@ -74,6 +94,18 @@ class Session {
74
94
  }
75
95
  });
76
96
  }
97
+
98
+ // Starts a navigation session.
99
+ //
100
+ // When run in a web browser, it could not only "start" a new session
101
+ // but also "resume" a previously-started session. That could happen
102
+ // when the user refreshes a page in a web browser which still retains
103
+ // the previous session's data but at the same time restarts the javascript code
104
+ // from scratch.
105
+ //
106
+ // So this `start()` method handles both cases: when there's previous session's data
107
+ // that should be restored and when there's no previous session's data.
108
+ //
77
109
  start(initialLocation) {
78
110
  if (this._stopped) {
79
111
  throw new Error('Can not be restarted');
@@ -86,7 +118,7 @@ class Session {
86
118
  // the initial location by the time javascript code starts execution.
87
119
  //
88
120
  if (!initialLocation) {
89
- initialLocation = this._navigation.getInitialLocation();
121
+ initialLocation = this.environment.navigation.getInitialLocation();
90
122
  if (initialLocation) {
91
123
  initialLocation = (0, _parseInputLocation.default)(initialLocation);
92
124
  }
@@ -97,18 +129,19 @@ class Session {
97
129
  if (this._currentLocationIndex !== INITIAL_INDEX) {
98
130
  throw new Error('Already started');
99
131
  }
100
- (0, _debug.default)('▶ start session', initialLocation.pathname);
132
+ this.environment.log.debug('▶ start session', initialLocation.pathname);
101
133
  this._started = true;
102
134
  const key = this._getNextLocationKey();
103
135
  const index = INITIAL_INDEX + 1;
104
136
  const delta = INIT_LOCATION_DELTA;
105
- const locationResult = this._navigation.init(initialLocation, {
137
+ const locationResult = this.environment.navigation.init(initialLocation, {
106
138
  operation: _operations.default.INIT,
107
139
  key,
108
140
  index,
109
141
  delta
110
142
  });
111
143
  if (locationResult) {
144
+ // Notify all subscribers about this "synchronous" location change.
112
145
  this._subscription.notifySubscribers(locationResult);
113
146
  }
114
147
  }
@@ -116,7 +149,7 @@ class Session {
116
149
  if (this._stopped) {
117
150
  throw Error('Already stopped');
118
151
  }
119
- (0, _debug.default)('⏹ stop session');
152
+ this.environment.log.debug('⏹ stop session');
120
153
 
121
154
  // Once stopped, it won't be able to be restarted.
122
155
  this._stopped = true;
@@ -142,16 +175,17 @@ class Session {
142
175
  });
143
176
  const key = this._getNextLocationKey();
144
177
  const index = this._currentLocationIndex + delta;
145
- (0, _debug.default)(operation === _operations.default.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
178
+ this.environment.log.debug(operation === _operations.default.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
146
179
 
147
180
  // Navigate to the location.
148
- const locationResult = this._navigation.navigate(location, {
181
+ const locationResult = this.environment.navigation.navigate(location, {
149
182
  operation,
150
183
  key,
151
184
  index,
152
185
  delta
153
186
  });
154
187
  if (locationResult) {
188
+ // Notify all subscribers about this "synchronous" location change.
155
189
  this._subscription.notifySubscribers(locationResult);
156
190
  }
157
191
  }
@@ -165,7 +199,7 @@ class Session {
165
199
  return;
166
200
  }
167
201
  const index = this._currentLocationIndex + delta;
168
- (0, _debug.default)(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
202
+ this.environment.log.debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
169
203
 
170
204
  // Validate that the new `index` is not out of bounds.
171
205
  if (index < 0 || index > this._terminalLocationIndex) {
@@ -173,12 +207,13 @@ class Session {
173
207
  }
174
208
 
175
209
  // Navigate to the location.
176
- const locationResult = this._navigation.shift({
210
+ const locationResult = this.environment.navigation.shift({
177
211
  operation: _operations.default.SHIFT,
178
212
  index,
179
213
  delta
180
214
  });
181
215
  if (locationResult) {
216
+ // Notify all subscribers about this "synchronous" location change.
182
217
  this._subscription.notifySubscribers(locationResult);
183
218
  }
184
219
  }
@@ -3,22 +3,34 @@
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  class Subscription {
6
- constructor({
7
- activateSubscription
8
- } = {}) {
9
- this.notifySubscribers = argument => {
10
- // `._latest` is only used in tests.
11
- this._latest = argument;
12
- for (const {
13
- listener
14
- } of this._listeners) {
15
- listener(argument);
16
- }
17
- };
18
- this._activateSubscription = activateSubscription;
19
-
6
+ constructor() {
20
7
  // This property is accessed in tests.
21
8
  this._listeners = [];
9
+
10
+ // These listeners will be called when the subscription enters "active" or "inactive" state.
11
+ // A subscription enters "active" state when it has at least one listener rather than zero.
12
+ // A subscription enters "inactive" state when it has no more listeners.
13
+ this._subscriptionActiveStateListeners = [];
14
+ this._subscriptionInactiveStateListeners = [];
15
+ }
16
+
17
+ // Adds a subscription active state listener.
18
+ // Returns a function that removes the subscription active state listener.
19
+ onFirstSubscriber(activeStateListener) {
20
+ this._subscriptionActiveStateListeners.push(activeStateListener);
21
+ // Return a function that removes the subscription active state listener.
22
+ return () => {
23
+ this._subscriptionActiveStateListeners = this._subscriptionActiveStateListeners.filter(_ => _ !== activeStateListener);
24
+ };
25
+ }
26
+ notifySubscribers(argument) {
27
+ // `._latest` is only used in tests.
28
+ this._latest = argument;
29
+ for (const {
30
+ listener
31
+ } of this._listeners) {
32
+ listener(argument);
33
+ }
22
34
  }
23
35
  subscribe(listener) {
24
36
  // If subscriptions are stopped, i.e. no new subscriptions are to be added,
@@ -38,7 +50,9 @@ class Subscription {
38
50
 
39
51
  // If it's the first listener, activate subscription.
40
52
  if (this._listeners.length === 0) {
41
- this._deactivateSubscription = this._activateSubscription(this.notifySubscribers);
53
+ // Run all subscription active state listeners.
54
+ // The functions returned from those will become subscription inactive state listeners.
55
+ this._subscriptionInactiveStateListeners = this._subscriptionActiveStateListeners.map(activeStateListener => activeStateListener());
42
56
  }
43
57
 
44
58
  // Add the `listener` to the list.
@@ -70,10 +84,14 @@ class Subscription {
70
84
  // Remove the `listener` from the list.
71
85
  this._listeners = this._listeners.filter(_ => _ !== listenerEntry);
72
86
 
73
- // If it was the last listener, deactivate subscription.
87
+ // If it was the last listener.
74
88
  if (this._listeners.length === 0) {
75
- this._deactivateSubscription();
76
- this._deactivateSubscription = undefined;
89
+ // Run any subscription inactive state listeners,
90
+ // after which clear the list of such listeners.
91
+ for (const inactiveStateListener of this._subscriptionInactiveStateListeners) {
92
+ inactiveStateListener();
93
+ }
94
+ this._subscriptionInactiveStateListeners = [];
77
95
  }
78
96
  }
79
97
  }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = stringifyQuery;
5
+ // "The more recent RFC3986 reserves !, ', (, ), and *,
6
+ // even though these characters have no formalized URI delimiting uses.
7
+ //
8
+ // https://datatracker.ietf.org/doc/html/rfc3986
9
+ //
10
+ // The following function encodes a string for RFC3986-compliant URL component format.
11
+ // It also encodes [ and ], which are part of the IPv6 URI syntax.
12
+ //
13
+ // An RFC3986-compliant encodeURI implementation should not escape them,
14
+ // which is demonstrated in the encodeURI() example.
15
+ //
16
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
17
+ //
18
+ // Can throw a `URIError` if the `string` contains a "lone surrogate".
19
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
20
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
21
+ // Example: "URIError: malformed URI sequence"
22
+ //
23
+ function encode(string) {
24
+ return encodeURIComponent(string).replace(/[!'()*]/g, character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
25
+ }
26
+ function stringifyQuery(query) {
27
+ let queryString = '';
28
+ if (!query) {
29
+ return queryString;
30
+ }
31
+ for (const key of Object.keys(query)) {
32
+ let value = query[key];
33
+ if (Array.isArray(value)) {
34
+ throw new Error('Array values are not supported');
35
+ }
36
+
37
+ // Ignore `value: undefined`.
38
+ if (value === undefined) {
39
+ continue;
40
+ }
41
+
42
+ // Stringify `value`.
43
+ if (value === null) {
44
+ value = '';
45
+ } else {
46
+ value = String(value);
47
+ }
48
+
49
+ // Can throw a `URIError` if the `string` contains a "lone surrogate".
50
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters
51
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError
52
+ // Example: "URIError: malformed URI sequence"
53
+ try {
54
+ const keyValuePair = `${encode(key)}${value ? '=' : ''}${encode(value)}`;
55
+ if (queryString.length > 1) {
56
+ queryString += '&';
57
+ }
58
+ queryString += keyValuePair;
59
+ } catch (error) {
60
+ // Simply ignore an invalid query parameter.
61
+ continue;
62
+ }
63
+ }
64
+ return queryString;
65
+ }
66
+ module.exports = exports.default;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = stringifyQueryAsSearch;
5
+ var _stringifyQuery = _interopRequireDefault(require("./stringifyQuery"));
6
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
+ function stringifyQueryAsSearch(query) {
8
+ const queryString = (0, _stringifyQuery.default)(query);
9
+ if (queryString) {
10
+ return `?${queryString}`;
11
+ }
12
+ return '';
13
+ }
14
+ module.exports = exports.default;