Let a text field select from right to left with the mouse (#236)
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m36s

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:
adminandClaude Opus 5.5 committed 2026-09-26 15:30:33 +02:00
1 parent 74b9c60661
commit 1b3cb625a3
4 files changed
+43 -23

No files matched your search

+19 -12
View File
@@ -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. */
+5 -10
View File
@@ -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);