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,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = createTransformLocationMiddleware;
|
|
5
|
+
var _ActionTypes = _interopRequireDefault(require("../ActionTypes"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
// Creates a "middleware" that transforms action payload (location).
|
|
8
|
+
function createTransformLocationMiddleware({
|
|
9
|
+
transformInputLocation,
|
|
10
|
+
transformEnvironmentLocation
|
|
11
|
+
}) {
|
|
12
|
+
return function transformLocationMiddleware() {
|
|
13
|
+
return next => action => {
|
|
14
|
+
const {
|
|
15
|
+
type,
|
|
16
|
+
payload
|
|
17
|
+
} = action;
|
|
18
|
+
switch (type) {
|
|
19
|
+
// Transforms `NAVIGATE` action payload (`location`).
|
|
20
|
+
case _ActionTypes.default.NAVIGATE:
|
|
21
|
+
return next({
|
|
22
|
+
type,
|
|
23
|
+
payload: transformInputLocation(payload)
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Transforms `UPDATE` action payload (input `location`).
|
|
27
|
+
case _ActionTypes.default.UPDATE:
|
|
28
|
+
return next({
|
|
29
|
+
type,
|
|
30
|
+
payload: transformEnvironmentLocation(payload)
|
|
31
|
+
});
|
|
32
|
+
default:
|
|
33
|
+
return next(action);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = navigationActionMiddleware;
|
|
5
|
+
var _ActionTypes = _interopRequireDefault(require("../ActionTypes"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
// This "middleware" transforms a `PUSH` / `REPLACE` action into a `NAVIGATE` action.
|
|
8
|
+
function navigationActionMiddleware() {
|
|
9
|
+
return next => action => {
|
|
10
|
+
const {
|
|
11
|
+
type,
|
|
12
|
+
payload
|
|
13
|
+
} = action;
|
|
14
|
+
switch (type) {
|
|
15
|
+
// Converts a `PUSH` action into a `NAVIGATE` action.
|
|
16
|
+
case _ActionTypes.default.PUSH:
|
|
17
|
+
return next({
|
|
18
|
+
type: _ActionTypes.default.NAVIGATE,
|
|
19
|
+
payload: Object.assign({}, payload, {
|
|
20
|
+
action: 'PUSH'
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Converts a `REPLACE` action into a `NAVIGATE` action.
|
|
25
|
+
case _ActionTypes.default.REPLACE:
|
|
26
|
+
return next({
|
|
27
|
+
type: _ActionTypes.default.NAVIGATE,
|
|
28
|
+
payload: Object.assign({}, payload, {
|
|
29
|
+
action: 'REPLACE'
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
default:
|
|
33
|
+
return next(action);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = normalizeInputLocationMiddleware;
|
|
5
|
+
var _ActionTypes = _interopRequireDefault(require("../ActionTypes"));
|
|
6
|
+
var _normalizeInputLocation = _interopRequireDefault(require("../normalizeInputLocation"));
|
|
7
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
// This "middleware" transforms input location argument into a proper `NormalizedInputLocation`.
|
|
9
|
+
function normalizeInputLocationMiddleware() {
|
|
10
|
+
return next => action => {
|
|
11
|
+
const {
|
|
12
|
+
type,
|
|
13
|
+
payload
|
|
14
|
+
} = action;
|
|
15
|
+
switch (type) {
|
|
16
|
+
case _ActionTypes.default.PUSH:
|
|
17
|
+
case _ActionTypes.default.REPLACE:
|
|
18
|
+
return next({
|
|
19
|
+
type,
|
|
20
|
+
payload: (0, _normalizeInputLocation.default)(payload)
|
|
21
|
+
});
|
|
22
|
+
default:
|
|
23
|
+
return next(action);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.addNavigationBlocker = addNavigationBlocker;
|
|
5
|
+
exports.getNavigationBlockers = getNavigationBlockers;
|
|
6
|
+
exports.removeAllNavigationBlockers = removeAllNavigationBlockers;
|
|
7
|
+
exports.runNavigationBlockers = runNavigationBlockers;
|
|
8
|
+
var _isPromise = _interopRequireDefault(require("./isPromise"));
|
|
9
|
+
var _onlyAllowedOnClientSide = _interopRequireDefault(require("./onlyAllowedOnClientSide"));
|
|
10
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
let navigationBlockersList = [];
|
|
12
|
+
let removeBeforeDestroyListener;
|
|
13
|
+
function getNavigationBlockers() {
|
|
14
|
+
return navigationBlockersList;
|
|
15
|
+
}
|
|
16
|
+
function addNavigationBlockerToTheList(blocker) {
|
|
17
|
+
(0, _onlyAllowedOnClientSide.default)();
|
|
18
|
+
navigationBlockersList.push(blocker);
|
|
19
|
+
}
|
|
20
|
+
function removeNavigationBlockerFromTheList(blocker) {
|
|
21
|
+
(0, _onlyAllowedOnClientSide.default)();
|
|
22
|
+
navigationBlockersList = navigationBlockersList.filter(_ => _ !== blocker);
|
|
23
|
+
}
|
|
24
|
+
function removeAllNavigationBlockers() {
|
|
25
|
+
(0, _onlyAllowedOnClientSide.default)();
|
|
26
|
+
if (getNavigationBlockers().some(blocker => blocker.beforeDestroy)) {
|
|
27
|
+
removeBeforeDestroyListener();
|
|
28
|
+
removeBeforeDestroyListener = undefined;
|
|
29
|
+
}
|
|
30
|
+
navigationBlockersList = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Runs the `listener` while ignoring any errors that might be thrown by it.
|
|
34
|
+
function runNavigationBlocker({
|
|
35
|
+
listener
|
|
36
|
+
}, location) {
|
|
37
|
+
let result;
|
|
38
|
+
try {
|
|
39
|
+
result = listener(location);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.warn(`Ignoring navigation blocker \`${listener.name}\` that failed with \`${error}\`.`);
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.error(error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If the listener returned a `Promise`, await for that `Promise`
|
|
48
|
+
// and then return the result.
|
|
49
|
+
if ((0, _isPromise.default)(result)) {
|
|
50
|
+
return result.catch(error => {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.warn(`Ignoring navigation blocker \`${listener.name}\` that failed with \`${error}\`.`);
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.error(error);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// The listener didn't return a `Promise`.
|
|
58
|
+
// Return the "synchronous" result.
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Runs all listeners in order.
|
|
63
|
+
// If any listener returns a non-`null` result, it stops and returns the result.
|
|
64
|
+
// If there's no such listener, returns `true`.
|
|
65
|
+
function runNavigationBlockers(navigationBlockers, toLocation) {
|
|
66
|
+
if (navigationBlockers.length === 0) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Call the first blocker in the list.
|
|
71
|
+
const result = runNavigationBlocker(navigationBlockers[0], toLocation);
|
|
72
|
+
const next = () => {
|
|
73
|
+
// Proceed to the next blocker.
|
|
74
|
+
return runNavigationBlockers(navigationBlockers.slice(1), toLocation);
|
|
75
|
+
};
|
|
76
|
+
if ((0, _isPromise.default)(result)) {
|
|
77
|
+
return result.then(resultValue => {
|
|
78
|
+
if (resultValue) {
|
|
79
|
+
return resultValue;
|
|
80
|
+
}
|
|
81
|
+
return next();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (result) {
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* istanbul ignore next: not testable with Karma */
|
|
91
|
+
function onBeforeDestroy() {
|
|
92
|
+
const result = runNavigationBlockers(getNavigationBlockers(), null);
|
|
93
|
+
|
|
94
|
+
// If no listener returned anything, don't prevent the "unload" event.
|
|
95
|
+
if (!result) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Web browsers don't allow displaying a custom modal in "beforeunload" phase.
|
|
100
|
+
// They only allow displaying a standard one, with the default text.
|
|
101
|
+
// Hence, "asynchronous" listeners should be ignored.
|
|
102
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
|
103
|
+
if ((0, _isPromise.default)(result)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Prevent the "unload" event.
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
function addNavigationBlocker(environment, listener) {
|
|
111
|
+
(0, _onlyAllowedOnClientSide.default)();
|
|
112
|
+
|
|
113
|
+
// All navigation blockers also run on `beforeDestroy` event.
|
|
114
|
+
// If required, this could be a parameter of this function.
|
|
115
|
+
// The rationale could be that adding `beforeunload` a listener
|
|
116
|
+
// disables web page caching in some browsers like Firefox.
|
|
117
|
+
const beforeDestroy = true;
|
|
118
|
+
|
|
119
|
+
// If it's the first "beforeDestroy" listener, add the global `onBeforeDestroy` listener.
|
|
120
|
+
//
|
|
121
|
+
// Sidenote: Add the "beforeunload" event listener only as needed, as its presence
|
|
122
|
+
// prevents the page from being added to the page navigation cache:
|
|
123
|
+
//
|
|
124
|
+
// "In Firefox, beforeunload is not compatible with the back/forward cache (bfcache):
|
|
125
|
+
// that is, Firefox will not place pages in the bfcache if they have beforeunload listeners,
|
|
126
|
+
// and this is bad for performance."
|
|
127
|
+
//
|
|
128
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
|
129
|
+
if (beforeDestroy && !getNavigationBlockers().some(blocker => blocker.beforeDestroy)) {
|
|
130
|
+
removeBeforeDestroyListener = environment.addBeforeDestroyListener(onBeforeDestroy);
|
|
131
|
+
}
|
|
132
|
+
const blocker = {
|
|
133
|
+
listener,
|
|
134
|
+
beforeDestroy
|
|
135
|
+
};
|
|
136
|
+
addNavigationBlockerToTheList(blocker);
|
|
137
|
+
return () => {
|
|
138
|
+
removeNavigationBlockerFromTheList(blocker);
|
|
139
|
+
|
|
140
|
+
// If it was the last "beforeDestroy" listener, remove the global `onBeforeDestroy` listener.
|
|
141
|
+
if (beforeDestroy && !getNavigationBlockers().some(navigationBlocker => navigationBlocker.beforeDestroy)) {
|
|
142
|
+
removeBeforeDestroyListener();
|
|
143
|
+
removeBeforeDestroyListener = undefined;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = normalizeInputLocation;
|
|
5
|
+
var _createSearchFromQuery = _interopRequireDefault(require("./createSearchFromQuery"));
|
|
6
|
+
var _parseLocationUrl = _interopRequireDefault(require("./parseLocationUrl"));
|
|
7
|
+
var _parseQueryFromSearch = _interopRequireDefault(require("./parseQueryFromSearch"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
// * If `location` is a string, it parses it into a `NormalizedInputLocation`.
|
|
10
|
+
// * If `location` is an object, it ensures that `search` and `hash` properties aren't `undefined`,
|
|
11
|
+
// i.e. it "ensures" that the `location` object can be used as a `NormalizedInputLocation`.
|
|
12
|
+
function normalizeInputLocation(location) {
|
|
13
|
+
if (typeof location === 'string') {
|
|
14
|
+
return (0, _parseLocationUrl.default)(location);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Convert `query` property values to strings.
|
|
18
|
+
if (location.query) {
|
|
19
|
+
for (const key of Object.keys(location.query)) {
|
|
20
|
+
location.query[key] = String(location.query[key]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create `query` from `search`.
|
|
25
|
+
if (location.search && !location.query) {
|
|
26
|
+
location = Object.assign({}, location, {
|
|
27
|
+
query: (0, _parseQueryFromSearch.default)(location.search)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Convert `query` object into a `search` string
|
|
32
|
+
// if `query` is present but `search` is not.
|
|
33
|
+
if (location.query && !location.search) {
|
|
34
|
+
location = Object.assign({}, location, {
|
|
35
|
+
search: (0, _createSearchFromQuery.default)(location.query)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Set default values on `search` and `hash`
|
|
40
|
+
// if those properties are not present.
|
|
41
|
+
return Object.assign({}, location, {
|
|
42
|
+
search: location.search || '',
|
|
43
|
+
hash: location.hash || ''
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = onlyAllowedOnClientSide;
|
|
5
|
+
function onlyAllowedOnClientSide() {
|
|
6
|
+
if (typeof window === 'undefined') {
|
|
7
|
+
throw new Error('This function can only be called on client side');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = parseLocationUrl;
|
|
5
|
+
var _parseQueryFromSearch = _interopRequireDefault(require("./parseQueryFromSearch"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
function parseLocationUrl(url) {
|
|
8
|
+
if (url[0] !== '/') {
|
|
9
|
+
throw new Error('Expected URL to start with a slash');
|
|
10
|
+
}
|
|
11
|
+
let remainingPath = url;
|
|
12
|
+
const hashIndex = remainingPath.indexOf('#');
|
|
13
|
+
let hash;
|
|
14
|
+
if (hashIndex !== -1) {
|
|
15
|
+
hash = remainingPath.slice(hashIndex);
|
|
16
|
+
remainingPath = remainingPath.slice(0, hashIndex);
|
|
17
|
+
} else {
|
|
18
|
+
hash = '';
|
|
19
|
+
}
|
|
20
|
+
const searchIndex = remainingPath.indexOf('?');
|
|
21
|
+
let search;
|
|
22
|
+
if (searchIndex !== -1) {
|
|
23
|
+
search = remainingPath.slice(searchIndex);
|
|
24
|
+
remainingPath = remainingPath.slice(0, searchIndex);
|
|
25
|
+
} else {
|
|
26
|
+
search = '';
|
|
27
|
+
}
|
|
28
|
+
const location = {
|
|
29
|
+
pathname: remainingPath,
|
|
30
|
+
search,
|
|
31
|
+
hash
|
|
32
|
+
};
|
|
33
|
+
const query = (0, _parseQueryFromSearch.default)(search);
|
|
34
|
+
if (query) {
|
|
35
|
+
location.query = query;
|
|
36
|
+
}
|
|
37
|
+
return location;
|
|
38
|
+
}
|
|
39
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = parseQueryFromSearch;
|
|
5
|
+
var _queryString = require("query-string");
|
|
6
|
+
function parseQueryFromSearch(search) {
|
|
7
|
+
if (search.length > '?'.length) {
|
|
8
|
+
try {
|
|
9
|
+
return (0, _queryString.parse)(search.slice(1));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
// Ignore any query parsing errors.
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
INIT: '@@navigation-stack/INIT',
|
|
3
|
+
PUSH: '@@navigation-stack/PUSH',
|
|
4
|
+
REPLACE: '@@navigation-stack/REPLACE',
|
|
5
|
+
NAVIGATE: '@@navigation-stack/NAVIGATE',
|
|
6
|
+
SHIFT: '@@navigation-stack/SHIFT',
|
|
7
|
+
UPDATE: '@@navigation-stack/UPDATE',
|
|
8
|
+
DISPOSE: '@@navigation-stack/DISPOSE'
|
|
9
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ActionTypes from './ActionTypes';
|
|
2
|
+
export default {
|
|
3
|
+
init: () => ({
|
|
4
|
+
type: ActionTypes.INIT
|
|
5
|
+
}),
|
|
6
|
+
push: location => ({
|
|
7
|
+
type: ActionTypes.PUSH,
|
|
8
|
+
payload: location
|
|
9
|
+
}),
|
|
10
|
+
replace: location => ({
|
|
11
|
+
type: ActionTypes.REPLACE,
|
|
12
|
+
payload: location
|
|
13
|
+
}),
|
|
14
|
+
shift: delta => ({
|
|
15
|
+
type: ActionTypes.SHIFT,
|
|
16
|
+
payload: delta
|
|
17
|
+
}),
|
|
18
|
+
dispose: () => ({
|
|
19
|
+
type: ActionTypes.DISPOSE
|
|
20
|
+
})
|
|
21
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import getLocationUrl from './getLocationUrl';
|
|
2
|
+
export default class LocationStateStorage {
|
|
3
|
+
constructor(environment, {
|
|
4
|
+
namespace
|
|
5
|
+
} = {}) {
|
|
6
|
+
this._environment = environment;
|
|
7
|
+
this._getFallbackLocationKey = getLocationUrl;
|
|
8
|
+
this._stateKeyPrefix = namespace ? `${namespace}|` : '';
|
|
9
|
+
}
|
|
10
|
+
get(location, key) {
|
|
11
|
+
const stateKey = this._getStateKey(location, key);
|
|
12
|
+
try {
|
|
13
|
+
const value = this._environment.getState(stateKey);
|
|
14
|
+
// === null is probably sufficient.
|
|
15
|
+
if (value === null) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// We want to catch JSON parse errors in case someone separately threw
|
|
20
|
+
// junk into sessionStorage under our namespace.
|
|
21
|
+
return JSON.parse(value);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Pretend that the entry doesn't exist.
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
set(location, key, value) {
|
|
28
|
+
const stateKey = this._getStateKey(location, key);
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
try {
|
|
31
|
+
this._environment.removeState(stateKey);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// No need to handle errors here.
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Unlike with read, we want to fail on invalid values here, since the
|
|
39
|
+
// value here is provided by the caller of this method.
|
|
40
|
+
const valueString = JSON.stringify(value);
|
|
41
|
+
try {
|
|
42
|
+
this._environment.setState(stateKey, valueString);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// No need to handle errors here either. If it didn't work, it didn't
|
|
45
|
+
// work. We make no guarantees about actually saving the value.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
_getStateKey(location, key) {
|
|
49
|
+
const locationKey = location.key || this._getFallbackLocationKey(location);
|
|
50
|
+
const keyPrefix = `${this._stateKeyPrefix}${locationKey}`;
|
|
51
|
+
return `${keyPrefix}|${key}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function normalizeBasePath(basePath) {
|
|
2
|
+
if (!basePath || basePath === '/') {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Validate `basePath`.
|
|
7
|
+
if (basePath[0] !== '/') {
|
|
8
|
+
throw new Error('`basePath` must start with a slash');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Remove trailing slash from `basePath`.
|
|
12
|
+
if (basePath.slice(-1) === '/') {
|
|
13
|
+
basePath = basePath.slice(0, -1);
|
|
14
|
+
}
|
|
15
|
+
return basePath;
|
|
16
|
+
}
|
|
17
|
+
function removeBasePathFromRelativeUrl(url, basePath) {
|
|
18
|
+
if (url.indexOf(basePath) === 0) {
|
|
19
|
+
// `farce` had a bug here:
|
|
20
|
+
// `location.pathname` is supposed to always be non-empty.
|
|
21
|
+
// If `basePath` is set to `/basePath` and the user navigates to `/basePath` URL,
|
|
22
|
+
// originally here it would simply strips the whole string from the URL
|
|
23
|
+
// and the result would be incorrect: `pathname: ""`.
|
|
24
|
+
// The fix below is adding `|| '/'` in the `return` statement.
|
|
25
|
+
// https://github.com/4Catalyzer/farce/issues/483
|
|
26
|
+
return url.slice(basePath.length) || '/';
|
|
27
|
+
}
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
export function addBasePath(location, basePath) {
|
|
31
|
+
basePath = normalizeBasePath(basePath);
|
|
32
|
+
if (!basePath) {
|
|
33
|
+
return location;
|
|
34
|
+
}
|
|
35
|
+
if (typeof location === 'string') {
|
|
36
|
+
return `${basePath}${location}`;
|
|
37
|
+
}
|
|
38
|
+
return Object.assign({}, location, {
|
|
39
|
+
pathname: `${basePath}${location.pathname}`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function removeBasePath(location, basePath) {
|
|
43
|
+
basePath = normalizeBasePath(basePath);
|
|
44
|
+
if (!basePath) {
|
|
45
|
+
return location;
|
|
46
|
+
}
|
|
47
|
+
if (typeof location === 'string') {
|
|
48
|
+
return removeBasePathFromRelativeUrl(location, basePath);
|
|
49
|
+
}
|
|
50
|
+
return Object.assign({}, location, {
|
|
51
|
+
pathname: removeBasePathFromRelativeUrl(location.pathname, basePath)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
export default function createMiddlewares(environment, options) {
|
|
7
|
+
// Allows temporarily ignoring certain environment location updates.
|
|
8
|
+
let shouldIgnoreEnvironmentLocationUpdates = false;
|
|
9
|
+
const ignoreEnvironmentLocationUpdates = func => {
|
|
10
|
+
shouldIgnoreEnvironmentLocationUpdates = true;
|
|
11
|
+
func();
|
|
12
|
+
shouldIgnoreEnvironmentLocationUpdates = false;
|
|
13
|
+
};
|
|
14
|
+
return [
|
|
15
|
+
// Validates that the action "payload" (input location) is a proper `NormalizedInputLocation`.
|
|
16
|
+
normalizeInputLocationMiddleware,
|
|
17
|
+
// Transforms a "PUSH" / "REPLACE" action into a "NAVIGATE" action.
|
|
18
|
+
navigationActionMiddleware,
|
|
19
|
+
// If a website is hosted under a certain path (`basePath`)
|
|
20
|
+
// then this middleware will automatically strip that starting segment from the `pathname` of `location`s.
|
|
21
|
+
createBasePathMiddleware(options && options.basePath),
|
|
22
|
+
// Allows blocking navigation.
|
|
23
|
+
// Handles `NAVIGATE` actions dispatched by the application itself.
|
|
24
|
+
createNavigationBlockerMiddleware(environment, {
|
|
25
|
+
ignoreEnvironmentLocationUpdates
|
|
26
|
+
}),
|
|
27
|
+
// This "middleware" performs the actual navigation according to the `environment` being used.
|
|
28
|
+
// For example, when `BrowserEnvironment` is used, it calls methods of the `history` object.
|
|
29
|
+
createEnvironmentMiddleware(environment, {
|
|
30
|
+
shouldIgnoreEnvironmentLocationUpdates: () => shouldIgnoreEnvironmentLocationUpdates
|
|
31
|
+
}),
|
|
32
|
+
// Allows blocking navigation.
|
|
33
|
+
// Handles location `UPDATE` actions dispatched by the environment.
|
|
34
|
+
createNavigationBlockerMiddleware(environment, {
|
|
35
|
+
ignoreEnvironmentLocationUpdates
|
|
36
|
+
})];
|
|
37
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import getLocationUrl from '../getLocationUrl';
|
|
2
|
+
import parseQueryFromSearch from '../parseQueryFromSearch';
|
|
3
|
+
export default class BrowserEnvironment {
|
|
4
|
+
constructor() {
|
|
5
|
+
this._keyPrefix = Math.random().toString(36).slice(2, 8);
|
|
6
|
+
this._keyIndex = 0;
|
|
7
|
+
this._index = null;
|
|
8
|
+
}
|
|
9
|
+
init() {
|
|
10
|
+
const {
|
|
11
|
+
pathname,
|
|
12
|
+
search,
|
|
13
|
+
hash
|
|
14
|
+
} = window.location;
|
|
15
|
+
const {
|
|
16
|
+
key,
|
|
17
|
+
index = 0,
|
|
18
|
+
state
|
|
19
|
+
} = window.history.state || {};
|
|
20
|
+
const delta = this._index != null ? index - this._index : 0;
|
|
21
|
+
this._index = index;
|
|
22
|
+
return {
|
|
23
|
+
action: 'POP',
|
|
24
|
+
pathname,
|
|
25
|
+
search,
|
|
26
|
+
query: search && parseQueryFromSearch(search),
|
|
27
|
+
hash,
|
|
28
|
+
key,
|
|
29
|
+
index,
|
|
30
|
+
delta,
|
|
31
|
+
state
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Subscribes to changes in location,
|
|
36
|
+
// excluding ones that happened as a result of calling `.navigate()`.
|
|
37
|
+
subscribe(listener) {
|
|
38
|
+
const onPopState = () => {
|
|
39
|
+
listener(this.init());
|
|
40
|
+
};
|
|
41
|
+
window.addEventListener('popstate', onPopState);
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener('popstate', onPopState);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
navigate(location) {
|
|
47
|
+
const {
|
|
48
|
+
action,
|
|
49
|
+
state
|
|
50
|
+
} = location;
|
|
51
|
+
const push = action === 'PUSH';
|
|
52
|
+
if (!push && action !== 'REPLACE') {
|
|
53
|
+
throw Error(`Unrecognized browser environment action: ${action}`);
|
|
54
|
+
}
|
|
55
|
+
const delta = push ? 1 : 0;
|
|
56
|
+
const extraState = this._createExtraState(delta);
|
|
57
|
+
const browserState = Object.assign({
|
|
58
|
+
state
|
|
59
|
+
}, extraState);
|
|
60
|
+
const url = getLocationUrl(location);
|
|
61
|
+
if (push) {
|
|
62
|
+
window.history.pushState(browserState, null, url);
|
|
63
|
+
} else {
|
|
64
|
+
window.history.replaceState(browserState, null, url);
|
|
65
|
+
}
|
|
66
|
+
return Object.assign({}, location, extraState, {
|
|
67
|
+
delta
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
shift(delta) {
|
|
71
|
+
window.history.go(delta);
|
|
72
|
+
}
|
|
73
|
+
addBeforeDestroyListener(onBeforeDestroy) {
|
|
74
|
+
const onBeforeUnload = event => {
|
|
75
|
+
if (onBeforeDestroy()) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
80
|
+
return () => {
|
|
81
|
+
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
_createExtraState(delta) {
|
|
85
|
+
const keyIndex = this._keyIndex++;
|
|
86
|
+
this._index += delta;
|
|
87
|
+
return {
|
|
88
|
+
key: `${this._keyPrefix}:${keyIndex.toString(36)}`,
|
|
89
|
+
index: this._index
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Returns either a `string` value or `null` if the key doesn't exist.
|
|
94
|
+
getState(key) {
|
|
95
|
+
// FYI: `sessionStorage` persists across page reloads.
|
|
96
|
+
return window.sessionStorage.getItem(key);
|
|
97
|
+
}
|
|
98
|
+
removeState(key) {
|
|
99
|
+
window.sessionStorage.removeItem(key);
|
|
100
|
+
}
|
|
101
|
+
setState(key, value) {
|
|
102
|
+
window.sessionStorage.setItem(key, value);
|
|
103
|
+
}
|
|
104
|
+
}
|