navigation-stack 0.3.1 → 0.5.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 (257) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +611 -163
  3. package/data-storage/package.json +6 -0
  4. package/karma.conf.cjs +21 -4
  5. package/lib/cjs/NavigationStack.js +88 -0
  6. package/lib/cjs/data-storage/DataStorage.js +71 -0
  7. package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
  8. package/lib/cjs/data-storage/index.js +9 -0
  9. package/lib/cjs/debug.js +12 -0
  10. package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
  11. package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
  12. package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
  13. package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
  14. package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
  16. package/lib/cjs/getLocationFromInternalLocation.js +14 -0
  17. package/lib/cjs/index.js +20 -16
  18. package/lib/cjs/navigationBlockers.js +28 -23
  19. package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
  20. package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  21. package/lib/cjs/redux/ActionTypesInternal.js +8 -0
  22. package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
  23. package/lib/cjs/redux/createMiddlewares.js +60 -0
  24. package/lib/cjs/redux/index.js +13 -0
  25. package/lib/cjs/redux/internalLocationReducer.js +14 -0
  26. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
  27. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
  28. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  29. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
  30. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
  31. package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
  32. package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
  33. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
  34. package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
  35. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +141 -0
  36. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +407 -0
  37. package/lib/cjs/scroll-position/ScrollPositionSaver.js +87 -0
  38. package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
  39. package/lib/cjs/scroll-position/constants.js +5 -0
  40. package/lib/cjs/scroll-position/index.js +7 -0
  41. package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
  42. package/lib/cjs/session/InMemorySession.js +22 -0
  43. package/lib/cjs/session/ServerSideRenderSession.js +17 -0
  44. package/lib/cjs/session/Session.js +202 -0
  45. package/lib/cjs/session/WebBrowserSession.js +20 -0
  46. package/lib/cjs/session/key/createSessionKey.js +23 -0
  47. package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
  48. package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
  49. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
  50. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
  51. package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
  52. package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
  53. package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
  54. package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
  55. package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
  56. package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
  57. package/lib/cjs/session/navigation/operation/operations.js +11 -0
  58. package/lib/cjs/session/subscription/Subscription.js +81 -0
  59. package/lib/data-storage/index.d.ts +35 -0
  60. package/lib/esm/NavigationStack.js +81 -0
  61. package/lib/esm/data-storage/DataStorage.js +65 -0
  62. package/lib/esm/data-storage/LocationDataStorage.js +22 -0
  63. package/lib/esm/data-storage/index.js +2 -0
  64. package/lib/esm/debug.js +7 -0
  65. package/lib/esm/environment/InMemoryEnvironment.js +8 -0
  66. package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
  67. package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
  68. package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
  69. package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
  70. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
  71. package/lib/esm/getLocationFromInternalLocation.js +9 -0
  72. package/lib/esm/index.js +10 -8
  73. package/lib/esm/navigationBlockers.js +28 -23
  74. package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
  75. package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  76. package/lib/esm/redux/ActionTypesInternal.js +3 -0
  77. package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
  78. package/lib/esm/redux/createMiddlewares.js +54 -0
  79. package/lib/esm/redux/index.js +4 -0
  80. package/lib/esm/redux/internalLocationReducer.js +8 -0
  81. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  82. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
  83. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
  84. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
  85. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
  86. package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
  87. package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  88. package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
  89. package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
  90. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +134 -0
  91. package/lib/esm/scroll-position/ScrollPositionRestoration.js +400 -0
  92. package/lib/esm/scroll-position/ScrollPositionSaver.js +80 -0
  93. package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
  94. package/lib/esm/scroll-position/constants.js +1 -0
  95. package/lib/esm/scroll-position/index.js +1 -0
  96. package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
  97. package/lib/esm/session/InMemorySession.js +15 -0
  98. package/lib/esm/session/ServerSideRenderSession.js +11 -0
  99. package/lib/esm/session/Session.js +195 -0
  100. package/lib/esm/session/WebBrowserSession.js +13 -0
  101. package/lib/esm/session/key/createSessionKey.js +18 -0
  102. package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  103. package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
  104. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
  105. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
  106. package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
  107. package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
  108. package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
  109. package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
  110. package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
  111. package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
  112. package/lib/esm/session/navigation/operation/operations.js +6 -0
  113. package/lib/esm/session/subscription/Subscription.js +75 -0
  114. package/lib/index.d.ts +179 -157
  115. package/lib/redux/index.d.ts +90 -0
  116. package/lib/scroll-position/index.d.ts +107 -0
  117. package/package.json +9 -5
  118. package/redux/package.json +6 -0
  119. package/scroll-position/package.json +6 -0
  120. package/src/NavigationStack.js +100 -0
  121. package/src/data-storage/DataStorage.js +69 -0
  122. package/src/data-storage/LocationDataStorage.js +23 -0
  123. package/src/data-storage/index.js +2 -0
  124. package/src/debug.js +8 -0
  125. package/src/environment/InMemoryEnvironment.js +9 -0
  126. package/src/environment/WebBrowserEnvironment.js +9 -0
  127. package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
  128. package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
  129. package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
  130. package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
  131. package/src/getLocationFromInternalLocation.js +7 -0
  132. package/src/index.js +10 -8
  133. package/src/navigationBlockers.js +31 -27
  134. package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
  135. package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  136. package/src/redux/ActionTypesInternal.js +3 -0
  137. package/src/{Actions.js → redux/Actions.js} +4 -3
  138. package/src/redux/createMiddlewares.js +65 -0
  139. package/src/redux/index.js +4 -0
  140. package/src/redux/internalLocationReducer.js +9 -0
  141. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  142. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
  143. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  144. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
  145. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
  146. package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
  147. package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  148. package/src/redux/middleware/updateLocationMiddleware.js +28 -0
  149. package/src/scroll-position/PageScrollPositionSetter.js +110 -0
  150. package/src/scroll-position/ScrollPositionAutoSaver.js +168 -0
  151. package/src/scroll-position/ScrollPositionRestoration.js +551 -0
  152. package/src/scroll-position/ScrollPositionSaver.js +120 -0
  153. package/src/scroll-position/ScrollPositionSetter.js +16 -0
  154. package/src/scroll-position/constants.js +1 -0
  155. package/src/scroll-position/index.js +1 -0
  156. package/src/scroll-position/scheduleNextTick.js +6 -0
  157. package/src/session/InMemorySession.js +13 -0
  158. package/src/session/ServerSideRenderSession.js +9 -0
  159. package/src/session/Session.js +238 -0
  160. package/src/session/WebBrowserSession.js +13 -0
  161. package/src/session/key/createSessionKey.js +18 -0
  162. package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  163. package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
  164. package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
  165. package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
  166. package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
  167. package/src/session/navigation/InMemoryNavigation.js +78 -0
  168. package/src/session/navigation/ServerSideNavigation.js +43 -0
  169. package/src/session/navigation/WebBrowserNavigation.js +224 -0
  170. package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
  171. package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
  172. package/src/session/navigation/operation/operations.js +6 -0
  173. package/src/session/subscription/Subscription.js +76 -0
  174. package/test/NavigationStack.test.js +296 -0
  175. package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
  176. package/test/data-storage/index.test.js +8 -0
  177. package/test/index.js +12 -0
  178. package/test/index.test.js +8 -7
  179. package/test/{helpers.js → middlewareTestUtil.js} +9 -12
  180. package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
  181. package/test/{Action.test.js → redux/Action.test.js} +7 -6
  182. package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
  183. package/test/redux/createMiddlewares.test.js +96 -0
  184. package/test/redux/index.test.js +10 -0
  185. package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
  186. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
  187. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
  188. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
  189. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
  190. package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
  191. package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
  192. package/test/scroll-position/ScrollPositionRestoration.test.js +435 -0
  193. package/test/scroll-position/addScrollableContainer.js +39 -0
  194. package/test/scroll-position/addScrollableContainerWithAnchors.js +56 -0
  195. package/test/scroll-position/createApp.js +132 -0
  196. package/test/scroll-position/delay.js +9 -0
  197. package/test/scroll-position/mockPageLifecycle.js +17 -0
  198. package/test/scroll-position/runApp.js +24 -0
  199. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
  200. package/test/session/InMemorySession.test.js +348 -0
  201. package/test/session/ServerSession.test.js +17 -9
  202. package/test/session/WebBrowserSession.test.js +265 -0
  203. package/test/testUtil.js +3 -0
  204. package/types/data-storage/index.d.ts +35 -0
  205. package/types/index.d.ts +179 -157
  206. package/types/redux/index.d.ts +90 -0
  207. package/types/scroll-position/index.d.ts +107 -0
  208. package/types/tsconfig.json +1 -1
  209. package/lib/cjs/LocationDataStorage.js +0 -61
  210. package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
  211. package/lib/cjs/beforeLocationChangeListeners.js +0 -51
  212. package/lib/cjs/createMiddlewares.js +0 -47
  213. package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
  214. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
  215. package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
  216. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
  217. package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
  218. package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
  219. package/lib/cjs/session/BrowserSession.js +0 -235
  220. package/lib/cjs/session/MemorySession.js +0 -223
  221. package/lib/cjs/session/ServerSession.js +0 -65
  222. package/lib/esm/LocationDataStorage.js +0 -54
  223. package/lib/esm/addBeforeLocationChangeListener.js +0 -2
  224. package/lib/esm/beforeLocationChangeListeners.js +0 -44
  225. package/lib/esm/createMiddlewares.js +0 -41
  226. package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
  227. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
  228. package/lib/esm/middleware/createLocationMiddleware.js +0 -50
  229. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
  230. package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
  231. package/lib/esm/onlyAllowedOnClientSide.js +0 -5
  232. package/lib/esm/session/BrowserSession.js +0 -229
  233. package/lib/esm/session/MemorySession.js +0 -217
  234. package/lib/esm/session/ServerSession.js +0 -58
  235. package/src/LocationDataStorage.js +0 -60
  236. package/src/addBeforeLocationChangeListener.js +0 -2
  237. package/src/beforeLocationChangeListeners.js +0 -54
  238. package/src/createMiddlewares.js +0 -45
  239. package/src/middleware/createBasePathMiddleware.js +0 -20
  240. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
  241. package/src/middleware/createLocationMiddleware.js +0 -55
  242. package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
  243. package/src/middleware/createTransformLocationMiddleware.js +0 -29
  244. package/src/onlyAllowedOnClientSide.js +0 -5
  245. package/src/session/BrowserSession.js +0 -235
  246. package/src/session/MemorySession.js +0 -219
  247. package/src/session/ServerSession.js +0 -67
  248. package/test/createMiddlewares.test.js +0 -62
  249. package/test/middleware/createBasePathMiddleware.test.js +0 -67
  250. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
  251. package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
  252. package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
  253. package/test/session/BrowserSession.test.js +0 -182
  254. package/test/session/MemorySession.test.js +0 -244
  255. /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
  256. /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
  257. /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
@@ -0,0 +1,195 @@
1
+ import debug from '../debug';
2
+ import parseInputLocation from '../parseInputLocation';
3
+ import createSessionKey from './key/createSessionKey';
4
+ import NavigationOutOfBoundsError from './navigation/error/NavigationOutOfBoundsError';
5
+ import NavigationOperations from './navigation/operation/operations';
6
+ import Subscription from './subscription/Subscription';
7
+ const INITIAL_KEY_INDEX = -1;
8
+ const INITIAL_INDEX = -1;
9
+ const INIT_LOCATION_DELTA = 0;
10
+ export default class Session {
11
+ constructor({
12
+ navigation
13
+ }) {
14
+ // This function is used by navigation.
15
+ this._getCurrentLocationIndex = () => {
16
+ return this._currentLocationIndex;
17
+ };
18
+ // `key` is used in `WebBrowserSession` to uniquely identify a session
19
+ // when storing data in a `WebBrowserDataStorage` which uses `window.sessionStorage`
20
+ // under the hood, and `window.sessionStorage` is shared between different sessions.
21
+ this.key = createSessionKey();
22
+
23
+ // `this._locationKeyIndex` is incremented every time the current location changes.
24
+ this._locationKeyIndex = INITIAL_KEY_INDEX;
25
+
26
+ // `this._currentLocationIndex` is the index of the top element in the navigation stack.
27
+ // I.e. it's the index of the "current" location in the navigation stack.
28
+ this._currentLocationIndex = INITIAL_INDEX;
29
+
30
+ // The `index` of the terminal (rightmost) location in the navigation history.
31
+ // In other words, this is the last location index that it can `.shift()` to.
32
+ this._terminalLocationIndex = this._currentLocationIndex;
33
+
34
+ // Create `navigation`.
35
+ this._navigation = navigation;
36
+
37
+ // Manages subscriptions.
38
+ this._subscription = new Subscription({
39
+ activateSubscription: listener => {
40
+ return this._navigation.subscribe(listener);
41
+ }
42
+ });
43
+
44
+ // Update current location index when a location change was not initiated
45
+ // by this session but rather by the user clicking "Back" or "Forward" button.
46
+ this._unsubscribe = this.subscribe(location => {
47
+ // Update `this._currentLocationIndex` when the location change was not initiated
48
+ // by this session but rather by the user clicking "Back" or "Forward" button.
49
+ this._currentLocationIndex = location.index;
50
+ // Since `currentLocationIndex` has been updated, update `terminalLocationIndex`.
51
+ // It's not really currently possible to see a "PUSH" or a "REPLACE" operation here,
52
+ // but if it was possible, this call would be required. It would also be required
53
+ // by `navigation` to call `session.getNextKey()` function to increment `locationKeyIndex`.
54
+ this._updateTerminalLocationIndex(location);
55
+ debug('current location', location.pathname, 'index', this._currentLocationIndex);
56
+ });
57
+ }
58
+
59
+ // Subscribes to changes in location.
60
+ subscribe(listener) {
61
+ return this._subscription.subscribe(location => {
62
+ if (!this._isStarted() && location.operation !== NavigationOperations.INIT) {
63
+ // eslint-disable-next-line no-console
64
+ console.error('Unexpected location change', location);
65
+ throw new Error('Not started');
66
+ } else {
67
+ // Call the listener.
68
+ listener(location);
69
+ }
70
+ });
71
+ }
72
+ start(initialLocation) {
73
+ if (this._stopped) {
74
+ throw new Error('Can not be restarted');
75
+ }
76
+
77
+ // Simplify "developer experience" by automatically calling
78
+ // `.init(initialLocation)` in case of using a `WebBrowserSession`.
79
+ //
80
+ // That's because `WebBrowserSession` environment already knows
81
+ // the initial location by the time javascript code starts execution.
82
+ //
83
+ if (!initialLocation) {
84
+ initialLocation = this._navigation.getInitialLocation();
85
+ if (initialLocation) {
86
+ initialLocation = parseInputLocation(initialLocation);
87
+ }
88
+ }
89
+ if (!initialLocation) {
90
+ throw new Error('`initialLocation` is required');
91
+ }
92
+ if (this._currentLocationIndex !== INITIAL_INDEX) {
93
+ throw new Error('Already started');
94
+ }
95
+ debug('▶ start session', initialLocation.pathname);
96
+ this._started = true;
97
+ const key = this._getNextLocationKey();
98
+ const index = INITIAL_INDEX + 1;
99
+ const delta = INIT_LOCATION_DELTA;
100
+ const locationResult = this._navigation.init(initialLocation, {
101
+ operation: NavigationOperations.INIT,
102
+ key,
103
+ index,
104
+ delta
105
+ });
106
+ if (locationResult) {
107
+ this._subscription.notifySubscribers(locationResult);
108
+ }
109
+ }
110
+ stop() {
111
+ if (this._stopped) {
112
+ throw Error('Already stopped');
113
+ }
114
+ debug('⏹ stop session');
115
+
116
+ // Once stopped, it won't be able to be restarted.
117
+ this._stopped = true;
118
+
119
+ // Remove location change subscription.
120
+ this._unsubscribe();
121
+
122
+ // Even if it calls `unsubscribe()` function above, any other subscriptions
123
+ // would still stay. For example, subscriptions created by the application code.
124
+ // To work around that, `.stop()` function removes all subscriptions.
125
+ this._subscription.stop();
126
+ }
127
+ navigate(operation, location) {
128
+ if (!this._isStarted()) {
129
+ throw Error('Not started');
130
+ }
131
+ if (operation !== NavigationOperations.PUSH && operation !== NavigationOperations.REPLACE) {
132
+ throw Error(`Unknown navigation operation: ${operation}`);
133
+ }
134
+ const delta = operation === NavigationOperations.PUSH ? 1 : 0;
135
+ this._updateTerminalLocationIndex({
136
+ operation
137
+ });
138
+ const key = this._getNextLocationKey();
139
+ const index = this._currentLocationIndex + delta;
140
+ debug(operation === NavigationOperations.PUSH ? '↓' : '⇅', operation, location.pathname, 'index', index);
141
+
142
+ // Navigate to the location.
143
+ const locationResult = this._navigation.navigate(location, {
144
+ operation,
145
+ key,
146
+ index,
147
+ delta
148
+ });
149
+ if (locationResult) {
150
+ this._subscription.notifySubscribers(locationResult);
151
+ }
152
+ }
153
+ shift(delta) {
154
+ if (!this._isStarted()) {
155
+ throw Error('Not started');
156
+ }
157
+
158
+ // If there'll be no navigation, return.
159
+ if (delta === 0) {
160
+ return;
161
+ }
162
+ const index = this._currentLocationIndex + delta;
163
+ debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
164
+
165
+ // Validate that the new `index` is not out of bounds.
166
+ if (index < 0 || index > this._terminalLocationIndex) {
167
+ throw new NavigationOutOfBoundsError(index);
168
+ }
169
+
170
+ // Navigate to the location.
171
+ const locationResult = this._navigation.shift({
172
+ operation: NavigationOperations.SHIFT,
173
+ index,
174
+ delta
175
+ });
176
+ if (locationResult) {
177
+ this._subscription.notifySubscribers(locationResult);
178
+ }
179
+ }
180
+ _updateTerminalLocationIndex({
181
+ operation
182
+ }) {
183
+ // A `PUSH` navigation sets a new terminal (rightmost) location.
184
+ if (operation === NavigationOperations.PUSH || operation === NavigationOperations.INIT) {
185
+ this._terminalLocationIndex = this._currentLocationIndex;
186
+ }
187
+ }
188
+ _getNextLocationKey() {
189
+ this._locationKeyIndex++;
190
+ return this._locationKeyIndex.toString(36);
191
+ }
192
+ _isStarted() {
193
+ return !this._stopped && this._currentLocationIndex !== INITIAL_INDEX;
194
+ }
195
+ }
@@ -0,0 +1,13 @@
1
+ import Session from './Session';
2
+ import WebBrowserEnvironment from '../environment/WebBrowserEnvironment';
3
+ import WebBrowserSessionLifecycle from './lifecycle/WebBrowserSessionLifecycle';
4
+ import WebBrowserNavigation from './navigation/WebBrowserNavigation';
5
+ export default class WebBrowserSession extends Session {
6
+ constructor() {
7
+ super({
8
+ navigation: new WebBrowserNavigation()
9
+ });
10
+ this.environment = new WebBrowserEnvironment();
11
+ this.lifecycle = new WebBrowserSessionLifecycle();
12
+ }
13
+ }
@@ -0,0 +1,18 @@
1
+ // `session.key` exists to avoid `location.key` collision after a page refresh.
2
+ // After a page refresh, a different `session.key` is created
3
+ // while the previous navigation history still exists because
4
+ // web browser navigation history survives a page reload.
5
+ // So web browser navigation history after a refresh contains records from different sessions.
6
+ // This means that some of those history records end up having same `location.key`s
7
+ // because `location.key` always starts from `0` for each different session.
8
+ // So `location.key` alone can't be used to identify navigation history entries
9
+ // because it's not unique among them. In order to get a unique key for a navigation history entry,
10
+ // one should combine a unique `session.key` with a `location.key`.
11
+ // That's what `session.key` exists for.
12
+ // Supplementary features such as scroll position restoration
13
+ // use `window.sessionStorage` to store supplementary data for a given navigation history entry.
14
+ // Because `window.sessionStorage` is shared between all navigation history entries from different websites,
15
+ // the keys used for storing that supplementary data have to be unique.
16
+ export default function createSessionKey() {
17
+ return Date.now().toString(36);
18
+ }
@@ -0,0 +1,13 @@
1
+ export default class InMemorySessionLifecycle {
2
+ // Termination blockers of an "in-memory session" are currently ignored.
3
+ // eslint-disable-next-line no-unused-vars
4
+ addTerminationBlocker(blocker) {
5
+ return () => {};
6
+ }
7
+
8
+ // An "in-memory session" execution status is always `running: true`.
9
+ // eslint-disable-next-line no-unused-vars
10
+ addExecutionStatusListener(listener) {
11
+ return () => {};
12
+ }
13
+ }
@@ -0,0 +1,120 @@
1
+ // https://developers.google.com/web/updates/2018/07/page-lifecycle-api
2
+ // https://github.com/GoogleChromeLabs/page-lifecycle
3
+ import PageLifecycle from './page-lifecycle/PageLifecycleInstance';
4
+ export default class WebBrowserSessionLifecycle {
5
+ constructor() {
6
+ this._running = true;
7
+ }
8
+ addTerminationBlocker(terminationBlocker) {
9
+ const onBeforeUnload = event => {
10
+ if (terminationBlocker()) {
11
+ // Calling `event.preventDefault()` will cause a web browser
12
+ // to show a generic "Ok"/"Cancel" modal with some generic text:
13
+ // "Are you sure to leave the current page?".
14
+ event.preventDefault();
15
+ }
16
+ };
17
+ window.addEventListener('beforeunload', onBeforeUnload);
18
+ return () => {
19
+ window.removeEventListener('beforeunload', onBeforeUnload);
20
+ };
21
+ }
22
+ addExecutionStatusListener(listener) {
23
+ const pageLifecycleListener = stateChange => {
24
+ // (stateChange: PageLifecycleStateChange)
25
+ const {
26
+ newState
27
+ } = stateChange;
28
+ const running = !['terminated', 'frozen', 'discarded'].includes(newState);
29
+ if (this._running !== running) {
30
+ this._running = running;
31
+ listener({
32
+ running
33
+ });
34
+ }
35
+ };
36
+ PageLifecycle.addEventListener('statechange', pageLifecycleListener);
37
+ return () => {
38
+ PageLifecycle.removeEventListener('statechange', pageLifecycleListener);
39
+ };
40
+ }
41
+ }
42
+
43
+ // interface PageLifecycleStateChange {
44
+ // newState: PageLifecycleState;
45
+ // oldState: PageLifecycleState;
46
+ // originalEvent: Event;
47
+ // }
48
+
49
+ // // Page Lifecycle API event types.
50
+ // // https://developer.chrome.com/docs/web-platform/page-lifecycle-api#states
51
+ // // https://wicg.github.io/page-lifecycle/spec.html
52
+ // //
53
+ // type PageLifecycleState =
54
+ // // The page is visible and is focused.
55
+ // | 'active'
56
+ // // The page is visible but is not focused.
57
+ // | 'passive'
58
+ // // The page is not visible (and has not been frozen, discarded, or terminated).
59
+ // | 'hidden'
60
+ // // If a page is hidden, a browser may choose to freeze it to reduce energy consumption.
61
+ // | 'frozen'
62
+ // // The process of terminating (destroying, closing) the page has started.
63
+ // | 'terminated'
64
+ // // The page is discarded by the web browser due to insufficient resources.
65
+ // // The page snapshot could still be visible to the user even though it's no longer running.
66
+ // | 'discarded';
67
+
68
+ // // Page Lifecycle API event types.
69
+ // // https://developer.chrome.com/docs/web-platform/page-lifecycle-api
70
+ // // https://wicg.github.io/page-lifecycle/spec.html
71
+ // //
72
+ // type PageLifecycleEvent =
73
+ // // When the web browser window with an opened page gets focus, a `focus` event is emitted.
74
+ // | 'focus'
75
+ //
76
+ // // When the web browser window with an opened page is no longer focused, a `blur` event is emitted.
77
+ // | 'blur'
78
+ //
79
+ // // `visibilitychange` event fires with `document.visibilityState` being "hidden"
80
+ // // when a user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser,
81
+ // // or, on mobile, switches from the browser to a different app.
82
+ // //
83
+ // // Transitioning to "hidden" is the last event that's reliably observable by the page,
84
+ // // so developers should treat it as the likely end of the user's session
85
+ // // (for example, for sending analytics data).
86
+ // //
87
+ // // The transition to "hidden" is also a good point at which pages can stop making UI updates
88
+ // // and stop any tasks that the user doesn't want to have running in the background.
89
+ // //
90
+ // | 'visibilitychange'
91
+ //
92
+ // // Sometimes browsers "freeze" hidden pages in order to reduce energy consumption on mobile devices.
93
+ // // In case of freezing an already-hidden page, a `freeze` event will be emitted, if supported by the browser.
94
+ // | 'freeze'
95
+ //
96
+ // // Sometimes browsers "freeze" hidden pages in order to reduce energy consumption on mobile devices.
97
+ // // In case of unfreezing an already-frozen page, a `resume` event will be emitted, if supported by the browser.
98
+ // | 'resume'
99
+ //
100
+ // // `pageshow` event is emitted when a new page gets shown.
101
+ // //
102
+ // // For example, `pageshow` event is emitted when visiting a web page
103
+ // // or after being navigated to a new page by clicking a hyperlink.
104
+ // //
105
+ // // `pageshow` event is also emitted when the user performs "Back" or "Forward" transition.
106
+ // //
107
+ // | 'pageshow'
108
+ //
109
+ // // `pagehide` event is emitted when the current page gets "destroyed".
110
+ // //
111
+ // // For example, `pagehide` event is emitted when the user performs "Back" or "Forward" transition.
112
+ // // In that case, `pagehide` event will be emitted for the current page before the transition.
113
+ // //
114
+ // // In any other cases of "destroying" The current page, `pagehide` event is not guaranteed to be emitted.
115
+ // // For example, it won't be emitted when closing the web browser app via a task manager.
116
+ // //
117
+ // // Hence, `pagehide` event is unreliable and it's adivised to use `visibilitychange` event instead.
118
+ // // Only if `visibilitychange` even is not supported by a web browser should one consider resorting to using `pagehide` event.
119
+ // //
120
+ // | 'pagehide';
@@ -0,0 +1,263 @@
1
+ /* eslint-disable max-classes-per-file */
2
+
3
+ // This code was copy-pasted from the final read-only version of `page-lifecycle` repo:
4
+ // https://github.com/GoogleChromeLabs/page-lifecycle/blob/master/src/Lifecycle.mjs
5
+
6
+ /*
7
+ Copyright 2018 Google Inc. All Rights Reserved.
8
+ Licensed under the Apache License, Version 2.0 (the "License");
9
+ you may not use this file except in compliance with the License.
10
+ You may obtain a copy of the License at
11
+
12
+ http://www.apache.org/licenses/LICENSE-2.0
13
+
14
+ Unless required by applicable law or agreed to in writing, software
15
+ distributed under the License is distributed on an "AS IS" BASIS,
16
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ See the License for the specific language governing permissions and
18
+ limitations under the License.
19
+ */
20
+
21
+ const ACTIVE = 'active';
22
+ const PASSIVE = 'passive';
23
+ const HIDDEN = 'hidden';
24
+ const FROZEN = 'frozen';
25
+ // const DISCARDED = 'discarded'; Not used but show to completeness.
26
+ const TERMINATED = 'terminated';
27
+ const EVENTS = ['focus', 'blur', 'visibilitychange', 'freeze', 'resume', 'pageshow',
28
+ // IE9-10 do not support the "pagehide" event, so the code could fall back to "unload" event here.
29
+ // Note: using "unload" event instead of "pagehide" event will
30
+ // prevent page navigation caching (a.k.a bfcache).
31
+ 'pagehide'];
32
+
33
+ /**
34
+ * @param {!Event} evt
35
+ * @return {string}
36
+ */
37
+ const onbeforeunload = evt => {
38
+ evt.preventDefault();
39
+ };
40
+
41
+ /**
42
+ * Converts an array of states into an object where the state is the key
43
+ * and the value is the index.
44
+ * @param {!Array<string>} arr
45
+ * @return {!Object}
46
+ */
47
+ const toIndexedObject = arr => arr.reduce((acc, val, idx) => {
48
+ acc[val] = idx;
49
+ return acc;
50
+ }, {});
51
+
52
+ /**
53
+ * @type {!Array<!Object>}
54
+ */
55
+ const LEGAL_STATE_TRANSITIONS = [
56
+ // The normal unload process (bfcache process is addressed above).
57
+ [ACTIVE, PASSIVE, HIDDEN, TERMINATED],
58
+ // An active page transitioning to frozen,
59
+ // or an unloading page going into the bfcache.
60
+ [ACTIVE, PASSIVE, HIDDEN, FROZEN],
61
+ // A hidden page transitioning back to active.
62
+ [HIDDEN, PASSIVE, ACTIVE],
63
+ // A frozen page being resumed
64
+ [FROZEN, HIDDEN],
65
+ // A frozen (bfcached) page navigated back to
66
+ // Note: [FROZEN, HIDDEN] can happen here, but it's already covered above.
67
+ [FROZEN, ACTIVE], [FROZEN, PASSIVE]].map(toIndexedObject);
68
+
69
+ /**
70
+ * Accepts a current state and a future state and returns an array of legal
71
+ * state transition paths. This is needed to normalize behavior across browsers
72
+ * since some browsers do not fire events in certain cases and thus skip
73
+ * states.
74
+ * @param {string} oldState
75
+ * @param {string} newState
76
+ * @return {!Array<string>}
77
+ */
78
+ const getLegalStateTransitionPath = (oldState, newState) => {
79
+ for (const order of LEGAL_STATE_TRANSITIONS) {
80
+ const oldIndex = order[oldState];
81
+ const newIndex = order[newState];
82
+ if (oldIndex >= 0 && newIndex >= 0 && newIndex > oldIndex) {
83
+ // Differences greater than one should be reported
84
+ // because it means a state was skipped.
85
+ return Object.keys(order).slice(oldIndex, newIndex + 1);
86
+ }
87
+ }
88
+ return [];
89
+ // TODO(philipwalton): it shouldn't be possible to get here, but
90
+ // consider some kind of warning or call to action if it happens.
91
+ // console.warn(`Invalid state change detected: ${oldState} > ${newState}`);
92
+ };
93
+
94
+ /**
95
+ * Returns the current state based on the document's visibility and
96
+ * in input focus states. Note this method is only used to determine
97
+ * active vs passive vs hidden states, as other states require listening
98
+ * for events.
99
+ * @return {string}
100
+ */
101
+ const getCurrentState = () => {
102
+ if (document.visibilityState === HIDDEN) {
103
+ return HIDDEN;
104
+ }
105
+ if (document.hasFocus()) {
106
+ return ACTIVE;
107
+ }
108
+ return PASSIVE;
109
+ };
110
+ class StateChangeEvent extends Event {
111
+ /**
112
+ * @param {string} type
113
+ * @param {!Object} initDict
114
+ */
115
+ constructor(type, initDict) {
116
+ super(type);
117
+ this.newState = initDict.newState;
118
+ this.oldState = initDict.oldState;
119
+ this.originalEvent = initDict.originalEvent;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Class definition for the exported, singleton lifecycle instance.
125
+ */
126
+ export default class Lifecycle extends EventTarget {
127
+ /**
128
+ * Initializes state, state history, and adds event listeners to monitor
129
+ * state changes.
130
+ */
131
+ constructor() {
132
+ super();
133
+ const state = getCurrentState();
134
+ this._state = state;
135
+ this._unsavedChanges = [];
136
+
137
+ // Bind the callback and add event listeners.
138
+ this._handleEvents = this._handleEvents.bind(this);
139
+
140
+ // Add capturing events on window so they run immediately.
141
+ EVENTS.forEach(evt => window.addEventListener(evt, this._handleEvents, true));
142
+ }
143
+
144
+ /**
145
+ * @return {string}
146
+ */
147
+ get state() {
148
+ return this._state;
149
+ }
150
+
151
+ /**
152
+ * Returns the value of document.wasDiscarded. This is arguably unnecessary
153
+ * but I think there's value in having the entire API in one place and
154
+ * consistent across browsers.
155
+ * @return {boolean}
156
+ */
157
+ get pageWasDiscarded() {
158
+ return document.wasDiscarded || false;
159
+ }
160
+
161
+ /**
162
+ * @param {Symbol|Object} id A unique symbol or object identifying the
163
+ *. pending state. This ID is required when removing the state later.
164
+ */
165
+ addUnsavedChanges(id) {
166
+ // Don't add duplicate state. Note: ideall this would be a set, but for
167
+ // better browser compatibility we're using an array.
168
+ if (!this._unsavedChanges.indexOf(id) > -1) {
169
+ // If this is the first state being added,
170
+ // also add a beforeunload listener.
171
+ if (this._unsavedChanges.length === 0) {
172
+ window.addEventListener('beforeunload', onbeforeunload);
173
+ }
174
+ this._unsavedChanges.push(id);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * @param {Symbol|Object} id A unique symbol or object identifying the
180
+ *. pending state. This ID is required when removing the state later.
181
+ */
182
+ removeUnsavedChanges(id) {
183
+ const idIndex = this._unsavedChanges.indexOf(id);
184
+ if (idIndex > -1) {
185
+ this._unsavedChanges.splice(idIndex, 1);
186
+
187
+ // If there's no more pending state, remove the event listener.
188
+ if (this._unsavedChanges.length === 0) {
189
+ window.removeEventListener('beforeunload', onbeforeunload);
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Transitions from `this._state` to `newState`
196
+ * going through any intermediate states if required.
197
+ * @private
198
+ * @param {!Event} originalEvent
199
+ * @param {string} newState
200
+ */
201
+ _dispatchChangesIfNeeded(originalEvent, newState) {
202
+ if (newState !== this._state) {
203
+ const oldState = this._state;
204
+ // Get the full chain of states for properly transitioning
205
+ // from `oldState` to `newState`.
206
+ const path = getLegalStateTransitionPath(oldState, newState);
207
+
208
+ // Start from `oldState` and transition to `newState`
209
+ // going through any intermediate states if required.
210
+ // Go through each intermediate state if required.
211
+ for (let i = 0; i < path.length - 1; i++) {
212
+ const prevState = path[i];
213
+ const nextState = path[i + 1];
214
+ this._state = nextState;
215
+ this.dispatchEvent(new StateChangeEvent('statechange', {
216
+ oldState: prevState,
217
+ newState: nextState,
218
+ originalEvent
219
+ }));
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * @private
226
+ * @param {!Event} evt
227
+ */
228
+ _handleEvents(evt) {
229
+ switch (evt.type) {
230
+ case 'pageshow':
231
+ case 'resume':
232
+ this._dispatchChangesIfNeeded(evt, getCurrentState());
233
+ break;
234
+ case 'focus':
235
+ this._dispatchChangesIfNeeded(evt, ACTIVE);
236
+ break;
237
+ case 'blur':
238
+ // The `blur` event can fire while the page is being unloaded, so we
239
+ // only need to update the state if the current state is "active".
240
+ if (this._state === ACTIVE) {
241
+ this._dispatchChangesIfNeeded(evt, getCurrentState());
242
+ }
243
+ break;
244
+ case 'pagehide':
245
+ case 'unload':
246
+ this._dispatchChangesIfNeeded(evt, evt.persisted ? FROZEN : TERMINATED);
247
+ break;
248
+ case 'visibilitychange':
249
+ // The document's `visibilityState` will change to hidden as the page
250
+ // is being unloaded, but in such cases the lifecycle state shouldn't
251
+ // change.
252
+ if (this._state !== FROZEN && this._state !== TERMINATED) {
253
+ this._dispatchChangesIfNeeded(evt, getCurrentState());
254
+ }
255
+ break;
256
+ case 'freeze':
257
+ this._dispatchChangesIfNeeded(evt, FROZEN);
258
+ break;
259
+ default:
260
+ break;
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,2 @@
1
+ import PageLifecycle from './PageLifecycle';
2
+ export default new PageLifecycle();
@@ -0,0 +1,30 @@
1
+ /*
2
+ Copyright 2018 Google Inc. All Rights Reserved.
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+ */
15
+
16
+ function doesSupportConstructableEventTarget() {
17
+ try {
18
+ // eslint-disable-next-line no-unused-vars
19
+ const eventTarget = new EventTarget();
20
+
21
+ // When transpiled with babel and rollup, the `IS_CODE_TRANSPILED` constant
22
+ // is replaced with the boolean `true`, so this statement will always
23
+ // evaluate to `false`. When not transpiled, it will be true.
24
+ return typeof IS_CODE_TRANSPILED === 'undefined';
25
+ } catch (err) {
26
+ return false;
27
+ }
28
+ }
29
+ const supportsConstructableEventTarget = doesSupportConstructableEventTarget();
30
+ export { supportsConstructableEventTarget };