navigation-stack 0.1.3 → 0.2.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.
- package/.github/workflows/main.yml +39 -39
- package/README.md +128 -27
- package/lib/cjs/{LocationStateStorage.js → LocationDataStorage.js} +5 -5
- package/lib/cjs/addBeforeLocationChangeListener.js +7 -0
- package/lib/cjs/beforeLocationChangeListeners.js +51 -0
- package/lib/cjs/createMiddlewares.js +21 -17
- package/lib/cjs/index.js +9 -9
- package/lib/cjs/middleware/createBasePathMiddleware.js +2 -2
- package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +39 -0
- package/lib/cjs/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +62 -29
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +2 -2
- package/lib/cjs/navigationBlockers.js +55 -47
- package/lib/cjs/normalizeInputLocation.js +1 -0
- package/lib/cjs/parseLocationUrl.js +2 -0
- package/lib/cjs/parseQueryFromSearch.js +1 -1
- package/lib/cjs/session/BrowserSession.js +229 -0
- package/lib/cjs/session/MemorySession.js +223 -0
- package/lib/cjs/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -16
- package/lib/esm/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
- package/lib/esm/addBeforeLocationChangeListener.js +2 -0
- package/lib/esm/beforeLocationChangeListeners.js +44 -0
- package/lib/esm/createMiddlewares.js +21 -17
- package/lib/esm/index.js +4 -4
- package/lib/esm/middleware/createBasePathMiddleware.js +2 -2
- package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +34 -0
- package/lib/esm/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +11 -13
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +62 -29
- package/lib/esm/middleware/createTransformLocationMiddleware.js +2 -2
- package/lib/esm/navigationBlockers.js +55 -47
- package/lib/esm/normalizeInputLocation.js +1 -0
- package/lib/esm/parseLocationUrl.js +2 -0
- package/lib/esm/parseQueryFromSearch.js +1 -1
- package/lib/esm/session/BrowserSession.js +223 -0
- package/lib/esm/session/MemorySession.js +217 -0
- package/lib/esm/{environment/ServerEnvironment.js → session/ServerSession.js} +27 -15
- package/lib/index.d.ts +64 -59
- package/package.json +4 -4
- package/src/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
- package/src/addBeforeLocationChangeListener.js +2 -0
- package/src/beforeLocationChangeListeners.js +54 -0
- package/src/createMiddlewares.js +21 -17
- package/src/index.js +4 -4
- package/src/middleware/createBasePathMiddleware.js +2 -2
- package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +40 -0
- package/src/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
- package/src/middleware/createNavigationBlockerMiddleware.js +68 -28
- package/src/middleware/createTransformLocationMiddleware.js +2 -2
- package/src/navigationBlockers.js +68 -49
- package/src/normalizeInputLocation.js +1 -0
- package/src/parseLocationUrl.js +2 -0
- package/src/parseQueryFromSearch.js +1 -1
- package/src/session/BrowserSession.js +225 -0
- package/src/session/MemorySession.js +219 -0
- package/src/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -15
- package/test/{LocationStateStorage.test.js → LocationDataStorage.test.js} +6 -6
- package/test/createMiddlewares.test.js +2 -2
- package/test/helpers.js +1 -1
- package/test/index.test.js +3 -3
- package/test/middleware/createBasePathMiddleware.test.js +7 -7
- package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +141 -0
- package/test/middleware/createNavigationBlockerMiddleware.test.js +96 -97
- package/test/middleware/createTransformLocationMiddleware.test.js +1 -1
- package/test/normalizeInputLocation.test.js +3 -0
- package/test/parseLocationUrl.test.js +2 -0
- package/test/{environment/BrowserEnvironment.test.js → session/BrowserSession.test.js} +35 -18
- package/test/session/MemorySession.test.js +244 -0
- package/test/session/ServerSession.test.js +23 -0
- package/types/index.d.ts +64 -59
- package/lib/cjs/environment/BrowserEnvironment.js +0 -111
- package/lib/cjs/environment/MemoryEnvironment.js +0 -150
- package/lib/esm/environment/BrowserEnvironment.js +0 -104
- package/lib/esm/environment/MemoryEnvironment.js +0 -143
- package/src/environment/BrowserEnvironment.js +0 -109
- package/src/environment/MemoryEnvironment.js +0 -151
- package/test/environment/MemoryEnvironment.test.js +0 -218
- package/test/environment/ServerEnvironment.test.js +0 -23
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export getLocationUrl from './getLocationUrl';
|
|
|
6
6
|
export parseLocationUrl from './parseLocationUrl';
|
|
7
7
|
export createMiddlewares from './createMiddlewares';
|
|
8
8
|
export locationReducer from './locationReducer';
|
|
9
|
-
export
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export
|
|
9
|
+
export LocationDataStorage from './LocationDataStorage';
|
|
10
|
+
export BrowserSession from './session/BrowserSession';
|
|
11
|
+
export MemorySession from './session/MemorySession';
|
|
12
|
+
export ServerSession from './session/ServerSession';
|
|
@@ -11,9 +11,9 @@ export default function createBasePathMiddleware(basePath) {
|
|
|
11
11
|
return addBasePath(location, basePath);
|
|
12
12
|
},
|
|
13
13
|
|
|
14
|
-
// Transforms
|
|
14
|
+
// Transforms subscription `Location` object:
|
|
15
15
|
// removes `basePath` from the URL.
|
|
16
|
-
|
|
16
|
+
transformSubscriptionLocation: (location) => {
|
|
17
17
|
return removeBasePath(location, basePath);
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import ActionTypes from '../ActionTypes';
|
|
2
|
+
import {
|
|
3
|
+
getBeforeLocationChangeListeners,
|
|
4
|
+
removeAllBeforeLocationChangeListeners,
|
|
5
|
+
runBeforeLocationChangeListeners,
|
|
6
|
+
} from '../beforeLocationChangeListeners';
|
|
7
|
+
|
|
8
|
+
// Creates a "middleware" that calls upcoming navigation listeners.
|
|
9
|
+
export default function createBeforeLocationChangeListenerMiddleware(session) {
|
|
10
|
+
return function navigationListenerMiddleware() {
|
|
11
|
+
return (next) => (action) => {
|
|
12
|
+
const { type, payload } = action;
|
|
13
|
+
|
|
14
|
+
switch (type) {
|
|
15
|
+
// Trigger navigation listeners before the `location` has been updated in Redux state.
|
|
16
|
+
// It doesn't matter that the new location URL has already been updated in the web browser's
|
|
17
|
+
// address bar, or that the web browser's history has already switched to the new locaiton.
|
|
18
|
+
// From the application's point of view, all of that doesn't matter and even doesn't exist.
|
|
19
|
+
// All that exists from the application's point of view is the `location` object in the Redux state.
|
|
20
|
+
// Until the `location` object in the Redux state is updated, the old page is still rendered.
|
|
21
|
+
// The appliation is only concerned with the updates of the `location` object in the Redux state
|
|
22
|
+
// and completely ignores any updates to the URL in the web browser's address bar.
|
|
23
|
+
case ActionTypes.UPDATE:
|
|
24
|
+
runBeforeLocationChangeListeners(
|
|
25
|
+
getBeforeLocationChangeListeners(session),
|
|
26
|
+
payload,
|
|
27
|
+
);
|
|
28
|
+
return next(action);
|
|
29
|
+
|
|
30
|
+
// Remove any navigation listeners on `DISPOSE` event.
|
|
31
|
+
case ActionTypes.DISPOSE:
|
|
32
|
+
removeAllBeforeLocationChangeListeners(session);
|
|
33
|
+
return next(action);
|
|
34
|
+
|
|
35
|
+
default:
|
|
36
|
+
return next(action);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -7,20 +7,18 @@ function updateLocation(location) {
|
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// Creates a "middleware" that performs the actual navigation according to the `
|
|
11
|
-
// For example, when `
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
environment,
|
|
16
|
-
{ shouldIgnoreEnvironmentLocationUpdates },
|
|
10
|
+
// Creates a "middleware" that performs the actual navigation according to the `session` being used.
|
|
11
|
+
// For example, when `BrowserSession` is used, it calls methods of the `window.history` object.
|
|
12
|
+
export default function createLocationMiddleware(
|
|
13
|
+
session,
|
|
14
|
+
{ shouldIgnoreLocationSubscriptionEvents },
|
|
17
15
|
) {
|
|
18
|
-
return function
|
|
16
|
+
return function locationMiddleware() {
|
|
19
17
|
return (next) => {
|
|
20
18
|
// Whenever browser location changes,
|
|
21
19
|
// perform the same changes with the internal `location` object.
|
|
22
|
-
const unsubscribe =
|
|
23
|
-
if (!
|
|
20
|
+
const unsubscribe = session.navigation.subscribe((location) => {
|
|
21
|
+
if (!shouldIgnoreLocationSubscriptionEvents()) {
|
|
24
22
|
next(updateLocation(location));
|
|
25
23
|
}
|
|
26
24
|
});
|
|
@@ -30,16 +28,16 @@ export default function createEnvironmentMiddleware(
|
|
|
30
28
|
|
|
31
29
|
switch (type) {
|
|
32
30
|
case ActionTypes.INIT:
|
|
33
|
-
return next(updateLocation(
|
|
31
|
+
return next(updateLocation(session.navigation.init()));
|
|
34
32
|
|
|
35
33
|
case ActionTypes.NAVIGATE:
|
|
36
|
-
// `
|
|
37
|
-
return next(updateLocation(
|
|
34
|
+
// `session.navigate()` doesn't trigger the `subscribe()` listener.
|
|
35
|
+
return next(updateLocation(session.navigation.navigate(payload)));
|
|
38
36
|
|
|
39
37
|
case ActionTypes.SHIFT:
|
|
40
38
|
// `shift()` will trigger the `subscribe()` listener,
|
|
41
39
|
// which will call `updateLocation()`.
|
|
42
|
-
|
|
40
|
+
session.navigation.shift(payload);
|
|
43
41
|
// eslint-disable-next-line consistent-return
|
|
44
42
|
return;
|
|
45
43
|
|
|
@@ -5,24 +5,22 @@ import {
|
|
|
5
5
|
removeAllNavigationBlockers,
|
|
6
6
|
runNavigationBlockers,
|
|
7
7
|
} from '../navigationBlockers';
|
|
8
|
-
import onlyAllowedOnClientSide from '../onlyAllowedOnClientSide';
|
|
9
8
|
|
|
10
9
|
// Creates a "middleware" that applies navigation blockers.
|
|
11
10
|
export default function createNavigationBlockerMiddleware(
|
|
12
|
-
|
|
13
|
-
{
|
|
11
|
+
session,
|
|
12
|
+
{ ignoreLocationSubscriptionEvents },
|
|
14
13
|
) {
|
|
15
|
-
// A "dummy" initial value that will be ignored.
|
|
16
|
-
let navigationBlockersEvaluationStatus = { cancelled: false };
|
|
17
|
-
|
|
18
14
|
function createNavigationBlockersEvaluationStatus() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
/* eslint-disable no-underscore-dangle */
|
|
16
|
+
if (session._navigationBlockersEvaluationStatus) {
|
|
17
|
+
session._navigationBlockersEvaluationStatus.cancelled = true;
|
|
18
|
+
}
|
|
19
|
+
session._navigationBlockersEvaluationStatus = { cancelled: false };
|
|
20
|
+
return session._navigationBlockersEvaluationStatus;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
return function
|
|
23
|
+
return function navigationBlockerMiddleware() {
|
|
26
24
|
return (next) => (action) => {
|
|
27
25
|
const { type, payload } = action;
|
|
28
26
|
|
|
@@ -31,11 +29,28 @@ export default function createNavigationBlockerMiddleware(
|
|
|
31
29
|
let result;
|
|
32
30
|
|
|
33
31
|
switch (type) {
|
|
34
|
-
// Prevent or allow navigation that was initiated by the application
|
|
32
|
+
// Prevent or allow navigation that was initiated by the application
|
|
33
|
+
// by dispatching a `.push()` or `.replace()` action.
|
|
34
|
+
//
|
|
35
|
+
// It doesn't handle `.shift()` navigation actions because it doesn't yet know
|
|
36
|
+
// the `location` that it's gonna `shift` to. Instead, it waits for the web browser
|
|
37
|
+
// to "shift" to that `location` and then reads it and rewinds the "shift"
|
|
38
|
+
// if it should've been blocked. That is handled in the `case ActionTypes.UPDATE` block.
|
|
39
|
+
//
|
|
40
|
+
// This type of "shifting" and then rewinding the "shift" doesn't really matter to the application at all.
|
|
41
|
+
// From the application's point of view, all of that doesn't matter and even doesn't exist.
|
|
42
|
+
// All that exists from the application's point of view is the `location` object in the Redux state.
|
|
43
|
+
// Until the `location` object in the Redux state is updated, the old page is still rendered.
|
|
44
|
+
// The appliation is only concerned with the updates of the `location` object in the Redux state
|
|
45
|
+
// and completely ignores any updates to the URL in the web browser's address bar.
|
|
46
|
+
//
|
|
35
47
|
case ActionTypes.NAVIGATE:
|
|
36
48
|
// `resultValue` variable name works around a stupid javascript error:
|
|
37
49
|
// "Cannot redeclare block-scoped variable 'result'".
|
|
38
|
-
result = runNavigationBlockers(
|
|
50
|
+
result = runNavigationBlockers(
|
|
51
|
+
getNavigationBlockers(session),
|
|
52
|
+
payload,
|
|
53
|
+
);
|
|
39
54
|
if (isPromise(result)) {
|
|
40
55
|
const status = createNavigationBlockersEvaluationStatus();
|
|
41
56
|
// eslint-disable-next-line consistent-return
|
|
@@ -52,15 +67,37 @@ export default function createNavigationBlockerMiddleware(
|
|
|
52
67
|
// eslint-disable-next-line consistent-return
|
|
53
68
|
return;
|
|
54
69
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
70
|
+
// One can notice that this "middleware" handles both `NAVIGATE` and `UPDATE` Redux actions,
|
|
71
|
+
// even though a `NAVIGATE` action normally always causes a follow-up `UPDATE` Redux action.
|
|
72
|
+
// There's no contradiction here: if a navigation blocker should block a certain navigation,
|
|
73
|
+
// it will do that at the `NAVIGATION` stage and it won't get to the `UPDATE` stage, so it
|
|
74
|
+
// won't be called "second time" or something like that.
|
|
75
|
+
//
|
|
76
|
+
// One could ask then: Why handle `UPDATE` Redux action at all?
|
|
77
|
+
// The reason why it handles `UPDATE` Redux actions here is because
|
|
78
|
+
// `NAVIGATE` Redux actions are only emitted for programmatic "push" or "replace" navigation
|
|
79
|
+
// initiated by the application code, and there're other cases of navigation such as
|
|
80
|
+
// programmatic "shift" navigation or when the user manually clicks "Back" or "Forward" button
|
|
81
|
+
// in a web browser. Such "other" cases could only be handled by reacting to an `UPDATE` Redux action
|
|
82
|
+
// which is only emitted after the URL in the browser's address bar has changed.
|
|
83
|
+
//
|
|
84
|
+
// But there's no real drawback in reacting to an `UPDATE` Redux action "post factum" because
|
|
85
|
+
// from the application's point of view the address bar doesn't matter and even doesn't exist.
|
|
86
|
+
// All that exists from the application's point of view is the `location` object in the Redux state.
|
|
87
|
+
// Until the `location` object in the Redux state is updated, the old page is still rendered.
|
|
88
|
+
// The appliation is only concerned with the updates of the `location` object in the Redux state
|
|
89
|
+
// and completely ignores any updates to the URL in the web browser's address bar.
|
|
57
90
|
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
91
|
+
// So here, the "middleware" attempts to prevent or allow navigation that has already happened
|
|
92
|
+
// in the web browser's address bar but hasn't yet happened in Redux state.
|
|
93
|
+
// For example, it could be a user clicking a "Back"/"Forward" button in their web browser.
|
|
94
|
+
// If such navigation should've been blocked, it will simply not update the `locaiton` object in Redux state,
|
|
95
|
+
// and it will also "rewind" the change of the URL in the web browser's address bar so that it's consistent
|
|
96
|
+
// with the `location` in Redux state.
|
|
60
97
|
//
|
|
61
98
|
case ActionTypes.UPDATE:
|
|
62
99
|
// If no navigation blockers to run, don't do anything.
|
|
63
|
-
if (getNavigationBlockers().length === 0) {
|
|
100
|
+
if (getNavigationBlockers(session).length === 0) {
|
|
64
101
|
return next(action);
|
|
65
102
|
}
|
|
66
103
|
|
|
@@ -70,23 +107,26 @@ export default function createNavigationBlockerMiddleware(
|
|
|
70
107
|
return next(action);
|
|
71
108
|
}
|
|
72
109
|
|
|
73
|
-
// It's not really possible for a location to not have a `delta` property in a web browser
|
|
110
|
+
// It's not really possible for a location to not have a `delta` property in a web browser session.
|
|
74
111
|
// So this case is not something that's supposed to happen in real life.
|
|
75
|
-
// Rather, it's a guard against an unsupported or
|
|
112
|
+
// Rather, it's a guard against an unsupported or incorrect session implementation or something like that.
|
|
76
113
|
// If there's no `delta` property on the location, it means that the previous location can't be rewound to,
|
|
77
114
|
// so it can't really "prevent" the navigation that has just happened.
|
|
78
115
|
if (payload.delta === null) {
|
|
79
116
|
return next(action);
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
result = runNavigationBlockers(
|
|
119
|
+
result = runNavigationBlockers(
|
|
120
|
+
getNavigationBlockers(session),
|
|
121
|
+
payload,
|
|
122
|
+
);
|
|
83
123
|
|
|
84
124
|
if (isPromise(result)) {
|
|
85
125
|
const status = createNavigationBlockersEvaluationStatus();
|
|
86
126
|
|
|
87
127
|
// While location blockers are running, rewind to the previous location.
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
ignoreLocationSubscriptionEvents(() => {
|
|
129
|
+
session.navigation.shift(-payload.delta);
|
|
90
130
|
});
|
|
91
131
|
|
|
92
132
|
result.then((promiseResult) => {
|
|
@@ -96,8 +136,8 @@ export default function createNavigationBlockerMiddleware(
|
|
|
96
136
|
} else if (!status.cancelled) {
|
|
97
137
|
// Navigation not blocked.
|
|
98
138
|
// Rewind back to the new location.
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
ignoreLocationSubscriptionEvents(() => {
|
|
140
|
+
session.navigation.shift(payload.delta);
|
|
101
141
|
});
|
|
102
142
|
// Update the location.
|
|
103
143
|
next(action);
|
|
@@ -105,8 +145,8 @@ export default function createNavigationBlockerMiddleware(
|
|
|
105
145
|
});
|
|
106
146
|
} else if (result) {
|
|
107
147
|
// Prevent the navigation: rewind to the previous location.
|
|
108
|
-
|
|
109
|
-
|
|
148
|
+
ignoreLocationSubscriptionEvents(() => {
|
|
149
|
+
session.navigation.shift(-payload.delta);
|
|
110
150
|
});
|
|
111
151
|
} else {
|
|
112
152
|
// Update the location.
|
|
@@ -117,7 +157,7 @@ export default function createNavigationBlockerMiddleware(
|
|
|
117
157
|
|
|
118
158
|
// Remove any navigation blockers on `DISPOSE` event.
|
|
119
159
|
case ActionTypes.DISPOSE:
|
|
120
|
-
removeAllNavigationBlockers();
|
|
160
|
+
removeAllNavigationBlockers(session);
|
|
121
161
|
return next(action);
|
|
122
162
|
|
|
123
163
|
default:
|
|
@@ -3,7 +3,7 @@ import ActionTypes from '../ActionTypes';
|
|
|
3
3
|
// Creates a "middleware" that transforms action payload (location).
|
|
4
4
|
export default function createTransformLocationMiddleware({
|
|
5
5
|
transformInputLocation,
|
|
6
|
-
|
|
6
|
+
transformSubscriptionLocation,
|
|
7
7
|
}) {
|
|
8
8
|
return function transformLocationMiddleware() {
|
|
9
9
|
return (next) => (action) => {
|
|
@@ -18,7 +18,7 @@ export default function createTransformLocationMiddleware({
|
|
|
18
18
|
case ActionTypes.UPDATE:
|
|
19
19
|
return next({
|
|
20
20
|
type,
|
|
21
|
-
payload:
|
|
21
|
+
payload: transformSubscriptionLocation(payload),
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
default:
|
|
@@ -1,67 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
import onlyAllowedOnClientSide from './onlyAllowedOnClientSide';
|
|
3
|
-
|
|
4
|
-
let navigationBlockersList = [];
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
5
2
|
|
|
6
|
-
|
|
3
|
+
import isPromise from './isPromise';
|
|
7
4
|
|
|
8
|
-
export function getNavigationBlockers() {
|
|
9
|
-
return
|
|
5
|
+
export function getNavigationBlockers(session) {
|
|
6
|
+
return session._navigationBlockersList || [];
|
|
10
7
|
}
|
|
11
8
|
|
|
12
|
-
function addNavigationBlockerToTheList(blocker) {
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
function addNavigationBlockerToTheList(blocker, session) {
|
|
10
|
+
if (!session._navigationBlockersList) {
|
|
11
|
+
session._navigationBlockersList = [];
|
|
12
|
+
}
|
|
13
|
+
session._navigationBlockersList.push(blocker);
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
function removeNavigationBlockerFromTheList(blocker) {
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
function removeNavigationBlockerFromTheList(blocker, session) {
|
|
17
|
+
if (session._navigationBlockersList) {
|
|
18
|
+
session._navigationBlockersList = session._navigationBlockersList.filter(
|
|
19
|
+
(_) => _ !== blocker,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export function removeAllNavigationBlockers() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
export function removeAllNavigationBlockers(session) {
|
|
25
|
+
if (
|
|
26
|
+
getNavigationBlockers(session).some((blocker) => blocker.beforeDestroy)
|
|
27
|
+
) {
|
|
28
|
+
if (!session._removeBeforeDestroyListener) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'`_removeBeforeDestroyListener` property not found in the `session`',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
session._removeBeforeDestroyListener();
|
|
34
|
+
session._removeBeforeDestroyListener = undefined;
|
|
27
35
|
}
|
|
28
|
-
|
|
36
|
+
session._navigationBlockersList = [];
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
// Runs the `
|
|
32
|
-
function runNavigationBlocker({
|
|
39
|
+
// Runs the `blocker` while ignoring any errors that might be thrown by it.
|
|
40
|
+
function runNavigationBlocker({ blocker }, location) {
|
|
33
41
|
let result;
|
|
34
42
|
try {
|
|
35
|
-
result =
|
|
43
|
+
result = blocker(location);
|
|
36
44
|
} catch (error) {
|
|
37
45
|
// eslint-disable-next-line no-console
|
|
38
46
|
console.warn(
|
|
39
|
-
`Ignoring navigation blocker \`${
|
|
47
|
+
`Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`,
|
|
40
48
|
);
|
|
41
49
|
// eslint-disable-next-line no-console
|
|
42
50
|
console.error(error);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
// If the
|
|
53
|
+
// If the blocker returned a `Promise`, await for that `Promise`
|
|
46
54
|
// and then return the result.
|
|
47
55
|
if (isPromise(result)) {
|
|
48
56
|
return result.catch((error) => {
|
|
49
57
|
// eslint-disable-next-line no-console
|
|
50
58
|
console.warn(
|
|
51
|
-
`Ignoring navigation blocker \`${
|
|
59
|
+
`Ignoring navigation blocker \`${blocker.name}\` that failed with \`${error}\`.`,
|
|
52
60
|
);
|
|
53
61
|
// eslint-disable-next-line no-console
|
|
54
62
|
console.error(error);
|
|
55
63
|
});
|
|
56
64
|
}
|
|
57
|
-
// The
|
|
65
|
+
// The blocker didn't return a `Promise`.
|
|
58
66
|
// Return the "synchronous" result.
|
|
59
67
|
return result;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
// Runs all
|
|
63
|
-
// If any
|
|
64
|
-
// If there's no such
|
|
70
|
+
// Runs all blockers in order.
|
|
71
|
+
// If any blocker returns `true`, it stops and returns the result.
|
|
72
|
+
// If there's no such blocker, returns `undefined`.
|
|
65
73
|
export function runNavigationBlockers(navigationBlockers, toLocation) {
|
|
66
74
|
if (navigationBlockers.length === 0) {
|
|
67
75
|
return undefined;
|
|
@@ -91,17 +99,17 @@ export function runNavigationBlockers(navigationBlockers, toLocation) {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
/* istanbul ignore next: not testable with Karma */
|
|
94
|
-
function onBeforeDestroy() {
|
|
95
|
-
const result = runNavigationBlockers(getNavigationBlockers(), null);
|
|
102
|
+
function onBeforeDestroy(session) {
|
|
103
|
+
const result = runNavigationBlockers(getNavigationBlockers(session), null);
|
|
96
104
|
|
|
97
|
-
// If no
|
|
105
|
+
// If no blocker returned anything, don't prevent the "unload" event.
|
|
98
106
|
if (!result) {
|
|
99
107
|
return undefined;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
// Web browsers don't allow displaying a custom modal in "beforeunload" phase.
|
|
103
111
|
// They only allow displaying a standard one, with the default text.
|
|
104
|
-
// Hence, "asynchronous"
|
|
112
|
+
// Hence, "asynchronous" blockers should be ignored.
|
|
105
113
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
|
106
114
|
if (isPromise(result)) {
|
|
107
115
|
return undefined;
|
|
@@ -111,48 +119,59 @@ function onBeforeDestroy() {
|
|
|
111
119
|
return true;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
export function addNavigationBlocker(
|
|
115
|
-
onlyAllowedOnClientSide();
|
|
116
|
-
|
|
122
|
+
export function addNavigationBlocker(session, blocker) {
|
|
117
123
|
// All navigation blockers also run on `beforeDestroy` event.
|
|
118
124
|
// If required, this could be a parameter of this function.
|
|
119
|
-
// The rationale could be that adding `beforeunload`
|
|
125
|
+
// The rationale could be that adding a `beforeunload` listener
|
|
120
126
|
// disables web page caching in some browsers like Firefox.
|
|
121
127
|
const beforeDestroy = true;
|
|
122
128
|
|
|
123
|
-
// If it's the first "beforeDestroy"
|
|
129
|
+
// If it's the first "beforeDestroy" blocker, add the global `onBeforeDestroy` listener.
|
|
124
130
|
//
|
|
125
131
|
// Sidenote: Add the "beforeunload" event listener only as needed, as its presence
|
|
126
132
|
// prevents the page from being added to the page navigation cache:
|
|
127
133
|
//
|
|
128
134
|
// "In Firefox, beforeunload is not compatible with the back/forward cache (bfcache):
|
|
129
|
-
// that is, Firefox will not place pages in the bfcache if they have beforeunload listeners,
|
|
135
|
+
// that is, Firefox will not place pages in the bfcache if they have "beforeunload" listeners,
|
|
130
136
|
// and this is bad for performance."
|
|
131
137
|
//
|
|
132
138
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
|
133
139
|
if (
|
|
134
140
|
beforeDestroy &&
|
|
135
|
-
!getNavigationBlockers().some(
|
|
141
|
+
!getNavigationBlockers(session).some(
|
|
142
|
+
(navigationBlocker) => navigationBlocker.beforeDestroy,
|
|
143
|
+
)
|
|
136
144
|
) {
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
if (session._removeBeforeDestroyListener) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'Unexpected `_removeBeforeDestroyListener` property found in the `session`',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
session._removeBeforeDestroyListener = session.addBeforeDestroyListener(
|
|
151
|
+
() => onBeforeDestroy(session),
|
|
152
|
+
);
|
|
139
153
|
}
|
|
140
154
|
|
|
141
|
-
const
|
|
142
|
-
addNavigationBlockerToTheList(
|
|
155
|
+
const newNavigationBlocker = { blocker, beforeDestroy };
|
|
156
|
+
addNavigationBlockerToTheList(newNavigationBlocker, session);
|
|
143
157
|
|
|
144
158
|
return () => {
|
|
145
|
-
removeNavigationBlockerFromTheList(
|
|
159
|
+
removeNavigationBlockerFromTheList(newNavigationBlocker, session);
|
|
146
160
|
|
|
147
|
-
// If it was the last "beforeDestroy"
|
|
161
|
+
// If it was the last "beforeDestroy" blocker, remove the global `onBeforeDestroy` listener.
|
|
148
162
|
if (
|
|
149
163
|
beforeDestroy &&
|
|
150
|
-
!getNavigationBlockers().some(
|
|
164
|
+
!getNavigationBlockers(session).some(
|
|
151
165
|
(navigationBlocker) => navigationBlocker.beforeDestroy,
|
|
152
166
|
)
|
|
153
167
|
) {
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
if (!session._removeBeforeDestroyListener) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'`_removeBeforeDestroyListener` property not found in the `session`',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
session._removeBeforeDestroyListener();
|
|
174
|
+
session._removeBeforeDestroyListener = undefined;
|
|
156
175
|
}
|
|
157
176
|
};
|
|
158
177
|
}
|
package/src/parseLocationUrl.js
CHANGED