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
package/.babelrc.cjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = (api) => ({
|
|
2
|
+
presets: [
|
|
3
|
+
[
|
|
4
|
+
'@4c',
|
|
5
|
+
{
|
|
6
|
+
modules: api.env() === 'esm' ? false : 'commonjs',
|
|
7
|
+
},
|
|
8
|
+
],
|
|
9
|
+
],
|
|
10
|
+
plugins: [api.env() !== 'esm' && 'add-module-exports'].filter(Boolean),
|
|
11
|
+
|
|
12
|
+
env: {
|
|
13
|
+
test: {
|
|
14
|
+
plugins: ['istanbul'],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
package/.eslintignore
ADDED
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Build and test
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [master]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [master]
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
DISPLAY: :99.0
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
browser: ['ChromeCi', 'Firefox']
|
|
17
|
+
node-version: [16.x]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v2
|
|
21
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v1
|
|
23
|
+
with:
|
|
24
|
+
node-version: ${{ matrix.node-version }}
|
|
25
|
+
|
|
26
|
+
- name: Setup firefox
|
|
27
|
+
if: ${{ matrix.browser == 'Firefox' }}
|
|
28
|
+
uses: browser-actions/setup-firefox@latest
|
|
29
|
+
with:
|
|
30
|
+
firefox-version: 'latest'
|
|
31
|
+
- name: Setup xvfb
|
|
32
|
+
run: |
|
|
33
|
+
sudo apt-get install xvfb
|
|
34
|
+
Xvfb $DISPLAY -screen 0 1024x768x24 > /dev/null 2>&1 &
|
|
35
|
+
- run: yarn install --frozen-lockfile
|
|
36
|
+
- env:
|
|
37
|
+
BROWSER: ${{ matrix.browser }}
|
|
38
|
+
run: yarn test --coverage
|
|
39
|
+
- run: node_modules/.bin/codecov
|
|
Binary file
|
package/.yarnrc.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodeLinker: node-modules
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and free environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a censorship-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
education, socio-economic status, nationality, personal appearance, race,
|
|
10
|
+
religion, or sexual identity and orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating an open and free environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
- Not constraining the language to be "welcoming" or "inclusive"
|
|
18
|
+
- Not demanding show of empathy towards other community members
|
|
19
|
+
- Not dictating anyone to be respectful of differing viewpoints and experiences
|
|
20
|
+
- Not forcing anyone to change their views or opinions regardless of those
|
|
21
|
+
- Not intimidating other people into accepting your own views or opinions
|
|
22
|
+
- Not blackmailing other people to disclose their personal views or opinions
|
|
23
|
+
- Not constraining other people from publishing their personal views or opinions in an unintrusive way
|
|
24
|
+
- Focusing on what is best for the ecosystem
|
|
25
|
+
|
|
26
|
+
Examples of acceptable behavior by participants include:
|
|
27
|
+
|
|
28
|
+
- The use of sexualized language
|
|
29
|
+
- Occasional trolling or insulting comments that are not completely off-topic
|
|
30
|
+
|
|
31
|
+
Examples of unacceptable behavior by participants include:
|
|
32
|
+
|
|
33
|
+
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
|
34
|
+
- Unwelcome sexual attention or advances
|
|
35
|
+
- Public harassment or personal attacks when carried out in an bold or intrusive way
|
|
36
|
+
- Private harassment
|
|
37
|
+
- Any actions that are in violation of the local laws or otherwise considered illegal
|
|
38
|
+
- Other conduct which could reasonably be considered inappropriate in an open and free setting
|
|
39
|
+
|
|
40
|
+
## Our Responsibilities
|
|
41
|
+
|
|
42
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
43
|
+
behavior and are free to take appropriate and fair corrective action in
|
|
44
|
+
response to any instances of unacceptable behavior.
|
|
45
|
+
|
|
46
|
+
Project maintainers have the right and authority to remove, edit, or
|
|
47
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
48
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
49
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
50
|
+
threatening, offensive, or harmful.
|
|
51
|
+
|
|
52
|
+
## Scope
|
|
53
|
+
|
|
54
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
55
|
+
when an individual is representing the project or its community. Examples of
|
|
56
|
+
representing a project or community include using an official project e-mail
|
|
57
|
+
address, posting via an official social media account, or acting as an appointed
|
|
58
|
+
representative at an online or offline event. Representation of a project may be
|
|
59
|
+
further defined and clarified by project maintainers.
|
|
60
|
+
|
|
61
|
+
## Enforcement
|
|
62
|
+
|
|
63
|
+
Instances of unacceptable behavior may be reported by contacting the project team.
|
|
64
|
+
The complaints will likely be reviewed and investigated and may result in a response that
|
|
65
|
+
is deemed necessary and appropriate to the circumstances. The project team should maintain confidentiality with regard to the reporter of an incident.
|
|
66
|
+
Further details of specific enforcement policies may be posted separately.
|
|
67
|
+
|
|
68
|
+
Project maintainers who do not follow the Code of Conduct in good
|
|
69
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
70
|
+
members of the project's leadership.
|
|
71
|
+
|
|
72
|
+
## Attribution
|
|
73
|
+
|
|
74
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
75
|
+
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
76
|
+
|
|
77
|
+
[homepage]: https://www.contributor-covenant.org
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 4Catalyzer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
2
|
+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
3
|
+
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
|
|
4
|
+
|
|
5
|
+
- [navigation-stack](#navigation-stack)
|
|
6
|
+
- [Install](#install)
|
|
7
|
+
- [Use](#use)
|
|
8
|
+
- [Current Location](#current-location)
|
|
9
|
+
- [Why Redux?](#why-redux)
|
|
10
|
+
- [Environment](#environment)
|
|
11
|
+
- [Base Path](#base-path)
|
|
12
|
+
- [Location State Storage](#location-state-storage)
|
|
13
|
+
- [Block Navigation](#block-navigation)
|
|
14
|
+
- [Utility](#utility)
|
|
15
|
+
- [Development](#development)
|
|
16
|
+
|
|
17
|
+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
18
|
+
|
|
19
|
+
# navigation-stack
|
|
20
|
+
|
|
21
|
+
[](https://www.npmjs.com/package/navigation-stack)
|
|
22
|
+
[](https://www.npmjs.com/package/navigation-stack)
|
|
23
|
+
|
|
24
|
+
Handles web browser navigation in a web application.
|
|
25
|
+
|
|
26
|
+
Originally forked from [`farce`](http://npmjs.com/package/farce) package to fix a [bug](https://github.com/4Catalyzer/farce/issues/483).
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
npm install navigation-stack
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
`navigation-stack` provides "middlewares", "actions" and a "reducer" that could be used with `redux` or any other `redux`-compatible package such as [`mini-redux`](https://www.npmjs.com/package/mini-redux).
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { createStore, applyMiddleware } from 'redux'
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
createMiddlewares,
|
|
43
|
+
locationReducer,
|
|
44
|
+
Actions,
|
|
45
|
+
BrowserEnvironment
|
|
46
|
+
} from 'navigation-stack'
|
|
47
|
+
|
|
48
|
+
const store = createStore(
|
|
49
|
+
locationReducer, // Reducer function. For example, `locationReducer()`.
|
|
50
|
+
applyMiddleware(...createMiddlewares(new BrowserEnvironment()))
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
store.dispatch(Actions.init())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After that, dispatch any of the `Actions` in order to navigate.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// To navigate to a new page.
|
|
60
|
+
store.dispatch(Actions.push('/new/location'))
|
|
61
|
+
|
|
62
|
+
// To redirect to a new page.
|
|
63
|
+
store.dispatch(Actions.replace('/new/location'))
|
|
64
|
+
|
|
65
|
+
// To go back.
|
|
66
|
+
store.dispatch(Actions.shift(-1))
|
|
67
|
+
|
|
68
|
+
// To go forward.
|
|
69
|
+
store.dispatch(Actions.shift(1))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To view the current location:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
// When `locationReducer()` is used,
|
|
76
|
+
// `store.getState()` is the current location.
|
|
77
|
+
console.log(store.getState())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
(optional) (advanced) Stop and clean up:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
store.dispatch(Actions.dispose())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Current Location
|
|
87
|
+
|
|
88
|
+
To track the current location, the application could listen to `ActionTypes.UPDATE` action. The `payload` of the action is the current location.
|
|
89
|
+
|
|
90
|
+
For example, below is the source code for the default `locationReducer`.
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { ActionTypes } from 'navigation-stack'
|
|
94
|
+
|
|
95
|
+
// With this reducer, `state` would always tell the current location.
|
|
96
|
+
function reducer(state, action) {
|
|
97
|
+
if (action.type === ActionTypes.UPDATE) {
|
|
98
|
+
// `action.payload` is the current location.
|
|
99
|
+
return action.payload
|
|
100
|
+
}
|
|
101
|
+
return state
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Calling `store.dispatch(Actions.init())` will trigger the initial `ActionTypes.UPDATE` action which will set the current location. From then on, the current location will always stay in sync with the web browser's URL bar.
|
|
106
|
+
|
|
107
|
+
The current location will also "magically" be updated when the user clicks "Back" or "Forward" button in the web browser.
|
|
108
|
+
|
|
109
|
+
## Why Redux?
|
|
110
|
+
|
|
111
|
+
Why complicate things by providing "middlewares", "actions" and a "reducer" when it could be just a conventional API? That's because always knowing the "current location" means having to deal with "state management" in one way or another, and the simplest and most popular "state management" toolkit to date seems to be Redux.
|
|
112
|
+
|
|
113
|
+
If it was just about dispatching the `Actions` then of course it wouldn't require any "state management". But it's the "get current location" piece that changes the whole picture. One could say that using Redux for such a simple task is an overkill but actually reinventing a wheel is what I would consider "overkill". It's like crafting your own screwdriver just because the one from Walmart feels too bulky.
|
|
114
|
+
|
|
115
|
+
## Environment
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import {
|
|
119
|
+
BrowserEnvironment,
|
|
120
|
+
ServerEnvironment,
|
|
121
|
+
MemoryEnvironment
|
|
122
|
+
} from 'navigation-stack'
|
|
123
|
+
|
|
124
|
+
new BrowserEnvironment()
|
|
125
|
+
new ServerEnvironment('/location-url')
|
|
126
|
+
new MemoryEnvironment('/location-url')
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- Use `BrowserEnvironment` in a web browser.
|
|
130
|
+
- Use `ServerEnvironment` in server-side rendering.
|
|
131
|
+
- Use `MemoryEnvironment` in tests.
|
|
132
|
+
- `MemoryEnvironment` supports an optional second argument — an `options` object with properties:
|
|
133
|
+
- `save(state)` — Saves the environment state.
|
|
134
|
+
- `load()` — Loads a previously-saved environment state.
|
|
135
|
+
|
|
136
|
+
## Base Path
|
|
137
|
+
|
|
138
|
+
If the web application is hosted under a certain URL prefix, it should be specified in `createMiddlewares()` call as `basePath` parameter.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
createMiddlewares(environment, { basePath?: '/base/path' })
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Location State Storage
|
|
145
|
+
|
|
146
|
+
One could use an environment-specific `LocationStateStorage` in order to store location-specific state. For example, one could store scroll position of a page and then restore that scroll position when the user decides to navigate "Back" to the page.
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { BrowserEnvironment, LocationStateStorage } from 'navigation-stack'
|
|
150
|
+
|
|
151
|
+
const environment = new BrowserEnvironment()
|
|
152
|
+
|
|
153
|
+
const storage = new LocationStateStorage(environment, { namespace?: 'optional-namespace' })
|
|
154
|
+
|
|
155
|
+
const location = { pathname: '/abc' }
|
|
156
|
+
|
|
157
|
+
storage.set(location, 'key', 123)
|
|
158
|
+
storage.get(location, 'key') === 123
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`LocationStateStorage` doesn't provide any guarantees about actually storing the data: if it encounters any errors in the process, it simply ignores them. This simplifies the API in a way that the application doesn't have to wrap `.get()`/`.set()` calls in a `try/catch` block. And judging by the nature of location-specific state, that type of data is inherently non-essential and rather "nice-to-have".
|
|
162
|
+
|
|
163
|
+
## Block Navigation
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
import { createStore, applyMiddleware } from 'redux'
|
|
167
|
+
|
|
168
|
+
import {
|
|
169
|
+
createMiddlewares,
|
|
170
|
+
locationReducer,
|
|
171
|
+
Actions,
|
|
172
|
+
BrowserEnvironment,
|
|
173
|
+
addNavigationBlocker
|
|
174
|
+
} from 'navigation-stack'
|
|
175
|
+
|
|
176
|
+
const environment = new BrowserEnvironment()
|
|
177
|
+
|
|
178
|
+
const store = createStore(
|
|
179
|
+
locationReducer, // Reducer function. For example, `locationReducer()`.
|
|
180
|
+
applyMiddleware(...createMiddlewares(environment))
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
store.dispatch(Actions.init())
|
|
184
|
+
|
|
185
|
+
const removeNavigationBlocker = addNavigationBlocker(
|
|
186
|
+
environment,
|
|
187
|
+
(newLocation) => {
|
|
188
|
+
// Returning `true` means "block this navigation".
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// This navigation won't be performed.
|
|
194
|
+
store.dispatch(Actions.push('/new/location'))
|
|
195
|
+
|
|
196
|
+
// Disable the navigation blocker.
|
|
197
|
+
removeNavigationBlocker()
|
|
198
|
+
|
|
199
|
+
// This navigation now will be performed.
|
|
200
|
+
store.dispatch(Actions.push('/new/location'))
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Navigation blocker should be a function that receives a `newLocation` argument and could be "synchronous" or "asynchronous" (i.e. return a `Promise`, aka `async`/`await`).
|
|
204
|
+
|
|
205
|
+
Navigation blockers fire both when navigating from one page to another and when closing the current browser tab. In the latter case, `newLocation` argument will be `null`, the function can't return a `Promise`, and returning `true` will cause the web browser to show a confirmation modal with a non-customizable browser-specific text.
|
|
206
|
+
|
|
207
|
+
## Utility
|
|
208
|
+
|
|
209
|
+
This package exports a couple of utility functions.
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
import {
|
|
213
|
+
addBasePath,
|
|
214
|
+
removeBasePath,
|
|
215
|
+
getLocationUrl,
|
|
216
|
+
parseLocationUrl
|
|
217
|
+
} from 'navigation-stack'
|
|
218
|
+
|
|
219
|
+
// Parses a location URL to a location object.
|
|
220
|
+
// If there're no query parameters, `query` property will not be added.
|
|
221
|
+
parseLocationUrl('/abc?d=e') === {
|
|
222
|
+
pathname: '/abc',
|
|
223
|
+
search: '?d=e',
|
|
224
|
+
query: { d: 'e' },
|
|
225
|
+
hash: ''
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Converts a location object to a location URL.
|
|
229
|
+
getLocationUrl({ pathname: '/abc', search: '?d=e', hash: '' }) === '/abc?d=e'
|
|
230
|
+
|
|
231
|
+
// Adds `basePath` to a location object or a location URL.
|
|
232
|
+
addBasePath('/abc', '/base-path') === '/base-path/abc'
|
|
233
|
+
addBasePath({ pathname: '/abc' }, '/base-path') === { pathname: '/base-path/abc' }
|
|
234
|
+
|
|
235
|
+
// Removes `basePath` from a location object or a location URL.
|
|
236
|
+
// If `basePath` is not present in location, it won't do anything.
|
|
237
|
+
removeBasePath('/base-path/abc', '/base-path') === '/abc';
|
|
238
|
+
removeBasePath({ pathname: '/base-path/abc' }, '/base-path') === { pathname: '/abc' }
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
Clone the repository. Then:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
yarn
|
|
247
|
+
yarn format
|
|
248
|
+
yarn test
|
|
249
|
+
```
|
package/codecov.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
comment: off
|
package/karma.conf.cjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// An attempted rewrite in ESM threw an error: "ReferenceError: require is not defined"
|
|
2
|
+
// when processing `import` in the `.js` files.
|
|
3
|
+
//
|
|
4
|
+
// import puppeteer from 'puppeteer';
|
|
5
|
+
// import webpack from 'webpack'; // eslint-disable-line import/no-extraneous-dependencies
|
|
6
|
+
//
|
|
7
|
+
// process.env.CHROME_BIN = puppeteer.executablePath();
|
|
8
|
+
|
|
9
|
+
const webpack = require('webpack'); // eslint-disable-line import/no-extraneous-dependencies
|
|
10
|
+
|
|
11
|
+
process.env.CHROME_BIN = require('puppeteer').executablePath();
|
|
12
|
+
|
|
13
|
+
module.exports = (config) => {
|
|
14
|
+
const { env } = process;
|
|
15
|
+
|
|
16
|
+
config.set({
|
|
17
|
+
frameworks: ['mocha', 'webpack', 'sinon-chai'],
|
|
18
|
+
|
|
19
|
+
files: ['test/index.js', { pattern: 'test/**/*.test.js', watched: false }],
|
|
20
|
+
|
|
21
|
+
preprocessors: {
|
|
22
|
+
'test/**/*.js': ['webpack', 'sourcemap'],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
webpack: {
|
|
26
|
+
mode: 'development',
|
|
27
|
+
module: {
|
|
28
|
+
rules: [
|
|
29
|
+
{ test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
plugins: [
|
|
33
|
+
new webpack.DefinePlugin({
|
|
34
|
+
__DEV__: true,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
webpackMiddleware: {
|
|
40
|
+
noInfo: true,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
reporters: ['mocha', 'coverage'],
|
|
44
|
+
|
|
45
|
+
mochaReporter: {
|
|
46
|
+
output: 'autowatch',
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
coverageReporter: {
|
|
50
|
+
type: 'lcov',
|
|
51
|
+
dir: 'coverage',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
customLaunchers: {
|
|
55
|
+
ChromeCi: {
|
|
56
|
+
base: 'ChromeHeadless',
|
|
57
|
+
flags: ['--no-sandbox'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome'],
|
|
62
|
+
});
|
|
63
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = void 0;
|
|
5
|
+
var _default = exports.default = {
|
|
6
|
+
INIT: '@@navigation-stack/INIT',
|
|
7
|
+
PUSH: '@@navigation-stack/PUSH',
|
|
8
|
+
REPLACE: '@@navigation-stack/REPLACE',
|
|
9
|
+
NAVIGATE: '@@navigation-stack/NAVIGATE',
|
|
10
|
+
SHIFT: '@@navigation-stack/SHIFT',
|
|
11
|
+
UPDATE: '@@navigation-stack/UPDATE',
|
|
12
|
+
DISPOSE: '@@navigation-stack/DISPOSE'
|
|
13
|
+
};
|
|
14
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = void 0;
|
|
5
|
+
var _ActionTypes = _interopRequireDefault(require("./ActionTypes"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
var _default = exports.default = {
|
|
8
|
+
init: () => ({
|
|
9
|
+
type: _ActionTypes.default.INIT
|
|
10
|
+
}),
|
|
11
|
+
push: location => ({
|
|
12
|
+
type: _ActionTypes.default.PUSH,
|
|
13
|
+
payload: location
|
|
14
|
+
}),
|
|
15
|
+
replace: location => ({
|
|
16
|
+
type: _ActionTypes.default.REPLACE,
|
|
17
|
+
payload: location
|
|
18
|
+
}),
|
|
19
|
+
shift: delta => ({
|
|
20
|
+
type: _ActionTypes.default.SHIFT,
|
|
21
|
+
payload: delta
|
|
22
|
+
}),
|
|
23
|
+
dispose: () => ({
|
|
24
|
+
type: _ActionTypes.default.DISPOSE
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = void 0;
|
|
5
|
+
var _getLocationUrl = _interopRequireDefault(require("./getLocationUrl"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
class LocationStateStorage {
|
|
8
|
+
constructor(environment, {
|
|
9
|
+
namespace
|
|
10
|
+
} = {}) {
|
|
11
|
+
this._environment = environment;
|
|
12
|
+
this._getFallbackLocationKey = _getLocationUrl.default;
|
|
13
|
+
this._stateKeyPrefix = namespace ? `${namespace}|` : '';
|
|
14
|
+
}
|
|
15
|
+
get(location, key) {
|
|
16
|
+
const stateKey = this._getStateKey(location, key);
|
|
17
|
+
try {
|
|
18
|
+
const value = this._environment.getState(stateKey);
|
|
19
|
+
// === null is probably sufficient.
|
|
20
|
+
if (value === null) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// We want to catch JSON parse errors in case someone separately threw
|
|
25
|
+
// junk into sessionStorage under our namespace.
|
|
26
|
+
return JSON.parse(value);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// Pretend that the entry doesn't exist.
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
set(location, key, value) {
|
|
33
|
+
const stateKey = this._getStateKey(location, key);
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
try {
|
|
36
|
+
this._environment.removeState(stateKey);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// No need to handle errors here.
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Unlike with read, we want to fail on invalid values here, since the
|
|
44
|
+
// value here is provided by the caller of this method.
|
|
45
|
+
const valueString = JSON.stringify(value);
|
|
46
|
+
try {
|
|
47
|
+
this._environment.setState(stateKey, valueString);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// No need to handle errors here either. If it didn't work, it didn't
|
|
50
|
+
// work. We make no guarantees about actually saving the value.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
_getStateKey(location, key) {
|
|
54
|
+
const locationKey = location.key || this._getFallbackLocationKey(location);
|
|
55
|
+
const keyPrefix = `${this._stateKeyPrefix}${locationKey}`;
|
|
56
|
+
return `${keyPrefix}|${key}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.default = LocationStateStorage;
|
|
60
|
+
module.exports = exports.default;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.addBasePath = addBasePath;
|
|
5
|
+
exports.removeBasePath = removeBasePath;
|
|
6
|
+
function normalizeBasePath(basePath) {
|
|
7
|
+
if (!basePath || basePath === '/') {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Validate `basePath`.
|
|
12
|
+
if (basePath[0] !== '/') {
|
|
13
|
+
throw new Error('`basePath` must start with a slash');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Remove trailing slash from `basePath`.
|
|
17
|
+
if (basePath.slice(-1) === '/') {
|
|
18
|
+
basePath = basePath.slice(0, -1);
|
|
19
|
+
}
|
|
20
|
+
return basePath;
|
|
21
|
+
}
|
|
22
|
+
function removeBasePathFromRelativeUrl(url, basePath) {
|
|
23
|
+
if (url.indexOf(basePath) === 0) {
|
|
24
|
+
// `farce` had a bug here:
|
|
25
|
+
// `location.pathname` is supposed to always be non-empty.
|
|
26
|
+
// If `basePath` is set to `/basePath` and the user navigates to `/basePath` URL,
|
|
27
|
+
// originally here it would simply strips the whole string from the URL
|
|
28
|
+
// and the result would be incorrect: `pathname: ""`.
|
|
29
|
+
// The fix below is adding `|| '/'` in the `return` statement.
|
|
30
|
+
// https://github.com/4Catalyzer/farce/issues/483
|
|
31
|
+
return url.slice(basePath.length) || '/';
|
|
32
|
+
}
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
function addBasePath(location, basePath) {
|
|
36
|
+
basePath = normalizeBasePath(basePath);
|
|
37
|
+
if (!basePath) {
|
|
38
|
+
return location;
|
|
39
|
+
}
|
|
40
|
+
if (typeof location === 'string') {
|
|
41
|
+
return `${basePath}${location}`;
|
|
42
|
+
}
|
|
43
|
+
return Object.assign({}, location, {
|
|
44
|
+
pathname: `${basePath}${location.pathname}`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function removeBasePath(location, basePath) {
|
|
48
|
+
basePath = normalizeBasePath(basePath);
|
|
49
|
+
if (!basePath) {
|
|
50
|
+
return location;
|
|
51
|
+
}
|
|
52
|
+
if (typeof location === 'string') {
|
|
53
|
+
return removeBasePathFromRelativeUrl(location, basePath);
|
|
54
|
+
}
|
|
55
|
+
return Object.assign({}, location, {
|
|
56
|
+
pathname: removeBasePathFromRelativeUrl(location.pathname, basePath)
|
|
57
|
+
});
|
|
58
|
+
}
|