mockaton 10.5.4 → 10.6.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/package.json +1 -1
- package/src/Dashboard.css +6 -15
- package/src/Dashboard.js +153 -58
package/package.json
CHANGED
package/src/Dashboard.css
CHANGED
|
@@ -95,7 +95,7 @@ body {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
select, a, input, button {
|
|
98
|
-
&:focus {
|
|
98
|
+
&:focus-visible {
|
|
99
99
|
outline: 2px solid var(--colorAccent);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -294,7 +294,6 @@ header {
|
|
|
294
294
|
fill: var(--colorAccent);
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
-
|
|
298
297
|
}
|
|
299
298
|
|
|
300
299
|
.SettingsMenu {
|
|
@@ -379,9 +378,9 @@ table {
|
|
|
379
378
|
border-collapse: collapse;
|
|
380
379
|
|
|
381
380
|
th {
|
|
382
|
-
padding-bottom:
|
|
381
|
+
padding-bottom: 4px;
|
|
383
382
|
padding-left: 4px;
|
|
384
|
-
border-top:
|
|
383
|
+
border-top: 24px solid transparent;
|
|
385
384
|
text-align: left;
|
|
386
385
|
}
|
|
387
386
|
|
|
@@ -422,6 +421,7 @@ table {
|
|
|
422
421
|
}
|
|
423
422
|
}
|
|
424
423
|
|
|
424
|
+
|
|
425
425
|
.MockSelector {
|
|
426
426
|
height: 24px;
|
|
427
427
|
padding-right: 20px;
|
|
@@ -458,7 +458,7 @@ table {
|
|
|
458
458
|
height: 22px;
|
|
459
459
|
opacity: 0;
|
|
460
460
|
|
|
461
|
-
&:focus {
|
|
461
|
+
&:focus-visible {
|
|
462
462
|
outline: 0;
|
|
463
463
|
& + svg {
|
|
464
464
|
outline: 2px solid var(--colorAccent)
|
|
@@ -556,7 +556,7 @@ table {
|
|
|
556
556
|
> input {
|
|
557
557
|
appearance: none;
|
|
558
558
|
|
|
559
|
-
&:focus {
|
|
559
|
+
&:focus-visible {
|
|
560
560
|
outline: 0;
|
|
561
561
|
& + span {
|
|
562
562
|
outline: 2px solid var(--colorAccent)
|
|
@@ -587,15 +587,6 @@ table {
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
|
|
590
|
-
.StaticFileLink {
|
|
591
|
-
display: inline-block;
|
|
592
|
-
padding: 6px 0;
|
|
593
|
-
margin-left: 4px;
|
|
594
|
-
border-radius: var(--radius);
|
|
595
|
-
color: var(--colorAccent);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
590
|
.PayloadViewer {
|
|
600
591
|
display: flex;
|
|
601
592
|
height: 100%;
|
package/src/Dashboard.js
CHANGED
|
@@ -26,7 +26,6 @@ const CSS = {
|
|
|
26
26
|
Resizer: null,
|
|
27
27
|
SaveProxiedCheckbox: null,
|
|
28
28
|
SettingsMenu: null,
|
|
29
|
-
StaticFileLink: null,
|
|
30
29
|
|
|
31
30
|
chosen: null,
|
|
32
31
|
dittoDir: null,
|
|
@@ -50,6 +49,13 @@ for (const k of Object.keys(CSS))
|
|
|
50
49
|
CSS[k] = k
|
|
51
50
|
|
|
52
51
|
|
|
52
|
+
const FocusGroup = {
|
|
53
|
+
ProxyToggler: 0,
|
|
54
|
+
DelayToggler: 1,
|
|
55
|
+
StatusToggler: 2,
|
|
56
|
+
PreviewLink: 3
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
const state = /** @type {State} */ {
|
|
54
60
|
brokersByMethod: {},
|
|
55
61
|
staticBrokers: {},
|
|
@@ -70,13 +76,22 @@ const state = /** @type {State} */ {
|
|
|
70
76
|
updateState()
|
|
71
77
|
},
|
|
72
78
|
|
|
73
|
-
leftSideWidth: undefined
|
|
79
|
+
leftSideWidth: undefined,
|
|
80
|
+
|
|
81
|
+
chosenLink: { method: '', urlMask: '' },
|
|
82
|
+
clearChosenLink() {
|
|
83
|
+
state.chosenLink = { method: '', urlMask: '' }
|
|
84
|
+
},
|
|
85
|
+
setChosenLink(method, urlMask) {
|
|
86
|
+
state.chosenLink = { method, urlMask }
|
|
87
|
+
}
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
|
|
77
91
|
const mockaton = new Commander(location.origin)
|
|
78
92
|
updateState()
|
|
79
|
-
|
|
93
|
+
initLongPoll()
|
|
94
|
+
initKeyboardNavigation()
|
|
80
95
|
|
|
81
96
|
async function updateState() {
|
|
82
97
|
try {
|
|
@@ -85,6 +100,10 @@ async function updateState() {
|
|
|
85
100
|
throw response.status
|
|
86
101
|
Object.assign(state, await response.json())
|
|
87
102
|
document.body.replaceChildren(...App())
|
|
103
|
+
findChosenLink()?.focus()
|
|
104
|
+
const { method, urlMask } = state.chosenLink
|
|
105
|
+
if (method && urlMask)
|
|
106
|
+
await previewMock(method, urlMask)
|
|
88
107
|
}
|
|
89
108
|
catch (error) {
|
|
90
109
|
onError(error)
|
|
@@ -132,12 +151,10 @@ function Header() {
|
|
|
132
151
|
CookieSelector(),
|
|
133
152
|
ProxyFallbackField(),
|
|
134
153
|
ResetButton(),
|
|
135
|
-
|
|
154
|
+
SettingsMenuTrigger())))
|
|
136
155
|
}
|
|
137
156
|
|
|
138
|
-
function
|
|
139
|
-
const { groupByMethod, toggleGroupByMethod } = state
|
|
140
|
-
|
|
157
|
+
function SettingsMenuTrigger() {
|
|
141
158
|
const id = '_settings_menu_'
|
|
142
159
|
return (
|
|
143
160
|
r('button', {
|
|
@@ -146,29 +163,35 @@ function SettingsMenu() {
|
|
|
146
163
|
className: CSS.MenuTrigger
|
|
147
164
|
},
|
|
148
165
|
SettingsIcon(),
|
|
166
|
+
Defer(() => SettingsMenu(id))))
|
|
167
|
+
}
|
|
149
168
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
type: 'checkbox',
|
|
159
|
-
checked: groupByMethod,
|
|
160
|
-
autofocus: true,
|
|
161
|
-
onChange: toggleGroupByMethod
|
|
162
|
-
}),
|
|
163
|
-
r('span', null, t`Group by Method`)),
|
|
169
|
+
function SettingsMenu(id) {
|
|
170
|
+
const { groupByMethod, toggleGroupByMethod } = state
|
|
171
|
+
return (
|
|
172
|
+
r('menu', {
|
|
173
|
+
id,
|
|
174
|
+
popover: '',
|
|
175
|
+
className: CSS.SettingsMenu
|
|
176
|
+
},
|
|
164
177
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
r('label', className(CSS.GroupByMethod),
|
|
179
|
+
r('input', {
|
|
180
|
+
type: 'checkbox',
|
|
181
|
+
checked: groupByMethod,
|
|
182
|
+
// autofocus: true, // TODO
|
|
183
|
+
onChange: toggleGroupByMethod
|
|
184
|
+
}),
|
|
185
|
+
r('span', null, t`Group by Method`)),
|
|
186
|
+
|
|
187
|
+
r('a', {
|
|
188
|
+
href: 'https://github.com/ericfortis/mockaton',
|
|
189
|
+
target: '_blank',
|
|
190
|
+
rel: 'noopener noreferrer'
|
|
191
|
+
}, t`Documentation`)))
|
|
170
192
|
}
|
|
171
193
|
|
|
194
|
+
|
|
172
195
|
function CookieSelector() {
|
|
173
196
|
const { cookies } = state
|
|
174
197
|
function onChange() {
|
|
@@ -196,6 +219,7 @@ function BulkSelector() {
|
|
|
196
219
|
// But this way is easier to implement, with a few hacks.
|
|
197
220
|
const firstOption = t`Pick Comment…`
|
|
198
221
|
function onChange() {
|
|
222
|
+
state.clearChosenLink()
|
|
199
223
|
const value = this.value
|
|
200
224
|
this.value = firstOption // Hack
|
|
201
225
|
mockaton.bulkSelectByComment(value)
|
|
@@ -233,8 +257,10 @@ function GlobalDelayField() {
|
|
|
233
257
|
.catch(onError)
|
|
234
258
|
}
|
|
235
259
|
function onWheel(event) {
|
|
236
|
-
|
|
237
|
-
|
|
260
|
+
if (event.deltaY > 0)
|
|
261
|
+
this.stepUp()
|
|
262
|
+
else
|
|
263
|
+
this.stepDown()
|
|
238
264
|
clearTimeout(onWheel.timer)
|
|
239
265
|
onWheel.timer = setTimeout(onChange.bind(this), 300)
|
|
240
266
|
}
|
|
@@ -254,9 +280,9 @@ function GlobalDelayField() {
|
|
|
254
280
|
|
|
255
281
|
function ProxyFallbackField() {
|
|
256
282
|
const { proxyFallback } = state
|
|
283
|
+
const checkboxRef = useRef()
|
|
257
284
|
function onChange() {
|
|
258
|
-
|
|
259
|
-
saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
|
|
285
|
+
checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
|
|
260
286
|
|
|
261
287
|
if (!this.validity.valid)
|
|
262
288
|
this.reportValidity()
|
|
@@ -278,10 +304,10 @@ function ProxyFallbackField() {
|
|
|
278
304
|
value: proxyFallback,
|
|
279
305
|
onChange
|
|
280
306
|
})),
|
|
281
|
-
SaveProxiedCheckbox()))
|
|
307
|
+
SaveProxiedCheckbox(checkboxRef)))
|
|
282
308
|
}
|
|
283
309
|
|
|
284
|
-
function SaveProxiedCheckbox() {
|
|
310
|
+
function SaveProxiedCheckbox(ref) {
|
|
285
311
|
const { collectProxied, canProxy } = state
|
|
286
312
|
function onChange() {
|
|
287
313
|
mockaton.setCollectProxied(this.checked)
|
|
@@ -291,6 +317,7 @@ function SaveProxiedCheckbox() {
|
|
|
291
317
|
return (
|
|
292
318
|
r('label', className(CSS.SaveProxiedCheckbox),
|
|
293
319
|
r('input', {
|
|
320
|
+
ref,
|
|
294
321
|
type: 'checkbox',
|
|
295
322
|
disabled: !canProxy,
|
|
296
323
|
checked: collectProxied,
|
|
@@ -301,6 +328,7 @@ function SaveProxiedCheckbox() {
|
|
|
301
328
|
|
|
302
329
|
function ResetButton() {
|
|
303
330
|
function onClick() {
|
|
331
|
+
state.clearChosenLink()
|
|
304
332
|
mockaton.reset()
|
|
305
333
|
.then(parseError)
|
|
306
334
|
.then(updateState)
|
|
@@ -327,7 +355,7 @@ function MockList() {
|
|
|
327
355
|
t`No mocks found`))
|
|
328
356
|
|
|
329
357
|
if (groupByMethod)
|
|
330
|
-
return Object.keys(brokersByMethod).map(
|
|
358
|
+
return Object.keys(brokersByMethod).map(method => Fragment(
|
|
331
359
|
r('tr', null,
|
|
332
360
|
r('th', { colspan: 2 + Number(canProxy) }),
|
|
333
361
|
r('th', null, method)),
|
|
@@ -337,7 +365,7 @@ function MockList() {
|
|
|
337
365
|
}
|
|
338
366
|
|
|
339
367
|
|
|
340
|
-
function Row({ method, urlMask, urlMaskDittoed, broker }) {
|
|
368
|
+
function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
|
|
341
369
|
const { canProxy, groupByMethod } = state
|
|
342
370
|
return (
|
|
343
371
|
r('tr', { 'data-method': method, 'data-urlMask': urlMask },
|
|
@@ -345,7 +373,7 @@ function Row({ method, urlMask, urlMaskDittoed, broker }) {
|
|
|
345
373
|
r('td', null, DelayRouteToggler(broker)),
|
|
346
374
|
r('td', null, InternalServerErrorToggler(broker)),
|
|
347
375
|
!groupByMethod && r('td', className(CSS.Method), method),
|
|
348
|
-
r('td', null, PreviewLink(method, urlMask, urlMaskDittoed)),
|
|
376
|
+
r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
|
|
349
377
|
r('td', null, MockSelector(broker))))
|
|
350
378
|
}
|
|
351
379
|
|
|
@@ -358,9 +386,7 @@ function rowsFor(targetMethod) {
|
|
|
358
386
|
for (const [urlMask, broker] of Object.entries(brokers))
|
|
359
387
|
rows.push({ method, urlMask, broker })
|
|
360
388
|
|
|
361
|
-
const sorted = rows
|
|
362
|
-
.filter((r) => r.broker.mocks.length > 1) // >1 because of autogen500
|
|
363
|
-
.sort((rA, rB) => rA.urlMask.localeCompare(rB.urlMask))
|
|
389
|
+
const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
364
390
|
|
|
365
391
|
const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
|
|
366
392
|
return sorted.map((r, i) => ({
|
|
@@ -369,39 +395,50 @@ function rowsFor(targetMethod) {
|
|
|
369
395
|
}))
|
|
370
396
|
}
|
|
371
397
|
|
|
372
|
-
|
|
398
|
+
|
|
399
|
+
function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
|
|
373
400
|
async function onClick(event) {
|
|
374
401
|
event.preventDefault()
|
|
375
402
|
try {
|
|
376
|
-
|
|
403
|
+
findChosenLink()?.classList.remove(CSS.chosen)
|
|
377
404
|
this.classList.add(CSS.chosen)
|
|
378
|
-
|
|
405
|
+
state.setChosenLink(method, urlMask)
|
|
406
|
+
await previewMock(method, urlMask)
|
|
379
407
|
}
|
|
380
408
|
catch (error) {
|
|
381
409
|
onError(error)
|
|
382
410
|
}
|
|
383
411
|
}
|
|
412
|
+
const { chosenLink } = state
|
|
413
|
+
const isChosen = chosenLink.method === method && chosenLink.urlMask === urlMask
|
|
384
414
|
const [ditto, tail] = urlMaskDittoed
|
|
385
415
|
return (
|
|
386
416
|
r('a', {
|
|
387
|
-
className
|
|
417
|
+
...className(CSS.PreviewLink, isChosen && CSS.chosen),
|
|
388
418
|
href: urlMask,
|
|
419
|
+
autofocus,
|
|
420
|
+
'data-focus-group': FocusGroup.PreviewLink,
|
|
389
421
|
onClick
|
|
390
422
|
}, ditto
|
|
391
423
|
? [r('span', className(CSS.dittoDir), ditto), tail]
|
|
392
424
|
: tail))
|
|
393
425
|
}
|
|
394
426
|
|
|
427
|
+
function findChosenLink() {
|
|
428
|
+
return document.querySelector(
|
|
429
|
+
`body > main > .${CSS.leftSide} .${CSS.PreviewLink}.${CSS.chosen}`)
|
|
430
|
+
}
|
|
431
|
+
|
|
395
432
|
const STR_PROXIED = t`Proxied`
|
|
396
433
|
|
|
397
434
|
/** @param {ClientMockBroker} broker */
|
|
398
435
|
function MockSelector(broker) {
|
|
399
436
|
function onChange() {
|
|
400
|
-
const {
|
|
437
|
+
const { method, urlMask } = parseFilename(this.value)
|
|
438
|
+
state.setChosenLink(method, urlMask)
|
|
401
439
|
mockaton.select(this.value)
|
|
402
440
|
.then(parseError)
|
|
403
441
|
.then(updateState)
|
|
404
|
-
.then(() => linkFor(method, urlMask)?.click())
|
|
405
442
|
.catch(onError)
|
|
406
443
|
}
|
|
407
444
|
|
|
@@ -455,7 +492,8 @@ function DelayRouteToggler(broker) {
|
|
|
455
492
|
}
|
|
456
493
|
return ClickDragToggler({
|
|
457
494
|
checked: broker.currentMock.delayed,
|
|
458
|
-
commit
|
|
495
|
+
commit,
|
|
496
|
+
focusGroup: FocusGroup.DelayToggler
|
|
459
497
|
})
|
|
460
498
|
}
|
|
461
499
|
|
|
@@ -464,10 +502,10 @@ function DelayRouteToggler(broker) {
|
|
|
464
502
|
function InternalServerErrorToggler(broker) {
|
|
465
503
|
function onChange() {
|
|
466
504
|
const { method, urlMask } = parseFilename(broker.mocks[0])
|
|
505
|
+
state.setChosenLink(method, urlMask)
|
|
467
506
|
mockaton.toggle500(method, urlMask)
|
|
468
507
|
.then(parseError)
|
|
469
508
|
.then(updateState)
|
|
470
|
-
.then(() => linkFor(method, urlMask)?.click())
|
|
471
509
|
.catch(onError)
|
|
472
510
|
}
|
|
473
511
|
return (
|
|
@@ -477,6 +515,7 @@ function InternalServerErrorToggler(broker) {
|
|
|
477
515
|
},
|
|
478
516
|
r('input', {
|
|
479
517
|
type: 'checkbox',
|
|
518
|
+
'data-focus-group': FocusGroup.StatusToggler,
|
|
480
519
|
name: broker.currentMock.file,
|
|
481
520
|
checked: parseFilename(broker.currentMock.file).status === 500,
|
|
482
521
|
onChange
|
|
@@ -488,10 +527,10 @@ function InternalServerErrorToggler(broker) {
|
|
|
488
527
|
function ProxyToggler(broker) {
|
|
489
528
|
function onChange() {
|
|
490
529
|
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
530
|
+
state.setChosenLink(method, urlMask)
|
|
491
531
|
mockaton.setRouteIsProxied(method, urlMask, this.checked)
|
|
492
532
|
.then(parseError)
|
|
493
533
|
.then(updateState)
|
|
494
|
-
.then(() => linkFor(method, urlMask)?.click())
|
|
495
534
|
.catch(onError)
|
|
496
535
|
}
|
|
497
536
|
return (
|
|
@@ -502,7 +541,8 @@ function ProxyToggler(broker) {
|
|
|
502
541
|
r('input', {
|
|
503
542
|
type: 'checkbox',
|
|
504
543
|
checked: !broker.currentMock.file,
|
|
505
|
-
onChange
|
|
544
|
+
onChange,
|
|
545
|
+
'data-focus-group': FocusGroup.ProxyToggler
|
|
506
546
|
}),
|
|
507
547
|
CloudIcon()))
|
|
508
548
|
}
|
|
@@ -532,7 +572,8 @@ function StaticFilesList() {
|
|
|
532
572
|
r('td', null, r('a', {
|
|
533
573
|
href: broker.route,
|
|
534
574
|
target: '_blank',
|
|
535
|
-
className: CSS.
|
|
575
|
+
className: CSS.PreviewLink,
|
|
576
|
+
'data-focus-group': FocusGroup.PreviewLink
|
|
536
577
|
}, dp[i]))
|
|
537
578
|
))))
|
|
538
579
|
}
|
|
@@ -546,7 +587,8 @@ function DelayStaticRouteToggler(broker) {
|
|
|
546
587
|
}
|
|
547
588
|
return ClickDragToggler({
|
|
548
589
|
checked: broker.delayed,
|
|
549
|
-
commit
|
|
590
|
+
commit,
|
|
591
|
+
focusGroup: FocusGroup.DelayToggler
|
|
550
592
|
})
|
|
551
593
|
}
|
|
552
594
|
|
|
@@ -565,13 +607,14 @@ function NotFoundToggler(broker) {
|
|
|
565
607
|
r('input', {
|
|
566
608
|
type: 'checkbox',
|
|
567
609
|
checked: broker.status === 404,
|
|
568
|
-
onChange
|
|
610
|
+
onChange,
|
|
611
|
+
'data-focus-group': FocusGroup.StatusToggler
|
|
569
612
|
}),
|
|
570
613
|
r('span', null, t`404`)))
|
|
571
614
|
}
|
|
572
615
|
|
|
573
616
|
|
|
574
|
-
function ClickDragToggler({ checked, commit }) {
|
|
617
|
+
function ClickDragToggler({ checked, commit, focusGroup }) {
|
|
575
618
|
function onPointerEnter(event) {
|
|
576
619
|
if (event.buttons === 1)
|
|
577
620
|
onPointerDown.call(this)
|
|
@@ -594,6 +637,7 @@ function ClickDragToggler({ checked, commit }) {
|
|
|
594
637
|
},
|
|
595
638
|
r('input', {
|
|
596
639
|
type: 'checkbox',
|
|
640
|
+
'data-focus-group': focusGroup,
|
|
597
641
|
checked,
|
|
598
642
|
onPointerEnter,
|
|
599
643
|
onPointerDown,
|
|
@@ -604,7 +648,6 @@ function ClickDragToggler({ checked, commit }) {
|
|
|
604
648
|
}
|
|
605
649
|
|
|
606
650
|
|
|
607
|
-
|
|
608
651
|
function Resizer() {
|
|
609
652
|
return (
|
|
610
653
|
r('div', {
|
|
@@ -682,7 +725,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
|
|
|
682
725
|
' ' + mime))
|
|
683
726
|
}
|
|
684
727
|
|
|
685
|
-
async function previewMock(method, urlMask
|
|
728
|
+
async function previewMock(method, urlMask) {
|
|
686
729
|
previewMock.controller?.abort()
|
|
687
730
|
previewMock.controller = new AbortController
|
|
688
731
|
|
|
@@ -692,7 +735,7 @@ async function previewMock(method, urlMask, href) {
|
|
|
692
735
|
}, SPINNER_DELAY)
|
|
693
736
|
|
|
694
737
|
try {
|
|
695
|
-
const response = await fetch(
|
|
738
|
+
const response = await fetch(urlMask, {
|
|
696
739
|
method,
|
|
697
740
|
signal: previewMock.controller.signal
|
|
698
741
|
})
|
|
@@ -736,7 +779,7 @@ async function updatePayloadViewer(method, urlMask, response) {
|
|
|
736
779
|
else if (isXML(mime))
|
|
737
780
|
payloadViewerRef.current.replaceChildren(syntaxXML(body))
|
|
738
781
|
else
|
|
739
|
-
payloadViewerRef.current.
|
|
782
|
+
payloadViewerRef.current.textContent = body
|
|
740
783
|
}
|
|
741
784
|
}
|
|
742
785
|
|
|
@@ -760,6 +803,50 @@ function focus(selector) {
|
|
|
760
803
|
document.querySelector(selector)?.focus()
|
|
761
804
|
}
|
|
762
805
|
|
|
806
|
+
function initKeyboardNavigation() {
|
|
807
|
+
addEventListener('keydown', onKeyDown)
|
|
808
|
+
|
|
809
|
+
function onKeyDown(event) {
|
|
810
|
+
const pivot = document.activeElement
|
|
811
|
+
switch (event.key) {
|
|
812
|
+
case 'ArrowDown':
|
|
813
|
+
case 'ArrowUp': {
|
|
814
|
+
let fg = pivot.getAttribute('data-focus-group')
|
|
815
|
+
if (fg !== null) {
|
|
816
|
+
const offset = event.key === 'ArrowDown' ? +1 : -1
|
|
817
|
+
circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
|
|
818
|
+
}
|
|
819
|
+
break
|
|
820
|
+
}
|
|
821
|
+
case 'ArrowRight':
|
|
822
|
+
case 'ArrowLeft': {
|
|
823
|
+
if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
|
|
824
|
+
const offset = event.key === 'ArrowRight' ? +1 : -1
|
|
825
|
+
rowFocusable(pivot, offset)?.focus()
|
|
826
|
+
}
|
|
827
|
+
break
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function rowFocusable(el, step) {
|
|
833
|
+
const row = el.closest('tr')
|
|
834
|
+
if (row) {
|
|
835
|
+
const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
|
|
836
|
+
return circularAdjacent(step, focusables, el)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function allInFocusGroup(focusGroup) {
|
|
841
|
+
return Array.from(document.querySelectorAll(
|
|
842
|
+
`body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function circularAdjacent(step = 1, arr, pivot) {
|
|
846
|
+
return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
763
850
|
/** # Misc */
|
|
764
851
|
|
|
765
852
|
async function parseError(response) {
|
|
@@ -817,7 +904,7 @@ function SettingsIcon() {
|
|
|
817
904
|
*/
|
|
818
905
|
|
|
819
906
|
function initLongPoll() {
|
|
820
|
-
poll.oldSyncVersion =
|
|
907
|
+
poll.oldSyncVersion = -1
|
|
821
908
|
poll.controller = new AbortController()
|
|
822
909
|
poll()
|
|
823
910
|
document.addEventListener('visibilitychange', () => {
|
|
@@ -835,9 +922,11 @@ async function poll() {
|
|
|
835
922
|
const response = await mockaton.getSyncVersion(poll.oldSyncVersion, poll.controller.signal)
|
|
836
923
|
if (response.ok) {
|
|
837
924
|
const syncVersion = await response.json()
|
|
925
|
+
const skipUpdate = poll.oldSyncVersion === -1
|
|
838
926
|
if (poll.oldSyncVersion !== syncVersion) { // because it could be < or >
|
|
839
927
|
poll.oldSyncVersion = syncVersion
|
|
840
|
-
|
|
928
|
+
if (!skipUpdate)
|
|
929
|
+
await updateState()
|
|
841
930
|
}
|
|
842
931
|
poll()
|
|
843
932
|
}
|
|
@@ -894,6 +983,12 @@ function Fragment(...args) {
|
|
|
894
983
|
return frag
|
|
895
984
|
}
|
|
896
985
|
|
|
986
|
+
function Defer(cb) {
|
|
987
|
+
const placeholder = document.createComment('')
|
|
988
|
+
deferred(() => placeholder.replaceWith(cb()))
|
|
989
|
+
return placeholder
|
|
990
|
+
}
|
|
991
|
+
|
|
897
992
|
function deferred(cb) {
|
|
898
993
|
return window.requestIdleCallback
|
|
899
994
|
? requestIdleCallback(cb)
|