Let a text field select from right to left with the mouse (#236)
Dragging from right to left in a canvas text field only moved the cursor. The hidden input behind the field got every selection sorted and without a direction. Its selectionchange echo then handed back the sorted range, and the anchor jumped to the mouse on the first move. - KeyboardBridge sets the selection with 'backward' when the focus lies before the anchor, and reads selectionDirection when the input reports a change. - TextField passes anchor and focus through unchanged. Its own sorting is gone. - Shift+Left runs through the same path. The cursor should now sit at the moving end there too. This is not tested on its own. Tests: - ./check.sh: 167 files, 2435 tests, CHECK EXIT 0 - e2e 2: ALL PASSED, 25 checks. New check 2d-ii drags right to left in the name field and reads start 0, end 13, direction backward. - Counter-check without the direction: 2d-ii fails with end 2, the reported symptom. - e2e check total in README goes to 1401 (computed, not run in full). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74b9c60661
commit
1b3cb625a3
4 files changed
+43
-23
No files matched your search
@@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1400 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1401 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
+19
-12
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
export interface TextEditTarget {
|
||||
onBridgeInput(value: string, selectionStart: number, selectionEnd: number): void;
|
||||
/** `anchor` is where the selection started, `focus` where the caret is; focus < anchor is backward. */
|
||||
onBridgeInput(value: string, anchor: number, focus: number): void;
|
||||
/** Called for navigation keys (Enter, Escape, Tab, arrows …). Return true to consume. */
|
||||
onBridgeKey(event: KeyboardEvent): boolean;
|
||||
onBridgeBlur(): void;
|
||||
@@ -40,8 +41,14 @@ export class KeyboardBridge {
|
||||
document.body.appendChild(input);
|
||||
this.input = input;
|
||||
|
||||
const sync = () =>
|
||||
this.target?.onBridgeInput(input.value, input.selectionStart ?? 0, input.selectionEnd ?? 0);
|
||||
// The input keeps its range ordered; the direction says which end is the anchor. Reporting
|
||||
// the ordered range lost the anchor of a right-to-left drag on the first selectionchange.
|
||||
const sync = () => {
|
||||
const start = input.selectionStart ?? 0;
|
||||
const end = input.selectionEnd ?? 0;
|
||||
const backward = input.selectionDirection === 'backward';
|
||||
this.target?.onBridgeInput(input.value, backward ? end : start, backward ? start : end);
|
||||
};
|
||||
input.addEventListener('input', sync);
|
||||
input.addEventListener('keyup', sync);
|
||||
document.addEventListener('selectionchange', () => {
|
||||
@@ -62,17 +69,12 @@ export class KeyboardBridge {
|
||||
}
|
||||
|
||||
/** Mirrors the field's value into the input and grabs DOM focus. */
|
||||
attach(
|
||||
target: TextEditTarget,
|
||||
value: string,
|
||||
selectionStart = value.length,
|
||||
selectionEnd = selectionStart,
|
||||
): void {
|
||||
attach(target: TextEditTarget, value: string, anchor = value.length, focus = anchor): void {
|
||||
this.suppressBlur = true;
|
||||
this.target = target;
|
||||
this.input.value = value;
|
||||
this.input.focus({ preventScroll: true });
|
||||
this.input.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.select(anchor, focus);
|
||||
this.suppressBlur = false;
|
||||
}
|
||||
|
||||
@@ -83,9 +85,14 @@ export class KeyboardBridge {
|
||||
}
|
||||
|
||||
/** Pushes a value the canvas side changed into the input, so the next keystroke edits that text. */
|
||||
setValue(value: string, selectionStart = value.length, selectionEnd = selectionStart): void {
|
||||
setValue(value: string, anchor = value.length, focus = anchor): void {
|
||||
this.input.value = value;
|
||||
if (document.activeElement === this.input) this.input.setSelectionRange(selectionStart, selectionEnd);
|
||||
if (document.activeElement === this.input) this.select(anchor, focus);
|
||||
}
|
||||
|
||||
private select(anchor: number, focus: number): void {
|
||||
const direction = focus < anchor ? 'backward' : 'forward';
|
||||
this.input.setSelectionRange(Math.min(anchor, focus), Math.max(anchor, focus), direction);
|
||||
}
|
||||
|
||||
/** Ignored when another field attached meanwhile, so a late blur cannot steal it. */
|
||||
|
||||
@@ -167,23 +167,18 @@ export class TextField extends Widget implements TextEditTarget {
|
||||
private setSelection(start: number, end: number, pushToBridge: boolean): void {
|
||||
this.selStart = clamp(start, 0, this.valueText.length);
|
||||
this.selEnd = clamp(end, 0, this.valueText.length);
|
||||
if (pushToBridge && this.editing) {
|
||||
// The DOM only takes an ordered range; selStart/selEnd keep the anchor/focus order a
|
||||
// drag selection needs.
|
||||
const lo = Math.min(this.selStart, this.selEnd);
|
||||
const hi = Math.max(this.selStart, this.selEnd);
|
||||
ui().keyboard.setValue(this.valueText, lo, hi);
|
||||
}
|
||||
// selStart/selEnd are anchor/focus; the bridge keeps that order across its selectionchange echo.
|
||||
if (pushToBridge && this.editing) ui().keyboard.setValue(this.valueText, this.selStart, this.selEnd);
|
||||
this.caretVisible = true;
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
/** Adopts the input's value and selection verbatim; fires `onInput` only when the text changed. */
|
||||
onBridgeInput(value: string, selectionStart: number, selectionEnd: number): void {
|
||||
onBridgeInput(value: string, anchor: number, focus: number): void {
|
||||
const changed = value !== this.valueText;
|
||||
this.valueText = value;
|
||||
this.selStart = selectionStart;
|
||||
this.selEnd = selectionEnd;
|
||||
this.selStart = anchor;
|
||||
this.selEnd = focus;
|
||||
this.caretVisible = true;
|
||||
this.invalidate();
|
||||
if (changed) this.onInput?.(value);
|
||||
|
||||
@@ -105,6 +105,24 @@ export async function inspector(t) {
|
||||
JSON.stringify((await object('o2')).name),
|
||||
);
|
||||
|
||||
// 2d-ii. A drag from right to left selects the whole name (#236). The hidden input reported its
|
||||
// range ordered, and its selectionchange echo made the caret the anchor after the first move.
|
||||
await click(nameLabel.x + 160, nameLabel.y);
|
||||
const nameField = await json('__gm.focusedRect()');
|
||||
await press('Escape');
|
||||
const midY = nameField.y + nameField.h / 2;
|
||||
await drag(nameField.x + nameField.w - 4, midY, nameField.x + 3, midY);
|
||||
const backward = await json(
|
||||
'({ start: document.activeElement?.selectionStart, end: document.activeElement?.selectionEnd,' +
|
||||
' direction: document.activeElement?.selectionDirection })',
|
||||
);
|
||||
await press('Escape');
|
||||
check(
|
||||
'dragging from right to left selects the whole name',
|
||||
backward.start === 0 && backward.end === 'Renamed crate'.length && backward.direction === 'backward',
|
||||
JSON.stringify(backward),
|
||||
);
|
||||
|
||||
// 2e. Scrub the X field (right of the "X" label) by 100 px.
|
||||
const xLabel = await locate('X');
|
||||
await drag(xLabel.x + 160, xLabel.y, xLabel.x + 260, xLabel.y);
|
||||
|
||||
Reference in new issue
Block a user