chore(web): Merge remote-tracking branch 'origin/epic/web-core' into refactor/web/downgrade-web-utils-from-package-to-module

This commit is contained in:
Marc Durdin 2025-11-07 09:25:10 +00:00
commit feff055d8a
20 changed files with 13 additions and 1230 deletions

View file

@ -42,7 +42,7 @@ function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) {
}
}
export default class ContextManager extends ContextManagerBase<BrowserConfiguration> {
export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
private _activeKeyboard: {keyboard: JSKeyboard, metadata: KeyboardStub};
private cookieManager = new CookieSerializer<KeyboardCookie>('KeymanWeb_Keyboard');
readonly focusAssistant = new FocusAssistant(() => this.activeTarget?.isForcingScroll());

View file

@ -6,7 +6,7 @@ import {
type TextStore
} from 'keyman/engine/keyboard';
import ContextManager from './contextManager.js';
import { ContextManager } from './contextManager.js';
export default class DefaultBrowserRules extends DefaultRules {
private contextManager: ContextManager;

View file

@ -7,7 +7,7 @@ import { DomEventTracker } from 'keyman/engine/events';
import { DesignIFrameElementTextStore, nestedInstanceOf } from 'keyman/engine/element-text-stores';
import { textStoreForEvent, textStoreForElement } from 'keyman/engine/attachment';
import ContextManager from './contextManager.js';
import { ContextManager } from './contextManager.js';
type KeyboardState = {
activeKeyboard: JSKeyboard,

View file

@ -2,7 +2,7 @@ import { type AbstractElementTextStore } from 'keyman/engine/element-text-stores
import { FloatingOSKView } from 'keyman/engine/osk';
import { KeyboardInterfaceBase } from 'keyman/engine/main';
import ContextManager from './contextManager.js';
import { ContextManager } from './contextManager.js';
import { KeymanEngine } from './keymanEngine.js';
export class KeyboardInterface extends KeyboardInterfaceBase<ContextManager> {

View file

@ -12,7 +12,7 @@ import KeyboardObject = KeymanWebKeyboard.KeyboardObject;
import * as views from './viewsAnchorpoint.js';
import { BrowserConfiguration, BrowserInitOptionDefaults, BrowserInitOptionSpec } from './configuration.js';
import { default as ContextManager } from './contextManager.js';
import { ContextManager } from './contextManager.js';
import DefaultBrowserRules from './defaultBrowserRules.js';
import HardwareEventKeyboard from './hardwareEventKeyboard.js';
import { FocusStateAPIObject } from './context/focusAssistant.js';
@ -443,7 +443,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
kbd = this.core.activeKeyboard;
}
// TODO-web-core: implement for KMX keyboards if needed
// We only support isCJK on legacy .js keyboards; see #7928
return kbd && kbd instanceof JSKeyboard && kbd.isCJK;
}

View file

@ -1,6 +1,6 @@
import { OSKView } from "keyman/engine/osk";
import { KEYMAN_VERSION } from "@keymanapp/keyman-version";
import ContextManager from "./contextManager.js";
import { ContextManager } from "./contextManager.js";
import { KeymanEngine } from "./keymanEngine.js";
import { LanguageMenu } from "./languageMenu.js";

View file

@ -1,5 +1,5 @@
export { BrowserConfiguration, BrowserInitOptionSpec } from './configuration.js';
export { default as ContextManager, KeyboardCookie } from "./contextManager.js";
export { ContextManager, KeyboardCookie } from "./contextManager.js";
export { preprocessKeyboardEvent, default as HardwareEventKeyboard } from './hardwareEventKeyboard.js';
export { KeymanEngine } from './keymanEngine.js';

View file

@ -116,7 +116,7 @@ export class HostTextStore extends SyntheticTextStore {
}
}
export default class ContextManager extends ContextManagerBase<WebviewConfiguration> {
export class ContextManager extends ContextManagerBase<WebviewConfiguration> {
// Change of context? Just replace the SyntheticTextStore. Context will be ENTIRELY controlled
// by whatever is hosting the WebView. (Some aspects of this context replacement have
// yet to be modularized at this time, though.)

View file

@ -5,7 +5,7 @@ import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';
import { toPrefixedKeyboardId, toUnprefixedKeyboardId } from 'keyman/engine/keyboard-storage';
import { WebviewConfiguration, WebviewInitOptionDefaults, WebviewInitOptionSpec } from './configuration.js';
import ContextManager, { HostTextStore } from './contextManager.js';
import { ContextManager, HostTextStore } from './contextManager.js';
import PassthroughKeyboard from './passthroughKeyboard.js';
import { buildEmbeddedGestureConfig, setupEmbeddedListeners } from './oskConfiguration.js';
import { WorkerFactory } from '@keymanapp/lexical-model-layer';

View file

@ -1,52 +0,0 @@
import { TextStore } from "keyman/engine/keyboard";
import { EventEmitter } from 'eventemitter3';
export abstract class AbstractElementTextStore<EventMap extends EventEmitter.ValidEventTypes> extends TextStore {
// JS/TS can't do true multiple inheritance, so we maintain class events on a readonly field.
public readonly events: EventEmitter<EventMap, this> = new EventEmitter<EventMap, this>();
/**
* A field that may be used to track whether or not the represented context has changed over an
* arbitrary period of time.
*/
public changed = false;
/**
* Returns the underlying element / document modeled by the wrapper.
*/
abstract getElement(): HTMLElement;
public focus(): void {
const ele = this.getElement();
if(ele.focus) {
ele.focus();
}
}
/**
* Denotes when the represented element is forcing a text scroll via focus manipulation.
* As the intent is not to change the focused element, but just to have the browser update
* the scroll location, standard focus handlers (for updating the active context) should
* not deactivate the element while this state is active.
*/
isForcingScroll(): boolean {
return false;
}
/**
* A helper method for doInputEvent; creates a simple common event and default dispatching.
* @param elem
*/
protected dispatchInputEventOn(elem: HTMLElement) {
let event: InputEvent;
// `undefined` in pre-Chrome Edge and Chrome for Android before version 60.
if(window['InputEvent']) { // can't condition on the type directly; TS optimizes that out.
event = new InputEvent('input', {"bubbles": true, "cancelable": false});
}
if(elem && event) {
elem.dispatchEvent(event);
}
}
}

View file

@ -1,273 +0,0 @@
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
class SelectionCaret {
node: Node;
offset: number;
constructor(node: Node, offset: number) {
this.node = node;
this.offset = offset;
}
}
class SelectionRange {
start: SelectionCaret;
end: SelectionCaret;
constructor(start: SelectionCaret, end: SelectionCaret) {
this.start = start;
this.end = end;
}
}
export class ContentEditableElementTextStore extends AbstractElementTextStore<{}> {
root: HTMLElement;
constructor(ele: HTMLElement) {
if(ele.isContentEditable) {
super();
this.root = ele;
} else {
throw "Specified element is not already content-editable!";
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLElement {
return this.root;
}
isSelectionEmpty(): boolean {
if(!this.hasSelection()) {
return true;
}
return this.root.ownerDocument.getSelection().isCollapsed;
}
hasSelection(): boolean {
const Lsel = this.root.ownerDocument.getSelection();
if(this.root != Lsel.anchorNode && !this.root.contains(Lsel.anchorNode)) {
return false;
}
if(this.root != Lsel.focusNode && !this.root.contains(Lsel.focusNode)) {
return false;
}
return true;
}
clearSelection(): void {
if(this.hasSelection()) {
const Lsel = this.root.ownerDocument.getSelection();
if(!Lsel.isCollapsed) {
Lsel.deleteFromDocument(); // I2134, I2192
}
} else {
console.warn("Attempted to clear an unowned Selection!");
}
}
invalidateSelection(): void { /* No cache maintenance needed here, partly because
* it's impossible to cache a Selection; it mutates.
*/ }
getCarets(): SelectionRange {
const Lsel = this.root.ownerDocument.getSelection();
let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);
if(Lsel.isCollapsed) {
const caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
return new SelectionRange(caret, caret);
} else {
const anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
const focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);
if(anchor.node == focus.node) {
code = (focus.offset - anchor.offset > 0) ? 2 : 4;
}
if(code & 2) {
return new SelectionRange(anchor, focus);
} else { // Default
// can test against code & 4 to ensure Focus is before anchor, though.
return new SelectionRange(focus, anchor);
}
}
}
getDeadkeyCaret(): number {
return KMWString.length(this.getTextBeforeCaret());
}
getTextBeforeCaret(): string {
if(!this.hasSelection()) {
return this.getText();
}
const caret = this.getCarets().start;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(0, caret.offset);
}
getSelectedText(): string {
// TODO: figure out the proper implementation.
// KMW 16 and before behavior may be maintained by just returning the empty string.
return '';
}
getTextAfterCaret(): string {
if(!this.hasSelection()) {
return '';
}
const caret = this.getCarets().end;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(caret.offset);
}
getText(): string {
return this.root.innerText;
}
deleteCharsBeforeCaret(dn: number) {
if(!this.hasSelection() || dn <= 0) {
return;
}
const start = this.getCarets().start;
// Bounds-check on the number of chars to delete.
if(dn > start.offset) {
dn = start.offset;
}
if(start.node.nodeType != 3) {
console.warn("Deletion of characters requested without available context!");
return; // No context to delete characters from.
}
const range = this.root.ownerDocument.createRange();
const dnOffset = start.offset - KMWString.substr(start.node.nodeValue.substr(0, start.offset), -dn).length;
range.setStart(start.node, dnOffset);
range.setEnd(start.node, start.offset);
this.adjustDeadkeys(-dn);
range.deleteContents();
// No need to reposition the caret - the DOM will auto-move the selection accordingly, since
// we didn't use the selection to delete anything.
}
insertTextBeforeCaret(s: string) {
if(!this.hasSelection()) {
return;
}
const start = this.getCarets().start;
const delta = KMWString.length(s);
const Lsel = this.root.ownerDocument.getSelection();
if(delta == 0) {
return;
}
this.adjustDeadkeys(delta);
// While Selection.extend() was really nice for this, IE didn't support it whatsoever.
// However, IE (11, at least) DID support setting selections via ranges, so we were still
// able to manage the caret properly.
//
// TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().
const finalCaret = this.root.ownerDocument.createRange();
if(start.node.nodeType == 3) {
const textStart = <Text> start.node;
textStart.insertData(start.offset, s);
finalCaret.setStart(textStart, start.offset + s.length);
} else {
// Create a new text node - empty control
const n = start.node.ownerDocument.createTextNode(s);
const range = this.root.ownerDocument.createRange();
range.setStart(start.node, start.offset);
range.collapse(true);
range.insertNode(n);
finalCaret.setStart(n, s.length);
}
finalCaret.collapse(true);
Lsel.removeAllRanges();
try {
Lsel.addRange(finalCaret);
} catch(e) {
// Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.
// scrollIntoView synchronises the content for selection
start.node.parentElement.scrollIntoView();
Lsel.addRange(finalCaret);
}
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;
}
const caret = this.getCarets().end;
const delta = KMWString.length(s);
if(delta == 0) {
return;
}
// This is designed explicitly for use in direct-setting operations; deadkeys
// will be handled after this method.
if(caret.node.nodeType == 3) {
const textStart = <Text> caret.node;
textStart.replaceData(caret.offset, textStart.length, s);
} else {
// Create a new text node - empty control
const n = caret.node.ownerDocument.createTextNode(s);
const range = this.root.ownerDocument.createRange();
range.setStart(caret.node, caret.offset);
range.collapse(true);
range.insertNode(n);
}
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}

View file

@ -1,42 +0,0 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { type AbstractElementTextStore } from './abstractElementTextStore.js';
import { InputElementTextStore } from './inputElementTextStore.js';
import { TextAreaElementTextStore } from './textAreaElementTextStore.js';
import { DesignIFrameElementTextStore } from './designIFrameElementTextStore.js';
import { ContentEditableElementTextStore } from './contentEditableElementTextStore.js';
import { nestedInstanceOf } from './utils.js';
/**
* Wraps an HTMLElement in a concrete text-store implementation.
*
* @param e - The HTMLElement to create a text-store for.
* @returns A concrete AbstractElementTextStore for the element, or null if the element
* type is not supported or no suitable store can be created.
*/
export function createTextStoreForElement(e: HTMLElement): AbstractElementTextStore<any> {
// Complex type scoping is implemented here so that kmwutils.ts is not a dependency for test compilations.
if(nestedInstanceOf(e, "HTMLInputElement")) {
return new InputElementTextStore(<HTMLInputElement> e);
} else if(nestedInstanceOf(e, "HTMLTextAreaElement")) {
return new TextAreaElementTextStore(<HTMLTextAreaElement> e);
} else if(nestedInstanceOf(e, "HTMLIFrameElement")) {
const iframe = <HTMLIFrameElement> e;
if(iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.designMode == "on") {
return new DesignIFrameElementTextStore(iframe);
} else if (e.isContentEditable) {
// Do content-editable <iframe>s make sense?
return new ContentEditableElementTextStore(e);
} else {
return null;
}
} else if(e.isContentEditable) {
return new ContentEditableElementTextStore(e);
}
return null;
}

View file

@ -1,358 +0,0 @@
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
class SelectionCaret {
node: Node;
offset: number;
constructor(node: Node, offset: number) {
this.node = node;
this.offset = offset;
}
}
class SelectionRange {
start: SelectionCaret;
end: SelectionCaret;
constructor(start: SelectionCaret, end: SelectionCaret) {
this.start = start;
this.end = end;
}
}
class StyleCommand {
cmd: string;
stateType: number;
cache: string|boolean;
constructor(c: string, s:number) {
this.cmd = c;
this.stateType = s;
}
}
export class DesignIFrameElementTextStore extends AbstractElementTextStore<{}> {
root: HTMLIFrameElement;
doc: Document;
docRoot: HTMLElement;
commandCache: StyleCommand[];
constructor(ele: HTMLIFrameElement) {
super();
this.root = ele;
if(ele.contentWindow && ele.contentWindow.document && ele.contentWindow.document.designMode == 'on') {
this.doc = ele.contentWindow.document;
this.docRoot = ele.contentWindow.document.documentElement;
} else {
throw "Specified IFrame is not in design-mode!";
}
}
get isSynthetic(): boolean {
return false;
}
getElement(): HTMLIFrameElement {
return this.root;
}
focus(): void {
this.doc.defaultView.focus(); // I3363 (Build 301)
}
isSelectionEmpty(): boolean {
if(!this.hasSelection()) {
return true;
}
return this.doc.getSelection().isCollapsed;
}
hasSelection(): boolean {
const Lsel = this.doc.getSelection();
const outerSel = document.getSelection();
// If the outer doc's selection matches, we're active.
if(outerSel.anchorNode == Lsel.anchorNode && outerSel.focusNode == Lsel.focusNode) {
return true;
} else {
// Problem: for testing, we can't enforce the ideal (ie: first) condition.
// Technically, the IFrame _will_ always have its own internal selection, though... so... it kinda works?
return true;
}
}
clearSelection(): void {
if(this.hasSelection()) {
const Lsel = this.doc.getSelection();
if(!Lsel.isCollapsed) {
Lsel.deleteFromDocument(); // I2134, I2192
}
} else {
console.warn("Attempted to clear an unowned Selection!");
}
}
invalidateSelection(): void { /* No cache maintenance needed here, partly because
* it's impossible to cache a Selection; it mutates.
*/ }
getCarets(): SelectionRange {
const Lsel = this.doc.getSelection();
let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);
if(Lsel.isCollapsed) {
const caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
return new SelectionRange(caret, caret);
} else {
const anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
const focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);
if(anchor.node == focus.node) {
code = (focus.offset - anchor.offset > 0) ? 2 : 4;
}
if(code & 2) {
return new SelectionRange(anchor, focus);
} else { // Default
// can test against code & 4 to ensure Focus is before anchor, though.
return new SelectionRange(focus, anchor);
}
}
}
getDeadkeyCaret(): number {
return KMWString.length(this.getTextBeforeCaret());
}
getTextBeforeCaret(): string {
if(!this.hasSelection()) {
return this.getText();
}
const caret = this.getCarets().start;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(0, caret.offset);
}
getSelectedText(): string {
// TODO: figure out the proper implementation.
// KMW 16 and before behavior may be maintained by just returning the empty string.
return '';
}
getTextAfterCaret(): string {
if(!this.hasSelection()) {
return '';
}
const caret = this.getCarets().end;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(caret.offset);
}
getText(): string {
return this.docRoot.innerText;
}
deleteCharsBeforeCaret(dn: number) {
if(!this.hasSelection() || dn <= 0) {
return;
}
const start = this.getCarets().start;
// Bounds-check on the number of chars to delete.
if(dn > start.offset) {
dn = start.offset;
}
if(start.node.nodeType != 3) {
console.warn("Deletion of characters requested without available context!");
return; // No context to delete characters from.
}
const range = this.doc.createRange();
const dnOffset = start.offset - KMWString.substr(start.node.nodeValue.substr(0, start.offset), -dn).length;
range.setStart(start.node, dnOffset);
range.setEnd(start.node, start.offset);
this.adjustDeadkeys(-dn);
range.deleteContents();
// No need to reposition the caret - the DOM will auto-move the selection accordingly, since
// we didn't use the selection to delete anything.
}
insertTextBeforeCaret(s: string) {
if(!this.hasSelection()) {
return;
}
const start = this.getCarets().start;
const delta = KMWString.length(s);
const Lsel = this.doc.getSelection();
if(delta == 0) {
return;
}
this.adjustDeadkeys(delta);
// While Selection.extend() was really nice for this, IE didn't support it whatsoever.
// However, IE (11, at least) DID support setting selections via ranges, so we were still
// able to manage the caret properly.
//
// TODO: double-check that it was only IE-motivated, re-implement with Selection.extend().
const finalCaret = this.root.ownerDocument.createRange();
if(start.node.nodeType == 3) {
const textStart = <Text> start.node;
textStart.insertData(start.offset, s);
finalCaret.setStart(textStart, start.offset + s.length);
} else {
// Create a new text node - empty control
const n = this.doc.createTextNode(s);
const range = this.doc.createRange();
range.setStart(start.node, start.offset);
range.collapse(true);
range.insertNode(n);
finalCaret.setStart(n, s.length);
}
finalCaret.collapse(true);
Lsel.removeAllRanges();
try {
Lsel.addRange(finalCaret);
} catch(e) {
// Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.
// scrollIntoView synchronises the content for selection
start.node.parentElement.scrollIntoView();
Lsel.addRange(finalCaret);
}
Lsel.collapseToEnd();
}
handleNewlineAtCaret(): void {
// TODO: Implement.
//
// As it turns out, we never had an implementation for handling newline inputs from the OSK for this element type.
// At least this way, it's more explicit.
//
// Note: consult "// Create a new text node - empty control" case in insertTextBeforeCaret -
// this helps to handle the browser-default implementation of newline handling. In particular,
// entry of the first character after a newline.
//
// If raw newlines are entered into the HTML, but as with usual HTML, they're interpreted as excess whitespace and
// have no effect. We need to add DOM elements for a functional newline.
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;
}
const caret = this.getCarets().end;
const delta = KMWString.length(s);
if(delta == 0) {
return;
}
// This is designed explicitly for use in direct-setting operations; deadkeys
// will be handled after this method.
if(caret.node.nodeType == 3) {
const textStart = <Text> caret.node;
textStart.replaceData(caret.offset, textStart.length, s);
} else {
// Create a new text node - empty control
const n = caret.node.ownerDocument.createTextNode(s);
const range = this.root.ownerDocument.createRange();
range.setStart(caret.node, caret.offset);
range.collapse(true);
range.insertNode(n);
}
}
/**
* Function saveProperties
* Scope Private
* Description Build and create list of styles that can be applied in iframes
*/
saveProperties() {
// Formerly _CacheCommands.
const _CacheableCommands=[
new StyleCommand('backcolor',1), new StyleCommand('fontname',1), new StyleCommand('fontsize',1),
new StyleCommand('forecolor',1), new StyleCommand('bold',0), new StyleCommand('italic',0),
new StyleCommand('strikethrough',0), new StyleCommand('subscript',0),
new StyleCommand('superscript',0), new StyleCommand('underline',0)
];
if(this.doc.defaultView) {
_CacheableCommands.push(new StyleCommand('hilitecolor',1));
}
for(let n=0; n < _CacheableCommands.length; n++) { // I1511 - array prototype extended
const cmd = _CacheableCommands[n];
//KeymanWeb._Debug('Command:'+_CacheableCommands[n][0]);
if(cmd.stateType == 1) {
cmd.cache = this.doc.queryCommandValue(cmd.cmd);
} else {
cmd.cache = this.doc.queryCommandState(cmd.cmd);
}
}
this.commandCache = _CacheableCommands;
}
/**
* Function restoreProperties
* Scope Private
* Description Restore styles in IFRAMEs (??)
*/
restoreProperties(_func?: () => void): void {
// Formerly _CacheCommandsReset.
if(!this.commandCache) {
console.error("No command cache exists to restore!");
}
for(let n=0; n < this.commandCache.length; n++) { // I1511 - array prototype extended
const cmd = this.commandCache[n];
//KeymanWeb._Debug('ResetCacheCommand:'+_CacheableCommands[n][0]+'='+_CacheableCommands[n][2]);
if(cmd.stateType == 1) {
if(this.doc.queryCommandValue(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
this.doc.execCommand(cmd.cmd, false, <string> cmd.cache);
}
} else if(this.doc.queryCommandState(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
//KeymanWeb._Debug('executing command '+_CacheableCommand[n][0]);
this.doc.execCommand(cmd.cmd, false, null);
}
}
}
doInputEvent() {
// Root = the iframe, the outermost component and the one we were originally told to attach to.
this.dispatchInputEventOn(this.root);
}
}

View file

@ -1,11 +0,0 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
export { createTextStoreForElement } from './createTextStoreForElement.js';
export { ContentEditableElementTextStore } from './contentEditableElementTextStore.js';
export { DesignIFrameElementTextStore } from './designIFrameElementTextStore.js';
export { InputElementTextStore } from './inputElementTextStore.js';
export { AbstractElementTextStore } from './abstractElementTextStore.js';
export { TextAreaElementTextStore } from './textAreaElementTextStore.js';
export { nestedInstanceOf } from './utils.js';

View file

@ -1,233 +0,0 @@
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
interface EventMap {
/**
* This event will be raised when a newline is received by wrapped elements not of
* the 'search' or 'submit' types.
*
* Original code this is replacing:
```
// 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);
}
```
* This does not belong in a modularized version of this class; it must be supplied
* by the consuming top-level products instead.
*/
'unhandlednewline': (element: HTMLInputElement) => void
}
export class InputElementTextStore extends AbstractElementTextStore<EventMap> {
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;
/**
* Set, then unset within the `forceScroll` method in order to facilitate the
* `isForcingScroll` flag.
*/
private _activeForcedScroll: boolean;
constructor(ele: HTMLInputElement) {
super();
this.root = ele;
this._cachedSelectionStart = -1;
}
get isSynthetic(): boolean {
return false;
}
static isSupportedType(type: string): boolean {
return type == 'email' || type == 'search' || type == 'text' || type == 'url';
}
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 = KMWString.substr(this.root.value, 0, this.processedSelectionStart) + KMWString.substr(this.root.value, 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 = KMWString.codeUnitToCodePoint(this.root.value, this.root.selectionStart); // I3319
this.processedSelectionEnd = KMWString.codeUnitToCodePoint(this.root.value, 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") {
const domStart = KMWString.codePointToCodeUnit(this.root.value, start);
const domEnd = KMWString.codePointToCodeUnit(this.root.value, end);
this.root.setSelectionRange(domStart, domEnd, direction);
this.processedSelectionStart = start;
this.processedSelectionEnd = end;
this.forceScroll();
this.root.setSelectionRange(domStart, domEnd, direction);
}
forceScroll() {
// Only executes when com.keyman.DOMEventHandlers is defined.
//
// We bypass this whenever operating in the embedded format.
const element = this.getElement();
const selectionStart = element.selectionStart;
const selectionEnd = element.selectionEnd;
this._activeForcedScroll = true;
try {
//Forces scrolling; the re-focus triggers the scroll, at least.
element.blur();
element.focus();
} finally {
// On Edge, it appears that the blur/focus combination will reset the caret position
// under certain scenarios during unit tests. So, we re-set it afterward.
element.selectionStart = selectionStart;
element.selectionEnd = selectionEnd;
this._activeForcedScroll = false;
}
}
isForcingScroll(): boolean {
return this._activeForcedScroll;
}
getSelectionDirection(): "forward" | "backward" | "none" {
return this.root.selectionDirection;
}
getTextBeforeCaret(): string {
this.getCaret();
return KMWString.substring(this.getText(), 0, this.processedSelectionStart);
}
getSelectedText(): string {
this.getCaret();
return KMWString.substring(this.getText(), this.processedSelectionStart, this.processedSelectionEnd);
}
setTextBeforeCaret(text: string) {
this.getCaret();
const selectionLength = this.processedSelectionEnd - this.processedSelectionStart;
const direction = this.getSelectionDirection();
const newCaret = KMWString.length(text);
this.root.value = text + KMWString.substring(this.getText(), this.processedSelectionStart);
this.setSelection(newCaret, newCaret + selectionLength, direction);
}
protected setTextAfterCaret(s: string) {
const direction = this.getSelectionDirection();
this.root.value = this.getTextBeforeCaret() + s;
this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);
}
getTextAfterCaret(): string {
this.getCaret();
return KMWString.substring(this.getText(), this.processedSelectionEnd);
}
getText(): string {
return this.root.value;
}
deleteCharsBeforeCaret(dn: number) {
if(dn > 0) {
const curText = this.getTextBeforeCaret();
const caret = this.processedSelectionStart;
if(dn > caret) {
dn = caret;
}
this.adjustDeadkeys(-dn);
this.setTextBeforeCaret(KMWString.substring(curText, 0, caret - dn));
this.setCaret(caret - dn);
}
}
insertTextBeforeCaret(s: string) {
if(!s) {
return;
}
const caret = this.getCaret();
const front = this.getTextBeforeCaret();
const back = KMWString.substring(this.getText(), this.processedSelectionStart);
this.adjustDeadkeys(KMWString.length(s));
this.root.value = front + s + back;
this.setCaret(caret + KMWString.length(s));
}
handleNewlineAtCaret(): void {
const inputEle = this.root;
// Can't occur for Mocks - just Input types.
if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {
inputEle.disabled=false;
inputEle.form.submit();
} else {
this.events.emit('unhandlednewline', inputEle);
}
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}

View file

@ -1,9 +0,0 @@
## engine/element-wrappers
This submodule provides a subset of the main engine's Web-oriented code that's used to 'wrap' webpage
elements as part of KMW attachment and interface the element with the `keyboard` submodule.
Please keep any code in this folder / namespace as free as possible from dependencies on other parts of KMW. Some of our unit tests wish to run against these types without requiring KMW to be active.
Note that with a little work, we _could_ completely spin this into its own separate module - this could be useful for development and testing purposes, especially now that we've dropped the old `TouchAlias` type that was a bit entangled with main engine code.

View file

@ -1,201 +0,0 @@
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
export class TextAreaElementTextStore extends AbstractElementTextStore<{}> {
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;
/**
* Set, then unset within the `forceScroll` method in order to facilitate the
* `isForcingScroll` flag.
*/
private _activeForcedScroll: boolean;
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 = KMWString.substr(this.root.value, 0, this.processedSelectionStart) + KMWString.substr(this.root.value, 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 = KMWString.codeUnitToCodePoint(this.root.value, this.root.selectionStart); // I3319
this.processedSelectionEnd = KMWString.codeUnitToCodePoint(this.root.value, 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") {
const domStart = KMWString.codePointToCodeUnit(this.root.value, start);
const domEnd = KMWString.codePointToCodeUnit(this.root.value, end);
this.root.setSelectionRange(domStart, domEnd, direction);
this.processedSelectionStart = start;
this.processedSelectionEnd = end;
this.forceScroll();
this.root.setSelectionRange(domStart, domEnd, direction);
}
forceScroll() {
// Only executes when com.keyman.DOMEventHandlers is defined.
//
// We bypass this whenever operating in the embedded format.
const element = this.getElement();
const selectionStart = element.selectionStart;
const selectionEnd = element.selectionEnd;
this._activeForcedScroll = true;
try {
//Forces scrolling; the re-focus triggers the scroll, at least.
element.blur();
element.focus();
} finally {
// On Edge, it appears that the blur/focus combination will reset the caret position
// under certain scenarios during unit tests. So, we re-set it afterward.
element.selectionStart = selectionStart;
element.selectionEnd = selectionEnd;
this._activeForcedScroll = false;
}
}
isForcingScroll(): boolean {
return this._activeForcedScroll;
}
getSelectionDirection(): "forward" | "backward" | "none" {
return this.root.selectionDirection;
}
getTextBeforeCaret(): string {
this.getCaret();
return KMWString.substring(this.getText(), 0, this.processedSelectionStart);
}
setTextBeforeCaret(text: string) {
this.getCaret();
const selectionLength = this.processedSelectionEnd - this.processedSelectionStart;
const direction = this.getSelectionDirection();
const newCaret = KMWString.length(text);
this.root.value = text + KMWString.substring(this.getText(), this.processedSelectionStart);
this.setSelection(newCaret, newCaret + selectionLength, direction);
}
protected setTextAfterCaret(s: string) {
const direction = this.getSelectionDirection();
this.root.value = this.getTextBeforeCaret() + s;
this.setSelection(this.processedSelectionStart, this.processedSelectionEnd, direction);
}
getTextAfterCaret(): string {
this.getCaret();
return KMWString.substring(this.getText(), this.processedSelectionEnd);
}
getSelectedText(): string {
this.getCaret();
return KMWString.substring(this.getText(), this.processedSelectionStart, this.processedSelectionEnd);
}
getText(): string {
return this.root.value;
}
deleteCharsBeforeCaret(dn: number) {
if(dn > 0) {
const curText = this.getTextBeforeCaret();
const caret = this.processedSelectionStart;
if(dn > caret) {
dn = caret;
}
this.adjustDeadkeys(-dn);
this.setTextBeforeCaret(KMWString.substr(curText, 0, caret - dn));
this.setCaret(caret - dn);
}
}
insertTextBeforeCaret(s: string) {
if(!s) {
return;
}
const caret = this.getCaret();
const front = this.getTextBeforeCaret();
const back = KMWString.substring(this.getText(), this.processedSelectionStart);
this.adjustDeadkeys(KMWString.length(s));
this.root.value = front + s + back;
this.setCaret(caret + KMWString.length(s));
}
handleNewlineAtCaret(): void {
this.insertTextBeforeCaret('\n');
}
doInputEvent() {
this.dispatchInputEventOn(this.root);
}
}

View file

@ -1,38 +0,0 @@
/**
* Checks the type of an input DOM-related object while ensuring that it is checked against the correct prototype,
* as class prototypes are (by specification) scoped upon the owning Window.
*
* See https://stackoverflow.com/questions/43587286/why-does-instanceof-return-false-on-chrome-safari-and-edge-and-true-on-firefox
* for more details.
*
* @param {EventTarget} Pelem An element of the web page or one of its IFrame-based subdocuments.
* @param {string} className The plain-text name of the expected Element type.
* @return {boolean}
*/
export function nestedInstanceOf(Pelem: EventTarget, className: string): boolean {
let scopedClass;
if(!Pelem) {
// If we're bothering to check something's type, null references don't match
// what we're looking for.
return false;
}
// @ts-ignore
if (Pelem['Window']) { // Window objects contain the class definitions for types held within them. So, we can check for those.
return className == 'Window';
// @ts-ignore
} else if (Pelem['defaultView']) { // Covers Document.
// @ts-ignore
scopedClass = (Pelem as Document)['defaultView'][className];
// @ts-ignore
} else if(Pelem['ownerDocument']) {
// @ts-ignore
scopedClass = (Pelem as Node).ownerDocument.defaultView[className];
}
if(scopedClass) {
return Pelem instanceof scopedClass;
} else {
return false;
}
}

View file

@ -1,4 +1,4 @@
## engine/element-wrappers
## engine/element-text-stores
This submodule provides a subset of the main engine's Web-oriented code that's used to 'wrap' webpage
elements as part of KMW attachment and interface the element with the `keyboard` submodule.

View file

@ -1,4 +1,4 @@
import { type KeyEvent, JSKeyboard, Keyboard, KeyboardProperties, KeyboardKeymanGlobal, ProcessorAction } from "keyman/engine/keyboard";
import { type KeyEvent, JSKeyboard, Keyboard, KeyboardProperties, KeyboardKeymanGlobal, ProcessorAction, KMXKeyboard } from "keyman/engine/keyboard";
import { ProcessorInitOptions } from 'keyman/engine/js-processor';
// TODO-web-core: remove alias
import { DOMKeyboardLoader as KeyboardLoader } from "keyman/engine/keyboard";
@ -566,7 +566,7 @@ export class KeymanEngineBase<
const kbdObj = this.keyboardRequisitioner.cache.getKeyboard(k0);
if (!kbdObj) {
throw new Error(`Keyboard '${k0}' has not been loaded.`);
} else if (!(kbdObj instanceof JSKeyboard)) {
} else if (kbdObj instanceof KMXKeyboard) {
return false; // TODO-web-core: implement for KMX keyboards
} else {
k0 = kbdObj;