navigation-stack 0.5.3 → 0.6.1

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
package/lib/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- // TypeScript Version: 3.0
2
-
3
1
  export {};
4
2
 
5
3
  export type Query = Record<string, string>;
@@ -97,11 +95,11 @@ export type NavigationBlockerResult =
97
95
  *
98
96
  * * The `location` argument is `null` when the web browser tab is about to be closed.
99
97
  * * The `location` argument is of type `LocationBase` when a `.push()` or `.replace()` navigation is blocked.
100
- * * The `location` argument is of type `Location` when blocking a navigation that was initiated outside of the application code.
98
+ * * The `location` argument is of type `LocationBase` when blocking a navigation that was initiated outside of the application code.
101
99
  * For example, when the user clicks "Back" or "Forward" button in a web browser.
102
100
  */
103
101
  export type NavigationBlocker = (
104
- location: Location | LocationBase | null,
102
+ location: LocationBase | null,
105
103
  ) => NavigationBlockerResult;
106
104
 
107
105
  // I dunno why did they use an `interface` here.
@@ -123,20 +121,18 @@ export function parseLocationUrl(locationUrl: string): LocationBase;
123
121
 
124
122
  export function parseInputLocation(location: InputLocation): LocationBase;
125
123
 
126
- export function addNavigationBlocker(
127
- session: Session,
128
- blocker: NavigationBlocker,
129
- ): () => void;
130
-
131
- export interface NavigationStackOptions {
124
+ export interface NavigationStackOptions<ScrollableContainer, Anchor> {
132
125
  basePath?: string;
133
- maintainScrollPosition?: boolean;
126
+ manageScrollPosition?: boolean;
127
+ scrollPositionSetter?: Constructor<
128
+ ScrollPositionSetter<ScrollableContainer, Anchor>
129
+ >;
134
130
  }
135
131
 
136
132
  export class NavigationStack<ScrollableContainer = any, Anchor = any> {
137
133
  constructor(
138
- session: Session<ScrollableContainer, Anchor>,
139
- options?: NavigationStackOptions,
134
+ environment: Constructor<Environment<ScrollableContainer, Anchor>>,
135
+ options?: NavigationStackOptions<ScrollableContainer, Anchor>,
140
136
  );
141
137
 
142
138
  addScrollableContainer(
@@ -144,6 +140,10 @@ export class NavigationStack<ScrollableContainer = any, Anchor = any> {
144
140
  scrollableContainer: ScrollableContainer,
145
141
  ): () => void;
146
142
 
143
+ addNavigationBlocker(blocker: NavigationBlocker): () => void;
144
+
145
+ dataStorage: LocationDataStorage;
146
+
147
147
  subscribe(listener: (location: Location) => void): () => void;
148
148
 
149
149
  current(): Location;
@@ -156,7 +156,7 @@ export class NavigationStack<ScrollableContainer = any, Anchor = any> {
156
156
 
157
157
  shift(delta: number): void;
158
158
 
159
- locationRendered(): Promise<void>;
159
+ locationRendered(location: Location): Promise<void>;
160
160
 
161
161
  stop(): void;
162
162
  }
@@ -173,7 +173,7 @@ export type SessionExecutionStatusListener = (
173
173
 
174
174
  export type ScrollListener = () => void;
175
175
 
176
- export class ServerSideNavigationError extends Error {
176
+ export class ServerSideRedirectError extends Error {
177
177
  constructor(location: LocationBase);
178
178
 
179
179
  location: LocationBase;
@@ -185,9 +185,11 @@ export class NavigationOutOfBoundsError extends Error {
185
185
  index: number;
186
186
  }
187
187
 
188
- export class Navigation {
189
- // Subscribes to "location change" events.
190
- subscribe(listener: (location: LocationInternal) => void): () => void;
188
+ export class EnvironmentNavigation {
189
+ // Subscribes to "asynchronous" changes of the current location.
190
+ subscribeToAsyncrhonousLocationUpdates(
191
+ listener: (location: LocationInternal) => void,
192
+ ): () => void;
191
193
 
192
194
  init(
193
195
  initialLocation: LocationBase,
@@ -218,13 +220,19 @@ export interface EnvironmentDataStorage {
218
220
  set(key: string, value: string): void;
219
221
  }
220
222
 
221
- export interface SessionLifecycle {
223
+ export interface EnvironmentLifecycle {
222
224
  addTerminationBlocker(blocker: SessionTerminationBlocker): () => void;
223
225
  addExecutionStatusListener(
224
226
  listener: SessionExecutionStatusListener,
225
227
  ): () => void;
226
228
  }
227
229
 
230
+ export interface EnvironmentLog {
231
+ debug(...args: any[]): void;
232
+ warn(...args: any[]): void;
233
+ error(...args: any[]): void;
234
+ }
235
+
228
236
  // Manages scroll position in an environment such as a web browser.
229
237
  export interface EnvironmentScrollPosition<ScrollableContainer, Anchor> {
230
238
  // Gets numeric scroll position of a page.
@@ -255,10 +263,13 @@ export interface EnvironmentScrollPosition<ScrollableContainer, Anchor> {
255
263
 
256
264
  export interface Environment<ScrollableContainer, Anchor> {
257
265
  dataStorage: EnvironmentDataStorage;
266
+ log: EnvironmentLog;
267
+ lifecycle: EnvironmentLifecycle;
268
+ navigation: EnvironmentNavigation;
258
269
  scrollPosition: EnvironmentScrollPosition<ScrollableContainer, Anchor>;
259
270
  }
260
271
 
261
- export interface Session<ScrollableContainer = any, Anchor = any> {
272
+ interface Session<ScrollableContainer = any, Anchor = any> {
262
273
  // `key` should be unique within `environment.dataStorage`.
263
274
  // For example, `BrowserEnvironment` uses `window.sessionStorage`
264
275
  // that is shared across different sessions within a given web browser tab,
@@ -268,7 +279,7 @@ export interface Session<ScrollableContainer = any, Anchor = any> {
268
279
  // Private varibles. Not public API.
269
280
  environment: Environment<ScrollableContainer, Anchor>;
270
281
 
271
- lifecycle: SessionLifecycle;
282
+ lifecycle: EnvironmentLifecycle;
272
283
 
273
284
  subscribe(listener: (location: LocationInternal) => void): () => void;
274
285
 
@@ -282,12 +293,12 @@ export interface Session<ScrollableContainer = any, Anchor = any> {
282
293
  }
283
294
 
284
295
  // This is just a copy-paste of the `session` interface above.
285
- declare abstract class SessionBaseClass<
286
- ScrollableContainer = any,
287
- Anchor = any,
288
- > implements Session<ScrollableContainer, Anchor>
296
+ declare abstract class SessionClass<ScrollableContainer = any, Anchor = any>
297
+ implements Session<ScrollableContainer, Anchor>
289
298
  {
290
- constructor(parameters: { navigation: Navigation });
299
+ constructor(
300
+ environmentClass: Constructor<Environment<ScrollableContainer, Anchor>>,
301
+ );
291
302
 
292
303
  // `key` should be unique within `environment.dataStorage`.
293
304
  // For example, `BrowserEnvironment` uses `window.sessionStorage`
@@ -295,10 +306,11 @@ declare abstract class SessionBaseClass<
295
306
  // hence the uniqueness requirement.
296
307
  key: string;
297
308
 
298
- // Private varibles. Not public API.
309
+ // Private varible. Not public API.
299
310
  environment: Environment<ScrollableContainer, Anchor>;
300
311
 
301
- lifecycle: SessionLifecycle;
312
+ // Private varible. Not public API.
313
+ lifecycle: EnvironmentLifecycle;
302
314
 
303
315
  subscribe(listener: (location: LocationInternal) => void): () => void;
304
316
 
@@ -311,14 +323,148 @@ declare abstract class SessionBaseClass<
311
323
  shift(delta: number): void;
312
324
  }
313
325
 
314
- export class WebBrowserSession extends SessionBaseClass<HTMLElement, string> {
315
- constructor();
326
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
327
+ export interface WebBrowserEnvironment
328
+ extends Environment<HTMLElement, string> {}
329
+
330
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
331
+ export interface ServerSideRenderEnvironment
332
+ extends Environment<string, string> {}
333
+
334
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
335
+ export interface InMemoryEnvironment extends Environment<string, string> {}
336
+
337
+ // Theoretically, a developer could pass their own `ScrollPositionSetter` implementation
338
+ // when calling `.addScrollableContainer()` or
339
+ export interface ScrollPositionSetter<ScrollableContainer, Anchor> {
340
+ // Sets scroll position of a page or a scrollable element.
341
+ // Returns a `Promise` that resolves when it has finished setting the scroll position.
342
+ set(
343
+ // This is the scrollable container whose scroll position should be set.
344
+ // * When setting page scroll position, `scrollableContainer` is `undefined`.
345
+ // * When setting scrollable element scroll position, `scrollableContainer` is the scrollable element.
346
+ scrollableContainer: ScrollableContainer,
347
+ // This is the scroll position to set.
348
+ // * When setting page scroll position, it could be either an anchor or numeric coordinates.
349
+ // * When setting scrollable element scroll position, it could only be numeric coordinates.
350
+ scrollPositionOrAnchor: Anchor | [number, number],
351
+ // `scrollPosition` provides various "helper" methods for setting scroll position according to the environment.
352
+ // For example, in the context of a `WebBrowserEnvironment`, it provides the methods for setting scroll position in a web browser.
353
+ scrollPositionHelper: EnvironmentScrollPosition<
354
+ ScrollableContainer,
355
+ Anchor
356
+ >,
357
+ ): Promise<void>;
358
+
359
+ // Cancels any pending (or in-progress) setting of scroll position.
360
+ cancel(): void;
361
+ }
362
+
363
+ // https://stackoverflow.com/questions/39392853/is-there-a-type-for-class-in-typescript-and-does-any-include-it
364
+ export type Constructor<T = any> = new (...args: any[]) => T;
365
+
366
+ export type DataStorageValue =
367
+ | string
368
+ | number
369
+ | boolean
370
+ | Record<string, unknown>
371
+ | null
372
+ | undefined;
373
+
374
+ declare class DataStorage<
375
+ Key extends string = string,
376
+ Value extends DataStorageValue = DataStorageValue,
377
+ > {
378
+ constructor(session: Session, options: { namespace: string });
379
+
380
+ get(key: Key): Value | undefined;
381
+
382
+ set(key: Key, value: Value | undefined): void;
316
383
  }
317
384
 
318
- export class ServerSideRenderSession extends SessionBaseClass<string, string> {
319
- constructor();
385
+ declare class LocationDataStorage<
386
+ Key extends string = string,
387
+ Value extends DataStorageValue = DataStorageValue,
388
+ > {
389
+ constructor(session: Session, options: { namespace: string });
390
+
391
+ get(location: Location, key: Key): Value;
392
+
393
+ set(location: Location, key: Key, value: Value): void;
320
394
  }
321
395
 
322
- export class InMemorySession extends SessionBaseClass<string, string> {
323
- constructor();
396
+ export class ScrollPositionRestoration<
397
+ ScrollableContainer = any,
398
+ Anchor = any,
399
+ > {
400
+ constructor(
401
+ session: Session<ScrollableContainer, Anchor>,
402
+
403
+ options?: {
404
+ // Using this option, a developer could provide their own implementation of setting
405
+ // a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
406
+ // When specified, it applies to both page and any scrollable containers.
407
+ scrollPositionSetter: ScrollPositionSetter<ScrollableContainer, Anchor>;
408
+
409
+ shouldChangePageScrollPositionOnLocationChange?: (
410
+ prevLocation: Location | undefined,
411
+ newLocation: Location,
412
+ ) => boolean;
413
+
414
+ // `options._getSavedPageScrollPositionOnLocationChange`
415
+ // isn't used in real life and is not part of the public API.
416
+ // It's only used in tests.
417
+ _getSavedPageScrollPositionOnLocationChange?: (
418
+ location: Location,
419
+ prevLocation: Location | undefined,
420
+ ) => [number, number] | undefined;
421
+
422
+ // Using this option, a developer could theoretically provide their own implementation
423
+ // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
424
+ // This could be part of the public API if anyone provided a sensible real-world use case for it.
425
+ _pageScrollPositionSetter?: ScrollPositionSetter<
426
+ ScrollableContainer,
427
+ Anchor
428
+ >;
429
+ },
430
+ );
431
+
432
+ addScrollableContainer(
433
+ scrollableContainerKey: string,
434
+ scrollableContainer: ScrollableContainer,
435
+
436
+ options?: {
437
+ shouldChangeScrollPositionOnLocationChange?: (
438
+ prevLocation: Location | undefined,
439
+ newLocation: Location,
440
+ ) => boolean;
441
+
442
+ // `_options._getSavedScrollPositionOnLocationChange`
443
+ // isn't used in real life and is not part of the public API.
444
+ // It's only used in tests.
445
+ _getSavedScrollPositionOnLocationChange?: (
446
+ location: Location,
447
+ prevLocation: Location | undefined,
448
+ ) => [number, number] | undefined;
449
+
450
+ // Using this option, a developer could theoretically provide their own implementation
451
+ // of setting a scroll position. For example, it could use "smooth" (animated) scrolling, etc.
452
+ // This could be part of the public API if anyone provided a sensible real-world use case for it.
453
+ _scrollPositionSetter: ScrollPositionSetter<ScrollableContainer, Anchor>;
454
+ },
455
+ ): () => void;
456
+
457
+ locationRendered: (location: Location) => Promise<void>;
458
+
459
+ stop(): void;
460
+
461
+ // `_enableSavingScrollPosition()` and `_disableSavingScrollPosition()`
462
+ // aren't used in real life and are not part of the public API.
463
+ // They're only used in tests.
464
+ _enableSavingScrollPosition(): void;
465
+
466
+ // `_enableSavingScrollPosition()` and `_disableSavingScrollPosition()`
467
+ // aren't used in real life and are not part of the public API.
468
+ // They're only used in tests.
469
+ _disableSavingScrollPosition(): void;
324
470
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "navigation-stack",
3
- "version": "0.5.3",
4
- "description": "Handles navigation in a web browser",
3
+ "version": "0.6.1",
4
+ "description": "Navigation in a Single-Page Application",
5
5
  "keywords": [
6
6
  "history",
7
7
  "browser",
@@ -25,7 +25,7 @@
25
25
  "scripts": {
26
26
  "build": "rimraf lib && 4c build --types false src && npm run build:pick && npm run build:types",
27
27
  "build:pick": "cherry-pick --cjs-dir cjs --esm-dir esm --cwd lib ../src",
28
- "build:types": "cpy types/*.d.ts lib && cpy types/data-storage/*.d.ts lib/data-storage && cpy types/scroll-position/*.d.ts lib/scroll-position && cpy types/redux/*.d.ts lib/redux",
28
+ "build:types": "cpy types/*.d.ts lib",
29
29
  "format": "4c format --prettier-ignore .eslintignore .",
30
30
  "lint": "4c lint --prettier-ignore .eslintignore .",
31
31
  "prepublishOnly": "npm run build",
@@ -44,10 +44,7 @@
44
44
  "*": "yarn 4c lint --fix --prettier-ignore .eslintignore"
45
45
  },
46
46
  "prettier": "@4c/prettier-config",
47
- "dependencies": {
48
- "query-string": "^5.1.1",
49
- "redux": "^5.0.1"
50
- },
47
+ "dependencies": {},
51
48
  "devDependencies": {
52
49
  "@4c/babel-preset": "^9.1.0",
53
50
  "@4c/cli": "^3.0.1",
@@ -1,40 +1,102 @@
1
- import { applyMiddleware, createStore } from 'redux';
2
-
3
- import Actions from './redux/Actions';
4
- import createMiddlewares from './redux/createMiddlewares';
5
- import locationReducer from './redux/locationReducer';
1
+ import { addBasePath, removeBasePath } from './basePath';
2
+ import LocationDataStorage from './data-storage/LocationDataStorage';
3
+ import getLocationFromInternalLocation from './getLocationFromInternalLocation';
4
+ import isPromise from './isPromise';
5
+ import {
6
+ addNavigationBlocker,
7
+ removeAllNavigationBlockers,
8
+ } from './navigationBlockers';
9
+ import {
10
+ blockNonProgrammaticNavigationIfRequired,
11
+ blockProgrammaticNavigationIfRequired,
12
+ } from './navigationBlockersEvaluation';
13
+ import parseInputLocation from './parseInputLocation';
6
14
  import ScrollPositionRestoration from './scroll-position/ScrollPositionRestoration';
7
-
8
- function getCreateMiddlewaresOptions(navigationStackOptions) {
9
- if (!navigationStackOptions) {
10
- return undefined;
11
- }
12
- // eslint-disable-next-line no-unused-vars
13
- const { maintainScrollPosition, ...restOptions } = navigationStackOptions;
14
- return restOptions;
15
- }
15
+ import Session from './session/Session';
16
16
 
17
17
  export default class NavigationStack {
18
- constructor(session, options) {
19
- this._session = session;
20
-
21
- // Create a Redux store.
22
- this._store = createStore(
23
- locationReducer,
24
- applyMiddleware(
25
- ...createMiddlewares(session, getCreateMiddlewaresOptions(options)),
26
- ),
27
- );
18
+ constructor(
19
+ Environment,
20
+ { basePath, manageScrollPosition, scrollPositionSetter } = {},
21
+ ) {
22
+ // Create a session.
23
+ this._session = new Session(Environment);
24
+
25
+ // Base path, if used.
26
+ this._basePath = basePath;
27
+
28
+ // Create location data storage.
29
+ this.dataStorage = new LocationDataStorage(this._session, {
30
+ namespace: 'navigation-stack',
31
+ });
32
+
33
+ // Allows temporarily ignoring location update events when set to `true`.
34
+ this._ignoreLocationUpdates = false;
35
+
36
+ // Subscribe to location updates.
37
+ // * Ignores location updates if `_ignoreLocationUpdates` flag is temporarily set.
38
+ // * Runs navigation blockers to see if the location update should be reverted.
39
+ // * Updates `this._location` if the update wasn't ignored or blocked.
40
+ this._unsubscribe = this._session.subscribe((location) => {
41
+ // If this location update shouldn't be temporarily ignored.
42
+ if (!this._ignoreLocationUpdates) {
43
+ // Remove `basePath` from `location`.
44
+ location = removeBasePath(location, this._basePath);
45
+
46
+ // See if the location update should've been blocked.
47
+ // If it should've, it will automatically "rewind" it.
48
+ const result = blockNonProgrammaticNavigationIfRequired(
49
+ location,
50
+ this._session,
51
+ this._doAndIgnoreLocationUpdates,
52
+ );
53
+
54
+ const onResult = (blocked) => {
55
+ if (!blocked) {
56
+ // Update `this._location`.
57
+ // Since it's gonna be returned from the public `this.current()` method,
58
+ // convert it from `LocationInternal` to `Location`.
59
+ this._location = getLocationFromInternalLocation(location);
60
+ }
61
+ };
62
+
63
+ if (isPromise(result)) {
64
+ result.then(onResult);
65
+ } else {
66
+ onResult(result);
67
+ }
68
+ }
69
+ });
28
70
 
29
71
  // Create `ScrollPositionRestoration`.
30
- if (options && options.maintainScrollPosition) {
31
- this._scrollPositionRestoration = new ScrollPositionRestoration(session);
72
+ if (manageScrollPosition) {
73
+ this._scrollPositionRestoration = new ScrollPositionRestoration(
74
+ this._session,
75
+ // Custom `ScrollPositionSetter`.
76
+ { scrollPositionSetter },
77
+ );
32
78
  }
33
79
  }
34
80
 
81
+ // Subscribes to any changes of the current location.
82
+ // The first subscriber is always the `NavigationStack` itself
83
+ // because its listener is what drives the actual navigation.
84
+ // Any additional application-specific listeners could be added, if required.
85
+ subscribe(listener) {
86
+ // `NavigationStack.subscribe()` is simply a proxy to `Session.subscribe()`
87
+ // with the only convenience feature that it "normalizes" the `location` argument.
88
+ return this._session.subscribe((locationInternal) => {
89
+ listener(getLocationFromInternalLocation(locationInternal));
90
+ });
91
+ }
92
+
93
+ addNavigationBlocker(blocker) {
94
+ return addNavigationBlocker(this._session, blocker);
95
+ }
96
+
35
97
  addScrollableContainer(scrollableContainerKey, scrollableContainer) {
36
98
  if (!this._scrollPositionRestoration) {
37
- throw new Error('`maintainScrollPosition: true` option not passed');
99
+ throw new Error('`manageScrollPosition: true` option not passed');
38
100
  }
39
101
  return this._scrollPositionRestoration.addScrollableContainer(
40
102
  scrollableContainerKey,
@@ -42,59 +104,107 @@ export default class NavigationStack {
42
104
  );
43
105
  }
44
106
 
45
- subscribe(listener) {
46
- // Subscribe to any potential Redux state changes.
47
- return this._store.subscribe(() => {
48
- // Initially, calls the listener when setting the initial location.
49
- // After that, calls it on any location change.
50
- const location = this.current();
51
- if (!this._latestLocation || location !== this._latestLocation) {
52
- this._latestLocation = location;
53
- listener(location);
54
- }
55
- });
56
- }
57
-
58
107
  init(initialLocation) {
59
- if (this._latestLocation) {
108
+ if (this._location) {
60
109
  throw new Error('Already initialized');
61
110
  }
62
111
 
63
- this._store.dispatch(Actions.init(initialLocation));
64
- this._latestLocation = this.current();
112
+ this._session.start(
113
+ initialLocation && this._parseInputLocation(initialLocation),
114
+ );
115
+
116
+ if (this._scrollPositionRestoration) {
117
+ this._scrollPositionRestoration.start();
118
+ }
65
119
  }
66
120
 
67
121
  current() {
68
- return this._store.getState();
122
+ // TypeScript definition of the `.current()` method tells that it always returns
123
+ // some non-`undefined` location.
124
+ // But `this._location` is `undefined` until `.init(initialLocation?)` is called.
125
+ // To work around that limitation, it simply throws if `.current()` is called before `.init()`.
126
+ if (!this._location) {
127
+ throw new Error('Not initialized');
128
+ }
129
+ return this._location;
69
130
  }
70
131
 
71
132
  push(location) {
72
- this._store.dispatch(Actions.push(location));
133
+ this._navigate('push', location);
73
134
  }
74
135
 
75
136
  replace(location) {
76
- this._store.dispatch(Actions.replace(location));
137
+ this._navigate('replace', location);
138
+ }
139
+
140
+ _navigate(operation, location) {
141
+ const toLocation = this._parseInputLocation(location);
142
+
143
+ const result = blockProgrammaticNavigationIfRequired(
144
+ toLocation,
145
+ this._session,
146
+ );
147
+
148
+ const onResult = (blocked) => {
149
+ if (!blocked) {
150
+ this._session.navigate(operation, toLocation);
151
+ }
152
+ };
153
+
154
+ if (isPromise(result)) {
155
+ result.then(onResult);
156
+ } else {
157
+ onResult(result);
158
+ }
77
159
  }
78
160
 
79
161
  shift(delta) {
80
- this._store.dispatch(Actions.shift(delta));
162
+ this._session.shift(delta);
81
163
  }
82
164
 
83
165
  stop() {
166
+ if (!this._unsubscribe) {
167
+ throw new Error('Already stopped');
168
+ }
169
+
170
+ this._unsubscribe();
171
+ this._unsubscribe = undefined;
172
+
173
+ // Even if it calls `unsubscribe()` function above, any other subscriptions
174
+ // would still stay. We're not talking about `navigationStack.subscribe()`
175
+ // subscriptions because those don't really matter in terms of cleaning them up:
176
+ // those're just Redux store subscriptions that don't have any side effects.
177
+ // Subscriptions we're talking here are `Session`'s own subscription
178
+ // via `session.subscribe()` and any hypothetical manual `session.subscribe()`
179
+ // calls that could be made by the application code for whatever purpose.
180
+ // Both of those should be cleared.
181
+ // To work around that, `.stop()` function removes all subscriptions.
182
+ this._session.stop();
183
+
184
+ removeAllNavigationBlockers(this._session);
185
+
84
186
  if (this._scrollPositionRestoration) {
85
187
  this._scrollPositionRestoration.stop();
86
188
  }
87
- this._store.dispatch(Actions.stop());
88
189
  }
89
190
 
90
- locationRendered() {
91
- if (this._scrollPositionRestoration) {
92
- const location = this.current();
93
- if (!location) {
94
- throw new Error('Not initialized');
95
- }
96
- return this._scrollPositionRestoration.locationRendered(location);
191
+ locationRendered(location) {
192
+ if (!this._scrollPositionRestoration) {
193
+ throw new Error('`manageScrollPosition: true` option not passed');
97
194
  }
98
- return Promise.resolve();
195
+ return this._scrollPositionRestoration.locationRendered(location);
99
196
  }
197
+
198
+ _parseInputLocation(inputLocation) {
199
+ // Parse input location (string or incomplete object) to a proper `location` object.
200
+ // Add `basePath` to `location`.
201
+ return addBasePath(parseInputLocation(inputLocation), this._basePath);
202
+ }
203
+
204
+ // Allows temporarily ignoring location update events.
205
+ _doAndIgnoreLocationUpdates = (func) => {
206
+ this._ignoreLocationUpdates = true;
207
+ func();
208
+ this._ignoreLocationUpdates = false;
209
+ };
100
210
  }
@@ -4,6 +4,7 @@ export default class DataStorage {
4
4
  throw new Error('`DataStorage` requires a `session.key`');
5
5
  }
6
6
  this._sessionKey = session.key;
7
+ this._log = session.environment.log;
7
8
  this._dataStorage = session.environment.dataStorage;
8
9
  this._namespace = namespace;
9
10
  }
@@ -22,8 +23,8 @@ export default class DataStorage {
22
23
  // junk into sessionStorage under our namespace.
23
24
  return JSON.parse(value);
24
25
  } catch (error) {
25
- // eslint-disable-next-line no-console
26
- console.error('[navigation-stack] Could not read data from storage');
26
+ this._log.error('[navigation-stack] Could not read data from storage');
27
+ this._log.error(error);
27
28
 
28
29
  // Pretend that the entry doesn't exist.
29
30
  return undefined;
@@ -38,8 +39,10 @@ export default class DataStorage {
38
39
  this._dataStorage.remove(storageKey);
39
40
  } catch (error) {
40
41
  // No need to handle errors here.
41
- // eslint-disable-next-line no-console
42
- console.error('[navigation-stack] Could not delete data from storage');
42
+ this._log.error(
43
+ '[navigation-stack] Could not delete data from storage',
44
+ );
45
+ this._log.error(error);
43
46
  }
44
47
 
45
48
  return;
@@ -54,8 +57,8 @@ export default class DataStorage {
54
57
  } catch (error) {
55
58
  // No need to handle errors here either. If it didn't work, it didn't
56
59
  // work. We make no guarantees about actually saving the value.
57
- // eslint-disable-next-line no-console
58
- console.error('[navigation-stack] Could not save data in storage');
60
+ this._log.error('[navigation-stack] Could not save data in storage');
61
+ this._log.error(error);
59
62
  }
60
63
  }
61
64
 
@@ -1,9 +1,15 @@
1
1
  import InMemoryDataStorage from './data-storage/InMemoryDataStorage';
2
+ import InMemorySessionLifecycle from './lifecycle/InMemorySessionLifecycle';
3
+ import InMemoryLog from './log/InMemoryLog';
4
+ import InMemoryNavigation from './navigation/InMemoryNavigation';
2
5
  import InMemoryScrollPosition from './scroll-position/InMemoryScrollPosition';
3
6
 
4
7
  export default class InMemoryEnvironment {
5
8
  constructor() {
6
9
  this.dataStorage = new InMemoryDataStorage();
10
+ this.log = new InMemoryLog();
11
+ this.lifecycle = new InMemorySessionLifecycle();
12
+ this.navigation = new InMemoryNavigation();
7
13
  this.scrollPosition = new InMemoryScrollPosition();
8
14
  }
9
15
  }