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
@@ -1,9 +1,10 @@
1
+ import InMemoryEnvironment from '../../src/environment/InMemoryEnvironment';
1
2
  import parseInputLocation from '../../src/parseInputLocation';
2
- import InMemorySession from '../../src/session/InMemorySession';
3
+ import Session from '../../src/session/Session';
3
4
 
4
- describe('InMemorySession', () => {
5
+ describe('Session (InMemoryEnvironment)', () => {
5
6
  it('should parse the initial location', () => {
6
- const session = new InMemorySession();
7
+ const session = new Session(InMemoryEnvironment);
7
8
 
8
9
  let location;
9
10
  session.subscribe((newLocation) => {
@@ -28,7 +29,7 @@ describe('InMemorySession', () => {
28
29
  });
29
30
 
30
31
  it('should support basic navigation', () => {
31
- const session = new InMemorySession();
32
+ const session = new Session(InMemoryEnvironment);
32
33
 
33
34
  let location;
34
35
  session.subscribe((newLocation) => {
@@ -124,7 +125,7 @@ describe('InMemorySession', () => {
124
125
  });
125
126
 
126
127
  it('should support subscribing and unsubscribing', () => {
127
- const session = new InMemorySession();
128
+ const session = new Session(InMemoryEnvironment);
128
129
  session.start(parseInputLocation('/initial'));
129
130
  session.navigate('push', { pathname: '/new' });
130
131
  session.navigate('push', { pathname: '/new-2' });
@@ -151,7 +152,7 @@ describe('InMemorySession', () => {
151
152
  });
152
153
 
153
154
  it('should respect stack bounds', () => {
154
- const session = new InMemorySession();
155
+ const session = new Session(InMemoryEnvironment);
155
156
  session.start(parseInputLocation('/initial'));
156
157
  session.navigate('push', { pathname: '/new' });
157
158
  session.navigate('push', { pathname: '/new-2' });
@@ -205,7 +206,7 @@ describe('InMemorySession', () => {
205
206
  });
206
207
 
207
208
  it('should not reset forward entries on replace', () => {
208
- const session = new InMemorySession();
209
+ const session = new Session(InMemoryEnvironment);
209
210
  session.start(parseInputLocation('/initial'));
210
211
  session.navigate('push', { pathname: '/new' });
211
212
  session.navigate('push', { pathname: '/new-2' });
@@ -228,7 +229,7 @@ describe('InMemorySession', () => {
228
229
  });
229
230
 
230
231
  it('should reset forward entries on push', () => {
231
- const session = new InMemorySession();
232
+ const session = new Session(InMemoryEnvironment);
232
233
 
233
234
  session.start(parseInputLocation('/initial'));
234
235
  session.navigate('push', { pathname: '/new' });
@@ -249,7 +250,7 @@ describe('InMemorySession', () => {
249
250
  });
250
251
 
251
252
  it('should remove all subscriptions on stop', () => {
252
- const session = new InMemorySession();
253
+ const session = new Session(InMemoryEnvironment);
253
254
 
254
255
  session.start(parseInputLocation('/initial'));
255
256
 
@@ -1,9 +1,10 @@
1
+ import ServerSideRenderEnvironment from '../../src/environment/ServerSideRenderEnvironment';
1
2
  import parseInputLocation from '../../src/parseInputLocation';
2
- import ServerSideRenderSession from '../../src/session/ServerSideRenderSession';
3
+ import Session from '../../src/session/Session';
3
4
 
4
- describe('ServerSideRenderSession', () => {
5
+ describe('Session (ServerSideRenderEnvironment)', () => {
5
6
  it('should parse the initial location', () => {
6
- const session = new ServerSideRenderSession();
7
+ const session = new Session(ServerSideRenderEnvironment);
7
8
 
8
9
  let location;
9
10
  session.subscribe((newLocation) => {
@@ -24,7 +25,7 @@ describe('ServerSideRenderSession', () => {
24
25
  });
25
26
 
26
27
  it('should support subscriptions', () => {
27
- const session = new ServerSideRenderSession();
28
+ const session = new Session(ServerSideRenderEnvironment);
28
29
  // eslint-disable-next-line no-unused-vars
29
30
  expect(() => session.subscribe((location) => {})).to.not.throw();
30
31
  });
@@ -1,9 +1,10 @@
1
1
  import delay from 'delay';
2
2
 
3
+ import WebBrowserEnvironment from '../../src/environment/WebBrowserEnvironment';
3
4
  import parseInputLocation from '../../src/parseInputLocation';
4
- import WebBrowserSession from '../../src/session/WebBrowserSession';
5
+ import Session from '../../src/session/Session';
5
6
 
6
- describe('WebBrowserSession.stop()', () => {
7
+ describe('Session.stop() (WebBrowserEnvironment)', () => {
7
8
  const sandbox = sinon.createSandbox();
8
9
 
9
10
  afterEach(() => {
@@ -16,7 +17,7 @@ describe('WebBrowserSession.stop()', () => {
16
17
  sandbox.spy(window, 'addEventListener');
17
18
  sandbox.spy(window, 'removeEventListener');
18
19
 
19
- const session = new WebBrowserSession();
20
+ const session = new Session(WebBrowserEnvironment);
20
21
 
21
22
  session.subscribe(() => {});
22
23
 
@@ -40,7 +41,7 @@ describe('WebBrowserSession.stop()', () => {
40
41
  });
41
42
  });
42
43
 
43
- describe('WebBrowserSession', () => {
44
+ describe('Session (WebBrowserEnvironment)', () => {
44
45
  let session;
45
46
 
46
47
  afterEach(() => {
@@ -52,7 +53,7 @@ describe('WebBrowserSession', () => {
52
53
 
53
54
  it('should parse the initial location', () => {
54
55
  window.history.replaceState(null, null, '/initial?bar=baz#qux');
55
- session = new WebBrowserSession();
56
+ session = new Session(WebBrowserEnvironment);
56
57
 
57
58
  let location;
58
59
  session.subscribe((newLocation) => {
@@ -75,7 +76,7 @@ describe('WebBrowserSession', () => {
75
76
  });
76
77
 
77
78
  it('should require initialization', () => {
78
- session = new WebBrowserSession();
79
+ session = new Session(WebBrowserEnvironment);
79
80
 
80
81
  expect(() =>
81
82
  session.navigate('push', {
@@ -89,7 +90,7 @@ describe('WebBrowserSession', () => {
89
90
  it('should support basic navigation', async () => {
90
91
  window.history.replaceState(null, null, '/initial');
91
92
 
92
- session = new WebBrowserSession();
93
+ session = new Session(WebBrowserEnvironment);
93
94
 
94
95
  let location;
95
96
  session.subscribe((newLocation) => {
@@ -178,7 +179,7 @@ describe('WebBrowserSession', () => {
178
179
  index: 2,
179
180
  delta: 0,
180
181
  });
181
- await delay(20);
182
+ await delay(100);
182
183
 
183
184
  expect(window.location.pathname).to.equal('/new-3');
184
185
 
@@ -192,7 +193,7 @@ describe('WebBrowserSession', () => {
192
193
  listener.resetHistory();
193
194
 
194
195
  session.shift(-1);
195
- await delay(20);
196
+ await delay(100);
196
197
 
197
198
  expect(window.location).to.include({
198
199
  pathname: '/new',
@@ -213,7 +214,7 @@ describe('WebBrowserSession', () => {
213
214
  listener.resetHistory();
214
215
 
215
216
  window.history.back();
216
- await delay(20);
217
+ await delay(100);
217
218
 
218
219
  expect(window.location.pathname).to.equal('/initial');
219
220
 
@@ -229,7 +230,7 @@ describe('WebBrowserSession', () => {
229
230
 
230
231
  it('should support subscribing and unsubscribing', async () => {
231
232
  window.history.replaceState(null, null, '/');
232
- session = new WebBrowserSession();
233
+ session = new Session(WebBrowserEnvironment);
233
234
  session.start(parseInputLocation(window.location));
234
235
  session.navigate('push', {
235
236
  pathname: '/new',
@@ -246,7 +247,7 @@ describe('WebBrowserSession', () => {
246
247
  const unsubscribe = session.subscribe(listener);
247
248
 
248
249
  session.shift(-1);
249
- await delay(20);
250
+ await delay(100);
250
251
 
251
252
  expect(listener).to.have.been.calledOnce();
252
253
  expect(listener.firstCall.args[0]).to.include({
@@ -258,8 +259,57 @@ describe('WebBrowserSession', () => {
258
259
  unsubscribe();
259
260
 
260
261
  session.shift(-1);
261
- await delay(20);
262
+ await delay(100);
262
263
 
263
264
  expect(listener).not.to.have.been.called();
264
265
  });
265
266
  });
267
+
268
+ describe('Session (WebBrowserEnvironment) (restart)', () => {
269
+ it('should restart a previously-started session', async () => {
270
+ window.history.replaceState(null, null, '/initial');
271
+
272
+ let currentLocation;
273
+ const locationListener = (location) => {
274
+ currentLocation = location;
275
+ };
276
+
277
+ const session = new Session(WebBrowserEnvironment);
278
+ session.subscribe(locationListener);
279
+ session.start();
280
+ session.navigate('push', parseInputLocation('/new'));
281
+ session.stop();
282
+
283
+ const latestLocationIndex = currentLocation.index;
284
+ expect(latestLocationIndex > 0).to.equal(true);
285
+
286
+ // Simulate the user refreshing the page in a web browser.
287
+ // In such case, `window.history` will be retained
288
+ // but every javascript variable will be recreated from scratch.
289
+ const newSession = new Session(WebBrowserEnvironment);
290
+ newSession.subscribe(locationListener);
291
+ newSession.start();
292
+
293
+ // eslint-disable-next-line no-underscore-dangle
294
+ expect(newSession._subscription._latest.operation).to.equal('init');
295
+
296
+ expect(currentLocation.pathname).to.equal('/new');
297
+ expect(currentLocation.index).to.equal(latestLocationIndex);
298
+
299
+ newSession.shift(-1);
300
+
301
+ await delay(100);
302
+
303
+ expect(currentLocation.pathname).to.equal('/initial');
304
+ expect(currentLocation.index).to.equal(latestLocationIndex - 1);
305
+
306
+ newSession.shift(1);
307
+
308
+ await delay(100);
309
+
310
+ expect(currentLocation.pathname).to.equal('/new');
311
+ expect(currentLocation.index).to.equal(latestLocationIndex);
312
+
313
+ newSession.stop();
314
+ });
315
+ });
@@ -0,0 +1,44 @@
1
+ // With such `import`, tests wouldn't work and would throw an error:
2
+ // "console.warn.restore is not a function".
3
+ // It could or could not be related to using `karma` test runner.
4
+ //
5
+ // import sinon from 'sinon';
6
+
7
+ export default function shouldWarn(about) {
8
+ console.warn.expected.push(about); // eslint-disable-line no-console
9
+ }
10
+
11
+ export function stubWarn() {
12
+ /* eslint-disable no-console */
13
+ sinon.stub(console, 'warn').callsFake((message) => {
14
+ let expected = false;
15
+
16
+ console.warn.expected.forEach((about) => {
17
+ if (message.includes(about)) {
18
+ console.warn.warned[about] = true;
19
+ expected = true;
20
+ }
21
+ });
22
+
23
+ if (expected) {
24
+ return;
25
+ }
26
+
27
+ console.warn.threw = true;
28
+ throw new Error(message);
29
+ });
30
+
31
+ console.warn.expected = [];
32
+ console.warn.warned = Object.create(null);
33
+ console.warn.threw = false;
34
+
35
+ return () => {
36
+ const { expected, warned, threw } = console.warn;
37
+ console.warn.restore();
38
+
39
+ if (!threw && expected.length) {
40
+ expect(warned).to.have.keys(expected);
41
+ }
42
+ };
43
+ /* eslint-enable no-console */
44
+ }
@@ -0,0 +1,65 @@
1
+ import stringifyQuery from '../src/stringifyQuery';
2
+
3
+ describe('stringifyQuery', () => {
4
+ it('stringify', () => {
5
+ expect(stringifyQuery({ foo: 'bar' })).to.equal('foo=bar');
6
+ expect(
7
+ stringifyQuery({
8
+ foo: 'bar',
9
+ bar: 'baz',
10
+ }),
11
+ ).to.equal('foo=bar&bar=baz');
12
+ });
13
+
14
+ it('different types', () => {
15
+ expect(stringifyQuery({})).to.equal('');
16
+ });
17
+
18
+ it('primitive types', () => {
19
+ expect(stringifyQuery({ a: 'string' })).to.equal('a=string');
20
+ expect(stringifyQuery({ a: true, b: false })).to.equal('a=true&b=false');
21
+ expect(stringifyQuery({ a: 0, b: 1n })).to.equal('a=0&b=1');
22
+ expect(stringifyQuery({ a: null, b: undefined })).to.equal('a');
23
+ });
24
+
25
+ it('URI encode', () => {
26
+ expect(stringifyQuery({ 'foo bar': 'baz faz' })).to.equal(
27
+ 'foo%20bar=baz%20faz',
28
+ );
29
+ expect(stringifyQuery({ 'foo bar': "baz'faz" })).to.equal(
30
+ 'foo%20bar=baz%27faz',
31
+ );
32
+ });
33
+
34
+ it('handle array value', () => {
35
+ expect(() => {
36
+ stringifyQuery({
37
+ abc: 'abc',
38
+ foo: ['bar', 'baz'],
39
+ });
40
+ }).to.throw('Array values are not supported');
41
+ });
42
+
43
+ it('should not encode undefined values', () => {
44
+ expect(
45
+ stringifyQuery({
46
+ abc: undefined,
47
+ foo: 'baz',
48
+ }),
49
+ ).to.equal('foo=baz');
50
+ });
51
+
52
+ it('should encode null values as just a key', () => {
53
+ expect(
54
+ stringifyQuery({
55
+ 'x y z': null,
56
+ 'abc': null,
57
+ 'foo': 'baz',
58
+ }),
59
+ ).to.equal('x%20y%20z&abc&foo=baz');
60
+ });
61
+
62
+ it('encoding', () => {
63
+ expect(stringifyQuery({ foo: "'bar'" })).to.equal('foo=%27bar%27');
64
+ });
65
+ });
package/types/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
  }
@@ -6,7 +6,6 @@
6
6
  "strict": true,
7
7
  "types": [],
8
8
  "noEmit": true,
9
- "baseUrl": ".",
10
9
  "paths": {
11
10
  "navigation-stack": ["."]
12
11
  }