From 5a324c2d4284c82b93c37ffeae44ba009b58e582 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 5 Nov 2025 17:12:50 +0100 Subject: [PATCH 1/3] refactor(web): change default export of ContextManager This change replaces the default export of ContextManager with a regular export. Test-bot: skip --- web/src/app/browser/src/contextManager.ts | 2 +- web/src/app/browser/src/defaultBrowserRules.ts | 2 +- web/src/app/browser/src/hardwareEventKeyboard.ts | 2 +- web/src/app/browser/src/keyboardInterface.ts | 2 +- web/src/app/browser/src/keymanEngine.ts | 2 +- web/src/app/browser/src/oskConfiguration.ts | 2 +- web/src/app/browser/src/test-index.ts | 2 +- web/src/app/webview/src/contextManager.ts | 2 +- web/src/app/webview/src/keymanEngine.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index d079ea6d9d..a7c3708a26 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -42,7 +42,7 @@ function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) { } } -export default class ContextManager extends ContextManagerBase { +export class ContextManager extends ContextManagerBase { private _activeKeyboard: {keyboard: JSKeyboard, metadata: KeyboardStub}; private cookieManager = new CookieSerializer('KeymanWeb_Keyboard'); readonly focusAssistant = new FocusAssistant(() => this.activeTarget?.isForcingScroll()); diff --git a/web/src/app/browser/src/defaultBrowserRules.ts b/web/src/app/browser/src/defaultBrowserRules.ts index f85baac16d..d6740978d6 100644 --- a/web/src/app/browser/src/defaultBrowserRules.ts +++ b/web/src/app/browser/src/defaultBrowserRules.ts @@ -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; diff --git a/web/src/app/browser/src/hardwareEventKeyboard.ts b/web/src/app/browser/src/hardwareEventKeyboard.ts index 1ce8893744..4db23cc0d5 100644 --- a/web/src/app/browser/src/hardwareEventKeyboard.ts +++ b/web/src/app/browser/src/hardwareEventKeyboard.ts @@ -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, diff --git a/web/src/app/browser/src/keyboardInterface.ts b/web/src/app/browser/src/keyboardInterface.ts index ded3a1d71c..dc9a8cbe80 100644 --- a/web/src/app/browser/src/keyboardInterface.ts +++ b/web/src/app/browser/src/keyboardInterface.ts @@ -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 { diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 1479f9be79..200b205a0a 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -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'; diff --git a/web/src/app/browser/src/oskConfiguration.ts b/web/src/app/browser/src/oskConfiguration.ts index d9f5764671..f8ee6360d9 100644 --- a/web/src/app/browser/src/oskConfiguration.ts +++ b/web/src/app/browser/src/oskConfiguration.ts @@ -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"; diff --git a/web/src/app/browser/src/test-index.ts b/web/src/app/browser/src/test-index.ts index 83df5dc8e7..d094fe7c25 100644 --- a/web/src/app/browser/src/test-index.ts +++ b/web/src/app/browser/src/test-index.ts @@ -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'; diff --git a/web/src/app/webview/src/contextManager.ts b/web/src/app/webview/src/contextManager.ts index dfb6c2fd30..e2057035f3 100644 --- a/web/src/app/webview/src/contextManager.ts +++ b/web/src/app/webview/src/contextManager.ts @@ -116,7 +116,7 @@ export class HostTextStore extends SyntheticTextStore { } } -export default class ContextManager extends ContextManagerBase { +export class ContextManager extends ContextManagerBase { // 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.) diff --git a/web/src/app/webview/src/keymanEngine.ts b/web/src/app/webview/src/keymanEngine.ts index e7d98ee987..14b63dcc51 100644 --- a/web/src/app/webview/src/keymanEngine.ts +++ b/web/src/app/webview/src/keymanEngine.ts @@ -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'; From fb728d922d2e285a881d09dab5addc5757d619dd Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 6 Nov 2025 18:55:22 +0100 Subject: [PATCH 2/3] refactor(web): remove duplicate element-text-stores folder Test-bot: skip --- .../abstractElementTextStore.ts | 52 --- .../contentEditableElementTextStore.ts | 273 ------------- .../createTextStoreForElement.ts | 42 -- .../designIFrameElementTextStore.ts | 358 ------------------ web/src/engine/element-text-stores/index.ts | 11 - .../inputElementTextStore.ts | 233 ------------ web/src/engine/element-text-stores/readme.md | 9 - .../textAreaElementTextStore.ts | 201 ---------- web/src/engine/element-text-stores/utils.ts | 38 -- .../engine/src/element-text-stores/readme.md | 2 +- 10 files changed, 1 insertion(+), 1218 deletions(-) delete mode 100644 web/src/engine/element-text-stores/abstractElementTextStore.ts delete mode 100644 web/src/engine/element-text-stores/contentEditableElementTextStore.ts delete mode 100644 web/src/engine/element-text-stores/createTextStoreForElement.ts delete mode 100644 web/src/engine/element-text-stores/designIFrameElementTextStore.ts delete mode 100644 web/src/engine/element-text-stores/index.ts delete mode 100644 web/src/engine/element-text-stores/inputElementTextStore.ts delete mode 100644 web/src/engine/element-text-stores/readme.md delete mode 100644 web/src/engine/element-text-stores/textAreaElementTextStore.ts delete mode 100644 web/src/engine/element-text-stores/utils.ts diff --git a/web/src/engine/element-text-stores/abstractElementTextStore.ts b/web/src/engine/element-text-stores/abstractElementTextStore.ts deleted file mode 100644 index e22d6588ae..0000000000 --- a/web/src/engine/element-text-stores/abstractElementTextStore.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TextStore } from "keyman/engine/keyboard"; -import { EventEmitter } from 'eventemitter3'; - -export abstract class AbstractElementTextStore extends TextStore { - // JS/TS can't do true multiple inheritance, so we maintain class events on a readonly field. - public readonly events: EventEmitter = new EventEmitter(); - - /** - * 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); - } - } -} \ No newline at end of file diff --git a/web/src/engine/element-text-stores/contentEditableElementTextStore.ts b/web/src/engine/element-text-stores/contentEditableElementTextStore.ts deleted file mode 100644 index bd2aebcf1f..0000000000 --- a/web/src/engine/element-text-stores/contentEditableElementTextStore.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { AbstractElementTextStore } from './abstractElementTextStore.js'; -import { KMWString } from '@keymanapp/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 = 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 = 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); - } -} \ No newline at end of file diff --git a/web/src/engine/element-text-stores/createTextStoreForElement.ts b/web/src/engine/element-text-stores/createTextStoreForElement.ts deleted file mode 100644 index a591ae32f9..0000000000 --- a/web/src/engine/element-text-stores/createTextStoreForElement.ts +++ /dev/null @@ -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 { - // Complex type scoping is implemented here so that kmwutils.ts is not a dependency for test compilations. - - if(nestedInstanceOf(e, "HTMLInputElement")) { - return new InputElementTextStore( e); - } else if(nestedInstanceOf(e, "HTMLTextAreaElement")) { - return new TextAreaElementTextStore( e); - } else if(nestedInstanceOf(e, "HTMLIFrameElement")) { - const iframe = e; - - if(iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.designMode == "on") { - return new DesignIFrameElementTextStore(iframe); - } else if (e.isContentEditable) { - // Do content-editable