saga-toolkit 2.1.1 → 2.2.1
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/README.md +106 -92
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +255 -0
- package/coverage/coverage-final.json +5 -0
- package/coverage/effects.ts.html +406 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +161 -0
- package/coverage/index.ts.html +190 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/types.ts.html +139 -0
- package/coverage/utils.ts.html +316 -0
- package/eslint.config.mjs +20 -0
- package/example/package-lock.json +650 -290
- package/example/package.json +1 -1
- package/package.json +33 -10
- package/src/effects.ts +107 -0
- package/src/index.ts +35 -0
- package/src/types.ts +18 -0
- package/src/utils.ts +77 -0
- package/test/sagaToolkit.test.ts +126 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +8 -0
- package/index.d.ts +0 -72
- package/index.js +0 -176
- package/sagaToolkit.js +0 -152
package/README.md
CHANGED
|
@@ -1,135 +1,149 @@
|
|
|
1
1
|
# Saga Toolkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Seamlessly integrate Redux Toolkit with Redux Saga.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`saga-toolkit` acts as a bridge between Redux Toolkit's `createAsyncThunk` and Redux Saga. It allows you to dispatch actions that trigger Sagas, while retaining the ability to `await` the result (promise) directly in your components or thunks.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
If you love the "fire-and-forget" nature of Sagas for complex flows but miss the convenience of `await dispatch(action)` for simple request/response patterns (like form submissions), this library is for you.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Features
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- 🤝 **Bridge Pattern**: Connects `createAsyncThunk` (RTK) with `takeEvery` / `takeLatest` (Saga).
|
|
12
|
+
- 🔄 **Promise Support**: `await` your Saga actions in React components.
|
|
13
|
+
- ⚡ **Reduce Boilerplate**: Easily handle loading/success/error states in slices using standard RTK patterns.
|
|
14
|
+
- 🛑 **Cancellation**: Propagates cancellation from the promise to the Saga.
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install saga-toolkit
|
|
20
|
+
# or
|
|
21
|
+
yarn add saga-toolkit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
*Peer Dependencies: `@reduxjs/toolkit`, `redux-saga`*
|
|
25
|
+
|
|
26
|
+
## Usage Guide
|
|
27
|
+
|
|
28
|
+
### 1. Create a "Saga Action"
|
|
29
|
+
|
|
30
|
+
Instead of `createAsyncThunk` or standard standard action creators, use `createSagaAction`. This creates a thunk that returns a promise which your Saga will resolve or reject.
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
/* slice.js */
|
|
15
34
|
import { createSlice } from '@reduxjs/toolkit'
|
|
16
|
-
import { createSagaAction
|
|
35
|
+
import { createSagaAction } from 'saga-toolkit'
|
|
36
|
+
|
|
37
|
+
const name = 'users'
|
|
17
38
|
|
|
18
|
-
|
|
39
|
+
// Define the action
|
|
40
|
+
export const fetchUser = createSagaAction(`${name}/fetchUser`)
|
|
19
41
|
|
|
20
42
|
const initialState = {
|
|
21
|
-
|
|
22
|
-
result: null,
|
|
43
|
+
data: null,
|
|
23
44
|
loading: false,
|
|
24
45
|
error: null,
|
|
25
46
|
}
|
|
26
47
|
|
|
27
|
-
export const appStart = createSagaAction(`${name}/appStart`)
|
|
28
|
-
export const fetchThings = createSagaAction(`${name}/fetchThings`)
|
|
29
|
-
|
|
30
48
|
const slice = createSlice({
|
|
31
49
|
name,
|
|
32
50
|
initialState,
|
|
33
|
-
extraReducers: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}),
|
|
48
|
-
[appStart.fulfilled]: state => {
|
|
49
|
-
state.started = true // immer allows this
|
|
50
|
-
return state
|
|
51
|
-
},
|
|
51
|
+
extraReducers: (builder) => {
|
|
52
|
+
builder
|
|
53
|
+
.addCase(fetchUser.pending, (state) => {
|
|
54
|
+
state.loading = true
|
|
55
|
+
state.error = null
|
|
56
|
+
})
|
|
57
|
+
.addCase(fetchUser.fulfilled, (state, { payload }) => {
|
|
58
|
+
state.loading = false
|
|
59
|
+
state.data = payload
|
|
60
|
+
})
|
|
61
|
+
.addCase(fetchUser.rejected, (state, { error }) => {
|
|
62
|
+
state.loading = false
|
|
63
|
+
state.error = error
|
|
64
|
+
})
|
|
52
65
|
},
|
|
53
66
|
})
|
|
54
67
|
|
|
55
68
|
export default slice.reducer
|
|
56
69
|
```
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
```js
|
|
60
|
-
import { put, call } from 'redux-saga/effects'
|
|
61
|
-
import { takeEveryAsync, putAsync } from 'saga-toolkit'
|
|
62
|
-
import API from 'hyper-super-api'
|
|
63
|
-
import * as actions from './slice'
|
|
64
|
-
|
|
65
|
-
function* appStart() {
|
|
66
|
-
const promise = yield put(fetchThings({ someArg: 'example' }))
|
|
67
|
-
try {
|
|
68
|
-
const fetchThingsActionFulfulled = yield promise // optionally we can wait for an action to finish and get its result
|
|
69
|
-
} catch(error) {
|
|
70
|
-
// we can handle error to avoid appStart to get rejected if we want
|
|
71
|
-
}
|
|
72
|
-
return result
|
|
73
|
-
}
|
|
71
|
+
### 2. Connect to a Saga
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
73
|
+
Use `takeEveryAsync` (or `takeLatestAsync`, etc.) to listen for the action. The return value of your saga becomes the `fulfilled` payload. Throwing an error becomes the `rejected` payload.
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
/* sagas.js */
|
|
77
|
+
import { call } from 'redux-saga/effects'
|
|
78
|
+
import { takeEveryAsync } from 'saga-toolkit'
|
|
79
|
+
import { fetchUser } from './slice'
|
|
80
|
+
import API from './api'
|
|
84
81
|
|
|
85
|
-
function*
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
82
|
+
function* fetchUserSaga({ meta }) {
|
|
83
|
+
// meta.arg contains the argument passed to the dispatch
|
|
84
|
+
const userId = meta.arg
|
|
85
|
+
|
|
86
|
+
// The return value here resolves the promise!
|
|
87
|
+
const user = yield call(API.getUser, userId)
|
|
88
|
+
return user
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export default
|
|
92
|
-
takeEveryAsync(
|
|
93
|
-
|
|
94
|
-
]
|
|
91
|
+
export default function* rootSaga() {
|
|
92
|
+
yield takeEveryAsync(fetchUser.type, fetchUserSaga)
|
|
93
|
+
}
|
|
95
94
|
```
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
### 3. Dispatch and Await in Component
|
|
97
|
+
|
|
98
|
+
Now you can treat the Saga logic as if it were a simple async function.
|
|
100
99
|
|
|
101
|
-
|
|
100
|
+
```javascript
|
|
101
|
+
/* UserComponent.jsx */
|
|
102
|
+
import { useDispatch } from 'react-redux'
|
|
103
|
+
import { useEffect } from 'react'
|
|
104
|
+
import { fetchUser } from './slice'
|
|
105
|
+
|
|
106
|
+
const UserComponent = ({ id }) => {
|
|
102
107
|
const dispatch = useDispatch()
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
const handleFetch = async () => {
|
|
110
|
+
try {
|
|
111
|
+
// This waits for the Saga to finish!
|
|
112
|
+
const user = await dispatch(fetchUser(id)).unwrap()
|
|
113
|
+
console.log('Got user:', user)
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('Failed to fetch:', error)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
108
118
|
|
|
109
|
-
return
|
|
110
|
-
...
|
|
111
|
-
)
|
|
119
|
+
return <button onClick={handleFetch}>Load User</button>
|
|
112
120
|
}
|
|
113
121
|
```
|
|
114
122
|
|
|
115
|
-
##
|
|
123
|
+
## API Reference
|
|
116
124
|
|
|
117
|
-
|
|
125
|
+
### `createSagaAction(typePrefix)`
|
|
126
|
+
Creates a Redux Toolkit Async Thunk that is specially designed to work with the effects below.
|
|
127
|
+
- **Returns**: An enhanced thunk action creator.
|
|
118
128
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```
|
|
129
|
+
### `takeEveryAsync(pattern, saga, ...args)`
|
|
130
|
+
Spawns a `saga` on each action dispatched to the Store that matches `pattern`.
|
|
131
|
+
- Automatically resolves the promise associated with the action when the saga returns.
|
|
132
|
+
- Automatically rejects the promise if the saga errors.
|
|
124
133
|
|
|
125
|
-
|
|
134
|
+
### `takeLatestAsync(pattern, saga, ...args)`
|
|
135
|
+
Same as `takeEveryAsync`, but cancels any previous running task if a new matching action is dispatched.
|
|
136
|
+
- **Note**: Cancelled tasks will reject the promise with an "Aborted" error (or similar).
|
|
126
137
|
|
|
127
|
-
|
|
138
|
+
### `takeAggregateAsync(pattern, saga, ...args)`
|
|
139
|
+
Wait for the saga to finish for the first action. If subsequent actions with the same pattern are dispatched *while only one is running*, they will all share the **same promise result** as the first one.
|
|
140
|
+
- Useful for de-duplicating identical requests (e.g., multiple components requesting "load config" simultaneously).
|
|
128
141
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
### `putAsync(action)`
|
|
143
|
+
Dispatches an action to the store and waits for the result.
|
|
144
|
+
- Useful when you want to call another saga-action from within a saga and wait for it (composition).
|
|
145
|
+
- **Example**: `const result = yield putAsync(otherAction())`
|
|
146
|
+
|
|
147
|
+
## License
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
ISC
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
body, html {
|
|
2
|
+
margin:0; padding: 0;
|
|
3
|
+
height: 100%;
|
|
4
|
+
}
|
|
5
|
+
body {
|
|
6
|
+
font-family: Helvetica Neue, Helvetica, Arial;
|
|
7
|
+
font-size: 14px;
|
|
8
|
+
color:#333;
|
|
9
|
+
}
|
|
10
|
+
.small { font-size: 12px; }
|
|
11
|
+
*, *:after, *:before {
|
|
12
|
+
-webkit-box-sizing:border-box;
|
|
13
|
+
-moz-box-sizing:border-box;
|
|
14
|
+
box-sizing:border-box;
|
|
15
|
+
}
|
|
16
|
+
h1 { font-size: 20px; margin: 0;}
|
|
17
|
+
h2 { font-size: 14px; }
|
|
18
|
+
pre {
|
|
19
|
+
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
-moz-tab-size: 2;
|
|
23
|
+
-o-tab-size: 2;
|
|
24
|
+
tab-size: 2;
|
|
25
|
+
}
|
|
26
|
+
a { color:#0074D9; text-decoration:none; }
|
|
27
|
+
a:hover { text-decoration:underline; }
|
|
28
|
+
.strong { font-weight: bold; }
|
|
29
|
+
.space-top1 { padding: 10px 0 0 0; }
|
|
30
|
+
.pad2y { padding: 20px 0; }
|
|
31
|
+
.pad1y { padding: 10px 0; }
|
|
32
|
+
.pad2x { padding: 0 20px; }
|
|
33
|
+
.pad2 { padding: 20px; }
|
|
34
|
+
.pad1 { padding: 10px; }
|
|
35
|
+
.space-left2 { padding-left:55px; }
|
|
36
|
+
.space-right2 { padding-right:20px; }
|
|
37
|
+
.center { text-align:center; }
|
|
38
|
+
.clearfix { display:block; }
|
|
39
|
+
.clearfix:after {
|
|
40
|
+
content:'';
|
|
41
|
+
display:block;
|
|
42
|
+
height:0;
|
|
43
|
+
clear:both;
|
|
44
|
+
visibility:hidden;
|
|
45
|
+
}
|
|
46
|
+
.fl { float: left; }
|
|
47
|
+
@media only screen and (max-width:640px) {
|
|
48
|
+
.col3 { width:100%; max-width:100%; }
|
|
49
|
+
.hide-mobile { display:none!important; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.quiet {
|
|
53
|
+
color: #7f7f7f;
|
|
54
|
+
color: rgba(0,0,0,0.5);
|
|
55
|
+
}
|
|
56
|
+
.quiet a { opacity: 0.7; }
|
|
57
|
+
|
|
58
|
+
.fraction {
|
|
59
|
+
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
60
|
+
font-size: 10px;
|
|
61
|
+
color: #555;
|
|
62
|
+
background: #E8E8E8;
|
|
63
|
+
padding: 4px 5px;
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
vertical-align: middle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
div.path a:link, div.path a:visited { color: #333; }
|
|
69
|
+
table.coverage {
|
|
70
|
+
border-collapse: collapse;
|
|
71
|
+
margin: 10px 0 0 0;
|
|
72
|
+
padding: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
table.coverage td {
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 0;
|
|
78
|
+
vertical-align: top;
|
|
79
|
+
}
|
|
80
|
+
table.coverage td.line-count {
|
|
81
|
+
text-align: right;
|
|
82
|
+
padding: 0 5px 0 20px;
|
|
83
|
+
}
|
|
84
|
+
table.coverage td.line-coverage {
|
|
85
|
+
text-align: right;
|
|
86
|
+
padding-right: 10px;
|
|
87
|
+
min-width:20px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
table.coverage td span.cline-any {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
padding: 0 5px;
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
.missing-if-branch {
|
|
96
|
+
display: inline-block;
|
|
97
|
+
margin-right: 5px;
|
|
98
|
+
border-radius: 3px;
|
|
99
|
+
position: relative;
|
|
100
|
+
padding: 0 4px;
|
|
101
|
+
background: #333;
|
|
102
|
+
color: yellow;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.skip-if-branch {
|
|
106
|
+
display: none;
|
|
107
|
+
margin-right: 10px;
|
|
108
|
+
position: relative;
|
|
109
|
+
padding: 0 4px;
|
|
110
|
+
background: #ccc;
|
|
111
|
+
color: white;
|
|
112
|
+
}
|
|
113
|
+
.missing-if-branch .typ, .skip-if-branch .typ {
|
|
114
|
+
color: inherit !important;
|
|
115
|
+
}
|
|
116
|
+
.coverage-summary {
|
|
117
|
+
border-collapse: collapse;
|
|
118
|
+
width: 100%;
|
|
119
|
+
}
|
|
120
|
+
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
|
121
|
+
.keyline-all { border: 1px solid #ddd; }
|
|
122
|
+
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
|
123
|
+
.coverage-summary tbody { border: 1px solid #bbb; }
|
|
124
|
+
.coverage-summary td { border-right: 1px solid #bbb; }
|
|
125
|
+
.coverage-summary td:last-child { border-right: none; }
|
|
126
|
+
.coverage-summary th {
|
|
127
|
+
text-align: left;
|
|
128
|
+
font-weight: normal;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
.coverage-summary th.file { border-right: none !important; }
|
|
132
|
+
.coverage-summary th.pct { }
|
|
133
|
+
.coverage-summary th.pic,
|
|
134
|
+
.coverage-summary th.abs,
|
|
135
|
+
.coverage-summary td.pct,
|
|
136
|
+
.coverage-summary td.abs { text-align: right; }
|
|
137
|
+
.coverage-summary td.file { white-space: nowrap; }
|
|
138
|
+
.coverage-summary td.pic { min-width: 120px !important; }
|
|
139
|
+
.coverage-summary tfoot td { }
|
|
140
|
+
|
|
141
|
+
.coverage-summary .sorter {
|
|
142
|
+
height: 10px;
|
|
143
|
+
width: 7px;
|
|
144
|
+
display: inline-block;
|
|
145
|
+
margin-left: 0.5em;
|
|
146
|
+
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
|
147
|
+
}
|
|
148
|
+
.coverage-summary .sorted .sorter {
|
|
149
|
+
background-position: 0 -20px;
|
|
150
|
+
}
|
|
151
|
+
.coverage-summary .sorted-desc .sorter {
|
|
152
|
+
background-position: 0 -10px;
|
|
153
|
+
}
|
|
154
|
+
.status-line { height: 10px; }
|
|
155
|
+
/* yellow */
|
|
156
|
+
.cbranch-no { background: yellow !important; color: #111; }
|
|
157
|
+
/* dark red */
|
|
158
|
+
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
|
159
|
+
.low .chart { border:1px solid #C21F39 }
|
|
160
|
+
.highlighted,
|
|
161
|
+
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
|
162
|
+
background: #C21F39 !important;
|
|
163
|
+
}
|
|
164
|
+
/* medium red */
|
|
165
|
+
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
|
166
|
+
/* light red */
|
|
167
|
+
.low, .cline-no { background:#FCE1E5 }
|
|
168
|
+
/* light green */
|
|
169
|
+
.high, .cline-yes { background:rgb(230,245,208) }
|
|
170
|
+
/* medium green */
|
|
171
|
+
.cstat-yes { background:rgb(161,215,106) }
|
|
172
|
+
/* dark green */
|
|
173
|
+
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
|
174
|
+
.high .chart { border:1px solid rgb(77,146,33) }
|
|
175
|
+
/* dark yellow (gold) */
|
|
176
|
+
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
|
177
|
+
.medium .chart { border:1px solid #f9cd0b; }
|
|
178
|
+
/* light yellow */
|
|
179
|
+
.medium { background: #fff4c2; }
|
|
180
|
+
|
|
181
|
+
.cstat-skip { background: #ddd; color: #111; }
|
|
182
|
+
.fstat-skip { background: #ddd; color: #111 !important; }
|
|
183
|
+
.cbranch-skip { background: #ddd !important; color: #111; }
|
|
184
|
+
|
|
185
|
+
span.cline-neutral { background: #eaeaea; }
|
|
186
|
+
|
|
187
|
+
.coverage-summary td.empty {
|
|
188
|
+
opacity: .5;
|
|
189
|
+
padding-top: 4px;
|
|
190
|
+
padding-bottom: 4px;
|
|
191
|
+
line-height: 1;
|
|
192
|
+
color: #888;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.cover-fill, .cover-empty {
|
|
196
|
+
display:inline-block;
|
|
197
|
+
height: 12px;
|
|
198
|
+
}
|
|
199
|
+
.chart {
|
|
200
|
+
line-height: 0;
|
|
201
|
+
}
|
|
202
|
+
.cover-empty {
|
|
203
|
+
background: white;
|
|
204
|
+
}
|
|
205
|
+
.cover-full {
|
|
206
|
+
border-right: none !important;
|
|
207
|
+
}
|
|
208
|
+
pre.prettyprint {
|
|
209
|
+
border: none !important;
|
|
210
|
+
padding: 0 !important;
|
|
211
|
+
margin: 0 !important;
|
|
212
|
+
}
|
|
213
|
+
.com { color: #999 !important; }
|
|
214
|
+
.ignore-none { color: #999; font-weight: normal; }
|
|
215
|
+
|
|
216
|
+
.wrapper {
|
|
217
|
+
min-height: 100%;
|
|
218
|
+
height: auto !important;
|
|
219
|
+
height: 100%;
|
|
220
|
+
margin: 0 auto -48px;
|
|
221
|
+
}
|
|
222
|
+
.footer, .push {
|
|
223
|
+
height: 48px;
|
|
224
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
var jumpToCode = (function init() {
|
|
3
|
+
// Classes of code we would like to highlight in the file view
|
|
4
|
+
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
|
5
|
+
|
|
6
|
+
// Elements to highlight in the file listing view
|
|
7
|
+
var fileListingElements = ['td.pct.low'];
|
|
8
|
+
|
|
9
|
+
// We don't want to select elements that are direct descendants of another match
|
|
10
|
+
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
|
11
|
+
|
|
12
|
+
// Selector that finds elements on the page to which we can jump
|
|
13
|
+
var selector =
|
|
14
|
+
fileListingElements.join(', ') +
|
|
15
|
+
', ' +
|
|
16
|
+
notSelector +
|
|
17
|
+
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
|
18
|
+
|
|
19
|
+
// The NodeList of matching elements
|
|
20
|
+
var missingCoverageElements = document.querySelectorAll(selector);
|
|
21
|
+
|
|
22
|
+
var currentIndex;
|
|
23
|
+
|
|
24
|
+
function toggleClass(index) {
|
|
25
|
+
missingCoverageElements
|
|
26
|
+
.item(currentIndex)
|
|
27
|
+
.classList.remove('highlighted');
|
|
28
|
+
missingCoverageElements.item(index).classList.add('highlighted');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeCurrent(index) {
|
|
32
|
+
toggleClass(index);
|
|
33
|
+
currentIndex = index;
|
|
34
|
+
missingCoverageElements.item(index).scrollIntoView({
|
|
35
|
+
behavior: 'smooth',
|
|
36
|
+
block: 'center',
|
|
37
|
+
inline: 'center'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function goToPrevious() {
|
|
42
|
+
var nextIndex = 0;
|
|
43
|
+
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
|
44
|
+
nextIndex = missingCoverageElements.length - 1;
|
|
45
|
+
} else if (missingCoverageElements.length > 1) {
|
|
46
|
+
nextIndex = currentIndex - 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
makeCurrent(nextIndex);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function goToNext() {
|
|
53
|
+
var nextIndex = 0;
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
typeof currentIndex === 'number' &&
|
|
57
|
+
currentIndex < missingCoverageElements.length - 1
|
|
58
|
+
) {
|
|
59
|
+
nextIndex = currentIndex + 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
makeCurrent(nextIndex);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return function jump(event) {
|
|
66
|
+
if (
|
|
67
|
+
document.getElementById('fileSearch') === document.activeElement &&
|
|
68
|
+
document.activeElement != null
|
|
69
|
+
) {
|
|
70
|
+
// if we're currently focused on the search input, we don't want to navigate
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
switch (event.which) {
|
|
75
|
+
case 78: // n
|
|
76
|
+
case 74: // j
|
|
77
|
+
goToNext();
|
|
78
|
+
break;
|
|
79
|
+
case 66: // b
|
|
80
|
+
case 75: // k
|
|
81
|
+
case 80: // p
|
|
82
|
+
goToPrevious();
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
})();
|
|
87
|
+
window.addEventListener('keydown', jumpToCode);
|