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
+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);
|
||||
|
||||
Reference in new issue
Block a user