navigation-stack 0.3.1 → 0.4.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 (253) hide show
  1. package/README.md +603 -163
  2. package/data-storage/package.json +6 -0
  3. package/karma.conf.cjs +21 -4
  4. package/lib/cjs/NavigationStack.js +73 -0
  5. package/lib/cjs/data-storage/DataStorage.js +71 -0
  6. package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
  7. package/lib/cjs/data-storage/index.js +9 -0
  8. package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
  9. package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
  10. package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
  11. package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
  12. package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
  13. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
  14. package/lib/cjs/getLocationFromInternalLocation.js +14 -0
  15. package/lib/cjs/index.js +20 -16
  16. package/lib/cjs/navigationBlockers.js +25 -23
  17. package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
  18. package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  19. package/lib/cjs/redux/ActionTypesInternal.js +8 -0
  20. package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
  21. package/lib/cjs/redux/createMiddlewares.js +60 -0
  22. package/lib/cjs/redux/index.js +13 -0
  23. package/lib/cjs/redux/internalLocationReducer.js +14 -0
  24. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
  25. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
  26. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  27. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
  28. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
  29. package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
  30. package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
  31. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
  32. package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
  33. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +130 -0
  34. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +383 -0
  35. package/lib/cjs/scroll-position/ScrollPositionSaver.js +81 -0
  36. package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
  37. package/lib/cjs/scroll-position/constants.js +5 -0
  38. package/lib/cjs/scroll-position/index.js +7 -0
  39. package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
  40. package/lib/cjs/session/InMemorySession.js +22 -0
  41. package/lib/cjs/session/ServerSideRenderSession.js +17 -0
  42. package/lib/cjs/session/Session.js +196 -0
  43. package/lib/cjs/session/WebBrowserSession.js +20 -0
  44. package/lib/cjs/session/key/createSessionKey.js +23 -0
  45. package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
  46. package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
  47. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
  48. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
  49. package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
  50. package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
  51. package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
  52. package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
  53. package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
  54. package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
  55. package/lib/cjs/session/navigation/operation/operations.js +11 -0
  56. package/lib/cjs/session/subscription/Subscription.js +81 -0
  57. package/lib/data-storage/index.d.ts +35 -0
  58. package/lib/esm/NavigationStack.js +66 -0
  59. package/lib/esm/data-storage/DataStorage.js +65 -0
  60. package/lib/esm/data-storage/LocationDataStorage.js +22 -0
  61. package/lib/esm/data-storage/index.js +2 -0
  62. package/lib/esm/environment/InMemoryEnvironment.js +8 -0
  63. package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
  64. package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
  65. package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
  66. package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
  67. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
  68. package/lib/esm/getLocationFromInternalLocation.js +9 -0
  69. package/lib/esm/index.js +10 -8
  70. package/lib/esm/navigationBlockers.js +25 -23
  71. package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
  72. package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  73. package/lib/esm/redux/ActionTypesInternal.js +3 -0
  74. package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
  75. package/lib/esm/redux/createMiddlewares.js +54 -0
  76. package/lib/esm/redux/index.js +4 -0
  77. package/lib/esm/redux/internalLocationReducer.js +8 -0
  78. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  79. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
  80. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
  81. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
  82. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
  83. package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
  84. package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  85. package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
  86. package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
  87. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +123 -0
  88. package/lib/esm/scroll-position/ScrollPositionRestoration.js +376 -0
  89. package/lib/esm/scroll-position/ScrollPositionSaver.js +74 -0
  90. package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
  91. package/lib/esm/scroll-position/constants.js +1 -0
  92. package/lib/esm/scroll-position/index.js +1 -0
  93. package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
  94. package/lib/esm/session/InMemorySession.js +15 -0
  95. package/lib/esm/session/ServerSideRenderSession.js +11 -0
  96. package/lib/esm/session/Session.js +189 -0
  97. package/lib/esm/session/WebBrowserSession.js +13 -0
  98. package/lib/esm/session/key/createSessionKey.js +18 -0
  99. package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  100. package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
  101. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
  102. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
  103. package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
  104. package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
  105. package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
  106. package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
  107. package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
  108. package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
  109. package/lib/esm/session/navigation/operation/operations.js +6 -0
  110. package/lib/esm/session/subscription/Subscription.js +75 -0
  111. package/lib/index.d.ts +178 -157
  112. package/lib/redux/index.d.ts +90 -0
  113. package/lib/scroll-position/index.d.ts +107 -0
  114. package/package.json +9 -5
  115. package/redux/package.json +6 -0
  116. package/scroll-position/package.json +6 -0
  117. package/src/NavigationStack.js +84 -0
  118. package/src/data-storage/DataStorage.js +69 -0
  119. package/src/data-storage/LocationDataStorage.js +23 -0
  120. package/src/data-storage/index.js +2 -0
  121. package/src/environment/InMemoryEnvironment.js +9 -0
  122. package/src/environment/WebBrowserEnvironment.js +9 -0
  123. package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
  124. package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
  125. package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
  126. package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
  127. package/src/getLocationFromInternalLocation.js +7 -0
  128. package/src/index.js +10 -8
  129. package/src/navigationBlockers.js +28 -27
  130. package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
  131. package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  132. package/src/redux/ActionTypesInternal.js +3 -0
  133. package/src/{Actions.js → redux/Actions.js} +4 -3
  134. package/src/redux/createMiddlewares.js +65 -0
  135. package/src/redux/index.js +4 -0
  136. package/src/redux/internalLocationReducer.js +9 -0
  137. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  138. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
  139. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  140. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
  141. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
  142. package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
  143. package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  144. package/src/redux/middleware/updateLocationMiddleware.js +28 -0
  145. package/src/scroll-position/PageScrollPositionSetter.js +110 -0
  146. package/src/scroll-position/ScrollPositionAutoSaver.js +151 -0
  147. package/src/scroll-position/ScrollPositionRestoration.js +506 -0
  148. package/src/scroll-position/ScrollPositionSaver.js +100 -0
  149. package/src/scroll-position/ScrollPositionSetter.js +16 -0
  150. package/src/scroll-position/constants.js +1 -0
  151. package/src/scroll-position/index.js +1 -0
  152. package/src/scroll-position/scheduleNextTick.js +6 -0
  153. package/src/session/InMemorySession.js +13 -0
  154. package/src/session/ServerSideRenderSession.js +9 -0
  155. package/src/session/Session.js +216 -0
  156. package/src/session/WebBrowserSession.js +13 -0
  157. package/src/session/key/createSessionKey.js +18 -0
  158. package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  159. package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
  160. package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
  161. package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
  162. package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
  163. package/src/session/navigation/InMemoryNavigation.js +78 -0
  164. package/src/session/navigation/ServerSideNavigation.js +43 -0
  165. package/src/session/navigation/WebBrowserNavigation.js +224 -0
  166. package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
  167. package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
  168. package/src/session/navigation/operation/operations.js +6 -0
  169. package/src/session/subscription/Subscription.js +76 -0
  170. package/test/NavigationStack.test.js +296 -0
  171. package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
  172. package/test/data-storage/index.test.js +8 -0
  173. package/test/index.js +12 -0
  174. package/test/index.test.js +8 -7
  175. package/test/{helpers.js → middlewareTestUtil.js} +9 -12
  176. package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
  177. package/test/{Action.test.js → redux/Action.test.js} +7 -6
  178. package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
  179. package/test/redux/createMiddlewares.test.js +96 -0
  180. package/test/redux/index.test.js +10 -0
  181. package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
  182. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
  183. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
  184. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
  185. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
  186. package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
  187. package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
  188. package/test/scroll-position/ScrollPositionRestoration.test.js +418 -0
  189. package/test/scroll-position/addScrollableContainer.js +36 -0
  190. package/test/scroll-position/addScrollableContainerWithHyperlink.js +50 -0
  191. package/test/scroll-position/createApp.js +112 -0
  192. package/test/scroll-position/delay.js +9 -0
  193. package/test/scroll-position/mockPageLifecycle.js +17 -0
  194. package/test/scroll-position/runApp.js +24 -0
  195. package/test/scroll-position/withScrollableContainerAtIndexPage.js +62 -0
  196. package/test/session/InMemorySession.test.js +348 -0
  197. package/test/session/ServerSession.test.js +17 -9
  198. package/test/session/WebBrowserSession.test.js +265 -0
  199. package/test/testUtil.js +3 -0
  200. package/types/data-storage/index.d.ts +35 -0
  201. package/types/index.d.ts +178 -157
  202. package/types/redux/index.d.ts +90 -0
  203. package/types/scroll-position/index.d.ts +107 -0
  204. package/types/tsconfig.json +1 -1
  205. package/lib/cjs/LocationDataStorage.js +0 -61
  206. package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
  207. package/lib/cjs/beforeLocationChangeListeners.js +0 -51
  208. package/lib/cjs/createMiddlewares.js +0 -47
  209. package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
  210. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
  211. package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
  212. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
  213. package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
  214. package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
  215. package/lib/cjs/session/BrowserSession.js +0 -235
  216. package/lib/cjs/session/MemorySession.js +0 -223
  217. package/lib/cjs/session/ServerSession.js +0 -65
  218. package/lib/esm/LocationDataStorage.js +0 -54
  219. package/lib/esm/addBeforeLocationChangeListener.js +0 -2
  220. package/lib/esm/beforeLocationChangeListeners.js +0 -44
  221. package/lib/esm/createMiddlewares.js +0 -41
  222. package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
  223. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
  224. package/lib/esm/middleware/createLocationMiddleware.js +0 -50
  225. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
  226. package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
  227. package/lib/esm/onlyAllowedOnClientSide.js +0 -5
  228. package/lib/esm/session/BrowserSession.js +0 -229
  229. package/lib/esm/session/MemorySession.js +0 -217
  230. package/lib/esm/session/ServerSession.js +0 -58
  231. package/src/LocationDataStorage.js +0 -60
  232. package/src/addBeforeLocationChangeListener.js +0 -2
  233. package/src/beforeLocationChangeListeners.js +0 -54
  234. package/src/createMiddlewares.js +0 -45
  235. package/src/middleware/createBasePathMiddleware.js +0 -20
  236. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
  237. package/src/middleware/createLocationMiddleware.js +0 -55
  238. package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
  239. package/src/middleware/createTransformLocationMiddleware.js +0 -29
  240. package/src/onlyAllowedOnClientSide.js +0 -5
  241. package/src/session/BrowserSession.js +0 -235
  242. package/src/session/MemorySession.js +0 -219
  243. package/src/session/ServerSession.js +0 -67
  244. package/test/createMiddlewares.test.js +0 -62
  245. package/test/middleware/createBasePathMiddleware.test.js +0 -67
  246. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
  247. package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
  248. package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
  249. package/test/session/BrowserSession.test.js +0 -182
  250. package/test/session/MemorySession.test.js +0 -244
  251. /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
  252. /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
  253. /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
@@ -0,0 +1,291 @@
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
+
28
+ const EVENTS = [
29
+ 'focus',
30
+ 'blur',
31
+ 'visibilitychange',
32
+ 'freeze',
33
+ 'resume',
34
+ 'pageshow',
35
+ // IE9-10 do not support the "pagehide" event, so the code could fall back to "unload" event here.
36
+ // Note: using "unload" event instead of "pagehide" event will
37
+ // prevent page navigation caching (a.k.a bfcache).
38
+ 'pagehide',
39
+ ];
40
+
41
+ /**
42
+ * @param {!Event} evt
43
+ * @return {string}
44
+ */
45
+ const onbeforeunload = (evt) => {
46
+ evt.preventDefault();
47
+ };
48
+
49
+ /**
50
+ * Converts an array of states into an object where the state is the key
51
+ * and the value is the index.
52
+ * @param {!Array<string>} arr
53
+ * @return {!Object}
54
+ */
55
+ const toIndexedObject = (arr) =>
56
+ arr.reduce((acc, val, idx) => {
57
+ acc[val] = idx;
58
+ return acc;
59
+ }, {});
60
+
61
+ /**
62
+ * @type {!Array<!Object>}
63
+ */
64
+ const LEGAL_STATE_TRANSITIONS = [
65
+ // The normal unload process (bfcache process is addressed above).
66
+ [ACTIVE, PASSIVE, HIDDEN, TERMINATED],
67
+
68
+ // An active page transitioning to frozen,
69
+ // or an unloading page going into the bfcache.
70
+ [ACTIVE, PASSIVE, HIDDEN, FROZEN],
71
+
72
+ // A hidden page transitioning back to active.
73
+ [HIDDEN, PASSIVE, ACTIVE],
74
+
75
+ // A frozen page being resumed
76
+ [FROZEN, HIDDEN],
77
+
78
+ // A frozen (bfcached) page navigated back to
79
+ // Note: [FROZEN, HIDDEN] can happen here, but it's already covered above.
80
+ [FROZEN, ACTIVE],
81
+ [FROZEN, PASSIVE],
82
+ ].map(toIndexedObject);
83
+
84
+ /**
85
+ * Accepts a current state and a future state and returns an array of legal
86
+ * state transition paths. This is needed to normalize behavior across browsers
87
+ * since some browsers do not fire events in certain cases and thus skip
88
+ * states.
89
+ * @param {string} oldState
90
+ * @param {string} newState
91
+ * @return {!Array<string>}
92
+ */
93
+ const getLegalStateTransitionPath = (oldState, newState) => {
94
+ for (const order of LEGAL_STATE_TRANSITIONS) {
95
+ const oldIndex = order[oldState];
96
+ const newIndex = order[newState];
97
+
98
+ if (oldIndex >= 0 && newIndex >= 0 && newIndex > oldIndex) {
99
+ // Differences greater than one should be reported
100
+ // because it means a state was skipped.
101
+ return Object.keys(order).slice(oldIndex, newIndex + 1);
102
+ }
103
+ }
104
+ return [];
105
+ // TODO(philipwalton): it shouldn't be possible to get here, but
106
+ // consider some kind of warning or call to action if it happens.
107
+ // console.warn(`Invalid state change detected: ${oldState} > ${newState}`);
108
+ };
109
+
110
+ /**
111
+ * Returns the current state based on the document's visibility and
112
+ * in input focus states. Note this method is only used to determine
113
+ * active vs passive vs hidden states, as other states require listening
114
+ * for events.
115
+ * @return {string}
116
+ */
117
+ const getCurrentState = () => {
118
+ if (document.visibilityState === HIDDEN) {
119
+ return HIDDEN;
120
+ }
121
+ if (document.hasFocus()) {
122
+ return ACTIVE;
123
+ }
124
+ return PASSIVE;
125
+ };
126
+
127
+ class StateChangeEvent extends Event {
128
+ /**
129
+ * @param {string} type
130
+ * @param {!Object} initDict
131
+ */
132
+ constructor(type, initDict) {
133
+ super(type);
134
+ this.newState = initDict.newState;
135
+ this.oldState = initDict.oldState;
136
+ this.originalEvent = initDict.originalEvent;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Class definition for the exported, singleton lifecycle instance.
142
+ */
143
+ export default class Lifecycle extends EventTarget {
144
+ /**
145
+ * Initializes state, state history, and adds event listeners to monitor
146
+ * state changes.
147
+ */
148
+ constructor() {
149
+ super();
150
+
151
+ const state = getCurrentState();
152
+
153
+ this._state = state;
154
+ this._unsavedChanges = [];
155
+
156
+ // Bind the callback and add event listeners.
157
+ this._handleEvents = this._handleEvents.bind(this);
158
+
159
+ // Add capturing events on window so they run immediately.
160
+ EVENTS.forEach((evt) =>
161
+ window.addEventListener(evt, this._handleEvents, true),
162
+ );
163
+ }
164
+
165
+ /**
166
+ * @return {string}
167
+ */
168
+ get state() {
169
+ return this._state;
170
+ }
171
+
172
+ /**
173
+ * Returns the value of document.wasDiscarded. This is arguably unnecessary
174
+ * but I think there's value in having the entire API in one place and
175
+ * consistent across browsers.
176
+ * @return {boolean}
177
+ */
178
+ get pageWasDiscarded() {
179
+ return document.wasDiscarded || false;
180
+ }
181
+
182
+ /**
183
+ * @param {Symbol|Object} id A unique symbol or object identifying the
184
+ *. pending state. This ID is required when removing the state later.
185
+ */
186
+ addUnsavedChanges(id) {
187
+ // Don't add duplicate state. Note: ideall this would be a set, but for
188
+ // better browser compatibility we're using an array.
189
+ if (!this._unsavedChanges.indexOf(id) > -1) {
190
+ // If this is the first state being added,
191
+ // also add a beforeunload listener.
192
+ if (this._unsavedChanges.length === 0) {
193
+ window.addEventListener('beforeunload', onbeforeunload);
194
+ }
195
+ this._unsavedChanges.push(id);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * @param {Symbol|Object} id A unique symbol or object identifying the
201
+ *. pending state. This ID is required when removing the state later.
202
+ */
203
+ removeUnsavedChanges(id) {
204
+ const idIndex = this._unsavedChanges.indexOf(id);
205
+
206
+ if (idIndex > -1) {
207
+ this._unsavedChanges.splice(idIndex, 1);
208
+
209
+ // If there's no more pending state, remove the event listener.
210
+ if (this._unsavedChanges.length === 0) {
211
+ window.removeEventListener('beforeunload', onbeforeunload);
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Transitions from `this._state` to `newState`
218
+ * going through any intermediate states if required.
219
+ * @private
220
+ * @param {!Event} originalEvent
221
+ * @param {string} newState
222
+ */
223
+ _dispatchChangesIfNeeded(originalEvent, newState) {
224
+ if (newState !== this._state) {
225
+ const oldState = this._state;
226
+ // Get the full chain of states for properly transitioning
227
+ // from `oldState` to `newState`.
228
+ const path = getLegalStateTransitionPath(oldState, newState);
229
+
230
+ // Start from `oldState` and transition to `newState`
231
+ // going through any intermediate states if required.
232
+ // Go through each intermediate state if required.
233
+ for (let i = 0; i < path.length - 1; i++) {
234
+ const prevState = path[i];
235
+ const nextState = path[i + 1];
236
+
237
+ this._state = nextState;
238
+ this.dispatchEvent(
239
+ new StateChangeEvent('statechange', {
240
+ oldState: prevState,
241
+ newState: nextState,
242
+ originalEvent,
243
+ }),
244
+ );
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @private
251
+ * @param {!Event} evt
252
+ */
253
+ _handleEvents(evt) {
254
+ switch (evt.type) {
255
+ case 'pageshow':
256
+ case 'resume':
257
+ this._dispatchChangesIfNeeded(evt, getCurrentState());
258
+ break;
259
+ case 'focus':
260
+ this._dispatchChangesIfNeeded(evt, ACTIVE);
261
+ break;
262
+ case 'blur':
263
+ // The `blur` event can fire while the page is being unloaded, so we
264
+ // only need to update the state if the current state is "active".
265
+ if (this._state === ACTIVE) {
266
+ this._dispatchChangesIfNeeded(evt, getCurrentState());
267
+ }
268
+ break;
269
+ case 'pagehide':
270
+ case 'unload':
271
+ this._dispatchChangesIfNeeded(
272
+ evt,
273
+ evt.persisted ? FROZEN : TERMINATED,
274
+ );
275
+ break;
276
+ case 'visibilitychange':
277
+ // The document's `visibilityState` will change to hidden as the page
278
+ // is being unloaded, but in such cases the lifecycle state shouldn't
279
+ // change.
280
+ if (this._state !== FROZEN && this._state !== TERMINATED) {
281
+ this._dispatchChangesIfNeeded(evt, getCurrentState());
282
+ }
283
+ break;
284
+ case 'freeze':
285
+ this._dispatchChangesIfNeeded(evt, FROZEN);
286
+ break;
287
+ default:
288
+ break;
289
+ }
290
+ }
291
+ }
@@ -0,0 +1,3 @@
1
+ import PageLifecycle from './PageLifecycle';
2
+
3
+ export default new PageLifecycle();
@@ -0,0 +1,32 @@
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
+
30
+ const supportsConstructableEventTarget = doesSupportConstructableEventTarget();
31
+
32
+ export { supportsConstructableEventTarget };
@@ -0,0 +1,78 @@
1
+ import parseInputLocation from '../../parseInputLocation';
2
+
3
+ export default class InMemoryNavigation {
4
+ constructor() {
5
+ // A stack of `LocationBase` objects.
6
+ this._stack = [];
7
+ }
8
+
9
+ // eslint-disable-next-line no-unused-vars
10
+ subscribe(listener) {
11
+ // `InMemoryNavigation` doesn't have any "asynchronycity" about it
12
+ // and performs any navigation immediately at the time of the call.
13
+ // Hence, no asynchronous listener would ever be called
14
+ // due to no asynchronous events being dispatched.
15
+ return () => {};
16
+ }
17
+
18
+ init(initialLocation, { operation, key, index, delta }) {
19
+ this._stack.push({
20
+ location: parseInputLocation(initialLocation),
21
+ key,
22
+ });
23
+
24
+ return this._createLocationObject({
25
+ operation,
26
+ index,
27
+ delta,
28
+ });
29
+ }
30
+
31
+ navigate(location, { operation, key, index, delta }) {
32
+ const { pathname, search, query, hash } = location;
33
+
34
+ this._stack[index] = {
35
+ location: { pathname, search, query, hash },
36
+ key,
37
+ };
38
+
39
+ // A `PUSH` navigation sets a new terminal (rightmost) location.
40
+ if (delta === 1) {
41
+ // Trim the location stack by removing any previous location history
42
+ // that might've existed after the current index.
43
+ this._stack.length = index + 1;
44
+ }
45
+
46
+ return {
47
+ ...location,
48
+ key,
49
+ operation,
50
+ index,
51
+ delta,
52
+ };
53
+ }
54
+
55
+ shift({ operation, index, delta }) {
56
+ return this._createLocationObject({
57
+ operation,
58
+ index,
59
+ delta,
60
+ });
61
+ }
62
+
63
+ getInitialLocation() {
64
+ return undefined;
65
+ }
66
+
67
+ _createLocationObject({ operation, index, delta }) {
68
+ const { location, key } = this._stack[index];
69
+
70
+ return {
71
+ ...location,
72
+ key,
73
+ operation,
74
+ index,
75
+ delta,
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ /* eslint-disable no-underscore-dangle, max-classes-per-file */
2
+
3
+ import ServerSideNavigationError from './error/ServerSideNavigationError';
4
+
5
+ export default class ServerSideNavigation {
6
+ init(initialLocation, { operation, key, index, delta }) {
7
+ return {
8
+ ...initialLocation,
9
+ operation,
10
+ key,
11
+ index,
12
+ delta,
13
+ };
14
+ }
15
+
16
+ // eslint-disable-next-line no-unused-vars
17
+ subscribe(listener) {
18
+ // `ServerSideNavigation` doesn't have any "asynchronycity" about it
19
+ // and any navigation is prohibited and would result in an error.
20
+ // So no asynchronous listener would ever be called
21
+ // due to no asynchronous events being dispatched.
22
+ return () => {};
23
+ }
24
+
25
+ // eslint-disable-next-line no-unused-vars
26
+ navigate(location, { operation, key, index, delta }) {
27
+ throw new ServerSideNavigationError(location);
28
+ }
29
+
30
+ // eslint-disable-next-line no-unused-vars
31
+ shift({ operation, index, delta }) {
32
+ // It's not supposed to ever get to this code.
33
+ // * If `delta` is `0` then it's a "do nothing" scenario and `Session` won't even call this code.
34
+ // * If `delta` is not `0` and is out of bounds then a `NavigationOutOfBoundsError` will be thrown.
35
+ // * If `delta` is not `0` and is not out of bounds then it implies that a valid navigation has happened before
36
+ // which can't be the case because no navigation is possible on server side without throwing an error.
37
+ throw new Error('Server side has no navigation history');
38
+ }
39
+
40
+ getInitialLocation() {
41
+ return undefined;
42
+ }
43
+ }
@@ -0,0 +1,224 @@
1
+ import getLocationUrl from '../../getLocationUrl';
2
+ // import parseInputLocation from '../../parseInputLocation';
3
+ import parseQueryFromSearch from '../../parseQueryFromSearch';
4
+ import Operations from './operation/operations';
5
+
6
+ const NO_LOCATION_INDEX = -1;
7
+
8
+ // A web browser has a notion of a "navigation history".
9
+ // A "navigation history" exists within a given web browser's tab.
10
+ // The user can click "Back" or "Forward" buttons in the web browser and it will automatically load
11
+ // "previous" or "next" page from scratch.
12
+ //
13
+ // Later, web browsers added a `window.history` object that the application can,
14
+ // but isn't required to, interact with. That `window.history` object allows the application
15
+ // to programmatically control the URL in the address bar of the web browser, as well as
16
+ // the "navigation history" by programmatically adding new entries to it or reading the current entry,
17
+ // and it also allows the application to override the default web browser's behavior
18
+ // when the user clicks "Back" or "Forward" buttons in the web browser.
19
+ //
20
+ // Specifically, the `window.history` object has a method called `.pushState()` which programmatically adds
21
+ // a new entry in the "navigation history" and updates the URL in the address bar and also
22
+ // tells the web browser that starting from the entry before this new entry in the "navigation history",
23
+ // the application would prefer to manually handle any "Back"/"Forward" transition when the user clicks
24
+ // those "Back" or "Forward" buttons in the web browser, and this behavior should persist for any future
25
+ // "navigation history" entries programmatically added by the application via `window.history.pushState()`,
26
+ // and will only stop if the user navigates from the page by the means of conventional navigation,
27
+ // that is by clicking a standard hyperlink, at which point the current page gets "destroyed".
28
+ //
29
+ // So for manually "pushed" entries of the "navigation history", the web browser won't load those pages
30
+ // from scratch after a user-initiated "Back" or "Forward" transition. In fact, it won't do anything and
31
+ // it will just step aside and let the application itself do those transitions. The web browser will only
32
+ // update the URL in the address bar and that's it.
33
+ //
34
+ // This whole thing allows the application to:
35
+ //
36
+ // * Load the "previous" or "next" page much faster than when using the default "from scratch" approach
37
+ // because it doesn't have to destroy the current page, then send a new HTTP request to the server,
38
+ // then parse the HTML response and initialize a new page, re-download all those images, etc.
39
+ //
40
+ // * Optionally render a snapshotted verison of the "previous" page thereby "restoring" the "previous" page
41
+ // rather than reloading it from scratch, i.e. the state of the "previous" page could be fully restored.
42
+ //
43
+ export default class WebBrowserNavigation {
44
+ constructor() {
45
+ // `_currentLocationIndex` is used when receiving a "popstate" event
46
+ // that wasn't initiated by the application code but rather by the user
47
+ // clicking "Back" or "Forward" button in their web browser.
48
+ this._currentLocationIndex = NO_LOCATION_INDEX;
49
+ }
50
+
51
+ // Subscribes to changes in the current location.
52
+ // Returns an `unsubscribe()` function which is "idempotent", i.e. it can be called multiple times.
53
+ subscribe(listener) {
54
+ const onPopState = () => {
55
+ // If "popstate" event is received before navigation is initialized,
56
+ // ignore such "popstate" event. This behavior is logical from the application code's view.
57
+ // And besides, `this._currentLocationIndex` is not defined in such conditions.
58
+ if (this._currentLocationIndex === NO_LOCATION_INDEX) {
59
+ throw new Error('Received a "popstate" event before initialized');
60
+ }
61
+ const prevIndex = this._currentLocationIndex;
62
+ const { index } = this._getCurrentLocationState();
63
+ this._currentLocationIndex = index;
64
+
65
+ listener(
66
+ this._createEntryFromCurrentLocation({
67
+ operation: Operations.SHIFT,
68
+ delta: index - prevIndex,
69
+ }),
70
+ );
71
+ };
72
+
73
+ // Due to how `popstate` event listener works, there should only be one listener at a time,
74
+ // otherwise two different `Session`s would react to the same `popstate` event,
75
+ // each interpreting it as its own, while in reality it only belongs to one of them
76
+ // and the other one should completely ignore it.
77
+ // In other words, there can't exist two navigation sessions simultaneously by design.
78
+ // There can only be one active navigation session at a given time.
79
+ // Another one could only start after the previous one ends,
80
+ // not both of them being active simultaneously.
81
+ if (this._subscribed) {
82
+ throw new Error(
83
+ 'There already is an active subscription. Only one subscription is allowed at a time.',
84
+ );
85
+ }
86
+
87
+ window.addEventListener('popstate', onPopState);
88
+ this._subscribed = true;
89
+
90
+ return () => {
91
+ window.removeEventListener('popstate', onPopState);
92
+ this._subscribed = false;
93
+ };
94
+ }
95
+
96
+ init(initialLocation, { operation, key, index, delta }) {
97
+ // Validate that `initialLocation` is same as `window.location`.
98
+ const isCurrentLocation =
99
+ initialLocation === this._getCurrentLocation() ||
100
+ this._isSameAsCurrentLocation(initialLocation);
101
+
102
+ if (!isCurrentLocation) {
103
+ throw new Error(
104
+ '`initialLocation` argument should be same as `window.location`',
105
+ );
106
+ }
107
+
108
+ // Set `window.history.state` of the initial location
109
+ // by calling `window.history.replaceState()` on page load.
110
+ // Otherwise, `window.history.state` would be `null` for the initial location
111
+ // and there'd be no place to store the additional properties of the initial location
112
+ // such as `location.key`.
113
+ // https://github.com/taion/scroll-behavior/issues/215
114
+ //
115
+ // If the user opens the initial page for the first time, `window.history.state` will be `null`.
116
+ // If the user refreshes the initial page, `window.history.state` will not be cleared
117
+ // and therefore will not be `null` and will instead have the previously-set value.
118
+ //
119
+ if (!this._getCurrentLocationState()) {
120
+ // Create additional properties for the initial locaiton.
121
+ const additionalProperties = { key, index };
122
+ // Call `history.replaceState()`.
123
+ this._storeAdditionalPropertiesForLocation(
124
+ initialLocation,
125
+ additionalProperties,
126
+ delta,
127
+ );
128
+ }
129
+
130
+ this._currentLocationIndex = index;
131
+
132
+ // Call the listeners.
133
+ return this._createEntryFromCurrentLocation({ operation, delta });
134
+ }
135
+
136
+ navigate(location, { operation, key, index, delta }) {
137
+ const additionalProperties = { key, index };
138
+
139
+ this._storeAdditionalPropertiesForLocation(
140
+ location,
141
+ additionalProperties,
142
+ delta,
143
+ );
144
+
145
+ this._currentLocationIndex = index;
146
+
147
+ // Call the listeners.
148
+ return {
149
+ operation,
150
+ delta,
151
+ ...location,
152
+ ...additionalProperties,
153
+ };
154
+ }
155
+
156
+ // shift({ operation, index, delta }) {
157
+ shift({ delta }) {
158
+ // Web browser `history` is extremely non-strict when it comes to `history.go(delta)` navigation.
159
+ // It will allow any number as `delta`, regardless of whether such history entry exists or not.
160
+ // To introduce strict validation of the `delta` argument, `Session` class code explicitly checks
161
+ // the new `index` on whether it's out of bounds of the navigation history stack, and after it verifies
162
+ // that the new `index` is valid, it calls the `.shift(delta)` method of `WebBrowserNavigation` class.
163
+ //
164
+ // Calling `window.history.go()` will trigger a "popstate" event which will trigger the listeners.
165
+ //
166
+ window.history.go(delta);
167
+ }
168
+
169
+ getInitialLocation() {
170
+ // Web browser environment already knows the initial location
171
+ // by the time javascript code starts execution.
172
+ return this._getCurrentLocation();
173
+ }
174
+
175
+ _getCurrentLocation() {
176
+ return window.location;
177
+ }
178
+
179
+ _getCurrentLocationState() {
180
+ return window.history.state;
181
+ }
182
+
183
+ _isSameAsCurrentLocation(inputLocation) {
184
+ return typeof inputLocation === 'string'
185
+ ? inputLocation === getLocationUrl(this._getCurrentLocation())
186
+ : inputLocation === this._getCurrentLocation() ||
187
+ getLocationUrl(inputLocation) ===
188
+ getLocationUrl(this._getCurrentLocation());
189
+ }
190
+
191
+ _createEntryFromCurrentLocation({ operation, delta }) {
192
+ const { pathname, search, hash } = this._getCurrentLocation();
193
+
194
+ const { key, index } = this._getCurrentLocationState();
195
+
196
+ return {
197
+ operation,
198
+ pathname,
199
+ search,
200
+ query: parseQueryFromSearch(search),
201
+ hash,
202
+ key,
203
+ index,
204
+ delta,
205
+ };
206
+ }
207
+
208
+ _storeAdditionalPropertiesForLocation(
209
+ location,
210
+ additionalProperties,
211
+ delta,
212
+ ) {
213
+ const url = getLocationUrl(location);
214
+ // `delta` property is not stored in `window.history.state`
215
+ // because it is supposed to be recalculated every time when reading from `window.history.state`.
216
+ if (delta === 1) {
217
+ window.history.pushState(additionalProperties, null, url);
218
+ } else if (delta === 0) {
219
+ window.history.replaceState(additionalProperties, null, url);
220
+ } else {
221
+ throw new Error(`Unsupported \`delta\`: ${delta}`);
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,7 @@
1
+ export default class NavigationOutOfBoundsError extends Error {
2
+ constructor(index) {
3
+ super(`Location index ${index} is out of navigation history bounds`);
4
+
5
+ this.index = index;
6
+ }
7
+ }