mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-09 10:25:32 +00:00
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.
170 lines
No EOL
5 KiB
TypeScript
170 lines
No EOL
5 KiB
TypeScript
namespace com.keyman.dom.targets {
|
|
export class TextArea extends OutputTarget {
|
|
root: HTMLTextAreaElement;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* Used to temporarily store the y-axis scroll coordinate.
|
|
*/
|
|
private scrollTop?: number;
|
|
|
|
/**
|
|
* Used to temporarily store the x-axis scroll coordinate.
|
|
*/
|
|
private scrollLeft?: number;
|
|
|
|
constructor(ele: HTMLTextAreaElement) {
|
|
super();
|
|
|
|
this.root = ele;
|
|
this._cachedSelectionStart = -1;
|
|
}
|
|
|
|
get isSynthetic(): boolean {
|
|
return false;
|
|
}
|
|
|
|
getElement(): HTMLTextAreaElement {
|
|
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 {
|
|
this.insertTextBeforeCaret('\n');
|
|
}
|
|
|
|
doInputEvent() {
|
|
this.dispatchInputEventOn(this.root);
|
|
}
|
|
}
|
|
} |