mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-29 03:37:42 +00:00
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:
commit
feff055d8a
20 changed files with 13 additions and 1230 deletions
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue