navigation-stack 0.1.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/.babelrc.cjs +17 -0
- package/.eslintignore +8 -0
- package/.eslintrc.cjs +10 -0
- package/.github/workflows/main.yml +39 -0
- package/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +1 -0
- package/CODE_OF_CONDUCT.md +77 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/codecov.yml +1 -0
- package/karma.conf.cjs +63 -0
- package/lib/cjs/ActionTypes.js +14 -0
- package/lib/cjs/Actions.js +27 -0
- package/lib/cjs/LocationStateStorage.js +60 -0
- package/lib/cjs/addNavigationBlocker.js +7 -0
- package/lib/cjs/basePath.js +58 -0
- package/lib/cjs/createMiddlewares.js +43 -0
- package/lib/cjs/createSearchFromQuery.js +13 -0
- package/lib/cjs/environment/BrowserEnvironment.js +111 -0
- package/lib/cjs/environment/MemoryEnvironment.js +150 -0
- package/lib/cjs/environment/ServerEnvironment.js +53 -0
- package/lib/cjs/getLocationUrl.js +20 -0
- package/lib/cjs/index.js +30 -0
- package/lib/cjs/isPromise.js +9 -0
- package/lib/cjs/locationReducer.js +13 -0
- package/lib/cjs/middleware/createBasePathMiddleware.js +24 -0
- package/lib/cjs/middleware/createEnvironmentMiddleware.js +58 -0
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +128 -0
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +38 -0
- package/lib/cjs/middleware/navigationActionMiddleware.js +37 -0
- package/lib/cjs/middleware/normalizeInputLocationMiddleware.js +27 -0
- package/lib/cjs/navigationBlockers.js +146 -0
- package/lib/cjs/normalizeInputLocation.js +46 -0
- package/lib/cjs/onlyAllowedOnClientSide.js +10 -0
- package/lib/cjs/parseLocationUrl.js +39 -0
- package/lib/cjs/parseQueryFromSearch.js +16 -0
- package/lib/esm/ActionTypes.js +9 -0
- package/lib/esm/Actions.js +21 -0
- package/lib/esm/LocationStateStorage.js +53 -0
- package/lib/esm/addNavigationBlocker.js +2 -0
- package/lib/esm/basePath.js +53 -0
- package/lib/esm/createMiddlewares.js +37 -0
- package/lib/esm/createSearchFromQuery.js +8 -0
- package/lib/esm/environment/BrowserEnvironment.js +104 -0
- package/lib/esm/environment/MemoryEnvironment.js +143 -0
- package/lib/esm/environment/ServerEnvironment.js +46 -0
- package/lib/esm/getLocationUrl.js +15 -0
- package/lib/esm/index.js +12 -0
- package/lib/esm/isPromise.js +4 -0
- package/lib/esm/locationReducer.js +7 -0
- package/lib/esm/middleware/createBasePathMiddleware.js +19 -0
- package/lib/esm/middleware/createEnvironmentMiddleware.js +52 -0
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +123 -0
- package/lib/esm/middleware/createTransformLocationMiddleware.js +33 -0
- package/lib/esm/middleware/navigationActionMiddleware.js +32 -0
- package/lib/esm/middleware/normalizeInputLocationMiddleware.js +22 -0
- package/lib/esm/navigationBlockers.js +138 -0
- package/lib/esm/normalizeInputLocation.js +41 -0
- package/lib/esm/onlyAllowedOnClientSide.js +5 -0
- package/lib/esm/parseLocationUrl.js +33 -0
- package/lib/esm/parseQueryFromSearch.js +11 -0
- package/lib/index.d.ts +301 -0
- package/package.json +100 -0
- package/renovate.json +3 -0
- package/src/ActionTypes.js +9 -0
- package/src/Actions.js +26 -0
- package/src/LocationStateStorage.js +59 -0
- package/src/addNavigationBlocker.js +2 -0
- package/src/basePath.js +65 -0
- package/src/createMiddlewares.js +41 -0
- package/src/createSearchFromQuery.js +9 -0
- package/src/environment/BrowserEnvironment.js +109 -0
- package/src/environment/MemoryEnvironment.js +151 -0
- package/src/environment/ServerEnvironment.js +54 -0
- package/src/getLocationUrl.js +12 -0
- package/src/index.js +12 -0
- package/src/isPromise.js +8 -0
- package/src/locationReducer.js +8 -0
- package/src/middleware/createBasePathMiddleware.js +20 -0
- package/src/middleware/createEnvironmentMiddleware.js +57 -0
- package/src/middleware/createNavigationBlockerMiddleware.js +128 -0
- package/src/middleware/createTransformLocationMiddleware.js +29 -0
- package/src/middleware/navigationActionMiddleware.js +27 -0
- package/src/middleware/normalizeInputLocationMiddleware.js +21 -0
- package/src/navigationBlockers.js +158 -0
- package/src/normalizeInputLocation.js +44 -0
- package/src/onlyAllowedOnClientSide.js +5 -0
- package/src/parseLocationUrl.js +40 -0
- package/src/parseQueryFromSearch.js +12 -0
- package/test/.eslintrc.cjs +17 -0
- package/test/Action.test.js +72 -0
- package/test/ActionTypes.test.js +13 -0
- package/test/LocationStateStorage.test.js +75 -0
- package/test/basePath.test.js +158 -0
- package/test/createMiddlewares.test.js +62 -0
- package/test/environment/BrowserEnvironment.test.js +165 -0
- package/test/environment/MemoryEnvironment.test.js +218 -0
- package/test/environment/ServerEnvironment.test.js +23 -0
- package/test/getLocationUrl.test.js +33 -0
- package/test/helpers.js +34 -0
- package/test/index.js +44 -0
- package/test/index.test.js +20 -0
- package/test/locationReducer.test.js +42 -0
- package/test/middleware/createBasePathMiddleware.test.js +67 -0
- package/test/middleware/createNavigationBlockerMiddleware.test.js +472 -0
- package/test/middleware/createTransformLocationMiddleware.test.js +44 -0
- package/test/middleware/navigationActionMiddleware.test.js +74 -0
- package/test/middleware/normalizeInputLocationMiddleware.test.js +62 -0
- package/test/normalizeInputLocation.test.js +81 -0
- package/test/parseLocationUrl.test.js +30 -0
- package/types/.eslintrc.cjs +3 -0
- package/types/index.d.ts +301 -0
- package/types/tsconfig.json +14 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import createBasePathMiddleware from './middleware/createBasePathMiddleware';
|
|
2
|
+
import createEnvironmentMiddleware from './middleware/createEnvironmentMiddleware';
|
|
3
|
+
import createNavigationBlockerMiddleware from './middleware/createNavigationBlockerMiddleware';
|
|
4
|
+
import navigationActionMiddleware from './middleware/navigationActionMiddleware';
|
|
5
|
+
import normalizeInputLocationMiddleware from './middleware/normalizeInputLocationMiddleware';
|
|
6
|
+
|
|
7
|
+
export default function createMiddlewares(environment, options) {
|
|
8
|
+
// Allows temporarily ignoring certain environment location updates.
|
|
9
|
+
let shouldIgnoreEnvironmentLocationUpdates = false;
|
|
10
|
+
const ignoreEnvironmentLocationUpdates = (func) => {
|
|
11
|
+
shouldIgnoreEnvironmentLocationUpdates = true;
|
|
12
|
+
func();
|
|
13
|
+
shouldIgnoreEnvironmentLocationUpdates = false;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
// Validates that the action "payload" (input location) is a proper `NormalizedInputLocation`.
|
|
18
|
+
normalizeInputLocationMiddleware,
|
|
19
|
+
// Transforms a "PUSH" / "REPLACE" action into a "NAVIGATE" action.
|
|
20
|
+
navigationActionMiddleware,
|
|
21
|
+
// If a website is hosted under a certain path (`basePath`)
|
|
22
|
+
// then this middleware will automatically strip that starting segment from the `pathname` of `location`s.
|
|
23
|
+
createBasePathMiddleware(options && options.basePath),
|
|
24
|
+
// Allows blocking navigation.
|
|
25
|
+
// Handles `NAVIGATE` actions dispatched by the application itself.
|
|
26
|
+
createNavigationBlockerMiddleware(environment, {
|
|
27
|
+
ignoreEnvironmentLocationUpdates,
|
|
28
|
+
}),
|
|
29
|
+
// This "middleware" performs the actual navigation according to the `environment` being used.
|
|
30
|
+
// For example, when `BrowserEnvironment` is used, it calls methods of the `history` object.
|
|
31
|
+
createEnvironmentMiddleware(environment, {
|
|
32
|
+
shouldIgnoreEnvironmentLocationUpdates: () =>
|
|
33
|
+
shouldIgnoreEnvironmentLocationUpdates,
|
|
34
|
+
}),
|
|
35
|
+
// Allows blocking navigation.
|
|
36
|
+
// Handles location `UPDATE` actions dispatched by the environment.
|
|
37
|
+
createNavigationBlockerMiddleware(environment, {
|
|
38
|
+
ignoreEnvironmentLocationUpdates,
|
|
39
|
+
}),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import getLocationUrl from '../getLocationUrl';
|
|
2
|
+
import parseQueryFromSearch from '../parseQueryFromSearch';
|
|
3
|
+
|
|
4
|
+
export default class BrowserEnvironment {
|
|
5
|
+
constructor() {
|
|
6
|
+
this._keyPrefix = Math.random().toString(36).slice(2, 8);
|
|
7
|
+
this._keyIndex = 0;
|
|
8
|
+
|
|
9
|
+
this._index = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
init() {
|
|
13
|
+
const { pathname, search, hash } = window.location;
|
|
14
|
+
|
|
15
|
+
const { key, index = 0, state } = window.history.state || {};
|
|
16
|
+
const delta = this._index != null ? index - this._index : 0;
|
|
17
|
+
this._index = index;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
action: 'POP',
|
|
21
|
+
pathname,
|
|
22
|
+
search,
|
|
23
|
+
query: search && parseQueryFromSearch(search),
|
|
24
|
+
hash,
|
|
25
|
+
key,
|
|
26
|
+
index,
|
|
27
|
+
delta,
|
|
28
|
+
state,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Subscribes to changes in location,
|
|
33
|
+
// excluding ones that happened as a result of calling `.navigate()`.
|
|
34
|
+
subscribe(listener) {
|
|
35
|
+
const onPopState = () => {
|
|
36
|
+
listener(this.init());
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
window.addEventListener('popstate', onPopState);
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener('popstate', onPopState);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
navigate(location) {
|
|
46
|
+
const { action, state } = location;
|
|
47
|
+
|
|
48
|
+
const push = action === 'PUSH';
|
|
49
|
+
|
|
50
|
+
if (!push && action !== 'REPLACE') {
|
|
51
|
+
throw Error(`Unrecognized browser environment action: ${action}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const delta = push ? 1 : 0;
|
|
55
|
+
const extraState = this._createExtraState(delta);
|
|
56
|
+
|
|
57
|
+
const browserState = { state, ...extraState };
|
|
58
|
+
const url = getLocationUrl(location);
|
|
59
|
+
|
|
60
|
+
if (push) {
|
|
61
|
+
window.history.pushState(browserState, null, url);
|
|
62
|
+
} else {
|
|
63
|
+
window.history.replaceState(browserState, null, url);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { ...location, ...extraState, delta };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
shift(delta) {
|
|
70
|
+
window.history.go(delta);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
addBeforeDestroyListener(onBeforeDestroy) {
|
|
74
|
+
const onBeforeUnload = (event) => {
|
|
75
|
+
if (onBeforeDestroy()) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
81
|
+
return () => {
|
|
82
|
+
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_createExtraState(delta) {
|
|
87
|
+
const keyIndex = this._keyIndex++;
|
|
88
|
+
this._index += delta;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
key: `${this._keyPrefix}:${keyIndex.toString(36)}`,
|
|
92
|
+
index: this._index,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Returns either a `string` value or `null` if the key doesn't exist.
|
|
97
|
+
getState(key) {
|
|
98
|
+
// FYI: `sessionStorage` persists across page reloads.
|
|
99
|
+
return window.sessionStorage.getItem(key);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
removeState(key) {
|
|
103
|
+
window.sessionStorage.removeItem(key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setState(key, value) {
|
|
107
|
+
window.sessionStorage.setItem(key, value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import normalizeInputLocation from '../normalizeInputLocation';
|
|
2
|
+
|
|
3
|
+
export default class MemoryEnvironment {
|
|
4
|
+
constructor(initialLocation, { save, load } = {}) {
|
|
5
|
+
this._save = save;
|
|
6
|
+
this._load = load;
|
|
7
|
+
|
|
8
|
+
const initialState = load ? this._loadState() : null;
|
|
9
|
+
if (initialState) {
|
|
10
|
+
this._stack = initialState.stack;
|
|
11
|
+
this._index = initialState.index;
|
|
12
|
+
this._state = initialState.state;
|
|
13
|
+
} else {
|
|
14
|
+
this._stack = [normalizeInputLocation(initialLocation)];
|
|
15
|
+
this._index = 0;
|
|
16
|
+
this._state = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this._keyPrefix = Math.random().toString(36).slice(2, 8);
|
|
20
|
+
this._keyIndex = 0;
|
|
21
|
+
|
|
22
|
+
this._listener = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_loadState() {
|
|
26
|
+
try {
|
|
27
|
+
const { stack, index, state } = JSON.parse(this._load());
|
|
28
|
+
|
|
29
|
+
// Check that the stack and index at least seem reasonable before using
|
|
30
|
+
// them as state. This isn't foolproof, but it might prevent mistakes.
|
|
31
|
+
// Also perform a basic validation of `state`.
|
|
32
|
+
if (
|
|
33
|
+
Array.isArray(stack) &&
|
|
34
|
+
typeof index === 'number' &&
|
|
35
|
+
stack[index] &&
|
|
36
|
+
typeof state === 'object' &&
|
|
37
|
+
state !== null
|
|
38
|
+
) {
|
|
39
|
+
return { stack, index, state };
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
init(delta = 0) {
|
|
47
|
+
return {
|
|
48
|
+
...this._stack[this._index],
|
|
49
|
+
action: 'POP',
|
|
50
|
+
index: this._index,
|
|
51
|
+
delta,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
subscribe(listener) {
|
|
56
|
+
this._listener = listener;
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
this._listener = null;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
navigate(location) {
|
|
64
|
+
const { action, pathname, search, query, hash, state } = location;
|
|
65
|
+
|
|
66
|
+
const push = action === 'PUSH';
|
|
67
|
+
|
|
68
|
+
if (!push && action !== 'REPLACE')
|
|
69
|
+
throw Error(`Unrecognized browser environment action: ${action}`);
|
|
70
|
+
|
|
71
|
+
const delta = push ? 1 : 0;
|
|
72
|
+
this._index += delta;
|
|
73
|
+
|
|
74
|
+
const keyIndex = this._keyIndex++;
|
|
75
|
+
const key = `${this._keyPrefix}:${keyIndex.toString(36)}`;
|
|
76
|
+
|
|
77
|
+
this._stack[this._index] = { pathname, search, query, hash, state, key };
|
|
78
|
+
if (push) {
|
|
79
|
+
this._stack.length = this._index + 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (this._save) {
|
|
83
|
+
this._saveState();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { ...location, key, index: this._index, delta };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
shift(delta) {
|
|
90
|
+
const prevIndex = this._index;
|
|
91
|
+
|
|
92
|
+
this._index = Math.min(
|
|
93
|
+
Math.max(this._index + delta, 0),
|
|
94
|
+
this._stack.length - 1,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (this._index === prevIndex) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this._save) {
|
|
102
|
+
this._saveState();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this._listener) {
|
|
106
|
+
this._listener(this.init(this._index - prevIndex));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// "Before destroy" listeners are currently ignored.
|
|
111
|
+
// If required, one could implement a `_destroy()` method
|
|
112
|
+
// and there check that the listeners actually do get called.
|
|
113
|
+
addBeforeDestroyListener(listener) {
|
|
114
|
+
return () => {
|
|
115
|
+
this._removeBeforeDestroyListener(listener);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// This method is used in tests.
|
|
120
|
+
_removeBeforeDestroyListener() {}
|
|
121
|
+
|
|
122
|
+
_saveState() {
|
|
123
|
+
try {
|
|
124
|
+
this._save(
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
stack: this._stack,
|
|
127
|
+
index: this._index,
|
|
128
|
+
state: this._state,
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Returns either a `string` value or `null` if the key doesn't exist.
|
|
135
|
+
getState(key) {
|
|
136
|
+
if (key in this._state) {
|
|
137
|
+
return this._state[key];
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
removeState(key) {
|
|
143
|
+
if (key in this._state) {
|
|
144
|
+
delete this._state[key];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setState(key, value) {
|
|
149
|
+
this._state[key] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import normalizeInputLocation from '../normalizeInputLocation';
|
|
2
|
+
|
|
3
|
+
function noop() {}
|
|
4
|
+
|
|
5
|
+
function serverSideNavigationNotPossible() {
|
|
6
|
+
throw new Error('Server-side navigation is not possible');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default class ServerEnvironment {
|
|
10
|
+
constructor(initialLocation) {
|
|
11
|
+
this._location = normalizeInputLocation(initialLocation);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
init() {
|
|
15
|
+
return {
|
|
16
|
+
action: 'POP',
|
|
17
|
+
...this._location,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
subscribe() {
|
|
22
|
+
// Server environment emits no location events.
|
|
23
|
+
return noop;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Navigation methods are not implemented, because `ServerPEnvironment` instances
|
|
27
|
+
// cannot navigate.
|
|
28
|
+
navigate() {
|
|
29
|
+
serverSideNavigationNotPossible();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Navigation methods are not implemented, because `ServerEnvironment` instances
|
|
33
|
+
// cannot navigate.
|
|
34
|
+
shift() {
|
|
35
|
+
serverSideNavigationNotPossible();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// "Before destroy" listeners are currently ignored.
|
|
39
|
+
// If required, one could implement a `_destroy()` method
|
|
40
|
+
// and there check that the listeners actually do get called.
|
|
41
|
+
addBeforeDestroyListener() {
|
|
42
|
+
return () => {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// It doesn't seem to make any sense to store anything on server side.
|
|
46
|
+
// Hence, state management methods are "no op" stubs.
|
|
47
|
+
getState() {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
removeState() {}
|
|
52
|
+
|
|
53
|
+
setState() {}
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { stringify as stringifyQuery } from 'query-string';
|
|
2
|
+
|
|
3
|
+
export default function getLocationUrl({ pathname, search, query, hash }) {
|
|
4
|
+
if (!search && query) {
|
|
5
|
+
const queryString = stringifyQuery(query);
|
|
6
|
+
if (queryString) {
|
|
7
|
+
search = `?${queryString}`;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return `${pathname}${search || ''}${hash || ''}`;
|
|
12
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export Actions from './Actions';
|
|
2
|
+
export ActionTypes from './ActionTypes';
|
|
3
|
+
export { addBasePath, removeBasePath } from './basePath';
|
|
4
|
+
export addNavigationBlocker from './addNavigationBlocker';
|
|
5
|
+
export getLocationUrl from './getLocationUrl';
|
|
6
|
+
export parseLocationUrl from './parseLocationUrl';
|
|
7
|
+
export createMiddlewares from './createMiddlewares';
|
|
8
|
+
export locationReducer from './locationReducer';
|
|
9
|
+
export LocationStateStorage from './LocationStateStorage';
|
|
10
|
+
export BrowserEnvironment from './environment/BrowserEnvironment';
|
|
11
|
+
export MemoryEnvironment from './environment/MemoryEnvironment';
|
|
12
|
+
export ServerEnvironment from './environment/ServerEnvironment';
|
package/src/isPromise.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { addBasePath, removeBasePath } from '../basePath';
|
|
2
|
+
import createTransformLocationMiddleware from './createTransformLocationMiddleware';
|
|
3
|
+
|
|
4
|
+
// Creates a "middleware" that, when a website is hosted under a certain path (`basePath`),
|
|
5
|
+
// automatically strips that starting segment from the `pathname` of `location`s.
|
|
6
|
+
export default function createBasePathMiddleware(basePath) {
|
|
7
|
+
return createTransformLocationMiddleware({
|
|
8
|
+
// Transforms input `Location`:
|
|
9
|
+
// prepends `basePath` to the URL.
|
|
10
|
+
transformInputLocation: (location) => {
|
|
11
|
+
return addBasePath(location, basePath);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
// Transforms environment `Location` object:
|
|
15
|
+
// removes `basePath` from the URL.
|
|
16
|
+
transformEnvironmentLocation: (location) => {
|
|
17
|
+
return removeBasePath(location, basePath);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import ActionTypes from '../ActionTypes';
|
|
2
|
+
|
|
3
|
+
function updateLocation(location) {
|
|
4
|
+
return {
|
|
5
|
+
type: ActionTypes.UPDATE,
|
|
6
|
+
payload: location,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Creates a "middleware" that performs the actual navigation according to the `environment` being used.
|
|
11
|
+
// For example, when `BrowserProtocol` is used, it calls methods of the `history` object.
|
|
12
|
+
// A better name for this function could be something like `createProtocolMiddleware(environment)`.
|
|
13
|
+
// A better name for "environment" could be something like "environment".
|
|
14
|
+
export default function createEnvironmentMiddleware(
|
|
15
|
+
environment,
|
|
16
|
+
{ shouldIgnoreEnvironmentLocationUpdates },
|
|
17
|
+
) {
|
|
18
|
+
return function environmentMiddleware() {
|
|
19
|
+
return (next) => {
|
|
20
|
+
// Whenever browser location changes,
|
|
21
|
+
// perform the same changes with the internal `location` object.
|
|
22
|
+
const unsubscribe = environment.subscribe((location) => {
|
|
23
|
+
if (!shouldIgnoreEnvironmentLocationUpdates()) {
|
|
24
|
+
next(updateLocation(location));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (action) => {
|
|
29
|
+
const { type, payload } = action;
|
|
30
|
+
|
|
31
|
+
switch (type) {
|
|
32
|
+
case ActionTypes.INIT:
|
|
33
|
+
return next(updateLocation(environment.init()));
|
|
34
|
+
|
|
35
|
+
case ActionTypes.NAVIGATE:
|
|
36
|
+
// `environment.navigate()` doesn't trigger the `subscribe()` listener.
|
|
37
|
+
return next(updateLocation(environment.navigate(payload)));
|
|
38
|
+
|
|
39
|
+
case ActionTypes.SHIFT:
|
|
40
|
+
// `shift()` will trigger the `subscribe()` listener,
|
|
41
|
+
// which will call `updateLocation()`.
|
|
42
|
+
environment.shift(payload);
|
|
43
|
+
// eslint-disable-next-line consistent-return
|
|
44
|
+
return;
|
|
45
|
+
|
|
46
|
+
case ActionTypes.DISPOSE:
|
|
47
|
+
unsubscribe();
|
|
48
|
+
// eslint-disable-next-line consistent-return
|
|
49
|
+
return;
|
|
50
|
+
|
|
51
|
+
default:
|
|
52
|
+
return next(action);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import ActionTypes from '../ActionTypes';
|
|
2
|
+
import isPromise from '../isPromise';
|
|
3
|
+
import {
|
|
4
|
+
getNavigationBlockers,
|
|
5
|
+
removeAllNavigationBlockers,
|
|
6
|
+
runNavigationBlockers,
|
|
7
|
+
} from '../navigationBlockers';
|
|
8
|
+
import onlyAllowedOnClientSide from '../onlyAllowedOnClientSide';
|
|
9
|
+
|
|
10
|
+
// Creates a "middleware" that applies navigation blockers.
|
|
11
|
+
export default function createNavigationBlockerMiddleware(
|
|
12
|
+
environment,
|
|
13
|
+
{ ignoreEnvironmentLocationUpdates },
|
|
14
|
+
) {
|
|
15
|
+
// A "dummy" initial value that will be ignored.
|
|
16
|
+
let navigationBlockersEvaluationStatus = { cancelled: false };
|
|
17
|
+
|
|
18
|
+
function createNavigationBlockersEvaluationStatus() {
|
|
19
|
+
onlyAllowedOnClientSide();
|
|
20
|
+
navigationBlockersEvaluationStatus.cancelled = true;
|
|
21
|
+
navigationBlockersEvaluationStatus = { cancelled: false };
|
|
22
|
+
return navigationBlockersEvaluationStatus;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return function navigationListenerMiddleware() {
|
|
26
|
+
return (next) => (action) => {
|
|
27
|
+
const { type, payload } = action;
|
|
28
|
+
|
|
29
|
+
// Declaring `result` variable here fixes ESLint error:
|
|
30
|
+
// "Unexpected lexical declaration in case block".
|
|
31
|
+
let result;
|
|
32
|
+
|
|
33
|
+
switch (type) {
|
|
34
|
+
// Prevent or allow navigation that was initiated by the application.
|
|
35
|
+
case ActionTypes.NAVIGATE:
|
|
36
|
+
// `resultValue` variable name works around a stupid javascript error:
|
|
37
|
+
// "Cannot redeclare block-scoped variable 'result'".
|
|
38
|
+
result = runNavigationBlockers(getNavigationBlockers(), payload);
|
|
39
|
+
if (isPromise(result)) {
|
|
40
|
+
const status = createNavigationBlockersEvaluationStatus();
|
|
41
|
+
// eslint-disable-next-line consistent-return
|
|
42
|
+
result.then((resultValue) => {
|
|
43
|
+
if (!status.cancelled) {
|
|
44
|
+
if (!resultValue) {
|
|
45
|
+
return next(action);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} else if (!result) {
|
|
50
|
+
return next(action);
|
|
51
|
+
}
|
|
52
|
+
// eslint-disable-next-line consistent-return
|
|
53
|
+
return;
|
|
54
|
+
|
|
55
|
+
// Prevent or allow navigation that was initiated by the environment itself.
|
|
56
|
+
// For example, in a web browser, it could happen when the user clicks the "Back"/"Forward" buttons.
|
|
57
|
+
//
|
|
58
|
+
// In this scenario, the web browser is already at the new location, i.e. the navigation has already happened.
|
|
59
|
+
// It could be "prevented" by rewinding back to the previous location.
|
|
60
|
+
//
|
|
61
|
+
case ActionTypes.UPDATE:
|
|
62
|
+
// If no navigation blockers to run, don't do anything.
|
|
63
|
+
if (getNavigationBlockers().length === 0) {
|
|
64
|
+
return next(action);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If it was the initial page load or a redirect,
|
|
68
|
+
// it's not really a navigation that could be rolled back.
|
|
69
|
+
if (payload.delta === 0) {
|
|
70
|
+
return next(action);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// It's not really possible for a location to not have a `delta` property in a web browser environment.
|
|
74
|
+
// So this case is not something that's supposed to happen in real life.
|
|
75
|
+
// Rather, it's a guard against an unsupported or incorrectly-implemented environment or something like that.
|
|
76
|
+
// If there's no `delta` property on the location, it means that the previous location can't be rewound to,
|
|
77
|
+
// so it can't really "prevent" the navigation that has just happened.
|
|
78
|
+
if (payload.delta === null) {
|
|
79
|
+
return next(action);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result = runNavigationBlockers(getNavigationBlockers(), payload);
|
|
83
|
+
|
|
84
|
+
if (isPromise(result)) {
|
|
85
|
+
const status = createNavigationBlockersEvaluationStatus();
|
|
86
|
+
|
|
87
|
+
// While location blockers are running, rewind to the previous location.
|
|
88
|
+
ignoreEnvironmentLocationUpdates(() => {
|
|
89
|
+
environment.shift(-payload.delta);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
result.then((promiseResult) => {
|
|
93
|
+
if (promiseResult) {
|
|
94
|
+
// Navigation blocked.
|
|
95
|
+
// Already rewound to a previous location.
|
|
96
|
+
} else if (!status.cancelled) {
|
|
97
|
+
// Navigation not blocked.
|
|
98
|
+
// Rewind back to the new location.
|
|
99
|
+
ignoreEnvironmentLocationUpdates(() => {
|
|
100
|
+
environment.shift(payload.delta);
|
|
101
|
+
});
|
|
102
|
+
// Update the location.
|
|
103
|
+
next(action);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
} else if (result) {
|
|
107
|
+
// Prevent the navigation: rewind to the previous location.
|
|
108
|
+
ignoreEnvironmentLocationUpdates(() => {
|
|
109
|
+
environment.shift(-payload.delta);
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
// Update the location.
|
|
113
|
+
return next(action);
|
|
114
|
+
}
|
|
115
|
+
// eslint-disable-next-line consistent-return
|
|
116
|
+
return;
|
|
117
|
+
|
|
118
|
+
// Remove any navigation blockers on `DISPOSE` event.
|
|
119
|
+
case ActionTypes.DISPOSE:
|
|
120
|
+
removeAllNavigationBlockers();
|
|
121
|
+
return next(action);
|
|
122
|
+
|
|
123
|
+
default:
|
|
124
|
+
return next(action);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ActionTypes from '../ActionTypes';
|
|
2
|
+
|
|
3
|
+
// Creates a "middleware" that transforms action payload (location).
|
|
4
|
+
export default function createTransformLocationMiddleware({
|
|
5
|
+
transformInputLocation,
|
|
6
|
+
transformEnvironmentLocation,
|
|
7
|
+
}) {
|
|
8
|
+
return function transformLocationMiddleware() {
|
|
9
|
+
return (next) => (action) => {
|
|
10
|
+
const { type, payload } = action;
|
|
11
|
+
|
|
12
|
+
switch (type) {
|
|
13
|
+
// Transforms `NAVIGATE` action payload (`location`).
|
|
14
|
+
case ActionTypes.NAVIGATE:
|
|
15
|
+
return next({ type, payload: transformInputLocation(payload) });
|
|
16
|
+
|
|
17
|
+
// Transforms `UPDATE` action payload (input `location`).
|
|
18
|
+
case ActionTypes.UPDATE:
|
|
19
|
+
return next({
|
|
20
|
+
type,
|
|
21
|
+
payload: transformEnvironmentLocation(payload),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
default:
|
|
25
|
+
return next(action);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ActionTypes from '../ActionTypes';
|
|
2
|
+
|
|
3
|
+
// This "middleware" transforms a `PUSH` / `REPLACE` action into a `NAVIGATE` action.
|
|
4
|
+
export default function navigationActionMiddleware() {
|
|
5
|
+
return (next) => (action) => {
|
|
6
|
+
const { type, payload } = action;
|
|
7
|
+
|
|
8
|
+
switch (type) {
|
|
9
|
+
// Converts a `PUSH` action into a `NAVIGATE` action.
|
|
10
|
+
case ActionTypes.PUSH:
|
|
11
|
+
return next({
|
|
12
|
+
type: ActionTypes.NAVIGATE,
|
|
13
|
+
payload: { ...payload, action: 'PUSH' },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Converts a `REPLACE` action into a `NAVIGATE` action.
|
|
17
|
+
case ActionTypes.REPLACE:
|
|
18
|
+
return next({
|
|
19
|
+
type: ActionTypes.NAVIGATE,
|
|
20
|
+
payload: { ...payload, action: 'REPLACE' },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
default:
|
|
24
|
+
return next(action);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|