spiegel-keyman/web/source/dom/targets/input.ts
Marc Durdin b05fda1c6a fix(web): selection direction and tests
Selection direction was not maintained in mutations, which could have
unexpected consequences. Added support for selection direction to input
and textarea.

The functions `getTextBeforeCaret()` and `getTextAfterCaret()` are named
somewhat incorrectly, as they actually get the text before and after the
active selection (and a collapsed zero-length selection is equivalent to
the caret). It would be worth renaming these in a future refactor.

This PR fixes the unit tests so that caret position is tested correctly
with an active selection -- the caret can be at either the start or the
end of the selection, corresponding with the direction in which the user
originally selected the text. It also fixes the assumptions around the
above named functions for `input` and `textarea` types.

Note that selection interactions are still buggy with prediction
selections; these bugs were present in 15.0.118-alpha and I will tackle
them in an upcoming commit.
2022-02-23 15:13:07 +11:00

177 lines
No EOL
5.5 KiB
TypeScript

namespace com.keyman.dom.targets {
export class Input extends OutputTarget {
root: HTMLInputElement;
/**
* Tracks the most recently-cached selection start index.
*/
private _cachedSelectionStart: number
/**
* Tracks the most recently processed, extended-string-based selection start index.
* When the element's selectionStart value changes, this should be invalidated.
*/
private processedSelectionStart: number;
/**
* Tracks the most recently processed, extended-string-based selection end index.
* When the element's selectionEnd value changes, this should be invalidated.
*/
private processedSelectionEnd: number;
constructor(ele: HTMLInputElement) {
super();
this.root = ele;
this._cachedSelectionStart = -1;
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLInputElement {
return this.root;
}
clearSelection(): void {
// Processes our codepoint-based variants of selectionStart and selectionEnd.
this.getCaret(); // updates processedSelectionStart if required
this.root.value = this.root.value._kmwSubstring(0, this.processedSelectionStart) + this.root.value._kmwSubstring(this.processedSelectionEnd); //I3319
this.setCaret(this.processedSelectionStart);
}
isSelectionEmpty(): boolean {
return this.root.selectionStart == this.root.selectionEnd;
}
hasSelection(): boolean {
return true;
}
invalidateSelection() {
// Since .selectionStart will never return this value, we use it to indicate
// the need to refresh our processed indices.
this._cachedSelectionStart = -1;
}
getCaret(): number {
if(this.root.selectionStart != this._cachedSelectionStart) {
this._cachedSelectionStart = this.root.selectionStart; // KMW-1
this.processedSelectionStart = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionStart); // I3319
this.processedSelectionEnd = this.root.value._kmwCodeUnitToCodePoint(this.root.selectionEnd); // I3319
}
return this.root.selectionDirection == 'forward' ? this.processedSelectionEnd : this.processedSelectionStart;
}
getDeadkeyCaret(): number {
return this.getCaret();
}
setCaret(caret: number) {
this.setSelection(caret, caret, "none");
}
setSelection(start: number, end: number, direction: "forward" | "backward" | "none") {
let domStart = this.root.value._kmwCodePointToCodeUnit(start);
let domEnd = this.root.value._kmwCodePointToCodeUnit(end);
this.root.setSelectionRange(domStart, domEnd, direction);
this.processedSelectionStart = start;
this.processedSelectionEnd = end;
Utils.forceScroll(this.root);
}
getSelectionDirection(): "forward" | "backward" | "none" {
return this.root.selectionDirection;
}
getTextBeforeCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(0, this.processedSelectionStart);
}
setTextBeforeCaret(text: string) {
this.getCaret();
let selectionLength = this.processedSelectionEnd - this.processedSelectionStart;
let direction = this.getSelectionDirection();
let newCaret = text._kmwLength();
this.root.value = text + this.getText()._kmwSubstring(this.processedSelectionStart);
this.setSelection(newCaret, newCaret + selectionLength, direction);
}
protected setTextAfterCaret(s: string) {
let c = this.getCaret();
let direction = this.getSelectionDirection();
this.root.value = this.getTextBeforeCaret() + s;
this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);
}
getTextAfterCaret(): string {
this.getCaret();
return this.getText()._kmwSubstring(this.processedSelectionEnd);
}
getText(): string {
return this.root.value;
}
deleteCharsBeforeCaret(dn: number) {
if(dn > 0) {
let curText = this.getTextBeforeCaret();
let caret = this.processedSelectionStart;
if(dn > caret) {
dn = caret;
}
this.adjustDeadkeys(-dn);
this.setTextBeforeCaret(curText.kmwSubstring(0, caret - dn));
this.setCaret(caret - dn);
}
}
insertTextBeforeCaret(s: string) {
if(!s) {
return;
}
let caret = this.getCaret();
let front = this.getTextBeforeCaret();
let back = this.getText()._kmwSubstring(this.processedSelectionStart);
this.adjustDeadkeys(s._kmwLength());
this.root.value = front + s + back;
this.setCaret(caret + s._kmwLength());
}
handleNewlineAtCaret(): void {
Input.newlineHandler(this.root);
}
static newlineHandler(inputEle: HTMLInputElement) {
// Can't occur for Mocks - just Input and TouchAlias types.
if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {
inputEle.disabled=false;
inputEle.form.submit();
} else {
// Allows compiling this separately from the main body of KMW.
// TODO: rework class to accept a class-static 'callback' from the DOM module that this can call.
// Would eliminate the need for this 'static' reference.
// Only strongly matters once we better modularize KMW, with web-dom vs web-dom-targets vs web-core, etc.
if(com.keyman["singleton"]) {
com.keyman["singleton"].domManager.moveToNext(false);
}
}
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}
}