Skip to content

Commit 3334063

Browse files
author
Ajit Kumar
committed
fix: Capture Swallows Later Input
1 parent e2afd0a commit 3334063

7 files changed

Lines changed: 376 additions & 54 deletions

File tree

‎src/handlers/quickTools.js‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ function adapterAction(action, value) {
6969
return null;
7070
}
7171

72+
function finishAdapterCapture() {
73+
const restoreFocus =
74+
(adapterCapture && !adapterCapture.consumed) ||
75+
document.activeElement === quickTools.$input;
76+
// Blurring may emit composition events synchronously. Keep an inert guard
77+
// for those events without leaving the hidden input as the typing target.
78+
if (adapterCapture) adapterCapture.consumed = true;
79+
clearQuickToolsModifierState();
80+
quickToolsAdapters.discardCapture();
81+
quickTools.$input.value = "";
82+
quickTools.$input.blur();
83+
if (restoreFocus) quickToolsAdapters.focus();
84+
}
85+
7286
function handleAdapterCapture(event) {
7387
if (!adapterCapture) return false;
7488
if (
@@ -111,12 +125,14 @@ function handleAdapterCapture(event) {
111125
...modifiers,
112126
});
113127
} else {
114-
if (result.outcome.kind === "duplicate") event.preventDefault();
128+
if (result.outcome.kind === "duplicate") {
129+
quickTools.$input.value = "";
130+
event.preventDefault();
131+
}
115132
return true;
116133
}
117-
if (!event.isComposing) quickTools.$input.value = "";
118134
event.preventDefault();
119-
clearQuickToolsModifierState();
135+
finishAdapterCapture();
120136
return true;
121137
}
122138

@@ -343,6 +359,7 @@ export function clearQuickToolsModifierState({ restoreFocus = false } = {}) {
343359

344360
export function cancelQuickToolsModifierInput() {
345361
adapterCapture = null;
362+
quickToolsAdapters.discardCapture();
346363
clearReadOnlyCaptureSession();
347364
const changed = clearQuickToolsModifierState();
348365
quickTools.$input.value = "";
@@ -368,7 +385,11 @@ export default function actions(action, value) {
368385
if (routed) {
369386
if (routed.type === "command" && routed.command === "openCommandPalette")
370387
return executeCommand("openCommandPalette", editor);
371-
if (!quickToolsAdapters.available(routed)) return false;
388+
if (!quickToolsAdapters.available(routed)) {
389+
if (!Object.values(state).some(Boolean))
390+
quickToolsAdapters.discardCapture();
391+
return false;
392+
}
372393
if (routed.type === "modifier") {
373394
quickToolsAdapters.capture();
374395
state[action] = !state[action];
@@ -389,7 +410,7 @@ export default function actions(action, value) {
389410
}
390411
const handled = quickToolsAdapters.dispatch(routed);
391412
if (routed.type !== "key" || !routed.key.startsWith("Arrow"))
392-
clearQuickToolsModifierState();
413+
finishAdapterCapture();
393414
return handled;
394415
}
395416

‎src/handlers/quickToolsInit.js‎

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import quickTools from "components/quickTools";
44
import { description } from "components/quickTools/items";
55
import { hideTooltip, showTooltip } from "components/tooltip";
66
import config from "lib/config";
7-
import appSettings from "lib/settings";
8-
import actions, { cancelQuickToolsModifierInput, key } from "./quickTools";
7+
import { syncQuickToolsVisibility } from "lib/editorFile";
98
import quickToolsAdapters from "lib/quickToolsAdapter";
109
import { watchQuickToolsOverlays } from "lib/quickToolsOverlays";
11-
import { syncQuickToolsVisibility } from "lib/editorFile";
10+
import appSettings from "lib/settings";
11+
import actions, { cancelQuickToolsModifierInput, key } from "./quickTools";
1212

1313
const CONTEXT_MENU_TIMEOUT = 500;
1414
const MOVE_X_THRESHOLD = 50;
@@ -53,6 +53,12 @@ function clearTouchFeedback() {
5353
}
5454
}
5555

56+
function discardAdapterCapture() {
57+
// Modifier sequences keep their selection until a key is dispatched.
58+
if (!key.shift && !key.ctrl && !key.alt && !key.meta)
59+
quickToolsAdapters.discardCapture();
60+
}
61+
5662
/**
5763
* Initialize quick tools
5864
* @param {HTMLElement} $footer
@@ -88,12 +94,24 @@ export default function init() {
8894
refreshAdapter();
8995
});
9096
watchQuickToolsOverlays(quickToolsAdapters, cancelAdapterInput);
91-
$footer.addEventListener(
92-
"pointerdown",
93-
() => quickToolsAdapters.capture(),
94-
true,
95-
);
96-
$footer.addEventListener("keydown", () => quickToolsAdapters.capture(), true);
97+
const capture = () => {
98+
discardAdapterCapture();
99+
quickToolsAdapters.capture();
100+
};
101+
$footer.addEventListener("pointerdown", capture, true);
102+
$footer.addEventListener("keydown", capture, true);
103+
$footer.addEventListener("pointercancel", discardAdapterCapture, true);
104+
$footer.addEventListener("scroll", discardAdapterCapture, true);
105+
const leaveQuickTools = (event) => {
106+
if (!quickToolsAdapters.has()) return;
107+
const path = event.composedPath();
108+
if (path.includes($footer) || path.includes($input)) return;
109+
quickToolsAdapters.discardCapture();
110+
if (key.shift || key.ctrl || key.alt || key.meta)
111+
cancelQuickToolsModifierInput();
112+
};
113+
document.addEventListener("pointerdown", leaveQuickTools, true);
114+
document.addEventListener("focusin", leaveQuickTools, true);
97115

98116
$toggler.addEventListener("click", (e) => {
99117
e.preventDefault();
@@ -189,6 +207,7 @@ export default function init() {
189207

190208
function onwheel(e) {
191209
e.preventDefault();
210+
discardAdapterCapture();
192211
const $el = e.target;
193212
const { $row1, $row2 } = quickTools;
194213
let $row;
@@ -208,6 +227,7 @@ function onclick(e) {
208227
reset();
209228

210229
if (e.target.disabled) {
230+
discardAdapterCapture();
211231
e.preventDefault();
212232
e.stopPropagation();
213233
return;
@@ -344,11 +364,10 @@ function touchend(e) {
344364
}
345365

346366
$row.scrollLeft = scroll;
347-
touchcancel(e);
348-
349-
if ($el === $touchstart && performance.now() - startTime < 100) {
350-
click($el);
351-
}
367+
const shouldClick =
368+
$el === $touchstart && performance.now() - startTime < 100;
369+
touchcancel(e, { preserveCapture: shouldClick });
370+
if (shouldClick) click($el);
352371
return;
353372
}
354373

@@ -357,15 +376,15 @@ function touchend(e) {
357376
return;
358377
}
359378

360-
touchcancel(e);
379+
touchcancel(e, { preserveCapture: true });
361380
click($el);
362381
}
363382

364383
/**
365384
*
366385
* @param {TouchEvent} e
367386
*/
368-
function touchcancel(e) {
387+
function touchcancel(e, { preserveCapture = false } = {}) {
369388
document.removeEventListener("keyup", touchcancel);
370389
document.removeEventListener("touchend", touchend);
371390
document.removeEventListener("touchcancel", touchcancel);
@@ -374,6 +393,7 @@ function touchcancel(e) {
374393
clearTimeout(contextmenuTimeout);
375394
clearTouchFeedback();
376395
hideTooltip();
396+
if (!preserveCapture) discardAdapterCapture();
377397
}
378398

379399
/**
@@ -414,7 +434,10 @@ function oncontextmenu(e) {
414434
* @param {HTMLElement} $el
415435
*/
416436
function click($el) {
417-
if ($el.disabled) return;
437+
if ($el.disabled) {
438+
discardAdapterCapture();
439+
return;
440+
}
418441

419442
$el.classList.add("click");
420443
clearTimeout($el.dataset.timeout);
@@ -427,15 +450,22 @@ function click($el) {
427450
}
428451

429452
const { action } = $el.dataset;
430-
if (!action) return;
453+
if (!action) {
454+
discardAdapterCapture();
455+
return;
456+
}
431457

432458
let { value } = $el.dataset;
433459

434460
if (!value) {
435461
value = $el.value;
436462
}
437463

438-
actions(action, value);
464+
try {
465+
actions(action, value);
466+
} finally {
467+
discardAdapterCapture();
468+
}
439469
}
440470

441471
function scheduleUpdateQuickToolsState() {

‎src/lib/editorFile.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import confirm from "dialogs/confirm";
1515
import DOMPurify from "dompurify";
1616
import startDrag from "handlers/editorFileTab";
1717
import actions from "handlers/quickTools";
18-
import quickToolsAdapters from "lib/quickToolsAdapter";
1918
import { openTabContextMenuOnRelease } from "handlers/tabContextMenu";
2019
import tag from "html-tag-js";
20+
import quickToolsAdapters from "lib/quickToolsAdapter";
2121
import mimeTypes from "mime-types";
2222
import { applyHighlightStyles } from "utils/codeHighlight";
2323
import helpers from "utils/helpers";

‎src/lib/quickToolsAdapter.js‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,18 @@ export function createQuickToolsAdapterRegistry(
120120
// Capture failures are reported by dispatch, without an unhandled rejection.
121121
entry.selection.catch(() => {});
122122
},
123+
discardCapture() {
124+
// Dispatched actions already own their snapshots. Leave their queue intact.
125+
const entry = current();
126+
if (entry) entry.selection = undefined;
127+
},
123128
dispatch(action) {
124129
const entry = current();
125130
if (!entry) return false;
126-
if (!this.available(action)) return true;
131+
if (!this.available(action)) {
132+
this.discardCapture();
133+
return true;
134+
}
127135
const signal = entry.controller.signal,
128136
selection = entry.selection;
129137
entry.selection = undefined;

‎src/pages/fileBrowser/fileBrowser.scss‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
display: none;
44
}
55

6-
.tile {
7-
> .icon:first-child {
8-
--file-icon-size: 24px;
9-
font-size: var(--file-icon-size);
10-
background-size: var(--file-icon-size);
11-
background-position: center;
12-
13-
> svg, > img {
14-
width: var(--file-icon-size);
15-
height: var(--file-icon-size);
16-
object-fit: contain;
17-
}
6+
#list .tile > .icon:first-child {
7+
--file-icon-size: 24px;
8+
font-size: var(--file-icon-size);
9+
background-size: var(--file-icon-size);
10+
background-position: center;
11+
12+
> svg, > img {
13+
width: var(--file-icon-size);
14+
height: var(--file-icon-size);
15+
object-fit: contain;
1816
}
17+
}
1918

19+
.tile {
2020
&[data-storage-type="notification"] {
2121
background-color: rgb(153, 153, 255);
2222
background-color: var(--primary-color);

‎tests/unit/quickToolsAdapter.test.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,23 @@ describe("custom-tab quicktools", () => {
103103
f.registry.focus(); f.registry.cancel(); await settle();
104104
expect(f.adapter.focus).not.toHaveBeenCalled();
105105
});
106+
it("discards unused captures without cancelling an already queued edit", async () => {
107+
const f = setup(); let finish, caret = 3;
108+
f.adapter.captureSelection.mockImplementation(() => caret);
109+
f.adapter.restoreSelection.mockImplementation(value => { caret = value; });
110+
f.adapter.execute.mockImplementationOnce(() => new Promise(resolve => { finish = resolve; }));
111+
f.registry.capture(); f.registry.dispatch(insert);
112+
await settle();
113+
const signal = f.adapter.execute.mock.calls[0][1].signal;
114+
f.registry.capture(); f.registry.discardCapture();
115+
finish(); await settle();
116+
caret = 20;
117+
f.registry.capture(); f.registry.dispatch(insert);
118+
await settle();
119+
expect(f.adapter.restoreSelection.mock.calls).toEqual([[3], [20]]);
120+
expect(f.adapter.execute).toHaveBeenCalledTimes(2);
121+
expect(signal.aborted).toBe(false);
122+
expect(f.adapter.cancel).not.toHaveBeenCalled();
123+
});
106124

107125
});

0 commit comments

Comments
 (0)