chore(web): Merge branch 'refactor/web/merge-engine-modules' into refactor/web/downgrade-web-utils-from-package-to-module

This commit is contained in:
Marc Durdin 2025-11-06 14:01:36 +01:00
commit 2e51955055
102 changed files with 2568 additions and 1048 deletions

View file

@ -78,7 +78,6 @@
"src/schemas/*",
"tests/",
"src/keyboard-object.ts",
"src/outputTarget.interface.ts",
"src/*.d.ts",
"src/main.ts",
"src/schema-validators.ts",

View file

@ -8,8 +8,8 @@ export type ComplexKeyboardStore = (string | { t: 'd', d: number } | { ['t']: 'b
// A stub for KeyEvent which is properly defined in KeymanWeb
type KeyEventStub = {};
// A stub for OutputTarget which is properly defined in KeymanWeb
type OutputTargetStub = {};
// A stub for TextStore which is properly defined in KeymanWeb
type TextStoreStub = {};
export interface EncodedVisualKeyboard {
/** Represents CSS font styling to use for VisualKeyboard text */
@ -43,31 +43,31 @@ export type KeyboardObject = {
* group-start: the function triggering processing for the keyboard's
* "Unicode" start group, corresponding to `begin Unicode > use(_____)` in
* Keyman keyboard language.
* @param outputTarget The context to which the keystroke applies
* @param textStore The context to which the keystroke applies
* @param keystroke The full, pre-processed keystroke triggering
* keyboard-rule application.
*/
gs(outputTarget: OutputTargetStub, keystroke: KeyEventStub): boolean;
gs(textStore: TextStoreStub, keystroke: KeyEventStub): boolean;
/**
* group-newcontext: the function triggering processing for the keyboard's
* "NewContext" start group, corresponding to `begin NewContext > use(_____)`
* in Keyman keyboard language.
* @param outputTarget The new context to be used with future keystrokes
* @param textStore The new context to be used with future keystrokes
* @param keystroke A 'null' `KeyEvent` providing current modifier + state information.
*/
gn?(outputTarget: OutputTargetStub, keystroke: KeyEventStub): boolean;
gn?(textStore: TextStoreStub, keystroke: KeyEventStub): boolean;
/**
* group-postkeystroke: the function triggering processing for the keyboard's
* "PostKeystroke" start group, corresponding to `begin PostKeystroke >
* use(_____)` in Keyman keyboard language.
* @param outputTarget The context altered by a recent keystroke. As a
* @param textStore The context altered by a recent keystroke. As a
* precondition, all changes due to `gs` / `begin Unicode` should already be
* applied.
* @param keystroke A 'null' `KeyEvent` providing current modifier + state information.
*/
gpk?(outputTarget: OutputTargetStub, keystroke: KeyEventStub): boolean;
gpk?(textStore: TextStoreStub, keystroke: KeyEventStub): boolean;
/**
* Keyboard ID: the uniquely-identifying name for this keyboard. Includes the standard
@ -175,6 +175,6 @@ export type KeyboardObject = {
* @param {number} _PData 1 or 0
* @returns
*/
KNS?: (_PCommand: number, _PTarget: OutputTargetStub, _PData: number) => void;
KNS?: (_PCommand: number, _PTarget: TextStoreStub, _PData: number) => void;
} & Record<`s${number}`, string>

3
package-lock.json generated
View file

@ -14222,6 +14222,9 @@
"name": "@keymanapp/web-utils",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@keymanapp/common-types": "*"
},
"devDependencies": {
"@keymanapp/keyman-version": "*",
"@keymanapp/resources-gosh": "*",

View file

@ -117,7 +117,7 @@ graph TD;
subgraph ClassicWeb["`**ClassicWeb**
Intermediate-level engine modules`"]
Elements["/web/src/engine/element-wrappers"];
Elements["/web/src/engine/element-text-stores"];
Elements-->JSProc;
KeyboardStorage["/web/src/engine/keyboard-storage"];
KeyboardStorage-->Interfaces;

View file

@ -112,7 +112,7 @@ build_action() {
precompile "${dir}"
done
cp "${KEYMAN_ROOT}/web/src/test/auto/dom/cases/attachment/outputTargetForElement.tests.html" \
cp "${KEYMAN_ROOT}/web/src/test/auto/dom/cases/attachment/textStoreForElement.tests.html" \
"${KEYMAN_ROOT}/web/build/test/dom/cases/attachment/"
}

View file

@ -11,13 +11,13 @@ gap between desktop and web core functionality for `context(n)` matching on `not
## Syntax
```js
keyman.interface.contextExOutput(dn, outputTarget, contextLength, contextOffset);
keyman.interface.contextExOutput(dn, textStore, contextLength, contextOffset);
```
or
```js
KeymanWeb.KCXO(dn, outputTarget, contextLength, contextOffset); // Shorthand
KeymanWeb.KCXO(dn, textStore, contextLength, contextOffset); // Shorthand
```
## Parameters
@ -26,8 +26,8 @@ or
: Type: `number`
: number of characters to delete left of cursor
`outputTarget`
: Type: `OutputTarget`
`textStore`
: Type: `TextStore`
: target to output to
`contextLength`

View file

@ -1,72 +1,169 @@
# Context State Management
## The `OutputTarget` Abstraction
## The `TextStore` Abstraction
The `OutputTarget` abstraction and its associated types and classes exist to facilitate handling different types of context sources within Keyman Engine for Web through a common interface. In essence, any implementing type is valid within the engine as a "target" for "output" from any existing keyboard supported by the engine. Through 18.0, only JS-based keyboards were supported due to lack of implementation of alternate keystroke-processing engines.
The `TextStore` abstraction and its associated types and classes exist
to facilitate handling different types of context sources within Keyman
Engine for Web through a common interface. In essence, any implementing
type is valid within the engine as a "target" for "output" from any
existing keyboard supported by the engine. Through 18.0, only JS-based
keyboards were supported due to lack of implementation of alternate
keystroke-processing engines.
At the most basic level, the abstraction is defined at [web/src/engine/keyboard/outputTarget.interface.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/outputTarget.interface.ts). The methods defined therein support properties and methods for fetching, setting, and manipulating deadkey markers, text-selection, and text within whatever context source it represents. JS-keyboard keystroke processing directly uses these methods during operation.
- `epic/web-core` note: moved to `outputTarget.ts` in the same folder, dropping the `.interface` component.
The base implmentation may be found at
[web/src/engine/keyboard/textStore.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/textStore.ts).
The methods defined therein support properties and methods for fetching,
setting, and manipulating deadkey markers, text-selection, and text
within whatever context source it represents. JS-keyboard keystroke
processing directly uses these methods during operation.
The base implementation for this type may be found at [web/src/engine/js-processor/src/outputTarget.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/js-processor/src/outputTarget.ts). This implementation provides common support for deadkey tracking, matching, and handling - the same strategy is used regardless of context-source type. A few additional methods are provided to assist with comparison and contrast between two different context states and restoration of a prior context state.
- `epic/web-core` note: renamed `OutputTargetBase`, moved to `outputTargetBase.ts` in the same folder
Since the context sources are the same for all keyboard processors, the
all use the same base implementation, although not all funcitonality is
needed by all keyboard processors (e.g. deadkey tracking functionality
is only needed by the JS-keyboard processor). This implementation
provides common support for deadkey tracking, matching, and handling -
the same strategy is used regardless of context-source type. A few
additional methods are provided to assist with comparison and contrast
between two different context states and restoration of a prior context
state.
- note: previously called `OutputTarget`
### Deadkey management
Specifics for the implementation of JS-keyboard deadkeys can be found here: https://github.com/keymanapp/keyman/blob/b4df4ab80862bc90da42bcdbd333df0a14da01ca/web/src/engine/js-processor/src/deadkeys.ts#L2-L6
- `epic/web-core` note: unaltered.
Specifics for the implementation of JS-keyboard deadkeys can be found
here: [web/src/engine/keyboard/src/deadkeys.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/deadkeys.ts)
`ordinal`: if two deadkeys are in the same "virtual" position, this field resolves which came first.
`matched`: used during keyboard-rule processing.
`ordinal`: if two deadkeys are in the same "virtual" position, this
field resolves which came first. `matched`: used during keyboard-rule
processing.
Note that we do NOT actually insert the deadkeys into the raw text-context! Web's pattern here is different than that of Keyman Core.
Note that we do NOT actually insert the deadkeys into the raw
text-context! Web's pattern here is different than that of Keyman Core.
## Manipulating context-states
Initially used to support predictive-text, the `Transform` type aims to encapsulate the minimal information necessary to transition from one context-state to another. It is both selection-agnostic and deadkey-agnostic. `Transform`s are used both by predictive-text and by the webview-embedded build of the engine in order to succinctly communicate the data needed to update context upon receiving keystrokes.
- `Transform`'s specification may be found at [common/web/types/src/lexical-model-types.ts](https://github.com/keymanapp/keyman/blob/master/common/web/types/src/lexical-model-types.ts).
Initially used to support predictive-text, the `Transform` type aims to
encapsulate the minimal information necessary to transition from one
context-state to another. It is both selection-agnostic and
deadkey-agnostic. `Transform`s are used both by predictive-text and by
the webview-embedded build of the engine in order to succinctly
communicate the data needed to update context upon receiving keystrokes.
- `Transform`'s specification may be found at
[common/web/types/src/lexical-model-types.ts](https://github.com/keymanapp/keyman/blob/master/common/web/types/src/lexical-model-types.ts).
`Transform`s consist of three values:
- `deleteLeft` - the number of codepoints prior to the caret/selection that should be deleted
- `insert` - the text to insert at the caret and/or replace currently-selected text
- `deleteRight` - the number of codepoints _after_ the caret/selection that should be deleted
- Note that `deleteRight` does not currently see actual use due to iOS platform limitations.
This type may then be used as an argument to `OutputTarget.apply()` (defined on `OutputTarget` (`epic/web-core`: `OutputTargetBase`)) to update any context source accordingly.
- `deleteLeft` - the number of codepoints prior to the caret/selection
that should be deleted
- `insert` - the text to insert at the caret and/or replace
currently-selected text
- `deleteRight` - the number of codepoints _after_ the caret/selection
that should be deleted
- Note that `deleteRight` does not currently see actual use due to iOS
platform limitations.
It is possible to determine the `Transform` needed to transition from one `OutputTarget` to another using `OutputTarget.buildTransformFrom` (defined on `OutputTarget` (`epic/web-core`: `OutputTargetBase`)).
This type may then be used as an argument to `TextStore.apply()`
(defined on `TextStore`) to update any context source accordingly.
### The `Mock` - representing context-state
It is possible to determine the `Transform` needed to transition from
one `TextStore` to another using `TextStore.buildTransformFrom`
(defined on `TextStore`).
The comparison and contrast methods mentioned above for `OutputTarget` are of particular use for predictive text, which usually operates with a headless implementation of the type, termed a `Mock`. This class may be found in [web/src/engine/js-processor/src/mock.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/js-processor/src/mock.ts). It is a fully-featured implementation of the `OutputTarget` interface.
- `epic/web-core` note: unaltered.
### The `SyntheticTextStore` - representing context-state
It is possible to make a `Mock`-based clone of any `OutputTarget`-derived type - a feature leveraged significantly within the inner workings of Keyman Engine for Web. As JS keyboards can have side effects beyond text-manipulation, predictive text generally operates by first _cloning_ the "true" context source. `Mock`s are also used when saving context states within the engine for later reference and/or reuse - a feature also utilized significantly for multitap support.
The comparison and contrast methods mentioned above for `TextStore`
are of particular use for predictive text, which usually operates with a
headless implementation of the type, termed a `SyntheticTextStore`. This class may be
found in
[web/src/engine/keyboard/src/syntheticTextStore.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/syntheticTextStore.ts).
It is a fully-featured implementation of the `TextStore` interface.
`Mock`s can also easily be constructed from scratch for a simple string. Optionally, caret position or selection data may be specified at construction time as well. `epic/web-core`: in theory, this should make them easy to utilize for integration with Keyman Core.
It is possible to make a `SyntheticTextStore`-based clone of any
`TextStore`-derived type - a feature leveraged significantly within
the inner workings of Keyman Engine for Web. As JS keyboards can have
side effects beyond text-manipulation, predictive text generally
operates by first _cloning_ the "true" context source. `SyntheticTextStore`s are
also used when saving context states within the engine for later
reference and/or reuse - a feature also utilized significantly for
multitap support.
`SyntheticTextStore`s can also easily be constructed from scratch for a simple string.
Optionally, caret position or selection data may be specified at
construction time as well. `epic/web-core`: in theory, this should make
them easy to utilize for integration with Keyman Core.
- note: previously called `Mock`
### The `Transcription` - representing context-state transitions
The `Transcription` class (defined within [web/src/engine/js-processor/src/outputTarget.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/js-processor/src/outputTarget.ts)) is the type within the engine that most closely matches a sense of transition in context state. These are generally constructed by comparing two `OutputTarget` instances to each other via `OutputTarget.buildTranscriptionFrom`, with the base instance corresponding to the "new" state and the first parameter matching the original state before transition.
- `epic/web-core` note: moved to `outputTargetBase.ts` in the same folder; the method is on `OutputTargetBase`.
The `Transcription` class (defined in
[web/src/engine/js-processor/src/transcription.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/js-processor/src/transcription.ts))
is the type within the engine that most closely matches a sense of
transition in context state. These are generally constructed by
comparing two `TextStore` instances to each other via
`TextStore.buildTranscriptionFrom`, with the base instance
corresponding to the "new" state and the first parameter matching the
original state before transition.
`Transcription`s are granted unique identifiers and are used within the engine for preservation of recent context states. These identifiers are currently generated within the class's constructor and are internally set. When predictive-text generates new suggestions or a multitap needs to revert to a prior context, both will use the `Transcription`'s unique identifier in order to find the corresponding context state and leverage it as needed for their operations.
`Transcription`s are granted unique identifiers and are used within the
engine for preservation of recent context states. These identifiers are
currently generated within the class's constructor and are internally
set. When predictive-text generates new suggestions or a multitap needs
to revert to a prior context, both will use the `Transcription`'s unique
identifier in order to find the corresponding context state and leverage
it as needed for their operations.
Important fields:
- `keystroke` - the keystroke that triggered the context change corresponding to this `Transcription`
- `keystroke` - the keystroke that triggered the context change
corresponding to this `Transcription`
- `transform` - the direct effects of the keystroke
- This uses a specialized variant that also notes if the transition destroyed previously-existing selected text.
- `preInput` - the state of the context immediately before the keystroke was processed
Note that the transition metadata does not include deadkeys generated by its triggering `keystroke`. The decision was made long ago to forgo directly recording deadkey changes when recording `Transcriptions`, as any operation that restores an old context also seeks to apply deadkey-destroying operations immediately afterward. Should we ever need to do so, PR #1611 contains code that was originally designed for actively detecting and recording deadkey transition data in `Transcription`s.
- This uses a specialized variant that also notes if the transition
destroyed previously-existing selected text.
A cache of recent context-state transitions is stored at `keyman.core.contextCache`, with `keyman.core` being an instance of `InputProcessor` ([web/src/engine/main/src/headless/inputProcessor.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/main/src/headless/inputProcessor.ts)), the component responsible for linking keystroke processing with predictive-text support and restoration of context-state for multitap-generated keystrokes.
- `preInput` - the state of the context immediately before the keystroke
was processed
Note that the transition metadata does not include deadkeys generated by
its triggering `keystroke`. The decision was made long ago to forgo
directly recording deadkey changes when recording `Transcriptions`, as
any operation that restores an old context also seeks to apply
deadkey-destroying operations immediately afterward. Should we ever
need to do so, PR #1611 contains code that was originally designed for
actively detecting and recording deadkey transition data in
`Transcription`s.
A cache of recent context-state transitions is stored at
`keyman.core.contextCache`, with `keyman.core` being an instance of
`InputProcessor`
([web/src/engine/main/src/headless/inputProcessor.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/main/src/headless/inputProcessor.ts)),
the component responsible for linking keystroke processing with
predictive-text support and restoration of context-state for
multitap-generated keystrokes.
### JS-keyboard keystroke processing
For JS-keyboard keystroke processing, a `Mock` clone of the context is generated before any actual keyboard rule checks are applied. This provides a clear "before" state (eventually saved at `Transcription.preInput`) useful for determining the scope of the keystroke's changes once processing is completed via `buildTranscriptionFrom`.
For JS-keyboard keystroke processing, a `SyntheticTextStore` clone of the context is
generated before any actual keyboard rule checks are applied. This
provides a clear "before" state (eventually saved at
`Transcription.preInput`) useful for determining the scope of the
keystroke's changes once processing is completed via
`buildTranscriptionFrom`.
Once keystroke processing is completed by a JS-keyboard, the JS-processor constructs a `ProcessorAction` object describing all primary and side effects of the keystroke. Defined at [web/src/engine/keyboard/src/keyboards/processorAction.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/keyboards/processorAction.ts), all of its fields aside from `transcription` are specific to JS-keyboard side effects, some of which do need special handling and support outside of the keystroke processor. None of these side effects apply for common-case keystrokes and so have default handling in place within the engine for cases where they are not needed.
Once keystroke processing is completed by a JS-keyboard, the
JS-processor constructs a `ProcessorAction` object describing all
primary and side effects of the keystroke. Defined at
[web/src/engine/keyboard/src/keyboards/processorAction.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/keyboards/processorAction.ts),
all of its fields aside from `transcription` are specific to JS-keyboard
side effects, some of which do need special handling and support outside
of the keystroke processor. None of these side effects apply for
common-case keystrokes and so have default handling in place within the
engine for cases where they are not needed.
---
In case referenced classes/files have moved: this doc was last updated in 19.0-alpha, based upon PR #14001.
In case referenced classes/files have moved: this doc was last updated
in 19.0-alpha, based upon PR #14001.

View file

@ -126,7 +126,7 @@ The bulk of DOM key events trigger keystroke processing on key-down, though modi
The `app/webview` version of the engine, which is designed to be used while embedded in a platform-specific host app, does not have direct access to standard hardware keystroke events, as those are handled by the host app's OS and by native code handlers run outside the host app's WebView containing the app/webview Web engine. It is the responsibility of the host app to handle hardware keystroke events and preprocess them on behalf of the Web engine, then forward them to the Web engine via JS call into the WebView. The `PassthroughKeyboard` class within `app/webview` space provides the method `raiseKeyEvent` as an internal API for this purpose, which converts the mobile-app format for hardware keystroke into the internal `KeyEvent` format. `raiseKeyEvent` also handles mnemonic keystroke processing and remapping.
Also note that this variant does not model the user's text context with Web elements - it is entirely managed through the `Mock` type.
Also note that this variant does not model the user's text context with Web elements - it is entirely managed through the `SyntheticTextStore` type.
### On-screen keyboard

View file

@ -6,7 +6,7 @@ In addition to handling keystroke events produced from hardware keyboards, Keyma
Defined at [web/src/engine/keyboard/src/keyEvent.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/keyEvent.ts), the `KeyEvent` is used to represent incoming _and_ potential keystrokes. This type is what the JS-keyboard processor references when evaluating keyboard rules during its keystroke processing. For versions of the engine that support AltGr aliasing, such aliasing will be applied during generation of `KeyEvent` objects.
Note that for predictive-text's fat-finger correction functionality, the engine will also generate versions of this type for nearby but _unpressed_ keys into the keystroke processing engine as well in order to facilitate predictions that follow context manipulations that could have resulted from specialized rules or reorders on neighboring keys. `Mock`-cloned copies of the active context-state will be leveraged to prevent unwanted manipulation of the true context source.
Note that for predictive-text's fat-finger correction functionality, the engine will also generate versions of this type for nearby but _unpressed_ keys into the keystroke processing engine as well in order to facilitate predictions that follow context manipulations that could have resulted from specialized rules or reorders on neighboring keys. `SyntheticTextStore`-cloned copies of the active context-state will be leveraged to prevent unwanted manipulation of the true context source.
### `isSynthetic`
`isSynthetic` should be set to `true` if generated through interaction with an on-screen-keyboard or for fat-finger simulation. It should only be set to `false` if sending the basic key-event data through to the destination, without rule processing, leads to default handling picking up the slack. Browsers provide default handling of keystrokes not directly defined within keyboards, but this is not available for keystrokes against the engine's OSK without internal support.
@ -31,7 +31,7 @@ See also: [context-state-management.md](context-state-management.md#js-keyboard
The "first stop" for incoming keystrokes is the `InputProcessor`, found at [web/src/engine/main/src/headless/inputProcessor.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/main/src/headless/.inputProcessor.ts), through its `processKeyEvent` method. This class manages higher-level functionality triggered by keystroke events while deferring actual interpretation of the incoming keystroke further down the line. Of particular note is that it also handles control-flows that require restoration of previously-occuring contexts.
This class is the connection point for generating prediction requests and receiving corresponding suggestions. In order to facilitate higher-quality predictive-text when enabled, the `InputProcessor` will _also_ generate and trigger processing for nearby keys. This process allows transforms, reorders, and KMN keyboard rules to take effect and be used as alternative context roots for predictions. These are generally run against `Mock`-based clones of the true context source and are additionally prevented from triggering long-term side-effects, such as changes to KMN-keyboard variable stores, by only calling `KeyboardProcessor.finalizeProcessorAction` for the true input keystroke's result object.
This class is the connection point for generating prediction requests and receiving corresponding suggestions. In order to facilitate higher-quality predictive-text when enabled, the `InputProcessor` will _also_ generate and trigger processing for nearby keys. This process allows transforms, reorders, and KMN keyboard rules to take effect and be used as alternative context roots for predictions. These are generally run against `SyntheticTextStore`-based clones of the true context source and are additionally prevented from triggering long-term side-effects, such as changes to KMN-keyboard variable stores, by only calling `KeyboardProcessor.finalizeProcessorAction` for the true input keystroke's result object.
Keys generated by OSK multitap need special handling here as well; they should always be applied to the context state as it existed at the time of the initial tap. To facilitate this, the `InputProcessor` will directly rewind the active context-source to match the corresponding context state before requesting that the `KeyEvent` be processed.
@ -64,7 +64,7 @@ The method linked above is the primary entrypoint for rule processing of individ
#### JS-keyboard interfacing
Certain Keyman language features can make permanent side-effect changes to state. In order to prevent these from taking place for every keystroke, the method that interfaces with JS keyboards - `JSKeyboardInterface.process` - saves the context state (as a `Mock`) and current variable store values, then prepares a fresh `ProcessorAction` instance, before passing control off to the keyboard's backing script. (Note that `JSKeyboardInterface` itself primarily consists of keyboard-script API called by JS-keyboard script.)
Certain Keyman language features can make permanent side-effect changes to state. In order to prevent these from taking place for every keystroke, the method that interfaces with JS keyboards - `JSKeyboardInterface.process` - saves the context state (as a `SyntheticTextStore`) and current variable store values, then prepares a fresh `ProcessorAction` instance, before passing control off to the keyboard's backing script. (Note that `JSKeyboardInterface` itself primarily consists of keyboard-script API called by JS-keyboard script.)
A few of the keyboard-script API methods will mark `ProcessorAction` properties directly when called, but the bulk of its data will be set once the keyboard-script returns control to Keyman Engine for Web. At this time, variable store values will also be reverted to prevent possible cross-contamination effects when predictive text is active - they're reapplied later if `KeyboardProcessor.finalizeProcessorAction` is leveraged on the resulting instance. Components documented in [context-state-management.md](./context-state-management.md) are then leveraged to determine the total change to context caused by the keystroke.

View file

@ -27,10 +27,10 @@
"types": "./build/engine/obj/dom-utils/index.d.ts",
"import": "./build/engine/obj/dom-utils/index.js"
},
"./engine/element-wrappers": {
"es6-bundling": "./src/engine/src/element-wrappers/index.ts",
"types": "./build/engine/obj/element-wrappers/index.d.ts",
"import": "./build/engine/obj/element-wrappers/index.js"
"./engine/element-text-stores": {
"es6-bundling": "./src/engine/src/element-text-stores/index.ts",
"types": "./build/engine/obj/element-text-stores/index.d.ts",
"import": "./build/engine/obj/element-text-stores/index.js"
},
"./engine/events": {
"es6-bundling": "./src/engine/src/events/index.ts",

View file

@ -1,6 +1,6 @@
import { type JSKeyboardInterface } from 'keyman/engine/js-processor';
import { JSKeyboard, type KeyboardMinimalInterface } from 'keyman/engine/keyboard';
import { DesignIFrame, OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
import { DesignIFrameElementTextStore, AbstractElementTextStore } from 'keyman/engine/element-text-stores';
// Utility object used to handle beep (keyboard error response) operations.
class BeepData {
@ -33,15 +33,15 @@ export class BeepHandler {
* @param {Object} Pelem element to flash
* Description Flash body as substitute for audible beep; notify embedded device to vibrate
*/
beep(outputTarget: OutputTargetElementWrapper<any>) {
if (!(outputTarget instanceof OutputTargetElementWrapper)) {
beep(textStore: AbstractElementTextStore<any>) {
if (!(textStore instanceof AbstractElementTextStore)) {
return;
}
// All code after this point is DOM-based, triggered by the beep.
let Pelem: HTMLElement = outputTarget.getElement();
if(outputTarget instanceof DesignIFrame) {
Pelem = outputTarget.docRoot; // I1446 - beep sometimes fails to flash when using OSK and rich control
let Pelem: HTMLElement = textStore.getElement();
if(textStore instanceof DesignIFrameElementTextStore) {
Pelem = textStore.docRoot; // I1446 - beep sometimes fails to flash when using OSK and rich control
}
if(!Pelem) {

View file

@ -1,8 +1,8 @@
import { EngineConfiguration, InitOptionSpec, InitOptionDefaults } from "keyman/engine/main";
import { OutputTargetElementWrapper as DOMOutputTarget } from 'keyman/engine/element-wrappers';
import { OutputTargetInterface, ProcessorAction } from 'keyman/engine/keyboard';
import { isEmptyTransform } from 'keyman/engine/js-processor';
import { AbstractElementTextStore } from 'keyman/engine/element-text-stores';
import { TextStore, ProcessorAction } from 'keyman/engine/keyboard';
import { isEmptyTransform } from 'keyman/common/web-utils';
import { AlertHost } from "./utils/alertHost.js";
import { whenDocumentReady } from "./utils/documentReady.js";
@ -66,14 +66,14 @@ export class BrowserConfiguration extends EngineConfiguration {
return baseReport;
}
onRuleFinalization(ruleBehavior: ProcessorAction, outputTarget: OutputTargetInterface) {
onRuleFinalization(ruleBehavior: ProcessorAction, textStore: TextStore) {
// TODO: Patch up to modularized form. But that doesn't exist yet for some of these...
// If the transform isn't empty, we've changed text - which should produce a 'changed' event in the DOM.
const ruleTransform = ruleBehavior.transcription.transform;
if(!isEmptyTransform(ruleTransform)) {
if(outputTarget instanceof DOMOutputTarget) {
outputTarget.changed = true;
if (textStore instanceof AbstractElementTextStore) {
textStore.changed = true;
}
}
}

View file

@ -13,7 +13,7 @@ export class FocusStateAPIObject {
activated: boolean;
/**
* Indicates that KMW is actively maintaining focus on the currently active OutputTarget control
* Indicates that KMW is actively maintaining focus on the currently active TextStore control
* while some UI element (the OSK, a keyboard-change UI) is the current focus of user-interaction.
*/
activationPending: boolean;
@ -35,7 +35,7 @@ interface EventMap {
// Formerly handled under "UIManager".
/**
* This class provides fields and methods useful for assisting context management. Control focus (and
* thus, activation of the corresponding OutputTarget) should not be lost to non-context components of
* thus, activation of the corresponding TextStore) should not be lost to non-context components of
* KMW, such as the OSK or a keyboard selector.
*/
export class FocusAssistant extends EventEmitter<EventMap> {
@ -57,16 +57,16 @@ export class FocusAssistant extends EventEmitter<EventMap> {
* Long-term idea here: about all of the relevant OSK events that would interact with this have "enter" and
* "leave" variants - we could take a stack of `Promise`s. On a `Promise` fulfillment, remove it from the
* stack. When the last one is removed, the focus-maintenance state would end, allowing further events
* to deactivate the active OutputTarget.
* to deactivate the active TextStore.
*/
/**
* Indicates that KMW is actively maintaining focus on the currently active OutputTarget control, rather
* Indicates that KMW is actively maintaining focus on the currently active TextStore control, rather
* than losing focus while some UI element (the OSK, a keyboard-change UI) is the most direct recipient
* of browser focus due to user-interaction - generally, with non-context engine components.
*
* While the flag is active, the context-management system should not deactivate an OutputTarget upon
* its element's loss of focus within the page unless setting a different OutputTarget as active.
* While the flag is active, the context-management system should not deactivate an TextStore upon
* its element's loss of focus within the page unless setting a different TextStore as active.
*
* TODO: (potential) Future enhancement - this should not be possible to set if there is no currently-active
* context target to maintain.

View file

@ -1,9 +1,9 @@
import { JSKeyboard, type Keyboard, KeyboardScriptError } from 'keyman/engine/keyboard';
import { type KeyboardStub } from 'keyman/engine/keyboard-storage';
import { CookieSerializer } from 'keyman/engine/dom-utils';
import { eventOutputTarget, outputTargetForElement, PageContextAttachment } from 'keyman/engine/attachment';
import { textStoreForEvent, textStoreForElement, PageContextAttachment } from 'keyman/engine/attachment';
import { DomEventTracker, LegacyEventEmitter } from 'keyman/engine/events';
import { DesignIFrame, OutputTargetElementWrapper, nestedInstanceOf } from 'keyman/engine/element-wrappers';
import { DesignIFrameElementTextStore, AbstractElementTextStore, nestedInstanceOf } from 'keyman/engine/element-text-stores';
import {
ContextManagerBase,
type KeyboardInterfaceBase,
@ -47,8 +47,8 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
private cookieManager = new CookieSerializer<KeyboardCookie>('KeymanWeb_Keyboard');
readonly focusAssistant = new FocusAssistant(() => this.activeTarget?.isForcingScroll());
readonly page: PageContextAttachment;
private mostRecentTarget: OutputTargetElementWrapper<any>;
private currentTarget: OutputTargetElementWrapper<any>;
private mostRecentTarget: AbstractElementTextStore<any>;
private currentTarget: AbstractElementTextStore<any>;
private globalKeyboard: {keyboard: JSKeyboard, metadata: KeyboardStub};
@ -92,7 +92,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
// For any elements being attached, or being enabled after having been disabled...
this.page.on('enabled', (elem) => {
if(!(elem._kmwAttachment.interface instanceof DesignIFrame)) {
if(!(elem._kmwAttachment.textStore instanceof DesignIFrameElementTextStore)) {
// For anything attached but (design-mode) iframes...
// This block: has to do with maintaining focus.
@ -126,7 +126,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
if(elem.ownerDocument.activeElement == elem) {
this.setActiveTarget(outputTargetForElement(elem), true);
this.setActiveTarget(textStoreForElement(elem), true);
}
});
@ -175,7 +175,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
});
}
get activeTarget(): OutputTargetElementWrapper<any> {
get activeTarget(): AbstractElementTextStore<any> {
/*
* Assumption: the maintainingFocus flag may only be set when there is a current target.
* This is not enforced proactively at present, but the assumption should hold. (2023-05-03)
@ -184,7 +184,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
return this.currentTarget || (maintainingFocus ? this.mostRecentTarget : null);
}
get lastActiveTarget(): OutputTargetElementWrapper<any> {
get lastActiveTarget(): AbstractElementTextStore<any> {
return this.mostRecentTarget;
}
@ -229,7 +229,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
}
public setActiveTarget(target: OutputTargetElementWrapper<any>, sendEvents?: boolean) {
public setActiveTarget(target: AbstractElementTextStore<any>, sendEvents?: boolean) {
const previousTarget = this.mostRecentTarget;
const originalTarget = this.activeTarget; // may differ, depending on focus state.
@ -261,7 +261,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
* and trigger a contextReset DURING keyboard rule processing without this
* guard.
*
* The #2 reason: the `forceScroll` method used within the Input and Textarea
* The #2 reason: the `forceScroll` method used within the InputElementTextStore and TextAreaTextStore
* types whenever the selection must be programatically updated. The blur
* is 'swallowed', preventing it from being dropped as 'active'. However, the
* corresponding focus is not swallowed... until this if-condition's check.
@ -290,7 +290,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
// Set element directionality (but only if element is empty)
let focusedElement = target?.getElement();
if(target instanceof DesignIFrame) {
if(target instanceof DesignIFrameElementTextStore) {
focusedElement = target.docRoot;
}
if(focusedElement && focusedElement.ownerDocument && focusedElement instanceof focusedElement.ownerDocument.defaultView.HTMLElement) {
@ -304,7 +304,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
//Execute external (UI) code needed on focus if required
if(sendEvents) {
let blurredElement = previousTarget?.getElement();
if(previousTarget instanceof DesignIFrame) {
if(previousTarget instanceof DesignIFrameElementTextStore) {
blurredElement = previousTarget.docRoot;
}
@ -350,13 +350,13 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
// original context element may have been lost.
this.restoreLastActiveTarget();
let outputTarget = this.activeTarget;
let textStore = this.activeTarget;
if(outputTarget == null && this.mostRecentTarget) {
outputTarget = this.activeTarget;
if(textStore == null && this.mostRecentTarget) {
textStore = this.activeTarget;
}
if(outputTarget != null) {
if(textStore != null) {
return super.insertText(kbdInterface, Ptext, PdeadKey);
}
return false;
@ -369,7 +369,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
*
* This is based on the current `.activeTarget` and its related attachment metadata.
*/
protected currentKeyboardSrcTarget(): OutputTargetElementWrapper<any> {
protected currentKeyboardSrcTarget(): AbstractElementTextStore<any> {
const target = this.currentTarget || this.mostRecentTarget;
if(this.isTargetKeyboardIndependent(target)) {
@ -379,15 +379,15 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
}
private isTargetKeyboardIndependent(target: OutputTargetElementWrapper<any>): boolean {
const attachmentInfo = target?.getElement()._kmwAttachment;
private isTargetKeyboardIndependent(textStore: AbstractElementTextStore<any>): boolean {
const attachment = textStore?.getElement()._kmwAttachment;
// If null or undefined, we're in 'global' mode.
return !!(attachmentInfo?.keyboard || attachmentInfo?.keyboard === '');
return !!(attachment?.keyboard || attachment?.keyboard === '');
}
// Note: is part of the keyboard activation process. Not to be called directly by published API.
activateKeyboardForTarget(kbd: { keyboard: JSKeyboard, metadata: KeyboardStub }, target: OutputTargetElementWrapper<any>) {
activateKeyboardForTarget(kbd: { keyboard: JSKeyboard, metadata: KeyboardStub }, target: AbstractElementTextStore<any>) {
const attachment = target?.getElement()._kmwAttachment;
if(!attachment) {
@ -421,8 +421,8 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
* @param target
* @param metadata
*/
public setKeyboardForTarget(target: OutputTargetElementWrapper<any>, kbdId: string, langId: string) {
if(target instanceof DesignIFrame) {
public setKeyboardForTarget(target: AbstractElementTextStore<any>, kbdId: string, langId: string) {
if(target instanceof DesignIFrameElementTextStore) {
console.warn("'keymanweb.setKeyboardForControl' cannot set keyboard on iframes.");
return;
}
@ -456,7 +456,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
}
public getKeyboardStubForTarget(target: OutputTargetElementWrapper<any>) {
public getKeyboardStubForTarget(target: AbstractElementTextStore<any>) {
if(!this.isTargetKeyboardIndependent(target)) {
return this.globalKeyboard.metadata;
} else {
@ -614,16 +614,16 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
* The return value indicates whether (true) or not (false) the calling event handler
* should be terminated immediately after the call.
*/
_CommonFocusHelper(outputTarget: OutputTargetElementWrapper<any>): boolean {
_CommonFocusHelper(textStore: AbstractElementTextStore<any>): boolean {
const focusAssistant = this.focusAssistant;
const activeKeyboard = this.activeKeyboard?.keyboard;
if(!focusAssistant.restoringFocus) {
outputTarget?.deadkeys().clear();
activeKeyboard?.notify(0, outputTarget, 1); // I2187
textStore?.deadkeys().clear();
activeKeyboard?.notify(0, textStore, 1); // I2187
}
if(!focusAssistant.restoringFocus && this.mostRecentTarget != outputTarget) {
if(!focusAssistant.restoringFocus && this.mostRecentTarget != textStore) {
focusAssistant.maintainingFocus = false;
}
focusAssistant.restoringFocus = false;
@ -642,19 +642,19 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
* Respond to KeymanWeb-aware input element receiving focus
*/
_ControlFocus = (e: FocusEvent): boolean => {
// Step 1: determine the corresponding OutputTarget instance.
const target = eventOutputTarget(e);
// Step 1: determine the corresponding TextStore instance.
const target = textStoreForEvent(e);
if(!target) {
// Probably should also make a warning or error?
return true;
}
// ???? ?: ensure it's properly active?
// if(target instanceof DesignIFrame) { //**TODO: check case reference
// if(target instanceof DesignIFrameElementTextStore) { //**TODO: check case reference
// // But... the following should already have been done during attachment...
// // attachmentEngine._AttachToIframe(Ltarg as HTMLIFrameElement);
// target.docRoot
// Ltarg=Ltarg.contentWindow.document.body; // And we only care about Ltarg b/c of finding the OutputTarget.
// Ltarg=Ltarg.contentWindow.document.body; // And we only care about Ltarg b/c of finding the TextStore.
// }
// Step 2: Make the newly-focused control the active control, and thus the active context.
@ -685,8 +685,8 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
return true;
}
// Step 1: determine the corresponding OutputTarget instance.
const target = eventOutputTarget(e);
// Step 1: determine the corresponding TextStore instance.
const target = textStoreForEvent(e);
if (target == null) {
return true;
}
@ -738,7 +738,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
return true;
}
doChangeEvent(target: OutputTargetElementWrapper<any>) {
doChangeEvent(target: AbstractElementTextStore<any>) {
if(target.changed) {
const event = new Event('change', {"bubbles": true, "cancelable": false});
target.getElement().dispatchEvent(event);

View file

@ -3,7 +3,7 @@ import {
Codes,
DefaultRules,
type KeyEvent,
type OutputTargetInterface
type TextStore
} from 'keyman/engine/keyboard';
import ContextManager from './contextManager.js';
@ -32,7 +32,7 @@ export default class DefaultBrowserRules extends DefaultRules {
/**
* applyCommand - used when a ProcessorAction represents a non-text "command" within the Engine.
*/
applyCommand(Lkc: KeyEvent, outputTarget: OutputTargetInterface): void {
applyCommand(Lkc: KeyEvent, textStore: TextStore): void {
const code = this.codeForEvent(Lkc);
const moveToNext = (back: boolean) => {
@ -56,6 +56,6 @@ export default class DefaultBrowserRules extends DefaultRules {
break;
}
super.applyCommand(Lkc, outputTarget);
super.applyCommand(Lkc, textStore);
}
}

View file

@ -4,8 +4,8 @@ import { ModifierKeyConstants } from '@keymanapp/common-types';
import { HardKeyboardBase, processForMnemonicsAndLegacy } from 'keyman/engine/main';
import { DomEventTracker } from 'keyman/engine/events';
import { DesignIFrame, nestedInstanceOf } from 'keyman/engine/element-wrappers';
import { eventOutputTarget, outputTargetForElement } from 'keyman/engine/attachment';
import { DesignIFrameElementTextStore, nestedInstanceOf } from 'keyman/engine/element-text-stores';
import { textStoreForEvent, textStoreForElement } from 'keyman/engine/attachment';
import ContextManager from './contextManager.js';
@ -235,9 +235,9 @@ export default class HardwareEventKeyboard extends HardKeyboardBase {
const eventTracker = this.domEventTracker;
page.on('enabled', (Pelem) => {
const target = outputTargetForElement(Pelem);
const target = textStoreForElement(Pelem);
if(!(target instanceof DesignIFrame)) {
if(!(target instanceof DesignIFrameElementTextStore)) {
// These need to be on the actual input element, as otherwise the keyboard will disappear on touch.
eventTracker.attachDOMEvent(Pelem, 'keypress', this._KeyPress);
eventTracker.attachDOMEvent(Pelem, 'keydown', this._KeyDown);
@ -251,9 +251,9 @@ export default class HardwareEventKeyboard extends HardKeyboardBase {
});
page.on('disabled', (Pelem) => {
const target = outputTargetForElement(Pelem);
const target = textStoreForElement(Pelem);
if(!(target instanceof DesignIFrame)) {
if(!(target instanceof DesignIFrameElementTextStore)) {
eventTracker.detachDOMEvent(Pelem, 'keypress', this._KeyPress);
eventTracker.detachDOMEvent(Pelem, 'keydown', this._KeyDown);
eventTracker.detachDOMEvent(Pelem, 'keyup', this._KeyUp);
@ -277,7 +277,7 @@ export default class HardwareEventKeyboard extends HardKeyboardBase {
*/
_KeyDown: (e: KeyboardEvent) => boolean = (e) => {
const activeKeyboard = this.contextManager.activeKeyboard;
const target = eventOutputTarget(e);
const target = textStoreForEvent(e);
if(!target || activeKeyboard == null) {
return true;
@ -298,7 +298,7 @@ export default class HardwareEventKeyboard extends HardKeyboardBase {
* Description Processes keypress event (does not pass data to keyboard)
*/
_KeyPress: (e: KeyboardEvent) => boolean = (e) => {
const target = eventOutputTarget(e);
const target = textStoreForEvent(e);
if(!target || this.contextManager.activeKeyboard?.keyboard == null) {
return true;
}
@ -312,7 +312,7 @@ export default class HardwareEventKeyboard extends HardKeyboardBase {
* Description Processes keyup event and passes event data to keyboard
*/
_KeyUp: (e: KeyboardEvent) => boolean = (e) => {
const target = eventOutputTarget(e);
const target = textStoreForEvent(e);
const Levent = preprocessKeyboardEvent(e, this.processor, this.hardDevice);
if(Levent == null || target == null) {
return true;
@ -399,8 +399,8 @@ export default class HardwareEventKeyboard extends HardKeyboardBase {
return true;
}
const outputTarget = eventOutputTarget(e);
return this.processor.doModifierPress(Levent, outputTarget, false);
const textStore = textStoreForEvent(e);
return this.processor.doModifierPress(Levent, textStore, false);
}
private keyPress(e: KeyboardEvent): boolean {

View file

@ -1,4 +1,4 @@
import { type OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
import { type AbstractElementTextStore } from 'keyman/engine/element-text-stores';
import { FloatingOSKView } from 'keyman/engine/osk';
import { KeyboardInterfaceBase } from 'keyman/engine/main';
@ -29,7 +29,7 @@ export class KeyboardInterface extends KeyboardInterfaceBase<ContextManager> {
/**
* Legacy entry points (non-standard names)- included only to allow existing IME keyboards to continue to be used
*/
getLastActiveElement(): OutputTargetElementWrapper<any> {
getLastActiveElement(): AbstractElementTextStore<any> {
return this.engine.contextManager.lastActiveTarget;
}

View file

@ -1,7 +1,7 @@
import { KeymanWebKeyboard } from '@keymanapp/common-types';
import { KeymanEngineBase, DeviceDetector } from 'keyman/engine/main';
import { getAbsoluteY } from 'keyman/engine/dom-utils';
import { OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
import { AbstractElementTextStore } from 'keyman/engine/element-text-stores';
import {
TwoStateActivator,
VisualKeyboard
@ -20,7 +20,7 @@ import { PageIntegrationHandlers } from './context/pageIntegrationHandlers.js';
import { LanguageMenu } from './languageMenu.js';
import { setupOskListeners } from './oskConfiguration.js';
import { whenDocumentReady } from './utils/documentReady.js';
import { outputTargetForElement } from 'keyman/engine/attachment';
import { textStoreForElement } from 'keyman/engine/attachment';
import { UtilApiEndpoint} from './utilApiEndpoint.js';
import { UIModule } from './uiModuleInterface.js';
@ -76,7 +76,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
// Scrolls the document-body to ensure that a focused element remains visible after the OSK appears.
this.contextManager.on('targetchange', (target) => {
const e = (target as OutputTargetElementWrapper<any>)?.getElement();
const e = (target as AbstractElementTextStore<any>)?.getElement();
if(this.osk) {
(this.osk.activationModel as TwoStateActivator<HTMLElement>).activationTrigger = e;
}
@ -302,7 +302,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
}
}
this.contextManager.setKeyboardForTarget(Pelem._kmwAttachment.interface, Pkbd, Plc);
this.contextManager.setKeyboardForTarget(Pelem._kmwAttachment.textStore, Pkbd, Plc);
}
/**
* Function getKeyboardForControl
@ -315,7 +315,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
* See https://help.keyman.com/developer/engine/web/current-version/reference/core/getKeyboardForControl
*/
public getKeyboardForControl(Pelem: HTMLElement) {
const target = outputTargetForElement(Pelem);
const target = textStoreForElement(Pelem);
return this.contextManager.getKeyboardStubForTarget(target).id;
}
@ -329,7 +329,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
* If it is currently following the global keyboard setting, returns null instead.
*/
getLanguageForControl(Pelem: HTMLElement) {
const target = outputTargetForElement(Pelem);
const target = textStoreForElement(Pelem);
return this.contextManager.getKeyboardStubForTarget(target).langId;
}
@ -570,7 +570,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
}
}
const target = outputTargetForElement(e);
const target = textStoreForElement(e);
if(!target) {
throw new Error(`KMW is not attached to the specified element (id: ${e.id}).`);
}

View file

@ -1,17 +1,20 @@
import { JSKeyboard, Keyboard, OutputTargetInterface, Transcription, TextTransform } from 'keyman/engine/keyboard';
// TODO-web-core: remove usage of OutputTargetBase, use OutputTargetInterface instead
import { Mock, findCommonSubstringEndIndex, isEmptyTransform, OutputTargetBase } from 'keyman/engine/js-processor';
import { JSKeyboard, Keyboard, TextStore, Transcription, TextTransform, SyntheticTextStore, findCommonSubstringEndIndex } from 'keyman/engine/keyboard';
import { KeyboardStub } from 'keyman/engine/keyboard-storage';
import { ContextManagerBase } from 'keyman/engine/main';
import { WebviewConfiguration } from './configuration.js';
import { LexicalModelTypes } from '@keymanapp/common-types';
import { KMWString } from 'keyman/common/web-utils';
import { KMWString, isEmptyTransform } from 'keyman/common/web-utils';
export type OnInsertTextFunc = (deleteLeft: number, text: string, deleteRight: number) => void;
export class ContextHost extends Mock {
/**
* WebView-specific synthetic TextStore implementation that can
* communicate and synchronize with the host app despite not being
* backed by any sort of Web element.
*/
export class HostTextStore extends SyntheticTextStore {
readonly oninserttext?: OnInsertTextFunc;
private savedState: Mock;
private savedState: SyntheticTextStore;
constructor(oninserttext: OnInsertTextFunc) {
super();
@ -32,7 +35,7 @@ export class ContextHost extends Mock {
if(transcription) {
//TODO-web-core: shouldn't need cast in the future?
const preInput = transcription.preInput as Mock;
const preInput = transcription.preInput as SyntheticTextStore;
// If our saved state matches the `preInput` from the incoming transcription, just reuse its transform.
// Will generally not match during multitap operations, though.
//
@ -57,18 +60,17 @@ export class ContextHost extends Mock {
}
saveState() {
this.savedState = Mock.from(this);
this.savedState = SyntheticTextStore.from(this);
}
restoreTo(original: OutputTargetInterface): void {
this.savedState = Mock.from(this);
// TODO-web-core
super.restoreTo(original as OutputTargetBase);
restoreTo(original: TextStore): void {
this.savedState = SyntheticTextStore.from(this);
super.restoreTo(original);
}
updateContext(text: string, selStart: number, selEnd: number): boolean {
let shouldResetContext = false;
const tempMock = new Mock(text, selStart ?? KMWString.length(text), selEnd ?? KMWString.length(text));
const tempMock = new SyntheticTextStore(text, selStart ?? KMWString.length(text), selEnd ?? KMWString.length(text));
const newLeft = tempMock.getTextBeforeCaret();
const oldLeft = this.getTextBeforeCaret();
@ -110,15 +112,15 @@ export class ContextHost extends Mock {
// and we want a consistent interface for context synchronization between
// host app + app/webview KMW.
this.setSelection(KMWString.length(this.text));
this.savedState = Mock.from(this);
this.savedState = SyntheticTextStore.from(this);
}
}
export default class ContextManager extends ContextManagerBase<WebviewConfiguration> {
// Change of context? Just replace the Mock. Context will be ENTIRELY controlled
// 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.)
private _rawContext: ContextHost;
private _hostTextStore: HostTextStore;
private _activeKeyboard: {keyboard: JSKeyboard, metadata: KeyboardStub};
@ -127,20 +129,20 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
}
initialize(): void {
this._rawContext = new ContextHost(this.engineConfig.oninserttext);
this._hostTextStore = new HostTextStore(this.engineConfig.oninserttext);
this.predictionContext.setCurrentTarget(this.activeTarget);
this.resetContext();
}
get activeTarget(): Mock {
return this._rawContext;
get activeTarget(): SyntheticTextStore {
return this._hostTextStore;
}
get activeKeyboard() {
return this._activeKeyboard;
}
activateKeyboardForTarget(kbd: { keyboard: JSKeyboard, metadata: KeyboardStub }, target: OutputTargetInterface) {
activateKeyboardForTarget(kbd: { keyboard: JSKeyboard, metadata: KeyboardStub }, target: TextStore) {
// `target` is irrelevant for `app/webview`, as it'll only ever use 'global' keyboard settings.
// Clone the object to prevent accidental by-reference changes.
@ -151,7 +153,7 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
* Reflects the active 'target' upon which any `set activeKeyboard` operation will take place.
* For app/webview... there's only one target, thus only a "global default" matters.
*/
protected currentKeyboardSrcTarget(): Mock {
protected currentKeyboardSrcTarget(): SyntheticTextStore {
return null;
}
@ -220,6 +222,6 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
public resetContext(): void {
super.resetContext();
this._rawContext.saveState();
this._hostTextStore.saveState();
}
}

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, { ContextHost } 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';
@ -18,7 +18,7 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
const config = new WebviewConfiguration(sourceUri); // currently set to perform device auto-detect.
config.onRuleFinalization = (ruleBehavior: ProcessorAction) => {
(this.context as ContextHost).updateHost(ruleBehavior.transcription);
(this.context as HostTextStore).updateHost(ruleBehavior.transcription);
}
config.stubNamespacer = (stub) => {

View file

@ -22,6 +22,7 @@ BUNDLE_CMD="node ${KEYMAN_ROOT}/web/src/tools/es-bundling/build/common-bundle.mj
builder_describe \
"Compiles the web-oriented utility function module." \
"@/common/web/keyman-version" \
"@/common/web/types" \
"@/web/src/tools/es-bundling" \
clean configure build test

View file

@ -23,6 +23,8 @@ export { default as TimeoutPromise, timedPromise } from "./timeoutPromise.js";
export { default as PriorityQueue, QueueComparator } from "./priority-queue.js"
export { isEmptyTransform } from './isEmptyTransform.js';
// // Uncomment the following line and run the bundled output to verify successful
// // esbuild bundling of this submodule:
// console.log(Version.CURRENT.toString());

View file

@ -0,0 +1,21 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { LexicalModelTypes } from '@keymanapp/common-types';
// Also relies on string-extensions provided by the web-utils package.
/**
* Determines whether a lexical model transform is empty (has no effect).
*
* @param transform - The lexical model transform to check.
* @returns True if the transform is empty (null, or has no insertions or deletions),
* false otherwise.
*/
export function isEmptyTransform(transform: LexicalModelTypes.Transform) {
if (!transform) {
return true;
}
return transform.insert === '' && transform.deleteLeft === 0 && (transform.deleteRight ?? 0) === 0;
}

View file

@ -1,7 +1,7 @@
import { OutputTargetBase } from "keyman/engine/js-processor";
import { TextStore } from "keyman/engine/keyboard";
import { EventEmitter } from 'eventemitter3';
export abstract class OutputTargetElementWrapper<EventMap extends EventEmitter.ValidEventTypes> extends OutputTargetBase {
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>();

View file

@ -1,4 +1,4 @@
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
class SelectionCaret {
@ -21,7 +21,7 @@ class SelectionRange {
}
}
export class ContentEditable extends OutputTargetElementWrapper<{}> {
export class ContentEditableElementTextStore extends AbstractElementTextStore<{}> {
root: HTMLElement;
constructor(ele: HTMLElement) {

View file

@ -0,0 +1,42 @@
/*
* 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,4 +1,4 @@
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
class SelectionCaret {
@ -32,7 +32,7 @@ class StyleCommand {
}
}
export class DesignIFrame extends OutputTargetElementWrapper<{}> {
export class DesignIFrameElementTextStore extends AbstractElementTextStore<{}> {
root: HTMLIFrameElement;
doc: Document;
docRoot: HTMLElement;

View file

@ -0,0 +1,11 @@
/*
* 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,4 +1,4 @@
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
interface EventMap {
@ -22,7 +22,7 @@ interface EventMap {
'unhandlednewline': (element: HTMLInputElement) => void
}
export class Input extends OutputTargetElementWrapper<EventMap> {
export class InputElementTextStore extends AbstractElementTextStore<EventMap> {
root: HTMLInputElement;
/**

View file

@ -1,7 +1,7 @@
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { AbstractElementTextStore } from './abstractElementTextStore.js';
import { KMWString } from 'keyman/common/web-utils';
export class TextArea extends OutputTargetElementWrapper<{}> {
export class TextAreaElementTextStore extends AbstractElementTextStore<{}> {
root: HTMLTextAreaElement;
/**

View file

@ -1,16 +1,6 @@
import { OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
import { AbstractElementTextStore } from 'keyman/engine/element-text-stores';
export class AttachmentInfo {
/**
* Provides the core interface between the DOM and the actual keyboard.
*/
interface: OutputTargetElementWrapper<any>;
/**
* Tracks the control's independent keyboard selection, when applicable.
*/
keyboard: string;
/**
* Tracks the language code corresponding to the `keyboard` field.
*/
@ -21,8 +11,11 @@ export class AttachmentInfo {
*/
inputMode?: string;
constructor(eleInterface: OutputTargetElementWrapper<any>, kbd: string, touch?: boolean) {
this.interface = eleInterface;
this.keyboard = kbd;
}
/**
* Constructor for AttachmentInfo.
*
* @param textStore - Provides the core interface between the DOM and the actual keyboard.
* @param keyboard - Provides the keyboard identifier.
*/
constructor(public readonly textStore: AbstractElementTextStore<any>, public keyboard: string) {}
}

View file

@ -5,11 +5,12 @@ export { AttachmentInfo } from './attachmentInfo.js';
* to match that of the actual objects within the browser when bundled, the
* **same bundle** must contain reference points for those classes' definitions.
*/
export { eventOutputTarget, outputTargetForElement } from './outputTargetForElement.js';
export { textStoreForElement } from './textStoreForElement.js';
export { textStoreForEvent } from './textStoreForEvent.js';
export { PageContextAttachment, PageAttachmentOptions } from './pageContextAttachment.js';
/*
* Following from the prior "Note:", we republish `engine/element-wrappers` here -
* Following from the prior "Note:", we republish `engine/element-text-stores` here -
* this matters quite strongly for certain unit tests.
*/
export * from 'keyman/engine/element-wrappers';
export * from 'keyman/engine/element-text-stores';

View file

@ -1,23 +1,12 @@
import { nestedInstanceOf } from "keyman/engine/element-wrappers";
import { nestedInstanceOf } from "keyman/engine/element-text-stores";
/**
* Given a DOM event related to an KMW-attached element, this function determines
* the corresponding OutputTargetInterface.
* the corresponding TextStore.
* @param e
* @returns
*/
export function eventOutputTarget(e: Event) {
const Ltarg: HTMLElement = e?.target as HTMLElement;
return outputTargetForElement(Ltarg);
}
/**
* Given a DOM event related to an KMW-attached element, this function determines
* the corresponding OutputTargetInterface.
* @param e
* @returns
*/
export function outputTargetForElement(Ltarg: HTMLElement) {
export function textStoreForElement(Ltarg: HTMLElement) {
if (Ltarg == null) {
return null;
}
@ -42,7 +31,7 @@ export function outputTargetForElement(Ltarg: HTMLElement) {
}
}
// Step 2: With the most likely host element determined, obtain the corresponding OutputTargetInterface
// Step 2: With the most likely host element determined, obtain the corresponding TextStore
// instance.
return Ltarg._kmwAttachment?.interface;
return Ltarg._kmwAttachment?.textStore;
}

View file

@ -1,7 +1,7 @@
import { EventEmitter } from 'eventemitter3';
import { DeviceSpec, InternalKeyboardFont } from "keyman/engine/keyboard";
import { Input, nestedInstanceOf, wrapElement } from "keyman/engine/element-wrappers";
import { InputElementTextStore, nestedInstanceOf, createTextStoreForElement } from "keyman/engine/element-text-stores";
import {
arrayFromNodeList,
createStyleSheet,
@ -201,14 +201,14 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
// The elements in the contained document get separately wrapped, so this doesn't need a proper wrapper.
//
// Its attachment process might need some work.
const eleInterface = wrapElement(x);
const textStore = createTextStoreForElement(x);
// May should filter better for IFrames.
if(!(eleInterface || nestedInstanceOf(x, "HTMLIFrameElement"))) {
if(!(textStore || nestedInstanceOf(x, "HTMLIFrameElement"))) {
console.warn("Could not create processing interface for newly-attached element!");
}
x._kmwAttachment = new AttachmentInfo(eleInterface, null, this.device.touchable);
x._kmwAttachment = new AttachmentInfo(textStore, null);
}
}
@ -236,7 +236,7 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
if(x instanceof x.ownerDocument.defaultView.HTMLTextAreaElement) {
return true;
} else if(x instanceof x.ownerDocument.defaultView.HTMLInputElement) {
if (Input.isSupportedType(x.type)) {
if (InputElementTextStore.isSupportedType(x.type)) {
return true;
}
} else if(x instanceof x.ownerDocument.defaultView.HTMLIFrameElement) {
@ -734,7 +734,7 @@ export class PageContextAttachment extends EventEmitter<EventMap> {
const t2=document.getElementsByTagName('textarea');
for(let i=0; i<t1.length; i++) {
if (Input.isSupportedType(t1[i].type) && t1[i].className.indexOf('kmw-disabled') < 0) {
if (InputElementTextStore.isSupportedType(t1[i].type) && t1[i].className.indexOf('kmw-disabled') < 0) {
eList.push({ip:t1[i], x: getAbsoluteX(t1[i]), y: getAbsoluteY(t1[i])});
}
}

View file

@ -0,0 +1,37 @@
import { nestedInstanceOf } from "keyman/engine/element-text-stores";
/**
* Given a DOM event related to an KMW-attached element, this function determines
* the corresponding TextStore.
* @param e
* @returns
*/
export function textStoreForElement(Ltarg: HTMLElement) {
if (Ltarg == null) {
return null;
}
// ... determine the element expected to hold the KMW attachment object based on
// its typing, properties, etc.
// @ts-ignore
if(Ltarg['body']) {
// @ts-ignore
Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes.
}
if (Ltarg.nodeType == 3) { // defeat Safari bug
Ltarg = Ltarg.parentNode as HTMLElement;
}
// Verify that the element does correspond to a remappable input field
if(nestedInstanceOf(Ltarg, "HTMLInputElement")) {
const et=(Ltarg as HTMLInputElement).type.toLowerCase();
if(!(et == 'text' || et == 'search')) {
return null;
}
}
// Step 2: With the most likely host element determined, obtain the corresponding TextStore
// instance.
return Ltarg._kmwAttachment?.textStore;
}

View file

@ -0,0 +1,13 @@
import { textStoreForElement } from './textStoreForElement.js';
/**
* Given a DOM event related to an KMW-attached element, this function determines
* the corresponding TextStore.
* @param e
* @returns
*/
export function textStoreForEvent(e: Event) {
const Ltarg: HTMLElement = e?.target as HTMLElement;
return textStoreForElement(Ltarg);
}

View file

@ -0,0 +1,52 @@
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

@ -0,0 +1,273 @@
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

@ -0,0 +1,31 @@
import { type AbstractElementTextStore } from './abstractElementTextStore.js';
import { InputElementTextStore } from './inputTextStore.js';
import { TextAreaElementTextStore } from './textareaTextStore.js';
import { DesignIFrameElementTextStore } from './designIFrameTextStore.js';
import { ContentEditableElementTextStore } from './contentEditableTextStore.js';
import { nestedInstanceOf } from './utils.js';
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

@ -0,0 +1,358 @@
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

@ -0,0 +1,7 @@
export { createTextStoreForElement } from './createTextStoreForElement.js';
export { ContentEditableElementTextStore } from './contentEditableTextStore.js';
export { DesignIFrameElementTextStore } from './designIFrameTextStore.js';
export { InputElementTextStore } from './inputTextStore.js';
export { AbstractElementTextStore } from './abstractElementTextStore.js';
export { TextAreaElementTextStore } from './textareaTextStore.js';
export { nestedInstanceOf } from './utils.js';

View file

@ -0,0 +1,233 @@
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

@ -0,0 +1,9 @@
## 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

@ -0,0 +1,201 @@
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

@ -0,0 +1,38 @@
/**
* 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,7 +0,0 @@
export { wrapElement } from './wrapElement.js';
export { ContentEditable } from './contentEditable.js';
export { DesignIFrame } from './designIFrame.js';
export { Input } from './input.js';
export { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
export { TextArea } from './textarea.js';
export { nestedInstanceOf } from './utils.js';

View file

@ -1,31 +0,0 @@
import { type OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { Input } from './input.js';
import { TextArea } from './textarea.js';
import { DesignIFrame } from './designIFrame.js';
import { ContentEditable } from './contentEditable.js';
import { nestedInstanceOf } from './utils.js';
export function wrapElement(e: HTMLElement): OutputTargetElementWrapper<any> {
// Complex type scoping is implemented here so that kmwutils.ts is not a dependency for test compilations.
if(nestedInstanceOf(e, "HTMLInputElement")) {
return new Input(<HTMLInputElement> e);
} else if(nestedInstanceOf(e, "HTMLTextAreaElement")) {
return new TextArea(<HTMLTextAreaElement> e);
} else if(nestedInstanceOf(e, "HTMLIFrameElement")) {
const iframe = <HTMLIFrameElement> e;
if(iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.designMode == "on") {
return new DesignIFrame(iframe);
} else if (e.isContentEditable) {
// Do content-editable <iframe>s make sense?
return new ContentEditable(e);
} else {
return null;
}
} else if(e.isContentEditable) {
return new ContentEditable(e);
}
return null;
}

View file

@ -1,6 +1,6 @@
import { LexicalModelTypes } from '@keymanapp/common-types';
import { EventEmitter } from "eventemitter3";
import { OutputTargetInterface } from "keyman/engine/keyboard";
import { TextStoreLanguageProcessorInterface } from "keyman/engine/keyboard";
export class ReadySuggestions {
suggestions: LexicalModelTypes.Suggestion[];
@ -45,10 +45,10 @@ export interface LanguageProcessorEventMap {
/**
* Is called synchronously once suggestion application is successful and the context has been updated.
*
* @param outputTarget The `OutputTargetInterface` representation of the context the suggestion was applied to.
* @param textStore The `TextStore` representation of the context the suggestion was applied to.
* @returns
*/
'suggestionapplied': (outputTarget: OutputTargetInterface) => boolean
'suggestionapplied': (textStore: TextStoreLanguageProcessorInterface) => boolean
}
@ -56,19 +56,19 @@ export interface LanguageProcessorSpec extends EventEmitter<LanguageProcessorEve
get state(): StateChangeEnum;
invalidateContext(outputTarget: OutputTargetInterface, layerId: string): Promise<LexicalModelTypes.Suggestion[]>;
invalidateContext(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<LexicalModelTypes.Suggestion[]>;
/**
*
* @param suggestion
* @param outputTarget
* @param textStore
* @param getLayerId a function that returns the current layerId,
* required because layerid can be changed by PostKeystroke
* @returns
*/
applySuggestion(suggestion: LexicalModelTypes.Suggestion, outputTarget: OutputTargetInterface, getLayerId: () => string): Promise<LexicalModelTypes.Reversion>;
applySuggestion(suggestion: LexicalModelTypes.Suggestion, textStore: TextStoreLanguageProcessorInterface, getLayerId: () => string): Promise<LexicalModelTypes.Reversion>;
applyReversion(reversion: LexicalModelTypes.Reversion, outputTarget: OutputTargetInterface): Promise<LexicalModelTypes.Suggestion[]>;
applyReversion(reversion: LexicalModelTypes.Reversion, textStore: TextStoreLanguageProcessorInterface): Promise<LexicalModelTypes.Suggestion[]>;
get wordbreaksAfterSuggestions(): boolean;

View file

@ -4,7 +4,7 @@ import Keep = LexicalModelTypes.Keep;
import Reversion = LexicalModelTypes.Reversion;
import Suggestion = LexicalModelTypes.Suggestion;
import { type LanguageProcessorSpec , ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js';
import { type OutputTargetInterface } from "keyman/engine/keyboard";
import { type TextStore } from "keyman/engine/keyboard";
interface PredictionContextEventMap {
update: (suggestions: Suggestion[]) => void;
@ -41,13 +41,13 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
/**
* Represents the active context used when requesting and applying predictive-text operations.
*/
private _currentTarget: OutputTargetInterface;
private _currentTarget: TextStore;
public get currentTarget(): OutputTargetInterface {
public get currentTarget(): TextStore {
return this._currentTarget;
}
public setCurrentTarget(target: OutputTargetInterface): Promise<Suggestion[]> {
public setCurrentTarget(target: TextStore): Promise<Suggestion[]> {
const originalTarget = this._currentTarget;
this._currentTarget = target;

View file

@ -1,8 +1,3 @@
export { BeepHandler, JSKeyboardProcessor, LogMessageHandler, ProcessorInitOptions } from "./jsKeyboardProcessor.js";
export { JSKeyboardInterface, KeyInformation, StoreNonCharEntry } from "./jsKeyboardInterface.js";
export * from "./deadkeys.js";
export { type ComplexKeyboardStore } from "./stores.js";
export { OutputTargetBase } from "./outputTargetBase.js";
export * from "./outputTargetBase.js";
export { Mock } from "./mock.js";
export * from "./stringDivergence.js";

View file

@ -13,19 +13,18 @@ import {
KeyboardHarness,
KeyboardKeymanGlobal,
KeyMapping,
SyntheticTextStore,
MutableSystemStore,
ProcessorAction,
SystemStore,
SystemStoreIDs,
type Deadkey,
type KeyEvent,
type OutputTargetInterface,
ProcessorAction,
type TextStore,
VariableStore,
VariableStoreDictionary,
VariableStoreSerializer,
} from "keyman/engine/keyboard";
import { type OutputTargetBase } from './outputTargetBase.js';
import { type Deadkey } from './deadkeys.js';
import { Mock } from "./mock.js";
import { PlatformSystemStore } from './platformSystemStore.js';
import { ComplexKeyboardStore, type KeyboardStore, KeyboardStoreElement } from "./stores.js";
@ -187,7 +186,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
cachedContextEx: CachedContextEx = new CachedContextEx();
ruleContextEx: CachedContextEx;
activeTargetOutput: OutputTargetInterface;
activeTargetOutput: TextStore;
ruleBehavior: ProcessorAction;
systemStores: {[storeID: number]: SystemStore};
@ -230,7 +229,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
* A text-insertion method used by custom OSKs for helpHTML interaction, like with sil_euro_latin.
*
* This function currently bypasses web-core's standard text handling control path and all predictive text processing.
* It also has DOM-dependencies that help ensure KMW's active OutputTarget retains focus during use.
* It also has DOM-dependencies that help ensure KMW's active TextStore retains focus during use.
*/
insertText?: (Ptext: string, PdeadKey: number) => void;
@ -255,7 +254,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
*
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @param {Object} textStore Element to work with (must be currently focused element)
* @return {string} Context string
*
* Example [abcdef|ghi] as INPUT, with the caret position marked by |:
@ -264,13 +263,13 @@ export class JSKeyboardInterface extends KeyboardHarness {
* KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string
*/
context(n: number, ln: number, outputTarget: OutputTargetInterface): string {
context(n: number, ln: number, textStore: TextStore): string {
const v = this.cachedContext.get(n, ln);
if(v !== null) {
return v;
}
const r = this.KC_(n, ln, outputTarget);
const r = this.KC_(n, ln, textStore);
this.cachedContext.set(n, ln, r);
return r;
}
@ -280,7 +279,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
*
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @param {Object} textStore Element to work with (must be currently focused element)
* @return {string} Context string
*
* Example [abcdef|ghi] as INPUT, with the caret position marked by |:
@ -288,11 +287,11 @@ export class JSKeyboardInterface extends KeyboardHarness {
* KC(3,3,Pelem) == "def"
* KC(10,10,Pelem) == "XXXXabcdef" i.e. return as much as possible of the requested string, where X = \uFFFE
*/
private KC_(n: number, ln: number, outputTarget: OutputTargetInterface): string {
private KC_(n: number, ln: number, textStore: TextStore): string {
let tempContext = '';
// If we have a selection, we have an empty context
tempContext = outputTarget.isSelectionEmpty() ? outputTarget.getTextBeforeCaret() : "";
tempContext = textStore.isSelectionEmpty() ? textStore.getTextBeforeCaret() : "";
if(KMWString.length(tempContext) < n) {
tempContext = Array(n-KMWString.length(tempContext)+1).join("\uFFFE") + tempContext;
@ -305,7 +304,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
* Function nul KN
* Scope Public
* @param {number} n Length of context to check
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @param {Object} textStore Element to work with (must be currently focused element)
* @return {boolean} True if length of context is less than or equal to n
* Description Test length of context, return true if the length of the context is less than or equal to n
*
@ -314,8 +313,8 @@ export class JSKeyboardInterface extends KeyboardHarness {
* KN(2,Pelem) == FALSE
* KN(4,Pelem) == TRUE
*/
nul(n: number, outputTarget: OutputTargetInterface): boolean {
const cx=this.context(n+1, 1, outputTarget);
nul(n: number, textStore: TextStore): boolean {
const cx=this.context(n+1, 1, textStore);
// With #31, the result will be a replacement character if context is empty.
return cx === "\uFFFE";
@ -325,18 +324,18 @@ export class JSKeyboardInterface extends KeyboardHarness {
* Function contextMatch KCM
* Scope Public
* @param {number} n Number of characters to move back from caret
* @param {Object} outputTarget Focused element
* @param {Object} textStore Focused element
* @param {string} val String to match
* @param {number} ln Number of characters to return
* @return {boolean} True if selected context matches val
* Description Test keyboard context for match
*/
contextMatch(n: number, outputTarget: OutputTargetInterface, val: string, ln: number): boolean {
const cx=this.context(n, ln, outputTarget);
contextMatch(n: number, textStore: TextStore, val: string, ln: number): boolean {
const cx=this.context(n, ln, textStore);
if(cx === val) {
return true; // I3318
}
(outputTarget as OutputTargetBase).deadkeys().resetMatched(); // I3318
textStore.deadkeys().resetMatched(); // I3318
return false;
}
@ -345,10 +344,10 @@ export class JSKeyboardInterface extends KeyboardHarness {
*
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @param {Object} textStore Element to work with (must be currently focused element)
* @return {Array} Context array (of strings and numbers)
*/
private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTargetBase): CachedExEntry {
private _BuildExtendedContext(n: number, ln: number, textStore: TextStore): CachedExEntry {
let cache: CachedExEntry = this.cachedContextEx.get(n, ln);
if(cache !== null) {
return cache;
@ -358,14 +357,14 @@ export class JSKeyboardInterface extends KeyboardHarness {
cache = this.cachedContextEx.get(n, n);
if(cache === null) {
// First, let's make sure we have a cloned, sorted copy of the deadkey array.
const unmatchedDeadkeys = outputTarget.deadkeys().toSortedArray(); // Is reverse-order sorted for us already.
const unmatchedDeadkeys = textStore.deadkeys().toSortedArray(); // Is reverse-order sorted for us already.
// Time to build from scratch!
let index = 0;
cache = { valContext: [], deadContext: []};
while(cache.valContext.length < n) {
// As adapted from `deadkeyMatch`.
const sp = outputTarget.getDeadkeyCaret();
const sp = textStore.getDeadkeyCaret();
const deadPos = sp - index;
if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p > deadPos) {
// We have deadkeys at the right-hand side of the caret! They don't belong in the context, so pop 'em off.
@ -378,7 +377,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
unmatchedDeadkeys.splice(0, 1);
} else {
// Take the character. We get "\ufffe" if it doesn't exist.
const kc = this.context(++index, 1, outputTarget);
const kc = this.context(++index, 1, textStore);
cache.valContext = ([kc] as (string|number)[]).concat(cache.valContext);
}
}
@ -398,16 +397,16 @@ export class JSKeyboardInterface extends KeyboardHarness {
* Function fullContextMatch KFCM
* Scope Private
* @param {number} n Number of characters to move back from caret
* @param {Object} outputTarget Focused element
* @param {Object} textStore Focused element
* @param {Array} rule An array of ContextEntries to match.
* @return {boolean} True if the fully-specified rule context matches the current KMW state.
*
* A KMW 10+ function designed to bring KMW closer to Keyman Desktop functionality,
* near-directly modeling (externally) the compiled form of Desktop rules' context section.
*/
fullContextMatch(n: number, outputTarget: OutputTargetBase, rule: ContextEntry[]): boolean {
fullContextMatch(n: number, textStore: TextStore, rule: ContextEntry[]): boolean {
// Stage one: build the context index map.
const fullContext = this._BuildExtendedContext(n, rule.length, outputTarget);
const fullContext = this._BuildExtendedContext(n, rule.length, textStore);
this.ruleContextEx = this.cachedContextEx.clone();
const context = fullContext.valContext;
const deadContext = fullContext.deadContext;
@ -498,7 +497,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
if(mismatch) {
// Reset the matched 'any' indices, if any.
outputTarget.deadkeys().resetMatched();
textStore.deadkeys().resetMatched();
this._AnyIndices = [];
}
@ -589,7 +588,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
retVal = (keyCode == Lrulekey); // I3318, I3555
}
if(!retVal) {
(this.activeTargetOutput as OutputTargetBase).deadkeys().resetMatched(); // I3318
this.activeTargetOutput.deadkeys().resetMatched(); // I3318
}
return retVal; // I3318
};
@ -624,22 +623,22 @@ export class JSKeyboardInterface extends KeyboardHarness {
* Function deadkeyMatch KDM
* Scope Public
* @param {number} n offset from current cursor position
* @param {Object} outputTarget target element
* @param {Object} textStore target element
* @param {number} d deadkey
* @return {boolean} True if deadkey found selected context matches val
* Description Match deadkey at current cursor position
*/
deadkeyMatch(n: number, outputTarget: OutputTargetInterface, d: number): boolean {
return outputTarget.hasDeadkeyMatch(n, d);
deadkeyMatch(n: number, textStore: TextStore, d: number): boolean {
return textStore.hasDeadkeyMatch(n, d);
}
/**
* Function beep KB
* Scope Public
* @param {Object} outputTarget element to flash
* @param {Object} textStore element to flash
* Description Flash body as substitute for audible beep; notify embedded device to vibrate
*/
beep(outputTarget: OutputTargetInterface): void {
beep(textStore: TextStore): void {
this.resetContextCache();
// Denote as part of the matched rule's behavior.
@ -729,10 +728,10 @@ export class JSKeyboardInterface extends KeyboardHarness {
* @param {number} Pdn no of character to overwrite (delete)
* @param {string} Ps string
* @param {number} Pn index
* @param {Object} outputTarget element to output to
* @param {Object} textStore element to output to
* Description Output a character selected from the string according to the offset in the index array
*/
indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTargetInterface): void {
indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, textStore: TextStore): void {
this.resetContextCache();
const assertNever = function(x: never): never {
@ -743,20 +742,20 @@ export class JSKeyboardInterface extends KeyboardHarness {
const indexChar = this._Index(Ps, Pn);
if(indexChar !== "") {
if(typeof indexChar == 'string' ) {
this.output(Pdn, outputTarget, indexChar); //I3319
this.output(Pdn, textStore, indexChar); //I3319
} else if(indexChar.t) {
switch(indexChar.t) {
case 'b': // Beep commands may appear within stores.
this.beep(outputTarget);
this.beep(textStore);
break;
case 'd':
this.deadkeyOutput(Pdn, outputTarget, indexChar.d);
this.deadkeyOutput(Pdn, textStore, indexChar.d);
break;
default:
assertNever(indexChar);
}
} else { // For keyboards developed during 10.0's alpha phase - t:'d' was assumed.
this.deadkeyOutput(Pdn, outputTarget, (indexChar as any).d);
this.deadkeyOutput(Pdn, textStore, (indexChar as any).d);
}
}
}
@ -766,15 +765,15 @@ export class JSKeyboardInterface extends KeyboardHarness {
* Function deleteContext KDC
* Scope Public
* @param {number} dn number of context entries to overwrite
* @param {Object} outputTarget element to output to
* @param {Object} textStore element to output to
* Description Keyboard output
*/
deleteContext(dn: number, outputTarget: OutputTargetInterface): void {
deleteContext(dn: number, textStore: TextStore): void {
let context: CachedExEntry;
// We want to control exactly which deadkeys get removed.
if(dn > 0) {
context = this._BuildExtendedContext(dn, dn, (outputTarget as OutputTargetBase));
context = this._BuildExtendedContext(dn, dn, textStore);
let nulCount = 0;
for(let i=0; i < context.valContext.length; i++) {
@ -782,7 +781,7 @@ export class JSKeyboardInterface extends KeyboardHarness {
if(dk) {
// Remove deadkey in context.
(outputTarget as OutputTargetBase).deadkeys().remove(dk);
textStore.deadkeys().remove(dk);
// Reduce our reported context size.
dn--;
@ -801,33 +800,33 @@ export class JSKeyboardInterface extends KeyboardHarness {
}
// If a matched deadkey hasn't been deleted, we don't WANT to delete it.
(outputTarget as OutputTargetBase).deadkeys().resetMatched();
textStore.deadkeys().resetMatched();
// Why reinvent the wheel? Delete the remaining characters by 'inserting a blank string'.
this.output(dn, outputTarget, '');
this.output(dn, textStore, '');
}
/**
* Function output KO
* Scope Public
* @param {number} dn number of characters to overwrite
* @param {Object} outputTarget element to output to
* @param {Object} textStore element to output to
* @param {string} s string to output
* Description Keyboard output
*/
output(dn: number, outputTarget: OutputTargetInterface, s:string): void {
output(dn: number, textStore: TextStore, s:string): void {
this.resetContextCache();
outputTarget.saveProperties();
outputTarget.clearSelection();
(outputTarget as OutputTargetBase).deadkeys().deleteMatched(); // I3318
textStore.saveProperties();
textStore.clearSelection();
textStore.deadkeys().deleteMatched(); // I3318
if(dn >= 0) {
// Automatically manages affected deadkey positions. Does not delete deadkeys b/c legacy behavior support.
outputTarget.deleteCharsBeforeCaret(dn);
textStore.deleteCharsBeforeCaret(dn);
}
// Automatically manages affected deadkey positions.
outputTarget.insertTextBeforeCaret(s);
outputTarget.restoreProperties();
textStore.insertTextBeforeCaret(s);
textStore.restoreProperties();
}
/**
@ -838,23 +837,23 @@ export class JSKeyboardInterface extends KeyboardHarness {
* @alias KCXO
* @public
* @param {number} Pdn number of characters to delete left of cursor
* @param {OutputTargetInterface} outputTarget target to output to
* @param {TextStore} textStore target to output to
* @param {number} contextLength length of current rule context to retrieve
* @param {number} contextOffset offset from start of current rule context, 1-based
*/
contextExOutput(Pdn: number, outputTarget: OutputTargetInterface, contextLength: number, contextOffset: number): void {
contextExOutput(Pdn: number, textStore: TextStore, contextLength: number, contextOffset: number): void {
this.resetContextCache();
if(Pdn >= 0) {
this.output(Pdn, outputTarget, "");
this.output(Pdn, textStore, "");
}
const context = this.ruleContextEx.get(contextLength, contextLength);
const dk = context.deadContext[contextOffset-1], vc = context.valContext[contextOffset-1];
if(dk) {
outputTarget.insertDeadkeyBeforeCaret(dk.d);
textStore.insertDeadkeyBeforeCaret(dk.d);
} else if(typeof vc == 'string') {
this.output(-1, outputTarget, vc);
this.output(-1, textStore, vc);
} else {
throw new Error("contextExOutput: should never be a numeric valContext with no corresponding deadContext");
}
@ -864,18 +863,18 @@ export class JSKeyboardInterface extends KeyboardHarness {
* Function deadkeyOutput KDO
* Scope Public
* @param {number} Pdn no of character to overwrite (delete)
* @param {OutputTargetInterface} outputTarget element to output to
* @param {TextStore} textStore element to output to
* @param {number} Pd deadkey id
* Description Record a deadkey at current cursor position, deleting Pdn characters first
*/
deadkeyOutput(Pdn: number, outputTarget: OutputTargetInterface, Pd: number): void {
deadkeyOutput(Pdn: number, textStore: TextStore, Pd: number): void {
this.resetContextCache();
if(Pdn >= 0) {
this.output(Pdn, outputTarget,""); //I3318 corrected to >=
this.output(Pdn, textStore,""); //I3318 corrected to >=
}
outputTarget.insertDeadkeyBeforeCaret(Pd);
textStore.insertDeadkeyBeforeCaret(Pd);
// _DebugDeadKeys(Pelem, 'KDeadKeyOutput: dn='+Pdn+'; deadKey='+Pd);
}
@ -884,10 +883,10 @@ export class JSKeyboardInterface extends KeyboardHarness {
*
* @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported)
* @param {string} strValue String value to compare to
* @param {OutputTargetInterface} outputTarget Currently active element (may be needed by future tests)
* @param {TextStore} textStore Currently active element (may be needed by future tests)
* @return {boolean} True if the test succeeds
*/
ifStore(systemId: number, strValue: string, outputTarget: OutputTargetInterface): boolean {
ifStore(systemId: number, strValue: string, textStore: TextStore): boolean {
let result=true;
const store = this.systemStores[systemId];
if(store) {
@ -901,14 +900,14 @@ export class JSKeyboardInterface extends KeyboardHarness {
*
* @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported)
* @param {string} strValue String to set as the system store content
* @param {OutputTargetInterface} outputTarget Currently active element (may be needed in future tests)
* @param {TextStore} textStore Currently active element (may be needed in future tests)
* @return {boolean} True if command succeeds
* (i.e. for TSS_LAYER, if the layer is successfully selected)
*
* Note that option/variable stores are instead set within keyboard script code, as they only
* affect keyboard behavior.
*/
setStore(systemId: number, strValue: string, outputTarget: OutputTargetInterface): boolean {
setStore(systemId: number, strValue: string, textStore: TextStore): boolean {
this.resetContextCache();
// Unique case: we only allow set(&layer) ops from keyboard rules triggered by touch OSKs.
if(systemId == SystemStoreIDs.TSS_LAYER && this.activeDevice.touchable) {
@ -978,64 +977,64 @@ export class JSKeyboardInterface extends KeyboardHarness {
this.cachedContextEx.reset();
}
defaultBackspace(outputTarget: OutputTargetInterface) {
if(outputTarget.isSelectionEmpty()) {
defaultBackspace(textStore: TextStore) {
if(textStore.isSelectionEmpty()) {
// Delete the character left of the caret
this.output(1, outputTarget, "");
this.output(1, textStore, "");
} else {
// Delete just the selection
this.output(0, outputTarget, "");
this.output(0, textStore, "");
}
}
/**
* Function processNewContextEvent
* Scope Private
* @param {Object} outputTarget The target receiving input
* @param {Object} textStore The target receiving input
* @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard.
* Description Calls the keyboard's `begin newContext` group
* @returns {ProcessorAction} Record of commands and state changes that result from executing `begin NewContext`
*/
processNewContextEvent(outputTarget: OutputTargetBase, keystroke: KeyEvent): ProcessorAction {
processNewContextEvent(textStore: TextStore, keystroke: KeyEvent): ProcessorAction {
if(!this.activeKeyboard) {
throw "No active keyboard for keystroke processing!";
}
return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), outputTarget, keystroke, true);
return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), textStore, keystroke, true);
}
/**
* Function processPostKeystroke
* Scope Private
* @param {Object} outputTarget The target receiving input
* @param {Object} textStore The target receiving input
* @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard.
* Description Calls the keyboard's `begin postKeystroke` group
* @returns {ProcessorAction} Record of commands and state changes that result from executing `begin PostKeystroke`
*/
processPostKeystroke(outputTarget: OutputTargetBase, keystroke: KeyEvent): ProcessorAction {
processPostKeystroke(textStore: TextStore, keystroke: KeyEvent): ProcessorAction {
if(!this.activeKeyboard) {
throw "No active keyboard for keystroke processing!";
}
return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), outputTarget, keystroke, true);
return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), textStore, keystroke, true);
}
/**
* Function processKeystroke
* Scope Private
* @param {Object} outputTarget The target receiving input
* @param {Object} textStore The target receiving input
* @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard.
* Description Encapsulates calls to keyboard input processing.
* @returns {ProcessorAction} Record of commands and state changes that result from executing `begin Unicode`
*/
processKeystroke(outputTarget: OutputTargetBase, keystroke: KeyEvent): ProcessorAction {
processKeystroke(textStore: TextStore, keystroke: KeyEvent): ProcessorAction {
if(!this.activeKeyboard) {
throw "No active keyboard for keystroke processing!";
}
return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke, false);
return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), textStore, keystroke, false);
}
private process(callee: (outputTarget: OutputTargetBase, keystroke: KeyEvent) => boolean, outputTarget: OutputTargetBase, keystroke: KeyEvent, readonly: boolean): ProcessorAction {
private process(callee: (textStore: TextStore, keystroke: KeyEvent) => boolean, textStore: TextStore, keystroke: KeyEvent, readonly: boolean): ProcessorAction {
// Clear internal state tracking data from prior keystrokes.
if(!outputTarget) {
if(!textStore) {
throw "No target specified for keyboard output!";
} else if(!this.activeKeyboard) {
throw "No active keyboard for keystroke processing!";
@ -1043,13 +1042,13 @@ export class JSKeyboardInterface extends KeyboardHarness {
throw "No callee for keystroke processing!";
}
outputTarget.invalidateSelection();
textStore.invalidateSelection();
outputTarget.deadkeys().resetMatched(); // I3318
textStore.deadkeys().resetMatched(); // I3318
this.resetContextCache();
// Capture the initial state of the OutputTarget before any rules are matched.
const preInput = Mock.from(outputTarget, true);
// Capture the initial state of the TextStore before any rules are matched.
const preInput = SyntheticTextStore.from(textStore, true);
// Capture the initial state of any variable stores
const cachedVariableStores = this.activeKeyboard.variableStores;
@ -1062,12 +1061,12 @@ export class JSKeyboardInterface extends KeyboardHarness {
this.activeDevice = keystroke.device;
// Calls the start-group of the active keyboard.
this.activeTargetOutput = outputTarget;
const matched = callee(outputTarget, keystroke);
this.activeTargetOutput = textStore;
const matched = callee(textStore, keystroke);
this.activeTargetOutput = null;
// Finalize the rule's results.
this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke, readonly);
this.ruleBehavior.transcription = textStore.buildTranscriptionFrom(preInput, keystroke, readonly);
// We always backup the changes to variable stores to the ProcessorAction, to
// be applied during finalization, then restore them to the cached initial

View file

@ -11,16 +11,14 @@ import { ModifierKeyConstants } from '@keymanapp/common-types';
import {
Codes, type JSKeyboard, MinimalKeymanGlobal, KeyEvent, Layouts,
DefaultRules, EmulationKeystrokes, type MutableSystemStore,
OutputTargetInterface, ProcessorAction, SystemStoreIDs
TextStore, ProcessorAction, SystemStoreIDs, SyntheticTextStore
} from "keyman/engine/keyboard";
import { Mock } from "./mock.js";
import { type OutputTargetBase } from "./outputTargetBase.js";
import { JSKeyboardInterface } from './jsKeyboardInterface.js';
import { DeviceSpec, globalObject, KMWString } from "keyman/common/web-utils";
// #endregion
export type BeepHandler = (outputTarget: OutputTargetInterface) => void;
export type BeepHandler = (textStore: TextStore) => void;
export type LogMessageHandler = (str: string) => void;
export interface ProcessorInitOptions {
@ -122,18 +120,18 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
* where and when appropriate.
*
* @param {object} Lkc The pre-analyzed KeyEvent object
* @param {OutputTargetBase} outputTarget The output target receiving the KeyEvent
* @param {TextStore} textStore The output target receiving the KeyEvent
* @param {boolean} readonly True if the target is read-only
* @return {string}
*/
private defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTargetBase, readonly: boolean): ProcessorAction {
const preInput = Mock.from(outputTarget, readonly);
private defaultRuleBehavior(Lkc: KeyEvent, textStore: TextStore, readonly: boolean): ProcessorAction {
const preInput = SyntheticTextStore.from(textStore, readonly);
const ruleBehavior = new ProcessorAction();
let matched = false;
let char = '';
let special: EmulationKeystrokes;
if(Lkc.isSynthetic || outputTarget.isSynthetic) {
if(Lkc.isSynthetic || textStore.isSynthetic) {
matched = true; // All the conditions below result in matches until the final else, which restores the expected default
// if no match occurs.
@ -146,10 +144,10 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
} else if((special = this.defaultRules.forSpecialEmulation(Lkc)) != null) {
switch(special) {
case EmulationKeystrokes.Backspace:
this.keyboardInterface.defaultBackspace(outputTarget);
this.keyboardInterface.defaultBackspace(textStore);
break;
case EmulationKeystrokes.Enter:
outputTarget.handleNewlineAtCaret();
textStore.handleNewlineAtCaret();
break;
// case '\u007f': // K_DEL
// // For (possible) future implementation.
@ -171,13 +169,13 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
special = this.defaultRules.forSpecialEmulation(Lkc)
if(special == EmulationKeystrokes.Backspace) {
// A browser's default backspace may fail to delete both parts of an SMP character.
this.keyboardInterface.defaultBackspace(outputTarget);
this.keyboardInterface.defaultBackspace(textStore);
} else if(special || this.defaultRules.isCommand(Lkc)) { // Filters out 'commands' like TAB.
// We only do the "for special emulation" cases under the condition above... aside from backspace
// Let the browser handle those.
return null;
} else {
this.keyboardInterface.output(0, outputTarget, char);
this.keyboardInterface.output(0, textStore, char);
}
} else {
// No match, no default ProcessorAction.
@ -190,33 +188,33 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
return ruleBehavior;
}
const transcription = outputTarget.buildTranscriptionFrom(preInput, Lkc, readonly);
const transcription = textStore.buildTranscriptionFrom(preInput, Lkc, readonly);
ruleBehavior.transcription = transcription;
return ruleBehavior;
}
private processNewContextEvent(device: DeviceSpec, outputTarget: OutputTargetBase): ProcessorAction {
private processNewContextEvent(device: DeviceSpec, textStore: TextStore): ProcessorAction {
return this.activeKeyboard ?
this.keyboardInterface.processNewContextEvent(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :
this.keyboardInterface.processNewContextEvent(textStore, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :
null;
}
public processPostKeystroke(device: DeviceSpec, outputTarget: OutputTargetBase): ProcessorAction {
public processPostKeystroke(device: DeviceSpec, textStore: TextStore): ProcessorAction {
return this.activeKeyboard ?
this.keyboardInterface.processPostKeystroke(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :
this.keyboardInterface.processPostKeystroke(textStore, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :
null;
}
public processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTargetBase): ProcessorAction {
public processKeystroke(keyEvent: KeyEvent, textStore: TextStore): ProcessorAction {
let matchBehavior: ProcessorAction;
// Before keyboard rules apply, check if the left-context is empty.
const nothingDeletable = KMWString.length(outputTarget.getTextBeforeCaret()) == 0 && outputTarget.isSelectionEmpty();
const nothingDeletable = KMWString.length(textStore.getTextBeforeCaret()) == 0 && textStore.isSelectionEmpty();
// Pass this key code and state to the keyboard program
if(this.activeKeyboard && keyEvent.Lcode != 0) {
matchBehavior = this.keyboardInterface.processKeystroke(outputTarget, keyEvent);
matchBehavior = this.keyboardInterface.processKeystroke(textStore, keyEvent);
}
// Final conditional component - if someone actually makes a keyboard rule that blocks output
@ -226,7 +224,7 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
// behavior in cases where such rules actually would appear. (Though, _that_ should be caught
// in the keyboard-review process and heavily discouraged, so... yeah.)
if(nothingDeletable && keyEvent.Lcode == Codes.keyCodes.K_BKSP && matchBehavior.triggerKeyDefault) {
matchBehavior = this.defaultRuleBehavior(keyEvent, outputTarget, false);
matchBehavior = this.defaultRuleBehavior(keyEvent, textStore, false);
matchBehavior.triggerKeyDefault = true;
// Force a single `deleteLeft`.
// @ts-ignore // force value override, because deleteLeft is marked readonly.
@ -238,11 +236,11 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
// Handle unmapped keys, including special keys
// The following is physical layout dependent, so should be avoided if possible. All keys should be mapped.
this.keyboardInterface.activeTargetOutput = outputTarget;
this.keyboardInterface.activeTargetOutput = textStore;
// Match against the 'default keyboard' - rules to mimic the default string output when typing in a browser.
// Many keyboards rely upon these 'implied rules'.
const defaultBehavior = this.defaultRuleBehavior(keyEvent, outputTarget, false);
const defaultBehavior = this.defaultRuleBehavior(keyEvent, textStore, false);
if(defaultBehavior) {
if(!matchBehavior) {
matchBehavior = defaultBehavior;
@ -537,13 +535,13 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
// Returns true if the key event is a modifier press, allowing keyPress to return selectively
// in those cases.
public doModifierPress(Levent: KeyEvent, outputTarget: OutputTargetBase, isKeyDown: boolean): boolean {
public doModifierPress(Levent: KeyEvent, textStore: TextStore, isKeyDown: boolean): boolean {
if(!this.activeKeyboard) {
return false;
}
if(Levent.isModifier) {
this.activeKeyboard.notify(Levent.Lcode, outputTarget, isKeyDown ? 1 : 0);
this.activeKeyboard.notify(Levent.Lcode, textStore, isKeyDown ? 1 : 0);
// For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode.
if(!Levent.device.touchable) {
return this._UpdateVKShift(Levent); // I2187
@ -553,7 +551,7 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
}
if(Levent.LmodifierChange) {
this.activeKeyboard.notify(0, outputTarget, 1);
this.activeKeyboard.notify(0, textStore, 1);
if(!Levent.device.touchable) {
this._UpdateVKShift(Levent);
}
@ -567,20 +565,20 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
* Tell the currently active keyboard that a new context has been selected,
* e.g. by focus change, selection change, keyboard change, etc.
*
* @param {Object} outputTarget The OutputTarget that has focus
* @param {Object} textStore The TextStore that has focus
* @returns {Object} A ProcessorAction object describing the cumulative effects of
* all matched keyboard rules
*/
private performNewContextEvent(outputTarget: OutputTargetBase): ProcessorAction {
const ruleBehavior = this.processNewContextEvent(this.contextDevice, outputTarget);
private performNewContextEvent(textStore: TextStore): ProcessorAction {
const ruleBehavior = this.processNewContextEvent(this.contextDevice, textStore);
if (ruleBehavior) {
this.finalizeProcessorAction(ruleBehavior, outputTarget);
this.finalizeProcessorAction(ruleBehavior, textStore);
}
return ruleBehavior;
}
public resetContext(target?: OutputTargetBase) {
public resetContext(target?: TextStore) {
this.layerId = 'default';
// Make sure all deadkeys for the context get cleared properly.
@ -608,13 +606,13 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
}
};
public finalizeProcessorAction(data: ProcessorAction, outputTarget: OutputTargetInterface): void {
public finalizeProcessorAction(data: ProcessorAction, textStore: TextStore): void {
if (!data.transcription) {
throw "Cannot finalize a ProcessorAction with no transcription.";
}
if (this.beepHandler && data.beep) {
this.beepHandler(outputTarget);
this.beepHandler(textStore);
}
for (const storeID in data.setStore) {
@ -642,7 +640,7 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
if (data.triggersDefaultCommand) {
const keyEvent = data.transcription.keystroke;
this.defaultRules.applyCommand(keyEvent, outputTarget);
this.defaultRules.applyCommand(keyEvent, textStore);
}
if (this.warningLogger && data.warningLog) {
@ -672,7 +670,7 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
first.triggersDefaultCommand = first.triggersDefaultCommand || other.triggersDefaultCommand;
const mergingMock = Mock.from(first.transcription.preInput, false);
const mergingMock = SyntheticTextStore.from(first.transcription.preInput, false);
mergingMock.apply(first.transcription.transform);
mergingMock.apply(other.transcription.transform);

View file

@ -0,0 +1,179 @@
// Defines the base Deadkey-tracking object.
export class Deadkey {
p: number; // Position of deadkey
d: number; // Numerical id of the deadkey
o: number; // Ordinal value of the deadkey (resolves same-place conflicts)
matched: number;
static ordinalSeed: number = 0;
constructor(pos: number, id: number) {
this.p = pos;
this.d = id;
this.o = Deadkey.ordinalSeed++;
}
match(p: number, d: number): boolean {
const result:boolean = (this.p == p && this.d == d);
return result;
}
set(): void {
this.matched = 1;
}
reset(): void {
this.matched = 0;
}
before(other: Deadkey): boolean {
return this.o < other.o;
}
clone(): Deadkey {
const dk = new Deadkey(this.p, this.d);
dk.o = this.o;
return dk;
}
equal(other: Deadkey) {
return this.d == other.d && this.p == other.d && this.o == other.o;
}
/**
* Sorts the deadkeys in reverse order.
*/
static sortFunc = function(a: Deadkey, b: Deadkey) {
// We want descending order, so we want 'later' deadkeys first.
if(a.p != b.p) {
return b.p - a.p;
} else {
return b.o - a.o;
}
};
}
// Object-orients deadkey management.
export class DeadkeyTracker {
dks: Deadkey[] = [];
toSortedArray(): Deadkey[] {
this.dks = this.dks.sort(Deadkey.sortFunc);
return [].concat(this.dks);
}
clone(): DeadkeyTracker {
const dkt = new DeadkeyTracker();
const dks = this.toSortedArray();
// Make sure to clone the deadkeys themselves - the Deadkey object is mutable.
dkt.dks = [];
dks.forEach(function(value: Deadkey) {
dkt.dks.push(value.clone());
});
return dkt;
}
/**
* Function isMatch
* Scope Public
* @param {number} caretPos current cursor position
* @param {number} n expected offset of deadkey from cursor
* @param {number} d deadkey
* @return {boolean} True if deadkey found selected context matches val
* Description Match deadkey at current cursor position
*/
isMatch(caretPos: number, n: number, d: number): boolean {
if(this.dks.length == 0) {
return false; // I3318
}
const sp=caretPos;
n = sp - n;
for(let i = 0; i < this.dks.length; i++) {
// Don't re-match an already-matched deadkey. It's possible to have two identical
// entries, and they should be kept separately.
if(this.dks[i].match(n, d) && !this.dks[i].matched) {
this.dks[i].set();
// Assumption: since we match the first possible entry in the array, we
// match the entry with the lower ordinal - the 'first' deadkey in the position.
return true; // I3318
}
}
this.resetMatched(); // I3318
return false;
}
add(dk: Deadkey) {
this.dks = this.dks.concat(dk);
}
remove(dk: Deadkey) {
const index = this.dks.indexOf(dk);
this.dks.splice(index, 1);
}
clear() {
this.dks = [];
}
resetMatched() {
for(const dk of this.dks) {
dk.reset();
}
}
deleteMatched(): void {
for(let Li = 0; Li < this.dks.length; Li++) {
if(this.dks[Li].matched) {
this.dks.splice(Li--, 1); // Don't forget to decrement!
}
}
}
/**
* Function adjustPositions (formerly _DeadkeyAdjustPos)
* Scope Private
* @param {number} Lstart start position in context
* @param {number} Ldelta characters to adjust by
* Description Adjust saved positions of deadkeys in context
*/
adjustPositions(Lstart: number, Ldelta: number): void {
if(Ldelta == 0) {
return;
}
for(const dk of this.dks) {
if(dk.p > Lstart) {
dk.p += Ldelta;
}
}
}
equal(other: DeadkeyTracker) {
if(this.dks.length != other.dks.length) {
return false;
}
const otherDks = other.dks;
const matchedDks: Deadkey[] = [];
for(const dk of this.dks) {
const match = otherDks.find((otherDk) => dk.equal(otherDk));
if(!match) {
return false;
}
}
return matchedDks.length == otherDks.length;
}
count(): number {
return this.dks.length;
}
}

View file

@ -7,7 +7,7 @@
import { ModifierKeyConstants } from '@keymanapp/common-types';
import { Codes } from './codes.js';
import { type KeyEvent } from './keyEvent.js';
import { type OutputTargetInterface } from './outputTargetInterface.js';
import { type TextStore } from './textStore.js';
export enum EmulationKeystrokes {
Enter = '\n',
@ -22,7 +22,7 @@ export class LogMessages {
/**
* Defines a collection of static library functions that define KeymanWeb's default (implied) keyboard rule behaviors.
*/
export default class DefaultRules {
export class DefaultRules {
codeForEvent(Lkc: KeyEvent) {
return Codes.keyCodes[Lkc.kName] || Lkc.Lcode;;
}
@ -78,12 +78,12 @@ export default class DefaultRules {
/**
* Used when a ProcessorAction represents a non-text "command" within the Engine. This will generally
* trigger events that require context reset - often by moving the caret or by moving what OutputTarget
* trigger events that require context reset - often by moving the caret or by moving what TextStore
* the caret is in. However, we let those events perform the actual context reset.
*
* Note: is extended by DOM-aware KeymanWeb code.
*/
public applyCommand(Lkc: KeyEvent, outputTarget: OutputTargetInterface): void {
public applyCommand(Lkc: KeyEvent, textStore: TextStore): void {
// Notes for potential default-handling extensions:
//
// switch(code) {
@ -112,7 +112,7 @@ export default class DefaultRules {
/**
* Codes matched here generally have default implementations when in a browser but require emulation
* for 'synthetic' `OutputTarget`s like `Mock`s, which have no default text handling.
* for 'synthetic' `TextStore`s like `SyntheticTextStore`s, which have no default text handling.
*/
public forSpecialEmulation(Lkc: KeyEvent): EmulationKeystrokes {
let code = this.codeForEvent(Lkc);

View file

@ -25,21 +25,24 @@ export { type Alternate, TextTransform } from "./keyboards/textTransform.js";
export { Transcription } from "./keyboards/transcription.js";
export { Codes } from "./codes.js";
export { default as DefaultRules } from "./defaultRules.js";
export * from "./defaultRules.js";
export { EmulationKeystrokes, LogMessages, DefaultRules } from "./defaultRules.js";
export { type KeyDistribution, KeyEventSpec, KeyEvent } from "./keyEvent.js";
export { default as KeyMapping } from "./keyMapping.js";
export { OutputTargetInterface } from "./outputTargetInterface.js";
export { KeyMapping } from "./keyMapping.js";
export { type SystemStoreMutationHandler, MutableSystemStore, SystemStore, SystemStoreIDs, type SystemStoreDictionary } from "./systemStore.js";
export { type VariableStore, VariableStoreSerializer, VariableStoreDictionary } from "./variableStore.js";
export { DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js';
export { SyntheticTextStore } from "./syntheticTextStore.js";
export { TextStore } from "./textStore.js";
export { TextStoreLanguageProcessorInterface } from "./textStoreLanguageProcessorInterface.js";
export { findCommonSubstringEndIndex } from "./stringDivergence.js";
export { Deadkey } from "./deadkeys.js";
// TODO-web-core: why do we export these here?
export * from "keyman/common/web-utils";
// At the top level, there should be no default export.
// Without the line below... OutputTarget would likely be aliased there, as it's
// Without the line below... TextStore would likely be aliased there, as it's
// the last `export { default as _ }` => `export * from` pairing seen above.
export default undefined;

View file

@ -10,7 +10,7 @@ import { type JSKeyboard } from "./keyboards/jsKeyboard.js";
import { type DeviceSpec } from "keyman/common/web-utils";
import { Codes } from './codes.js';
import DefaultRules from "./defaultRules.js";
import { DefaultRules } from "./defaultRules.js";
import { ActiveKeyBase } from './keyboards/activeLayout.js';
// Represents a probability distribution over a keyboard's keys.

View file

@ -54,7 +54,7 @@ class LanguageKeyMaps {
}
}
export default class KeyMapping {
export class KeyMapping {
static readonly browserMap: BrowserKeyMaps = new BrowserKeyMaps();
static readonly languageMap: LanguageKeyMaps = new LanguageKeyMaps();

View file

@ -1,6 +1,6 @@
import { Codes } from "../codes.js";
import { KeyEvent, KeyEventSpec } from "../keyEvent.js";
import KeyMapping from "../keyMapping.js";
import { KeyMapping } from "../keyMapping.js";
import { ButtonClasses, Layouts } from "./defaultLayouts.js";
import type { LayoutKey, LayoutSubKey, LayoutRow, LayoutLayer, LayoutFormFactor, ButtonClass } from "./defaultLayouts.js";
import { type JSKeyboard } from "./jsKeyboard.js";

View file

@ -2,7 +2,7 @@ import { Codes } from "../codes.js";
import { Layouts } from "./defaultLayouts.js";
import { ActiveKey, ActiveLayout, ActiveSubKey } from "./activeLayout.js";
import { KeyEvent } from "../keyEvent.js";
import { type OutputTargetInterface } from "../outputTargetInterface.js";
import { type TextStore } from "../textStore.js";
import { KeymanWebKeyboard, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types";
import { VariableStoreDictionary } from "../variableStore.js";
@ -50,7 +50,7 @@ type KmwKeyboardObject = KeyboardObject & {
*/
export class JSKeyboard {
public static DEFAULT_SCRIPT_OBJECT: KmwKeyboardObject = {
'gs': function(outputTarget: OutputTargetInterface, keystroke: KeyEvent) { return false; }, // no matching rules; rely on defaultRuleOutput entirely
'gs': function(textStore: TextStore, keystroke: KeyEvent) { return false; }, // no matching rules; rely on defaultRuleOutput entirely
'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this.
'KN': '',
'KV': Layouts.DEFAULT_RAW_SPEC,
@ -78,22 +78,22 @@ export class JSKeyboard {
/**
* Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group.
*/
process(outputTarget: OutputTargetInterface, keystroke: KeyEvent): boolean {
return this.scriptObject['gs'](outputTarget, keystroke);
process(textStore: TextStore, keystroke: KeyEvent): boolean {
return this.scriptObject['gs'](textStore, keystroke);
}
/**
* Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group.
*/
processNewContextEvent(outputTarget: OutputTargetInterface, keystroke: KeyEvent): boolean {
return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false;
processNewContextEvent(textStore: TextStore, keystroke: KeyEvent): boolean {
return this.scriptObject['gn'] ? this.scriptObject['gn'](textStore, keystroke) : false;
}
/**
* Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group.
*/
processPostKeystroke(outputTarget: OutputTargetInterface, keystroke: KeyEvent): boolean {
return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false;
processPostKeystroke(textStore: TextStore, keystroke: KeyEvent): boolean {
return this.scriptObject['gpk'] ? this.scriptObject['gpk'](textStore, keystroke) : false;
}
get isHollow(): boolean {
@ -359,7 +359,7 @@ export class JSKeyboard {
* @param {number} _PData 1 or 0
* Notifies keyboard of keystroke or other event
*/
notify(_PCommand: number, _PTarget: OutputTargetInterface, _PData: number) { // I2187
notify(_PCommand: number, _PTarget: TextStore, _PData: number) { // I2187
// Good example use case - the Japanese CJK-picker keyboard
if(typeof(this.scriptObject['KNS']) == 'function') {
this.scriptObject['KNS'](_PCommand, _PTarget, _PData);

View file

@ -1,7 +1,7 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { OutputTargetInterface } from '../outputTargetInterface.js';
import { TextStore } from '../textStore.js';
import { KeyEvent } from '../keyEvent.js';
import { Alternate, TextTransform } from './textTransform.js';
@ -10,11 +10,11 @@ export class Transcription {
readonly keystroke: KeyEvent;
readonly transform: TextTransform;
alternates: Alternate[]; // constructed after the rest of the transcription.
readonly preInput: OutputTargetInterface;
readonly preInput: TextStore;
private static tokenSeed: number = 0;
constructor(keystroke: KeyEvent, transform: TextTransform, preInput: OutputTargetInterface, alternates?: Alternate[]) {
constructor(keystroke: KeyEvent, transform: TextTransform, preInput: TextStore, alternates?: Alternate[]) {
const token = this.token = Transcription.tokenSeed++;
this.keystroke = keystroke;

View file

@ -1,108 +0,0 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
export interface OutputTargetInterface {
/**
* Signifies that this OutputTargetInterface has no default key processing behaviors.
* This should be false for OutputTargets backed by web elements like HTMLInputElement
* or HTMLTextAreaElement.
*/
get isSynthetic(): boolean;
resetContext(): void;
hasDeadkeyMatch(n: number, d: number): boolean;
insertDeadkeyBeforeCaret(d: number): void;
/**
* Clears any selected text within the wrapper's element(s).
* Silently does nothing if no such text exists.
*/
clearSelection(): void;
/**
* Clears any cached selection-related state values.
*/
invalidateSelection(): void;
/**
* Indicates whether or not the underlying element has its own selection (input, textarea)
* or is part of (or possesses) the DOM's active selection. Don't confuse with isSelectionEmpty().
*
* TODO: rename to supportsOwnSelection
*/
hasSelection(): boolean;
/**
* Returns true if there is no current selection -- that is, the selection range is empty
*/
isSelectionEmpty(): boolean;
/**
* Returns an index corresponding to the caret's position for use with deadkeys.
*/
getDeadkeyCaret(): number;
/**
* Relative to the caret, gets the current context within the wrapper's element.
*/
getTextBeforeCaret(): string;
/**
* Gets the element's-currently selected text.
*/
getSelectedText(): string;
/**
* Relative to the caret (and/or active selection), gets the element's text after the caret,
* excluding any actively selected text that would be immediately replaced upon text entry.
*/
getTextAfterCaret(): string;
/**
* Gets the element's full text, including any text that is actively selected.
*/
getText(): string;
/**
* Performs context deletions (from the left of the caret) as needed by the KeymanWeb engine and
* corrects the location of any affected deadkeys.
*
* Does not delete deadkeys (b/c KMW 1 & 2 behavior maintenance).
* @param dn The number of characters to delete. If negative, context will be left unchanged.
*/
deleteCharsBeforeCaret(dn: number): void;
/**
* Inserts text immediately before the caret's current position, moving the caret after the
* newly inserted text in the process along with any affected deadkeys.
*
* @param s Text to insert before the caret's current position.
*/
insertTextBeforeCaret(s: string): void;
/**
* Allows element-specific handling for ENTER key inputs. Conceptually, this should usually
* correspond to `insertTextBeforeCaret('\n'), but actual implementation will vary greatly among
* elements.
*/
handleNewlineAtCaret(): void;
/**
* Saves element-specific state properties prone to mutation, enabling restoration after
* text-output operations.
*/
saveProperties(): void;
/**
* Restores previously-saved element-specific state properties. Designed for use after text-output
* ops to facilitate more-seamless web-dev and user interactions.
*/
restoreProperties(): void;
/**
* Generates a synthetic event on the underlying element, signalling that its value has changed.
*/
doInputEvent(): void;
}

View file

@ -0,0 +1,92 @@
import { Uni_IsSurrogate1, Uni_IsSurrogate2 } from '@keymanapp/common-types';
/**
* Returns the index for the code point divergence point between two strings, as measured in code
* unit coordinates.
* @param str1
* @param str2
* @param commonSuffix If false, asserts a common prefix to the strings. If true, asserts a common suffix.
* @returns The code unit index within `str1` for the start of the code point not common to both.
*
* Follows the convention of (start, end) substring parameterizations having 'end' be exclusive.
*/
export function findCommonSubstringEndIndex(str1: string, str2: string, commonSuffix: boolean): number {
/**
* The maximum number of iterations to consider; exceeding this would go past a string boundary.
*/
const maxInterval = Math.min(str1.length, str2.length);
/**
* The first valid index within the string.
*/
let start: number;
/**
* The current index within the string under consideration as the divergence point.
*/
let index: number;
/**
* The index at which to terminate the search for a divergence point.
*/
let end: number;
/**
* Index shift per loop iteration.
*/
let inc: number;
/**
* Difference in index for comparison between strings.
* Mostly matters when assuming a common right-hand side.
*/
let offset: number;
if(commonSuffix) {
start = index = str1.length - 1; // e.g. str.length == 10 => start = 9.
end = index - maxInterval; // e.g. maxInterval 8, start 9 => iterate from 9 to 2, end at 1.
inc = -1;
offset = str2.length - str1.length;
} else {
start = index = 0;
end = maxInterval; // last valid index: - 1. e.g. maxInterval 8 => iterate from 0 to 7, end at 8.
inc = 1;
offset = 0;
}
// Step 1: Find the index for the first code unit different between the strings.
for(; index != end; index += inc) {
if(str1.charAt(index) != str2.charAt(index + offset)) {
break;
}
}
// Step 2: Ensure that we're not splitting a surrogate pair.
// `index` corresponds to the first char that is different _in the direction indicated by inc_.
// If it's the start position, it can't split a (completed) surrogate pair.
if(index != start && index != end) {
// if commonLeft, high surrogate; if commonRight, low surrogate.
const commonPotentialSurrogate = str1.charCodeAt(index - inc);
// Opposite surrogate type from the previous variable.
const divergentChar1 = str1.charCodeAt(index);
const divergentChar2 = str2.charCodeAt(index + offset);
const commonSurrogateChecker = commonSuffix ? Uni_IsSurrogate2 : Uni_IsSurrogate1;
const divergentSurrogateChecker = commonSuffix ? Uni_IsSurrogate1 : Uni_IsSurrogate2;
// If the last common character if of the direction-appropriate surrogate type (for
// comprising a potential split surrogate pair representing a non-BMP char)...
if(commonSurrogateChecker(commonPotentialSurrogate)) {
// And one of the two divergent chars is a qualifying match - a surrogate
// of the opposite type...
if(divergentSurrogateChecker(divergentChar1) || divergentSurrogateChecker(divergentChar2)) {
// Our current index would split a surrogate pair; decrement the index to
// preserve the pair.
return index - inc;
}
}
}
return index;
}

View file

@ -1,8 +1,8 @@
import { OutputTargetInterface } from 'keyman/engine/keyboard';
import { OutputTargetBase } from './outputTargetBase.js';
import { TextStore } from './textStore.js';
import { TextStoreLanguageProcessorInterface } from './textStoreLanguageProcessorInterface.js';
import { KMWString } from 'keyman/common/web-utils';
export class Mock extends OutputTargetBase {
export class SyntheticTextStore extends TextStore {
text: string;
selStart: number;
@ -26,46 +26,40 @@ export class Mock extends OutputTargetBase {
this.selForward = this.selEnd >= this.selStart;
}
static assertIsOutputTargetBase(outputTarget: OutputTargetInterface): asserts outputTarget is OutputTargetBase {
if (!(outputTarget instanceof OutputTargetBase)) {
throw new TypeError("outputTarget is not a OutputTargetBase");
}
}
// Clones the state of an existing EditableElement, creating a SyntheticTextStore version of its state.
static from(textStore: TextStoreLanguageProcessorInterface, readonly?: boolean): SyntheticTextStore {
let clone: SyntheticTextStore;
// Clones the state of an existing EditableElement, creating a Mock version of its state.
static from(outputTarget: OutputTargetInterface, readonly?: boolean): Mock {
let clone: Mock;
this.assertIsTextStore(textStore);
this.assertIsOutputTargetBase(outputTarget);
if (outputTarget instanceof Mock) {
if (textStore instanceof SyntheticTextStore) {
// Avoids the need to run expensive kmwstring.ts `length()`
// calculations when deep-copying Mock instances.
const priorMock = outputTarget as Mock;
clone = new Mock(priorMock.text, priorMock.selStart, priorMock.selEnd);
// calculations when deep-copying SyntheticTextStore instances.
const priorMock = textStore as SyntheticTextStore;
clone = new SyntheticTextStore(priorMock.text, priorMock.selStart, priorMock.selEnd);
} else {
const text = outputTarget.getText();
const text = textStore.getText();
const textLen = KMWString.length(text);
// If !hasSelection()
let selectionStart: number = textLen;
let selectionEnd: number = 0;
if (outputTarget.hasSelection()) {
const beforeText = outputTarget.getTextBeforeCaret();
const afterText = outputTarget.getTextAfterCaret();
if (textStore.hasSelection()) {
const beforeText = textStore.getTextBeforeCaret();
const afterText = textStore.getTextAfterCaret();
selectionStart = KMWString.length(beforeText);
selectionEnd = textLen - KMWString.length(afterText);
}
// readonly group or not, the returned Mock remains the same.
// readonly group or not, the returned SyntheticTextStore remains the same.
// New-context events should act as if the caret were at the earlier-in-context
// side of the selection, same as standard keyboard rules.
clone = new Mock(text, selectionStart, selectionEnd);
clone = new SyntheticTextStore(text, selectionStart, selectionEnd);
}
// Also duplicate deadkey state! (Needed for fat-finger ops.)
clone.setDeadkeys((outputTarget as OutputTargetBase).deadkeys());
clone.setDeadkeys(textStore.deadkeys());
return clone;
}
@ -148,11 +142,11 @@ export class Mock extends OutputTargetBase {
}
/**
* Indicates if this Mock represents an identical context to that of another Mock.
* Indicates if this SyntheticTextStore represents an identical context to that of another SyntheticTextStore.
* @param other
* @returns
*/
isEqual(other: Mock) {
isEqual(other: SyntheticTextStore) {
return this.text == other.text
&& this.selStart == other.selStart
&& this.selEnd == other.selEnd
@ -160,6 +154,6 @@ export class Mock extends OutputTargetBase {
}
doInputEvent() {
// Mock isn't backed by an element, so it won't have any event listeners.
// SyntheticTextStore isn't backed by an element, so it won't have any event listeners.
}
}

View file

@ -1,23 +1,16 @@
import { KMWString } from "keyman/common/web-utils";
import { Alternate, OutputTargetInterface, TextTransform, Transcription } from 'keyman/engine/keyboard';
import { Alternate, TextTransform } from "./keyboards/textTransform.js";
import { Transcription } from "./keyboards/transcription.js";
import { findCommonSubstringEndIndex } from "./stringDivergence.js";
import { Mock } from "./mock.js";
import { SyntheticTextStore } from "./syntheticTextStore.js";
// Defines deadkey management in a manner attachable to each element interface.
import { type KeyEvent } from 'keyman/engine/keyboard';
import { type KeyEvent } from './keyEvent.js';
import { TextStoreLanguageProcessorInterface } from './textStoreLanguageProcessorInterface.js';
import { Deadkey, DeadkeyTracker } from "./deadkeys.js";
import { LexicalModelTypes } from '@keymanapp/common-types';
// Also relies on string-extensions provided by the web-utils package.
export function isEmptyTransform(transform: LexicalModelTypes.Transform) {
if(!transform) {
return true;
}
return transform.insert === '' && transform.deleteLeft === 0 && (transform.deleteRight ?? 0) === 0;
}
export abstract class OutputTargetBase implements OutputTargetInterface {
export abstract class TextStore {
private _dks: DeadkeyTracker;
constructor() {
@ -25,7 +18,7 @@ export abstract class OutputTargetBase implements OutputTargetInterface {
}
/**
* Signifies that this OutputTarget has no default key processing behaviors. This should be false
* Signifies that this TextStore has no default key processing behaviors. This should be false
* for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement.
*/
get isSynthetic(): boolean {
@ -60,22 +53,31 @@ export abstract class OutputTargetBase implements OutputTargetInterface {
}
/**
* Needed to properly clone deadkeys for use with Mock element interfaces toward predictive text purposes.
* Needed to properly clone deadkeys for use with SyntheticTextStore element interfaces toward predictive text purposes.
* @param {object} dks An existing set of deadkeys to deep-copy for use by this element interface.
*/
protected setDeadkeys(dks: DeadkeyTracker) {
this._dks = dks.clone();
}
static assertIsTextStore(textStore: TextStoreLanguageProcessorInterface): asserts textStore is TextStore {
if (!(textStore instanceof TextStore)) {
throw new TypeError("textStore is not a TextStore");
}
}
/**
* Determines the basic operations needed to reconstruct the current OutputTarget's text from the prior state specified
* by another OutputTarget based on their text and caret positions.
* Determines the basic operations needed to reconstruct the current TextStore's text from the prior state specified
* by another TextStore based on their text and caret positions.
*
* This is designed for use as a "before and after" comparison to determine the effect of a single keyboard rule at a time.
* As such, it assumes that the caret is immediately after any inserted text.
* @param from An output target (preferably a Mock) representing the prior state of the input/output system.
* @param from An output target (preferably a SyntheticTextStore) representing the prior state of the input/output system.
*/
buildTransformFrom(original: OutputTargetInterface): TextTransform {
buildTransformFrom(original: TextStoreLanguageProcessorInterface): TextTransform {
TextStore.assertIsTextStore(original);
const toLeft = this.getTextBeforeCaret();
const fromLeft = original.getTextBeforeCaret();
@ -96,19 +98,19 @@ export abstract class OutputTargetBase implements OutputTargetInterface {
return new TextTransform(insertedText, deletedLeft, deletedRight, original.getSelectedText() && !this.getSelectedText());
}
buildTranscriptionFrom(original: OutputTargetInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
buildTranscriptionFrom(original: TextStore, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
const transform = this.buildTransformFrom(original);
// If we ever decide to re-add deadkey tracking, this is the place for it.
return new Transcription(keyEvent, transform, Mock.from(original, readonly), alternates);
return new Transcription(keyEvent, transform, SyntheticTextStore.from(original, readonly), alternates);
}
/**
* Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`.
* @param original An `OutputTarget` (usually a `Mock`).
* Restores the `TextStore` to the indicated state. Designed for use with `Transcription.preInput`.
* @param original An `TextStore` (usually a `SyntheticTextStore`).
*/
restoreTo(original: OutputTargetBase) {
restoreTo(original: TextStore) {
this.clearSelection();
// We currently do not restore selected text; the mechanism isn't supported at present for
// all output target types - especially in regard to re-selecting the text if restored.
@ -148,7 +150,7 @@ export abstract class OutputTargetBase implements OutputTargetInterface {
/**
* Helper to `restoreTo` - allows directly setting the 'before' context to that of another
* `OutputTarget`.
* `TextStore`.
* @param s
*/
protected setTextBeforeCaret(s: string): void {
@ -159,7 +161,7 @@ export abstract class OutputTargetBase implements OutputTargetInterface {
/**
* Helper to `restoreTo` - allows directly setting the 'after' context to that of another
* `OutputTarget`.
* `TextStore`.
* @param s
*/
protected abstract setTextAfterCaret(s: string): void;

View file

@ -0,0 +1,17 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { LexicalModelTypes } from '@keymanapp/common-types';
import { KeyEvent } from './keyEvent.js';
import { Transcription } from './keyboards/transcription.js';
import { Alternate } from './keyboards/textTransform.js';
/**
* Interface with the methods LanguageProcessor needs from TextStore
* for transcription building and applying transforms.
*/
export interface TextStoreLanguageProcessorInterface {
buildTranscriptionFrom(original: TextStoreLanguageProcessorInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription;
apply(transform: LexicalModelTypes.Transform): void;
}

View file

@ -1,5 +1,5 @@
import { EventEmitter } from 'eventemitter3';
import { ManagedPromise, type Keyboard, type OutputTargetInterface } from 'keyman/engine/keyboard';
import { ManagedPromise, type Keyboard, type TextStore } from 'keyman/engine/keyboard';
import { type JSKeyboardInterface } from 'keyman/engine/js-processor';
import { StubAndKeyboardCache, type KeyboardStub } from 'keyman/engine/keyboard-storage';
import { PredictionContext } from 'keyman/engine/interfaces';
@ -7,7 +7,7 @@ import { EngineConfiguration } from './engineConfiguration.js';
interface EventMap {
// target, then keyboard.
'targetchange': (target: OutputTargetInterface) => boolean;
'targetchange': (target: TextStore) => boolean;
/**
* This event is raised whenever a keyboard change is requested.
@ -44,11 +44,11 @@ export interface ContextManagerConfiguration {
/**
* A function that resets any state-dependent keyboard key-state information such as
* emulated modifier state and layer id. Also purges the context cache.
* If an `outputTarget` is specified, it will also trigger new-context rule processing.
* If an `textStore` is specified, it will also trigger new-context rule processing.
*
* Does not reset option-stores, variable-stores, etc.
*/
readonly resetContext: (outputTarget?: OutputTargetInterface) => void;
readonly resetContext: (textStore?: TextStore) => void;
/**
* A predictive-state management object that interfaces the predictive-text banner
@ -64,7 +64,7 @@ export interface ContextManagerConfiguration {
}
interface PendingActivation {
target: OutputTargetInterface,
target: TextStore,
keyboard: Promise<Keyboard>,
stub: KeyboardStub;
}
@ -74,11 +74,11 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
abstract initialize(): void;
abstract get activeTarget(): OutputTargetInterface;
abstract get activeTarget(): TextStore;
private _predictionContext: PredictionContext;
protected keyboardCache: StubAndKeyboardCache;
private _resetContext: (outputTarget?: OutputTargetInterface) => void;
private _resetContext: (textStore?: TextStore) => void;
private pendingActivations: PendingActivation[] = [];
protected engineConfig: MainConfig;
@ -101,18 +101,18 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
insertText(kbdInterface: JSKeyboardInterface, Ptext: string, PdeadKey: number) {
// Find the correct output target to manipulate.
const outputTarget = this.activeTarget;
const textStore = this.activeTarget;
if(outputTarget != null) {
if(textStore != null) {
if(Ptext != null) {
kbdInterface.output(0, outputTarget, Ptext);
kbdInterface.output(0, textStore, Ptext);
}
if((typeof(PdeadKey)!=='undefined') && (PdeadKey !== null)) {
kbdInterface.deadkeyOutput(0, outputTarget, PdeadKey);
kbdInterface.deadkeyOutput(0, textStore, PdeadKey);
}
outputTarget.invalidateSelection();
textStore.invalidateSelection();
return true;
}
@ -135,7 +135,7 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
* attached elements within the app/browser target. For `app/webview`, this should
* always return a consistent value - likely, `null`.
*/
protected abstract currentKeyboardSrcTarget(): OutputTargetInterface;
protected abstract currentKeyboardSrcTarget(): TextStore;
/**
* Ensures that newly activated keyboards are set correctly within managed context, possibly
@ -143,16 +143,16 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
* @param kbd
* @param target
*/
protected abstract activateKeyboardForTarget(kbd: { keyboard: Keyboard, metadata: KeyboardStub }, target: OutputTargetInterface): void;
protected abstract activateKeyboardForTarget(kbd: { keyboard: Keyboard, metadata: KeyboardStub }, target: TextStore): void;
/**
* Checks the pending keyboard-activation array for an entry corresponding to the specified
* OutputTarget. If found, also removes the entry for bookkeeping purposes.
* @param target The specific OutputTarget affected by the pending Keyboard activation.
* TextStore. If found, also removes the entry for bookkeeping purposes.
* @param target The specific TextStore affected by the pending Keyboard activation.
* May be `null`, which corresponds to the global default Keyboard.
* @returns `true` if pending activation is still valid, `false` otherwise.
*/
private findAndPopActivation(target: OutputTargetInterface): PendingActivation {
private findAndPopActivation(target: TextStore): PendingActivation {
// Array.findIndex requires Chrome 45+. :(
let activationIndex;
for(activationIndex = 0; activationIndex < this.pendingActivations.length; activationIndex++) {
@ -180,7 +180,7 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
protected async deferredKeyboardActivation(
kbdPromise: Promise<Keyboard>,
metadata: KeyboardStub,
target: OutputTargetInterface
target: TextStore
): Promise<PendingActivation> {
const activation: PendingActivation = {
target: target,

View file

@ -1,7 +1,7 @@
import { EventEmitter } from "eventemitter3";
import {
DeviceSpec, KeyboardProperties, ManagedPromise, OutputTargetInterface,
DeviceSpec, KeyboardProperties, ManagedPromise, TextStore,
ProcessorAction, physicalKeyDeviceAlias, SpacebarText
} from "keyman/engine/keyboard";
import { PathConfiguration, PathOptionDefaults, PathOptionSpec } from "keyman/engine/interfaces";
@ -110,9 +110,9 @@ export class EngineConfiguration extends EventEmitter<EventMap> {
* after postKeystroke takes effect. Any behaviors defined here should be considered 'readonly' in
* terms of context and should instead facilitate integration with the engine's host platform.
* @param ruleBehavior The full effects of keystroke + postkeystroke rules from a processed keystroke.
* @param outputTarget The engine's current source for context
* @param textStore The engine's current source for context
*/
onRuleFinalization(ruleBehavior: ProcessorAction, outputTarget: OutputTargetInterface) {};
onRuleFinalization(ruleBehavior: ProcessorAction, textStore: TextStore) {};
}
export interface InitOptionSpec extends PathOptionSpec {

View file

@ -1,5 +1,5 @@
import { LexicalModelTypes } from '@keymanapp/common-types';
import { Mock } from "keyman/engine/js-processor";
import { SyntheticTextStore } from "keyman/engine/keyboard";
import { KMWString } from 'keyman/common/web-utils';
export default class ContextWindow implements LexicalModelTypes.Context {
@ -18,7 +18,7 @@ export default class ContextWindow implements LexicalModelTypes.Context {
casingForm?: LexicalModelTypes.CasingForm;
constructor(mock: Mock, config: LexicalModelTypes.Configuration, layerId: string) {
constructor(mock: SyntheticTextStore, config: LexicalModelTypes.Configuration, layerId: string) {
this.left = mock.getTextBeforeCaret();
this.startOfBuffer = KMWString.length(this.left) <= config.leftContextCodePoints;
if(!this.startOfBuffer) {
@ -38,9 +38,9 @@ export default class ContextWindow implements LexicalModelTypes.Context {
null;
}
public toMock(): Mock {
public toMock(): SyntheticTextStore {
const caretPos = KMWString.length(this.left);
return new Mock(this.left + (this.right || ""), caretPos);
return new SyntheticTextStore(this.left + (this.right || ""), caretPos);
}
}

View file

@ -3,28 +3,25 @@
import ContextWindow from "./contextWindow.js";
import { LanguageProcessor } from "./languageProcessor.js";
import type { ModelSpec, PathConfiguration } from "keyman/engine/interfaces";
import { globalObject, DeviceSpec } from "keyman/common/web-utils";
import { globalObject, DeviceSpec, isEmptyTransform } from "keyman/common/web-utils";
import { KM_Core } from 'keyman/engine/core-processor';
import {
type Alternate,
Codes,
JSKeyboard,
KeyboardMinimalInterface,
SyntheticTextStore,
TextStore,
ProcessorAction,
SystemStoreIDs,
type Alternate,
type Keyboard,
type KeyEvent,
type OutputTargetInterface,
ProcessorAction,
SystemStoreIDs
} from "keyman/engine/keyboard";
// TODO-web-core: remove usage of OutputTargetBase
import {
isEmptyTransform,
JSKeyboardProcessor,
Mock,
type ProcessorInitOptions,
OutputTargetBase
} from 'keyman/engine/js-processor';
import { TranscriptionCache } from "./transcriptionCache.js";
@ -100,11 +97,11 @@ export class InputProcessor {
* Handles default output and keyboard processing for both OSK and physical keystrokes.
*
* @param {Object} keyEvent The abstracted KeyEvent to use for keystroke processing
* @param {Object} outputTarget The OutputTarget receiving the KeyEvent
* @param {Object} textStore The TextStore receiving the KeyEvent
* @returns {Object} A ProcessorAction object describing the cumulative effects of
* all matched keyboard rules.
*/
processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTargetInterface): ProcessorAction {
processKeyEvent(keyEvent: KeyEvent, textStore: TextStore): ProcessorAction {
const kbdMismatch = keyEvent.srcKeyboard && this.activeKeyboard != keyEvent.srcKeyboard;
const trueActiveKeyboard = this.activeKeyboard;
@ -123,10 +120,10 @@ export class InputProcessor {
// to revert it. If not, we assume it's a layer-change multitap, in which case
// no such reset is needed.
// TODO-web-core
if(!isEmptyTransform(transcription.transform) || !(transcription.preInput as Mock).isEqual(Mock.from(outputTarget))) {
if(!isEmptyTransform(transcription.transform) || !(transcription.preInput as SyntheticTextStore).isEqual(SyntheticTextStore.from(textStore))) {
// Restores full context, including deadkeys in their exact pre-keystroke state.
// TODO-web-core
(outputTarget as OutputTargetBase).restoreTo(transcription.preInput as Mock);
textStore.restoreTo(transcription.preInput as SyntheticTextStore);
}
/*
else:
@ -142,7 +139,7 @@ export class InputProcessor {
}
}
return this._processKeyEvent(keyEvent, outputTarget);
return this._processKeyEvent(keyEvent, textStore);
} finally {
if(kbdMismatch) {
// Restore our "current" activeKeyboard to its setting before the mismatching KeyEvent.
@ -155,10 +152,10 @@ export class InputProcessor {
* Acts as the core of `processKeyEvent` once we're comfortable asserting that the incoming
* keystroke matches the current `activeKeyboard`.
* @param keyEvent
* @param outputTarget
* @param textStore
* @returns
*/
private _processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTargetInterface): ProcessorAction {
private _processKeyEvent(keyEvent: KeyEvent, textStore: TextStore): ProcessorAction {
const formFactor = keyEvent.device.formFactor;
const fromOSK = keyEvent.isSynthetic;
@ -174,8 +171,7 @@ export class InputProcessor {
// Will handle keystroke-based non-layer change modifier & state keys, mapping them through the physical keyboard's version
// of state management. `doModifierPress` must always run.
// TODO-web-core
if (this.keyboardProcessor.doModifierPress(keyEvent, outputTarget as OutputTargetBase, !fromOSK)) {
if (this.keyboardProcessor.doModifierPress(keyEvent, textStore, !fromOSK)) {
// If run on a desktop platform, we know that modifier & state key presses may not
// produce output, so we may make an immediate return safely.
if(!fromOSK) {
@ -200,17 +196,15 @@ export class InputProcessor {
// // ...end I3363 (Build 301)
// Create a "mock" backup of the current outputTarget in its pre-input state.
// Create a "mock" backup of the current textStore in its pre-input state.
// Current, long-existing assumption - it's DOM-backed.
// TODO-web-core
const preInputMock = Mock.from(outputTarget as OutputTargetBase, true);
const preInputMock = SyntheticTextStore.from(textStore, true);
const startingLayerId = this.keyboardProcessor.layerId;
// We presently need the true keystroke to run on the FULL context. That index is still
// needed for some indexing operations when comparing two different output targets.
// TODO-web-core
let ruleBehavior = this.keyboardProcessor.processKeystroke(keyEvent, outputTarget as OutputTargetBase);
let ruleBehavior = this.keyboardProcessor.processKeystroke(keyEvent, textStore);
// Swap layer as appropriate.
if(keyEvent.kNextLayer) {
@ -246,7 +240,7 @@ export class InputProcessor {
// Now that we've done all the keystroke processing needed, ensure any extra effects triggered
// by the actual keystroke occur.
this.keyboardProcessor.finalizeProcessorAction(ruleBehavior, outputTarget);
this.keyboardProcessor.finalizeProcessorAction(ruleBehavior, textStore);
// -- All keystroke (and 'alternate') processing is now complete. Time to finalize everything! --
@ -257,8 +251,7 @@ export class InputProcessor {
} else {
// We need a dummy ProcessorAction for keys which have no output (e.g. Shift)
ruleBehavior = new ProcessorAction();
// TODO-web-core
ruleBehavior.transcription = (outputTarget as OutputTargetBase).buildTranscriptionFrom(outputTarget as OutputTargetBase, null, false);
ruleBehavior.transcription = textStore.buildTranscriptionFrom(textStore, null, false);
ruleBehavior.triggersDefaultCommand = true;
}
@ -277,10 +270,9 @@ export class InputProcessor {
this.keyboardProcessor.newLayerStore.set(hasLayerChanged ? this.keyboardProcessor.layerId : '');
this.keyboardProcessor.oldLayerStore.set(hasLayerChanged ? startingLayerId : '');
// TODO-web-core
const postRuleBehavior = this.keyboardProcessor.processPostKeystroke(this.contextDevice, outputTarget as OutputTargetBase);
const postRuleBehavior = this.keyboardProcessor.processPostKeystroke(this.contextDevice, textStore);
if (postRuleBehavior) {
this.keyboardProcessor.finalizeProcessorAction(postRuleBehavior, outputTarget);
this.keyboardProcessor.finalizeProcessorAction(postRuleBehavior, textStore);
}
// Yes, even for ruleBehavior.triggersDefaultCommand. Those tend to change the context.
@ -289,13 +281,13 @@ export class InputProcessor {
// Text did not change (thus, no text "input") if we tabbed or merely moved the caret.
if(!ruleBehavior.triggersDefaultCommand) {
// For DOM-aware targets, this will trigger a DOM event page designers may listen for.
outputTarget.doInputEvent();
textStore.doInputEvent();
}
return keepRuleBehavior ? ruleBehavior : null;
}
private buildAlternates(ruleBehavior: ProcessorAction, keyEvent: KeyEvent, preInputMock: Mock): Alternate[] {
private buildAlternates(ruleBehavior: ProcessorAction, keyEvent: KeyEvent, preInputMock: SyntheticTextStore): Alternate[] {
let alternates: Alternate[];
// If we're performing a 'default command', it's not a standard 'typing' event - don't do fat-finger stuff.
@ -363,7 +355,7 @@ export class InputProcessor {
break;
}
const mock = Mock.from(windowedMock, false);
const mock = SyntheticTextStore.from(windowedMock, false);
const altKey = pair.keySpec;
if(!altKey) {
@ -401,11 +393,10 @@ export class InputProcessor {
return alternates;
}
public resetContext(outputTarget?: OutputTargetInterface) {
public resetContext(textStore?: TextStore) {
// Also handles new-context events, which may modify the layer
// TODO-web-core
this.keyboardProcessor.resetContext(outputTarget as OutputTargetBase);
this.keyboardProcessor.resetContext(textStore);
// With the layer now set, we trigger new predictions.
this.languageProcessor.invalidateContext(outputTarget, this.keyboardProcessor.layerId);
this.languageProcessor.invalidateContext(textStore, this.keyboardProcessor.layerId);
}
}

View file

@ -1,8 +1,6 @@
import { EventEmitter } from "eventemitter3";
import { LMLayer, WorkerFactory } from "@keymanapp/lexical-model-layer/web";
// TODO-web-core: remove use of OutputTargetBase
import { Mock, OutputTargetBase } from "keyman/engine/js-processor";
import { Transcription, OutputTargetInterface } from 'keyman/engine/keyboard';
import { Transcription, TextStoreLanguageProcessorInterface, SyntheticTextStore } from 'keyman/engine/keyboard';
import { LanguageProcessorEventMap, ModelSpec, StateChangeEnum, ReadySuggestions } from 'keyman/engine/interfaces';
import ContextWindow from "./contextWindow.js";
import { TranscriptionCache } from "./transcriptionCache.js";
@ -127,7 +125,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
});
}
public invalidateContext(outputTarget: OutputTargetInterface, layerId: string): Promise<Suggestion[]> {
public invalidateContext(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<Suggestion[]> {
// If there's no active model, there can be no predictions.
// We'll also be missing important data needed to even properly REQUEST the predictions.
if(!this.currentModel || !this.configuration) {
@ -143,9 +141,8 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
// Signal to any predictive text UI that the context has changed, invalidating recent predictions.
this.emit('invalidatesuggestions', 'context');
if(outputTarget) {
// TODO-web-core
const transcription = (outputTarget as OutputTargetBase).buildTranscriptionFrom((outputTarget as OutputTargetBase), null, false);
if(textStore) {
const transcription = textStore.buildTranscriptionFrom(textStore, null, false);
return this.predict_internal(transcription, true, layerId);
} else {
// if there's no active context source, there's nothing to
@ -156,13 +153,12 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
}
}
public wordbreak(target: OutputTargetInterface, layerId: string): Promise<string> {
public wordbreak(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<string> {
if(!this.isActive) {
return null;
}
// TODO-web-core
const context = new ContextWindow(Mock.from((target as OutputTargetBase), false), this.configuration, layerId);
const context = new ContextWindow(SyntheticTextStore.from(textStore, false), this.configuration, layerId);
return this.lmEngine.wordbreak(context);
}
@ -187,14 +183,14 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
/**
*
* @param suggestion
* @param outputTarget
* @param textStore
* @param getLayerId a function that returns the current layerId,
* required because layerid can be changed by PostKeystroke
* @returns
*/
public applySuggestion(suggestion: Suggestion, outputTarget: OutputTargetInterface, getLayerId: ()=>string): Promise<Reversion> {
if(!outputTarget) {
throw "Accepting suggestions requires a destination OutputTargetInterface instance."
public applySuggestion(suggestion: Suggestion, textStore: TextStoreLanguageProcessorInterface, getLayerId: ()=>string): Promise<Reversion> {
if(!textStore) {
throw new Error("Accepting suggestions requires a destination TextStore instance.");
}
if(!this.isActive) {
@ -218,29 +214,27 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
// Apply the Suggestion!
// Step 1: determine the final output text
const final = Mock.from(original.preInput, false);
const final = SyntheticTextStore.from(original.preInput, false);
final.apply(suggestion.transform);
// Step 2: build a final, master Transform that will produce the desired results from the CURRENT state.
// In embedded mode, both Android and iOS are best served by calculating this transform and applying its
// values as needed for use with their IME interfaces.
// TODO-web-core
const transform = final.buildTransformFrom((outputTarget as OutputTargetBase));
// TODO-web-core
(outputTarget as OutputTargetBase).apply(transform);
const transform = final.buildTransformFrom(textStore);
textStore.apply(transform);
// Tell the banner that a suggestion was applied, so it can call the
// keyboard's PostKeystroke entry point as needed
this.emit('suggestionapplied', outputTarget);
this.emit('suggestionapplied', textStore);
// Build a 'reversion' Transcription that can be used to undo this apply() if needed,
// replacing the suggestion transform with the original input text.
const preApply = Mock.from(original.preInput, false);
const preApply = SyntheticTextStore.from(original.preInput, false);
preApply.apply(original.transform);
// Builds the reversion option according to the loaded lexical model's known
// syntactic properties.
const suggestionContext = new ContextWindow(original.preInput as Mock, this.configuration, getLayerId());
const suggestionContext = new ContextWindow(original.preInput as SyntheticTextStore, this.configuration, getLayerId());
// We must accept the Suggestion from its original context, which was before
// `original.transform` was applied.
@ -261,7 +255,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
// // If using the version from lm-layer:
// let mappedReversion = reversion;
// mappedReversion.transformId = reversionTranscription.token;
this.predictFromTarget(outputTarget, getLayerId());
this.predictFromTarget(textStore, getLayerId());
return mappedReversion;
});
@ -269,9 +263,9 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
}
}
public applyReversion(reversion: Reversion, outputTarget: OutputTargetInterface) {
if(!outputTarget) {
throw "Accepting suggestions requires a destination OutputTargetInterface instance."
public applyReversion(reversion: Reversion, textStore: TextStoreLanguageProcessorInterface) {
if(!textStore) {
throw new Error("Accepting suggestions requires a destination TextStore instance.");
}
if(!this.isActive) {
@ -293,19 +287,17 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
// Apply the Reversion!
// Step 1: determine the final output text
const final = Mock.from(original.preInput, false);
const final = SyntheticTextStore.from(original.preInput, false);
final.apply(reversion.transform); // Should match original.transform, actually. (See applySuggestion)
// Step 2: build a final, master Transform that will produce the desired results from the CURRENT state.
// In embedded mode, both Android and iOS are best served by calculating this transform and applying its
// values as needed for use with their IME interfaces.
// TODO-web-core
const transform = final.buildTransformFrom(outputTarget as OutputTargetBase);
// TODO-web-core
(outputTarget as OutputTargetBase).apply(transform);
const transform = final.buildTransformFrom(textStore);
textStore.apply(transform);
// The reason we need to preserve the additive-inverse 'transformId' property on Reversions.
const promise = this.currentPromise = this.lmEngine.revertSuggestion(reversion, new ContextWindow(original.preInput as Mock, this.configuration, null))
const promise = this.currentPromise = this.lmEngine.revertSuggestion(reversion, new ContextWindow(original.preInput as SyntheticTextStore, this.configuration, null))
// If the "current Promise" is as set above, clear it.
// If another one has been triggered since... don't.
promise.then(() => this.currentPromise = (this.currentPromise == promise) ? null : this.currentPromise);
@ -313,13 +305,12 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
return promise;
}
public predictFromTarget(outputTarget: OutputTargetInterface, layerId: string): Promise<Suggestion[]> {
if(!this.isActive || !outputTarget) {
public predictFromTarget(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<Suggestion[]> {
if(!this.isActive || !textStore) {
return null;
}
// TODO-web-core
const transcription = (outputTarget as OutputTargetBase).buildTranscriptionFrom(outputTarget as OutputTargetBase, null, false);
const transcription = textStore.buildTranscriptionFrom(textStore, null, false);
return this.predict(transcription, layerId);
}
@ -333,7 +324,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
return null;
}
const context = new ContextWindow(transcription.preInput as Mock, this.configuration, layerId);
const context = new ContextWindow(transcription.preInput as SyntheticTextStore, this.configuration, layerId);
this.recordTranscription(transcription);
if(resetContext) {

View file

@ -112,7 +112,7 @@ export class KeyboardInterfaceBase<ContextManagerType extends ContextManagerBase
insertText = (Ptext: string, PdeadKey:number): void => {
this.resetContextCache();
// As this function isn't provided a handle to an active outputTarget, we rely on
// As this function isn't provided a handle to an active textStore, we rely on
// the context manager to resolve said issue.
this.engine.contextManager.insertText(this, Ptext, PdeadKey);
}

View file

@ -1,6 +1,5 @@
import { type KeyEvent, JSKeyboard, Keyboard, KeyboardProperties, KeyboardKeymanGlobal, ProcessorAction } from "keyman/engine/keyboard";
// TODO-web-core: remove usage of OutputTargetBase
import { OutputTargetBase, ProcessorInitOptions } from 'keyman/engine/js-processor';
import { ProcessorInitOptions } from 'keyman/engine/js-processor';
// TODO-web-core: remove alias
import { DOMKeyboardLoader as KeyboardLoader } from "keyman/engine/keyboard";
import { WorkerFactory } from "@keymanapp/lexical-model-layer/web"
@ -55,9 +54,9 @@ export class KeymanEngineBase<
protected keyEventRefocus?: () => void;
private keyEventListener: KeyEventFullHandler = (event, callback) => {
const outputTarget = this.contextManager.activeTarget;
const textStore = this.contextManager.activeTarget;
if(!this.contextManager.activeKeyboard || !outputTarget) {
if(!this.contextManager.activeKeyboard || !textStore) {
if(callback) {
callback(null, null);
}
@ -69,18 +68,17 @@ export class KeymanEngineBase<
}
if(this.keyEventRefocus) {
// Do anything needed to guarantee that the outputTarget stays active (`app/browser`: maintains focus).
// Do anything needed to guarantee that the textStore stays active (`app/browser`: maintains focus).
// (Interaction with the OSK may have de-focused the element providing active context;
// we want to restore it in case the user swaps back to the hardware keyboard afterward.)
this.keyEventRefocus();
}
// Clear any cached codepoint data; we can rebuild it if it's unchanged.
outputTarget.invalidateSelection();
textStore.invalidateSelection();
// Deadkey matching continues to be troublesome.
// Deleting matched deadkeys here seems to correct some of the issues. (JD 6/6/14)
// TODO-web-core
(outputTarget as OutputTargetBase).deadkeys().deleteMatched(); // Delete any matched deadkeys before continuing
textStore.deadkeys().deleteMatched(); // Delete any matched deadkeys before continuing
if(event.isSynthetic) {
const oskLayer = this.osk.vkbd.layerId;
@ -90,7 +88,7 @@ export class KeymanEngineBase<
this.core.keyboardProcessor.layerId = oskLayer;
}
}
const result = this.core.processKeyEvent(event, outputTarget);
const result = this.core.processKeyEvent(event, textStore);
if(result && result.transcription?.transform) {
this.config.onRuleFinalization(result, this.contextManager.activeTarget);
@ -278,8 +276,7 @@ export class KeymanEngineBase<
keyboardProcessor.newLayerStore.set('');
keyboardProcessor.oldLayerStore.set('');
// Call the keyboard's entry point.
// TODO-web-core
const data = keyboardProcessor.processPostKeystroke(keyboardProcessor.contextDevice, predictionContext.currentTarget as OutputTargetBase)
const data = keyboardProcessor.processPostKeystroke(keyboardProcessor.contextDevice, predictionContext.currentTarget)
// If we have a ProcessorAction as a result, run it on the target. This should
// only change system store and variable store values.
if (data) {

View file

@ -15,7 +15,7 @@ import {
timedPromise,
ActiveKeyBase
} from 'keyman/engine/keyboard';
import { isEmptyTransform } from 'keyman/engine/js-processor';
import { isEmptyTransform } from 'keyman/common/web-utils';
import { buildCorrectiveLayout } from './correctionLayout.js';
import { distributionFromDistanceMaps, keyTouchDistances } from './corrections.js';

View file

@ -1,12 +1,12 @@
import {
// Exposed within the engine/attachment bundle b/c of unit tests requiring `instanceof` relations.
ContentEditable,
DesignIFrame,
Input,
TextArea,
ContentEditableElementTextStore,
DesignIFrameElementTextStore,
InputElementTextStore,
TextAreaElementTextStore,
eventOutputTarget,
outputTargetForElement,
textStoreForEvent,
textStoreForElement,
PageContextAttachment,
PageAttachmentOptions,
} from 'keyman/engine/attachment';
@ -42,7 +42,7 @@ function promiseForIframeLoad(iframe: HTMLIFrameElement) {
}
}
describe('outputTargetForElement()', function () {
describe('textStoreForElement()', function () {
this.timeout(DEFAULT_BROWSER_TIMEOUT);
before(async function() {
@ -61,44 +61,44 @@ describe('outputTargetForElement()', function () {
this.attacher = null;
});
describe('standard `OutputTarget` roots', () => {
describe('standard `TextStore` roots', () => {
// So, for these unit tests, attachment has already been established. We just need to
// ensure it meets our expectations.
it('<input> => Input', () => {
it('<input> => InputElementTextStore', () => {
const inputElement = document.getElementById('input');
const inputTarget = outputTargetForElement(inputElement);
const inputTarget = textStoreForElement(inputElement);
assert.isTrue(inputTarget instanceof Input);
assert.isTrue(inputTarget instanceof InputElementTextStore);
});
it('<iframe>.<input> => Input', async () => {
it('<iframe>.<input> => InputElementTextStore', async () => {
const iframe = document.getElementById('iframe') as HTMLIFrameElement;
const iframeInput = iframe.contentDocument.getElementById('iframe-input');
const inputTarget = outputTargetForElement(iframeInput);
const inputTarget = textStoreForElement(iframeInput);
assert.isTrue(inputTarget instanceof Input);
assert.isTrue(inputTarget instanceof InputElementTextStore);
});
it('<textarea> => TextArea', () => {
it('<textarea> => TextAreaElementTextStore', () => {
const textElement = document.getElementById('textarea');
const textTarget = outputTargetForElement(textElement);
const textTarget = textStoreForElement(textElement);
assert.isTrue(textTarget instanceof TextArea);
assert.isTrue(textTarget instanceof TextAreaElementTextStore);
});
it('<iframe>.#doc.designMode = "on" => DesignIFrame', () => {
it('<iframe>.#doc.designMode = "on" => DesignIFrameElementTextStore', () => {
const designElement = document.getElementById('design-iframe');
const designTarget = outputTargetForElement(designElement);
const designTarget = textStoreForElement(designElement);
assert.isTrue(designTarget instanceof DesignIFrame);
assert.isTrue(designTarget instanceof DesignIFrameElementTextStore);
});
it('<div contenteditable="true"/> => ContentEditable', () => {
it('<div contenteditable="true"/> => ContentEditableElementTextStore', () => {
const divElement = document.getElementById('editable');
const divTarget = outputTargetForElement(divElement);
const divTarget = textStoreForElement(divElement);
assert.isTrue(divTarget instanceof ContentEditable);
assert.isTrue(divTarget instanceof ContentEditableElementTextStore);
});
});
@ -106,41 +106,41 @@ describe('outputTargetForElement()', function () {
// So, for these unit tests, attachment has already been established. We just need to
// ensure it meets our expectations.
it('DesignIFrame: from .contentDocument.body', () => {
it('DesignIFrameElementTextStore: from .contentDocument.body', () => {
const designElement = document.getElementById('design-iframe') as HTMLIFrameElement;
const designTarget = outputTargetForElement(designElement.contentDocument.body);
const designTarget = textStoreForElement(designElement.contentDocument.body);
assert.isTrue(designTarget instanceof DesignIFrame);
assert.isTrue(designTarget instanceof DesignIFrameElementTextStore);
assert.strictEqual(designTarget.getElement(), designElement);
assert.strictEqual(designTarget, outputTargetForElement(designElement));
assert.strictEqual(designTarget, textStoreForElement(designElement));
});
it('DesignIFrame: from .contentDocument', () => {
it('DesignIFrameElementTextStore: from .contentDocument', () => {
const designElement = document.getElementById('design-iframe') as HTMLIFrameElement;
const designTarget = outputTargetForElement(designElement.contentDocument as any as HTMLElement);
const designTarget = textStoreForElement(designElement.contentDocument as any as HTMLElement);
assert.isTrue(designTarget instanceof DesignIFrame);
assert.isTrue(designTarget instanceof DesignIFrameElementTextStore);
assert.strictEqual(designTarget.getElement(), designElement);
assert.strictEqual(designTarget, outputTargetForElement(designElement));
assert.strictEqual(designTarget, textStoreForElement(designElement));
});
it('ContentEditable: from direct #text child', () => {
it('ContentEditableElementTextStore: from direct #text child', () => {
const divElement = document.getElementById('editable');
const textNode = divElement.firstChild as HTMLTextAreaElement;
// Text node! Corresponds to a `// defeat Safari bug` comment in the codebase.
assert.equal(textNode.nodeType, 3);
const divTarget = outputTargetForElement(textNode);
const divTarget = textStoreForElement(textNode);
assert.isTrue(divTarget instanceof ContentEditable);
assert.isTrue(divTarget instanceof ContentEditableElementTextStore);
assert.strictEqual(divTarget.getElement(), divElement);
assert.strictEqual(divTarget, outputTargetForElement(divElement));
assert.strictEqual(divTarget, textStoreForElement(divElement));
});
});
});
describe('eventOutputTarget()', function () {
describe('textStoreForEvent()', function () {
this.timeout(DEFAULT_BROWSER_TIMEOUT);
before(async function() {
@ -158,7 +158,7 @@ describe('eventOutputTarget()', function () {
this.attacher = null;
});
it('KeyEvent on <input> => Input', () => {
it('KeyEvent on <input> => InputElementTextStore', () => {
const inputElement = document.getElementById('input');
const fake = sinon.fake();
@ -178,12 +178,12 @@ describe('eventOutputTarget()', function () {
inputElement.removeEventListener('keydown', fake);
}
const inputTarget = outputTargetForElement(inputElement);
const inputTarget = textStoreForElement(inputElement);
assert.isTrue(inputTarget instanceof Input);
assert.isTrue(inputTarget instanceof InputElementTextStore);
});
it('FocusEvent on <textarea> => TextArea', () => {
it('FocusEvent on <textarea> => TextAreaElementTextStore', () => {
const textElement = document.getElementById('textarea');
const fake = sinon.fake();
@ -200,12 +200,12 @@ describe('eventOutputTarget()', function () {
textElement.blur();
}
const textTarget = eventOutputTarget(fake.firstCall.args[0]);
const textTarget = textStoreForEvent(fake.firstCall.args[0]);
assert.isTrue(textTarget instanceof TextArea);
assert.isTrue(textTarget instanceof TextAreaElementTextStore);
});
it('FocusEvent on design iframe .contentDocument => DesignIFrame', () => {
it('FocusEvent on design iframe .contentDocument => DesignIFrameElementTextStore', () => {
const designElement = document.getElementById('design-iframe') as HTMLIFrameElement;
const fake = sinon.fake();
@ -220,12 +220,12 @@ describe('eventOutputTarget()', function () {
designElement.contentDocument.removeEventListener('focus', fake, true);
}
const designTarget = eventOutputTarget(fake.firstCall.args[0]);
const designTarget = textStoreForEvent(fake.firstCall.args[0]);
assert.isTrue(designTarget instanceof DesignIFrame);
assert.isTrue(designTarget instanceof DesignIFrameElementTextStore);
});
it('KeyEvent on ContentEditable', () => {
it('KeyEvent on ContentEditableElementTextStore', () => {
const divElement = document.getElementById('editable');
const fake = sinon.fake();
@ -245,8 +245,8 @@ describe('eventOutputTarget()', function () {
divElement.removeEventListener('keydown', fake);
}
const divTarget = outputTargetForElement(divElement);
const divTarget = textStoreForElement(divElement);
assert.isTrue(divTarget instanceof ContentEditable);
assert.isTrue(divTarget instanceof ContentEditableElementTextStore);
});
});

View file

@ -4,7 +4,7 @@
import { runTests } from '@web/test-runner-mocha';
runTests(async() => {
await import('./outputTargetForElement.def.mjs');
await import('./textStoreForElement.def.mjs');
});
</script>
</head>

View file

@ -1,6 +1,6 @@
import { ContextManager } from 'keyman/app/browser';
import {
outputTargetForElement
textStoreForElement
} from 'keyman/engine/attachment';
import { LegacyEventEmitter } from 'keyman/engine/events';
import { StubAndKeyboardCache, toPrefixedKeyboardId as prefixed } from 'keyman/engine/keyboard-storage';
@ -239,8 +239,8 @@ describe('app/browser: ContextManager', function () {
// Check our expectations re: the `targetchange` event.
assert.isTrue(targetchange.calledOnce, 'targetchange event not raised');
const outputTarget = targetchange.firstCall.args[0]; // Should be an `Input` instance.
assert.equal(outputTarget.getElement(), input, '.activeTarget does not match the newly-focused element');
const textStore = targetchange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), input, '.activeTarget does not match the newly-focused element');
});
it('change: null -> textarea', () => {
@ -254,8 +254,8 @@ describe('app/browser: ContextManager', function () {
// Check our expectations re: the `targetchange` event.
assert.isTrue(targetchange.calledOnce, 'targetchange event not raised');
const outputTarget = targetchange.firstCall.args[0]; // Should be an `Input` instance.
assert.equal(outputTarget.getElement(), textarea, '.activeTarget does not match the newly-focused element');
const textStore = targetchange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), textarea, '.activeTarget does not match the newly-focused element');
});
it('change: null -> designIframe', () => {
@ -270,7 +270,7 @@ describe('app/browser: ContextManager', function () {
// Either way, note that focus is handled specially for design-iframes, thus
// we need slightly different focus-dispatch here.
//
// Possible future improvement: OutputTarget.focusElement (property)?
// Possible future improvement: TextStore.focusElement (property)?
// Though that may be affected by the Chrome vs Firefox bit noted above.
dispatchFocus('focus', iframe.contentDocument.body);
@ -278,8 +278,8 @@ describe('app/browser: ContextManager', function () {
// Check our expectations re: the `targetchange` event.
assert.isTrue(targetchange.calledOnce, 'targetchange event not raised');
const outputTarget = targetchange.firstCall.args[0]; // Should be an `Input` instance.
assert.equal(outputTarget.getElement(), iframe, '.activeTarget does not match the newly-focused element');
const textStore = targetchange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), iframe, '.activeTarget does not match the newly-focused element');
});
it('change: null -> contentEditable', () => {
@ -293,8 +293,8 @@ describe('app/browser: ContextManager', function () {
// Check our expectations re: the `targetchange` event.
assert.isTrue(targetchange.calledOnce, 'targetchange event not raised');
const outputTarget = targetchange.firstCall.args[0]; // Should be an `Input` instance.
assert.equal(outputTarget.getElement(), editable, '.activeTarget does not match the newly-focused element');
const textStore = targetchange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), editable, '.activeTarget does not match the newly-focused element');
});
it('change: input -> null', () => {
@ -312,8 +312,8 @@ describe('app/browser: ContextManager', function () {
// Check our expectations re: the `targetchange` event.
assert.isTrue(targetchange.calledTwice, 'targetchange event not raised');
const outputTarget = targetchange.secondCall.args[0]; // Should be null, since we lost focus.
assert.equal(outputTarget, null, 'targetchange event did not indicate clearing of .activeTarget');
const textStore = targetchange.secondCall.args[0]; // Should be null, since we lost focus.
assert.equal(textStore, null, 'targetchange event did not indicate clearing of .activeTarget');
});
it('change: input disabled, -> null', async () => {
@ -335,8 +335,8 @@ describe('app/browser: ContextManager', function () {
// Check our expectations re: the `targetchange` event.
assert.isTrue(targetchange.calledTwice, 'targetchange event not raised');
const outputTarget = targetchange.secondCall.args[0]; // Should be null, since we lost focus.
assert.equal(outputTarget, null, 'targetchange event did not indicate clearing of .activeTarget');
const textStore = targetchange.secondCall.args[0]; // Should be null, since we lost focus.
assert.equal(textStore, null, 'targetchange event did not indicate clearing of .activeTarget');
});
it('change: input -> textarea', () => {
@ -358,8 +358,8 @@ describe('app/browser: ContextManager', function () {
// Check our expectations re: the `targetchange` event.
assert.isTrue(targetchange.calledThrice, 'targetchange event not raised');
const outputTarget = targetchange.thirdCall.args[0]; // Should be an `Input` instance.
assert.equal(outputTarget.getElement(), textarea, '.activeTarget does not match the newly-focused element');
const textStore = targetchange.thirdCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), textarea, '.activeTarget does not match the newly-focused element');
});
it('restoration: input (no flags set)', () => {
@ -862,8 +862,8 @@ describe('app/browser: ContextManager', function () {
contextManager.on('keyboardchange', keyboardchange);
const textarea = document.getElementById('textarea');
const target = outputTargetForElement(textarea);
contextManager.setKeyboardForTarget(target, 'lao_2008_basic', 'lo');
const textStore = textStoreForElement(textarea);
contextManager.setKeyboardForTarget(textStore, 'lao_2008_basic', 'lo');
// As we haven't yet focused the affected target, no keyboard-change events should have triggered yet.
assert.equal((contextManager as any).currentKeyboardSrcTarget(), null);
@ -878,7 +878,7 @@ describe('app/browser: ContextManager', function () {
await timedPromise(10);
// No need to 'keyboardchange' when the same keyboard is kept active.
assert.equal((contextManager as any).currentKeyboardSrcTarget(), target);
assert.equal((contextManager as any).currentKeyboardSrcTarget(), textStore);
assert.isTrue(beforekeyboardchange.calledOnce);
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.notCalled);
@ -908,7 +908,7 @@ describe('app/browser: ContextManager', function () {
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const target = outputTargetForElement(textarea);
const target = textStoreForElement(textarea);
contextManager.setKeyboardForTarget(target, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
@ -949,7 +949,7 @@ describe('app/browser: ContextManager', function () {
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const target = outputTargetForElement(textarea);
const target = textStoreForElement(textarea);
contextManager.setKeyboardForTarget(target, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
@ -998,7 +998,7 @@ describe('app/browser: ContextManager', function () {
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const target = outputTargetForElement(textarea);
const target = textStoreForElement(textarea);
contextManager.setKeyboardForTarget(target, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
@ -1035,7 +1035,7 @@ describe('app/browser: ContextManager', function () {
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const target = outputTargetForElement(textarea);
const target = textStoreForElement(textarea);
contextManager.setKeyboardForTarget(target, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
@ -1085,7 +1085,7 @@ describe('app/browser: ContextManager', function () {
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const target = outputTargetForElement(textarea);
const target = textStoreForElement(textarea);
contextManager.setKeyboardForTarget(target, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
@ -1145,7 +1145,7 @@ describe('app/browser: ContextManager', function () {
// BUT the async load component should be resolved.
await assertPromiseResolved(keyboardasyncload.firstCall.args[1], 0);
// Aspect 4: swap BACK to the async-loading keyboard's OutputTarget, which should
// Aspect 4: swap BACK to the async-loading keyboard's TextStore, which should
// now be fully set to the keyboard that had been requested for activation upon it.
dispatchFocus('blur', input);
dispatchFocus('focus', textarea);
@ -1179,7 +1179,7 @@ describe('app/browser: ContextManager', function () {
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const target = outputTargetForElement(textarea);
const target = textStoreForElement(textarea);
// Matches the current global keyboard, but still sets it to independent-mode.
contextManager.setKeyboardForTarget(target, 'khmer_angkor', 'km');
dispatchFocus('focus', textarea);

View file

@ -1,8 +1,7 @@
import { assert } from 'chai';
import { KMWString } from 'keyman/engine/keyboard';
import { Mock } from 'keyman/engine/js-processor';
import * as wrappers from 'keyman/engine/element-wrappers';
import { KMWString, SyntheticTextStore } from 'keyman/engine/keyboard';
import * as wrappers from 'keyman/engine/element-text-stores';
import { DynamicElements } from '../../test_utils.js';
import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs';
@ -34,12 +33,12 @@ interface TestHelper {
setText(pair: ElementPair<any>, text: string): void;
}
//#region Defines helpers related to HTMLInputElement / Input test setup.
//#region Defines helpers related to HTMLInputElement / InputElementTextStore test setup.
class InputTestHelper implements TestHelper {
setupElement(): InputElementPair {
const id = DynamicElements.addInput();
const elem = document.getElementById(id) as HTMLInputElement;
const wrapper = new wrappers.Input(elem);
const wrapper = new wrappers.InputElementTextStore(elem);
return { elem: elem, wrapper: wrapper };
}
@ -80,12 +79,12 @@ class InputTestHelper implements TestHelper {
}
//#endregion
//#region Defines helpers related to HTMLTextAreaElement / TextArea test setup.
//#region Defines helpers related to HTMLTextAreaElement / TextAreaElementTextStore test setup.
class TextAreaTestHelper implements TestHelper {
setupElement(): TextElementPair {
const id = DynamicElements.addText();
const elem = document.getElementById(id) as HTMLTextAreaElement;
const wrapper = new wrappers.TextArea(elem);
const wrapper = new wrappers.TextAreaElementTextStore(elem);
return { elem: elem, wrapper: wrapper };
}
@ -126,7 +125,7 @@ class TextAreaTestHelper implements TestHelper {
}
//#endregion
//#region Defines helpers related to ContentEditable element test setup.
//#region Defines helpers related to ContentEditableElementTextStore element test setup.
// These functions simply make the basic (within a single text node) tests
// compatible with the more advanced element types; more complex tests may
@ -135,7 +134,7 @@ class ContentEditableTestHelper implements TestHelper {
setupElement(): HTMLElementPair {
const id = DynamicElements.addEditable();
const elem = document.getElementById(id);
const wrapper = new wrappers.ContentEditable(elem);
const wrapper = new wrappers.ContentEditableElementTextStore(elem);
return { elem: elem, wrapper: wrapper, node: null };
}
@ -143,7 +142,7 @@ class ContentEditableTestHelper implements TestHelper {
setupDummyElement(): HTMLElementPair {
const id = DynamicElements.addEditable();
const elem = document.getElementById(id);
const wrapper = new wrappers.ContentEditable(elem);
const wrapper = new wrappers.ContentEditableElementTextStore(elem);
return { elem: elem, wrapper: wrapper, node: null };
}
@ -236,8 +235,8 @@ class DesignIFrameTestHelper implements TestHelper {
const elem1 = document.getElementById(id1) as HTMLIFrameElement;
const elem2 = document.getElementById(id2) as HTMLIFrameElement;
obj.mainPair = { elem: elem1, wrapper: new wrappers.DesignIFrame(elem1), document: elem1.contentWindow.document };
obj.dummyPair = { elem: elem2, wrapper: new wrappers.DesignIFrame(elem2), document: elem1.contentWindow.document };
obj.mainPair = { elem: elem1, wrapper: new wrappers.DesignIFrameElementTextStore(elem1), document: elem1.contentWindow.document };
obj.dummyPair = { elem: elem2, wrapper: new wrappers.DesignIFrameElementTextStore(elem2), document: elem1.contentWindow.document };
done();
});
@ -321,10 +320,10 @@ class DesignIFrameTestHelper implements TestHelper {
}
//#endregion
//#region Defines helpers related to Mock test setup.
//#region Defines helpers related to SyntheticTextStore test setup.
class MockTestHelper implements TestHelper {
setupElement(): ElementPair<any> {
return { elem: null, wrapper: new Mock() };
return { elem: null, wrapper: new SyntheticTextStore() };
}
resetWithText(pair: ElementPair<any>, string: string) {
@ -364,15 +363,15 @@ class InterfaceTests {
}
};
public static Input = new InputTestHelper();
public static InputElementTextStore = new InputTestHelper();
public static TextArea = new TextAreaTestHelper();
public static TextAreaElementTextStore = new TextAreaTestHelper();
public static ContentEditable = new ContentEditableTestHelper();
public static ContentEditableElementTextStore = new ContentEditableTestHelper();
public static DesignIFrame = new DesignIFrameTestHelper();
public static DesignIFrameElementTextStore = new DesignIFrameTestHelper();
public static Mock = new MockTestHelper();
public static SyntheticTextStore = new MockTestHelper();
//#region Defines common test patterns across element tests
public static Tests = class {
@ -985,7 +984,7 @@ class InterfaceTests {
}
}
describe('Element Input/Output Interfacing', function () {
describe('Element InputElementTextStore/Output Interfacing', function () {
this.timeout(DEFAULT_BROWSER_TIMEOUT);
before(function () {
@ -1005,21 +1004,21 @@ describe('Element Input/Output Interfacing', function () {
describe('Caret Handling', function () {
describe('setCaret', function () {
it('correctly places the caret (no prior selection)', function () {
InterfaceTests.Tests.setCaretNoSelection(InterfaceTests.Input);
InterfaceTests.Tests.setCaretNoSelection(InterfaceTests.InputElementTextStore);
});
it('correctly places the caret (prior selection)', function () {
InterfaceTests.Tests.setCaretWithSelection(InterfaceTests.Input);
InterfaceTests.Tests.setCaretWithSelection(InterfaceTests.InputElementTextStore);
});
});
describe('getCaret', function () {
it('correctly reports the position of the caret (no selection)', function () {
InterfaceTests.Tests.getCaretNoSelection(InterfaceTests.Input);
InterfaceTests.Tests.getCaretNoSelection(InterfaceTests.InputElementTextStore);
});
it('correctly reports the position of the caret (active selection)', function () {
InterfaceTests.Tests.getCaretWithSelection(InterfaceTests.Input);
InterfaceTests.Tests.getCaretWithSelection(InterfaceTests.InputElementTextStore);
});
});
});
@ -1027,35 +1026,35 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Retrieval', function () {
describe('getText', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.Input);
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.InputElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.Input);
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.InputElementTextStore);
});
});
describe('getTextBeforeCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.Input);
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.InputElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.Input);
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.InputElementTextStore);
});
});
it('getSelectedText', function () {
InterfaceTests.Tests.getSelectedText(InterfaceTests.Input);
InterfaceTests.Tests.getSelectedText(InterfaceTests.InputElementTextStore);
});
describe('getTextAfterCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.Input);
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.InputElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.Input);
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.InputElementTextStore);
});
});
});
@ -1063,32 +1062,32 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Mutation', function () {
describe('clearSelection', function () {
it('properly deletes selected text', function () {
InterfaceTests.Tests.clearSelection(InterfaceTests.Input);
InterfaceTests.Tests.clearSelection(InterfaceTests.InputElementTextStore);
});
});
describe('deleteCharsBeforeCaret', function () {
it("correctly deletes characters from 'context' (no active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.Input);
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.InputElementTextStore);
});
it("correctly deletes characters from 'context' (with active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.Input);
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.InputElementTextStore);
});
});
describe('insertTextBeforeCaret', function () {
it("correctly replaces the element's 'context' (no active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.Input);
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.InputElementTextStore);
});
it("correctly replaces the element's 'context' (with active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.Input);
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.InputElementTextStore);
});
});
it('correctly maintains deadkeys', function () {
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.Input);
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.InputElementTextStore);
});
});
});
@ -1103,21 +1102,21 @@ describe('Element Input/Output Interfacing', function () {
describe('Caret Handling', function () {
describe('setCaret', function () {
it('correctly places the caret (no prior selection)', function () {
InterfaceTests.Tests.setCaretNoSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.setCaretNoSelection(InterfaceTests.TextAreaElementTextStore);
});
it('correctly places the caret (prior selection)', function () {
InterfaceTests.Tests.setCaretWithSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.setCaretWithSelection(InterfaceTests.TextAreaElementTextStore);
});
});
describe('getCaret', function () {
it('correctly reports the position of the caret (no selection)', function () {
InterfaceTests.Tests.getCaretNoSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getCaretNoSelection(InterfaceTests.TextAreaElementTextStore);
});
it('correctly reports the position of the caret (active selection)', function () {
InterfaceTests.Tests.getCaretWithSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getCaretWithSelection(InterfaceTests.TextAreaElementTextStore);
});
});
});
@ -1125,35 +1124,35 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Retrieval', function () {
describe('getText', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.TextAreaElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.TextAreaElementTextStore);
});
});
describe('getTextBeforeCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.TextAreaElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.TextAreaElementTextStore);
});
});
it('getSelectedText', function () {
InterfaceTests.Tests.getSelectedText(InterfaceTests.Input);
InterfaceTests.Tests.getSelectedText(InterfaceTests.InputElementTextStore);
});
describe('getTextAfterCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.TextAreaElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.TextAreaElementTextStore);
});
});
});
@ -1161,32 +1160,32 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Mutation', function () {
describe('clearSelection', function () {
it('properly deletes selected text', function () {
InterfaceTests.Tests.clearSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.clearSelection(InterfaceTests.TextAreaElementTextStore);
});
});
describe('deleteCharsBeforeCaret', function () {
it("correctly deletes characters from 'context' (no active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.TextAreaElementTextStore);
});
it("correctly deletes characters from 'context' (with active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.TextAreaElementTextStore);
});
});
describe('insertTextBeforeCaret', function () {
it("correctly replaces the element's 'context' (no active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.TextAreaElementTextStore);
});
it("correctly replaces the element's 'context' (with active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.TextArea);
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.TextAreaElementTextStore);
});
});
it('correctly maintains deadkeys', function () {
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.TextArea);
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.TextAreaElementTextStore);
});
});
});
@ -1198,11 +1197,11 @@ describe('Element Input/Output Interfacing', function () {
describe('Caret Handling', function () {
describe('hasSelection', function () {
it('correctly recognizes Selection ownership', function () {
InterfaceTests.Tests.getSelectionOwned(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getSelectionOwned(InterfaceTests.ContentEditableElementTextStore);
});
it('correctly rejects lack of Selection ownership', function () {
InterfaceTests.Tests.getSelectionUnowned(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getSelectionUnowned(InterfaceTests.ContentEditableElementTextStore);
});
// Need to design a test (and necessary helpers!) for a 'partial ownership rejection' test.
@ -1212,35 +1211,35 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Retrieval', function () {
describe('getText', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.ContentEditableElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.ContentEditableElementTextStore);
});
});
describe('getTextBeforeCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.ContentEditableElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.ContentEditableElementTextStore);
});
});
it.skip('getSelectedText', function () { // Not yet properly supported.
InterfaceTests.Tests.getSelectedText(InterfaceTests.Input);
InterfaceTests.Tests.getSelectedText(InterfaceTests.InputElementTextStore);
});
describe('getTextAfterCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.ContentEditableElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.ContentEditableElementTextStore);
});
});
});
@ -1248,32 +1247,32 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Mutation', function () {
describe('clearSelection', function () {
it('properly deletes selected text', function () {
InterfaceTests.Tests.clearSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.clearSelection(InterfaceTests.ContentEditableElementTextStore);
});
});
describe('deleteCharsBeforeCaret', function () {
it("correctly deletes characters from 'context' (no active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.ContentEditableElementTextStore);
});
it("correctly deletes characters from 'context' (with active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.ContentEditableElementTextStore);
});
});
describe('insertTextBeforeCaret', function () {
it("correctly replaces the element's 'context' (no active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.ContentEditableElementTextStore);
});
it("correctly replaces the element's 'context' (with active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.ContentEditable);
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.ContentEditableElementTextStore);
});
});
it('correctly maintains deadkeys', function () {
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.ContentEditable);
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.ContentEditableElementTextStore);
});
});
});
@ -1289,7 +1288,7 @@ describe('Element Input/Output Interfacing', function () {
beforeEach(function (done) {
// Per-test creation of reg. pair and dummy elements, since IFrames are async.
// Relies on the main-level's renewal of the overall fixture to be processed first.
InterfaceTests.DesignIFrame.InitAsyncElements(done);
InterfaceTests.DesignIFrameElementTextStore.InitAsyncElements(done);
});
/**
@ -1300,11 +1299,11 @@ describe('Element Input/Output Interfacing', function () {
// describe.skip('Caret Handling', function() {
// describe('hasSelection', function() {
// it('correctly recognizes Selection ownership', function () {
// InterfaceTests.Tests.getSelectionOwned(InterfaceTests.DesignIFrame);
// InterfaceTests.Tests.getSelectionOwned(InterfaceTests.DesignIFrameElementTextStore);
// });
// it('correctly rejects lack of Selection ownership', function () {
// InterfaceTests.Tests.getSelectionUnowned(InterfaceTests.DesignIFrame);
// InterfaceTests.Tests.getSelectionUnowned(InterfaceTests.DesignIFrameElementTextStore);
// });
// // Need to design a test (and necessary helpers!) for a 'partial ownership rejection' test.
@ -1314,35 +1313,35 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Retrieval', function () {
describe('getText', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.getTextNoSelection(InterfaceTests.DesignIFrameElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.getTextWithSelection(InterfaceTests.DesignIFrameElementTextStore);
});
});
describe('getTextBeforeCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.getTextBeforeCaretNoSelection(InterfaceTests.DesignIFrameElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.getTextBeforeCaretWithSelection(InterfaceTests.DesignIFrameElementTextStore);
});
});
it.skip('getSelectedText', function () { // Not yet properly supported.
InterfaceTests.Tests.getSelectedText(InterfaceTests.Input);
InterfaceTests.Tests.getSelectedText(InterfaceTests.InputElementTextStore);
});
describe('getTextAfterCaret', function () {
it('correctly returns text (no active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.getTextAfterCaretNoSelection(InterfaceTests.DesignIFrameElementTextStore);
});
it('correctly returns text (with active selection)', function () {
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.getTextAfterCaretWithSelection(InterfaceTests.DesignIFrameElementTextStore);
});
});
});
@ -1350,38 +1349,38 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Mutation', function () {
describe('clearSelection', function () {
it('properly deletes selected text', function () {
InterfaceTests.Tests.clearSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.clearSelection(InterfaceTests.DesignIFrameElementTextStore);
});
});
describe('deleteCharsBeforeCaret', function () {
it("correctly deletes characters from 'context' (no active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.DesignIFrameElementTextStore);
});
it("correctly deletes characters from 'context' (with active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.DesignIFrameElementTextStore);
});
});
describe('insertTextBeforeCaret', function () {
it("correctly replaces the element's 'context' (no active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.DesignIFrameElementTextStore);
});
it("correctly replaces the element's 'context' (with active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.DesignIFrameElementTextStore);
});
});
it('correctly maintains deadkeys', function () {
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.DesignIFrame);
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.DesignIFrameElementTextStore);
});
});
});
describe('The "Mock" output target', function () {
// Unique to the Mock type - element interface cloning tests. Is element state properly copied?
describe('The "SyntheticTextStore" output target', function () {
// Unique to the SyntheticTextStore type - element interface cloning tests. Is element state properly copied?
// As those require a very different setup, they're in the target_mocks.js test case file instead.
// Basic text-retrieval unit tests are now done headlessly in keyman/engine/keyboard.
@ -1389,26 +1388,26 @@ describe('Element Input/Output Interfacing', function () {
describe('Text Mutation', function () {
describe('deleteCharsBeforeCaret', function () {
it("correctly deletes characters from 'context' (no active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.Mock);
InterfaceTests.Tests.deleteCharsBeforeCaretNoSelection(InterfaceTests.SyntheticTextStore);
});
it("correctly deletes characters from 'context' (with active selection)", function () {
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.Mock);
InterfaceTests.Tests.deleteCharsBeforeCaretWithSelection(InterfaceTests.SyntheticTextStore);
});
});
describe('insertTextBeforeCaret', function () {
it("correctly replaces the element's 'context' (no active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.Mock);
InterfaceTests.Tests.insertTextBeforeCaretNoSelection(InterfaceTests.SyntheticTextStore);
});
it("correctly replaces the element's 'context' (with active selection)", function () {
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.Mock);
InterfaceTests.Tests.insertTextBeforeCaretWithSelection(InterfaceTests.SyntheticTextStore);
});
});
it('correctly maintains deadkeys', function () {
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.Mock);
InterfaceTests.Tests.deadkeyMaintenance(InterfaceTests.SyntheticTextStore);
});
});
});

View file

@ -1,8 +1,7 @@
import { assert } from 'chai';
import { KMWString } from 'keyman/engine/keyboard';
import { Mock } from 'keyman/engine/js-processor';
import { Input } from 'keyman/engine/element-wrappers';
import { KMWString, SyntheticTextStore } from 'keyman/engine/keyboard';
import { InputElementTextStore } from 'keyman/engine/element-text-stores';
import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs';
@ -11,7 +10,7 @@ document.body.appendChild(host);
const u = (code: number) => String.fromCodePoint(code);
// Define common interface testing functions that can be run upon the OutputTarget interface.
// Define common interface testing functions that can be run upon the TextStore interface.
class MockTests {
public static Apple = {
normal: 'apple',
@ -25,11 +24,11 @@ class MockTests {
{ d: 0, p: 3 } // After the normal 'p' character of Apple.mixed.
];
//#region Defines helpers related to HTMLInputElement / Input test setup.
//#region Defines helpers related to HTMLInputElement / InputElementTextStore test setup.
public static initBase() {
const elem = document.createElement('input');
host.appendChild(elem);
const wrapper = new Input(elem);
const wrapper = new InputElementTextStore(elem);
return wrapper;
}
@ -63,7 +62,7 @@ class MockTests {
//#endregion
}
describe('OutputTarget Mocking', function() {
describe('SyntheticTextStore', function() {
this.timeout(DEFAULT_BROWSER_TIMEOUT);
before(function() {
@ -79,27 +78,27 @@ describe('OutputTarget Mocking', function() {
KMWString.enableSupplementaryPlane(false);
})
describe('The "Mock" output target', function() {
describe('The "SyntheticTextStore" output target', function() {
describe('Initialization', function() {
it('properly initializes from a raw string', function() {
const mock = new Mock(MockTests.Apple.mixed);
const mock = new SyntheticTextStore(MockTests.Apple.mixed);
assert.equal(mock.getText(), MockTests.Apple.mixed);
assert.equal(mock.getDeadkeyCaret(), 5);
});
it('copies an existing OutputTarget without a text selection', function() {
it('copies an existing TextStore without a text selection', function() {
const base = MockTests.setupBase(4);
const mock = Mock.from(base);
const mock = SyntheticTextStore.from(base);
assert.equal(mock.getText(), MockTests.Apple.mixed);
assert.deepEqual(mock.deadkeys(), base.deadkeys());
});
it('copies an existing OutputTarget with a text selection', function() {
it('copies an existing TextStore with a text selection', function() {
const base = MockTests.setupBase(4, 5);
const mock = Mock.from(base);
const mock = SyntheticTextStore.from(base);
// The selection should appear to be automatically deleted, as any text mutation
// by KMW would automatically erase the text anyway.
assert.equal(mock.getTextBeforeCaret(), MockTests.Apple.mixed.substr(0, 5));
@ -115,7 +114,7 @@ describe('OutputTarget Mocking', function() {
it('is not affected by mutation of the source element', function() {
// Already-verified code
const base = MockTests.setupBase(4);
const mock = Mock.from(base);
const mock = SyntheticTextStore.from(base);
const baseInitDks = base.deadkeys().clone();
// Now for the actual test.
@ -125,7 +124,7 @@ describe('OutputTarget Mocking', function() {
base.deadkeys().deleteMatched();
base.deleteCharsBeforeCaret(2);
assert.notDeepEqual(base.deadkeys(), baseInitDks, 'OutputTarget deadkey return is not a proper deep-copy');
assert.notDeepEqual(base.deadkeys(), baseInitDks, 'TextStore deadkey return is not a proper deep-copy');
assert.equal(mock.getText(), MockTests.Apple.mixed);
assert.deepEqual(mock.deadkeys(), baseInitDks);
@ -134,7 +133,7 @@ describe('OutputTarget Mocking', function() {
it('does not affect the source element when mutated', function() {
// Already-verified code
const base = MockTests.setupBase(4);
const mock = Mock.from(base);
const mock = SyntheticTextStore.from(base);
const baseInitDks = base.deadkeys().clone();
// Now for the actual test.

View file

@ -1,8 +1,8 @@
import { assert } from 'chai';
import { DOMKeyboardLoader } from 'keyman/engine/keyboard';
import { KeyboardHarness, JSKeyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError, Keyboard } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { KeyboardHarness, JSKeyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError, Keyboard, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface } from 'keyman/engine/js-processor';
import { assertThrowsAsync } from 'keyman/tools/testing/test-utils';
declare let window: typeof globalThis;
@ -82,7 +82,7 @@ describe('Keyboard loading in DOM', function() {
// TODO: verify actual rule processing.
const nullKeyEvent = jsKeyboard.constructNullKeyEvent(device);
const mock = new Mock();
const mock = new SyntheticTextStore();
const result = jsHarness.processKeystroke(mock, nullKeyEvent);
assert.isOk(result);

View file

@ -69,9 +69,9 @@ export default {
files: ['web/build/test/dom/cases/dom-utils/**/*.tests.mjs']
},
{
name: 'engine/element-wrappers',
name: 'engine/element-text-stores',
// Relative, from the containing package.json
files: ['web/build/test/dom/cases/element-wrappers/**/*.tests.mjs']
files: ['web/build/test/dom/cases/element-text-stores/**/*.tests.mjs']
},
{
name: 'engine/gesture-processor',

View file

@ -4,8 +4,7 @@ import sinon from 'sinon';
import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main';
import { PredictionContext } from 'keyman/engine/interfaces';
import { Worker as LMWorker } from "@keymanapp/lexical-model-layer/node";
import { DeviceSpec } from 'keyman/engine/keyboard';
import { Mock } from 'keyman/engine/js-processor';
import { DeviceSpec, SyntheticTextStore } from 'keyman/engine/keyboard';
function compileDummyModel(suggestionSets) {
return `
@ -82,9 +81,9 @@ describe("PredictionContext", () => {
let updateFake = sinon.fake();
predictiveContext.on('update', updateFake);
let mock = new Mock("appl", 4); // "appl|", with '|' as the caret position.
const initialMock = Mock.from(mock);
const promise = predictiveContext.setCurrentTarget(mock);
let textStore = new SyntheticTextStore("appl", 4); // "appl|", with '|' as the caret position.
const initialMock = SyntheticTextStore.from(textStore);
const promise = predictiveContext.setCurrentTarget(textStore);
// Initial predictive state: no suggestions. context.initializeState() has not yet been called.
assert.equal(updateFake.callCount, 1);
@ -100,8 +99,8 @@ describe("PredictionContext", () => {
assert.isNotOk(suggestions.find((obj) => obj.tag == 'keep'));
assert.isNotOk(suggestions.find((obj) => obj.transform.deleteLeft != 0));
mock.insertTextBeforeCaret('e'); // appl| + e = apple
let transcription = mock.buildTranscriptionFrom(initialMock, null, true);
textStore.insertTextBeforeCaret('e'); // appl| + e = apple
let transcription = textStore.buildTranscriptionFrom(initialMock, null, true);
await langProcessor.predict(transcription, dummiedGetLayer());
// First predict call results: our second set of dummy suggestions, the first of which includes
@ -121,9 +120,9 @@ describe("PredictionContext", () => {
let updateFake = sinon.fake();
predictiveContext.on('update', updateFake);
let mock = new Mock("appl", 4); // "appl|", with '|' as the caret position.
const initialMock = Mock.from(mock);
const promise = predictiveContext.setCurrentTarget(mock);
let textStore = new SyntheticTextStore("appl", 4); // "appl|", with '|' as the caret position.
const initialMock = SyntheticTextStore.from(textStore);
const promise = predictiveContext.setCurrentTarget(textStore);
// Initial predictive state: no suggestions. context.initializeState() has not yet been called.
assert.equal(updateFake.callCount, 1);
@ -139,14 +138,14 @@ describe("PredictionContext", () => {
assert.isNotOk(suggestions.find((obj) => obj.tag == 'keep'));
assert.isNotOk(suggestions.find((obj) => obj.transform.deleteLeft != 0));
const baseTranscription = mock.buildTranscriptionFrom(initialMock, null, true);
const baseTranscription = textStore.buildTranscriptionFrom(initialMock, null, true);
// Mocking: corresponds to the second set of mocked predictions - round 2 of
// 'apple', 'apply', 'apples'.
const skippedPromise = langProcessor.predict(baseTranscription, dummiedGetLayer());
mock.insertTextBeforeCaret('e'); // appl| + e = apple
const finalTranscription = mock.buildTranscriptionFrom(initialMock, null, true);
textStore.insertTextBeforeCaret('e'); // appl| + e = apple
const finalTranscription = textStore.buildTranscriptionFrom(initialMock, null, true);
// Mocking: corresponds to the third set of mocked predictions - 'applied'.
const expectedPromise = langProcessor.predict(finalTranscription, dummiedGetLayer());
@ -181,8 +180,8 @@ describe("PredictionContext", () => {
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
let mock = new Mock("appl", 4); // "appl|", with '|' as the caret position.
const initialSuggestions = await predictiveContext.setCurrentTarget(mock);
let textStore = new SyntheticTextStore("appl", 4); // "appl|", with '|' as the caret position.
const initialSuggestions = await predictiveContext.setCurrentTarget(textStore);
let updateFake = sinon.fake();
predictiveContext.on('update', updateFake);
@ -205,7 +204,7 @@ describe("PredictionContext", () => {
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
let textState = new Mock("appl", 4); // "appl|", with '|' as the caret position.
let textState = new SyntheticTextStore("appl", 4); // "appl|", with '|' as the caret position.
await predictiveContext.setCurrentTarget(textState);
@ -214,7 +213,7 @@ describe("PredictionContext", () => {
let suggestions;
let previousTextState = Mock.from(textState);
let previousTextState = SyntheticTextStore.from(textState);
textState.insertTextBeforeCaret('e'); // appl| + e = apple
let transcription = textState.buildTranscriptionFrom(previousTextState, null, true);
await langProcessor.predict(transcription, dummiedGetLayer());
@ -227,7 +226,7 @@ describe("PredictionContext", () => {
assert.equal(suggestions.find((obj) => obj.transform.deleteLeft != 0).displayAs, 'apps');
// Now for the real test.
previousTextState = Mock.from(textState); // snapshot it!
previousTextState = SyntheticTextStore.from(textState); // snapshot it!
const suggestionApply = suggestions.find((obj) => obj.displayAs == 'apply');
assert.isOk(suggestionApply);
@ -271,14 +270,14 @@ describe("PredictionContext", () => {
const predictiveContext = new PredictionContext(langProcessor, dummiedGetLayer);
let textState = new Mock("appl", 4); // "appl|", with '|' as the caret position.
let textState = new SyntheticTextStore("appl", 4); // "appl|", with '|' as the caret position.
// Test setup - return to the state at the end of the prior-defined unit test ('suggestion application...')
await predictiveContext.setCurrentTarget(textState);
// This is the point in time that a reversion operation will rewind the context to.
const revertBaseTextState = Mock.from(textState);
const revertBaseTextState = SyntheticTextStore.from(textState);
textState.insertTextBeforeCaret('e'); // appl| + e = apple
let transcription = textState.buildTranscriptionFrom(revertBaseTextState, null, true);
@ -292,7 +291,7 @@ describe("PredictionContext", () => {
const suggestionApply = originalSuggestionSet.find((obj) => obj.displayAs == 'apply');
assert.isOk(suggestionApply);
let previousTextState = Mock.from(textState);
let previousTextState = SyntheticTextStore.from(textState);
// For awaiting the suggestions generated upon applying our desired suggestion.
// We aren't given a direct Promise for that, but we can construct one this way.
@ -321,7 +320,7 @@ describe("PredictionContext", () => {
assert.equal(reversion.displayAs.length, previousTextState.getText().length + 2); // +2: opening + closing quotes.
// Fire away! Time to apply the reversion.
previousTextState = Mock.from(textState);
previousTextState = SyntheticTextStore.from(textState);
// Since the test uses a separate thread via Worker, make sure to set up any important event handlers
// before we request the reversion.
@ -341,7 +340,7 @@ describe("PredictionContext", () => {
assert.isNull(returnValue); // as per the method's spec.
// Verify that the rewind + application of reversion worked!
let rewoundTextStateWithInput = Mock.from(revertBaseTextState); // appl
let rewoundTextStateWithInput = SyntheticTextStore.from(revertBaseTextState); // appl
rewoundTextStateWithInput.apply(reversion.transform); // + e
assert.equal(rewoundTextStateWithInput.getText(), 'apple'); // For visual clarity.

View file

@ -1,7 +1,7 @@
import { assert } from "chai";
import * as Package from "keyman/engine/js-processor";
import * as Package2 from "keyman/engine/keyboard";
const KMWString = Package2.KMWString;
import * as JSProcessorModule from "keyman/engine/js-processor";
import * as KeyboardModule from "keyman/engine/keyboard";
const KMWString = KeyboardModule.KMWString;
// A few small tests to ensure that the ES Module bundle was successfully constructed and is usable.
@ -17,14 +17,31 @@ let u = toSupplementaryPairString;
describe('Bundled ES Module for js-processor', function() {
describe('JSKeyboardProcessor', function () {
it('should initialize without errors', function () {
let kp = new Package.JSKeyboardProcessor();
let kp = new JSProcessorModule.JSKeyboardProcessor();
assert.isNotNull(kp);
});
});
describe('Mock', () => {
});
describe('Bundled ES Module for keyboard', function () {
describe('Keyboard', function () {
it('should initialize without errors', function () {
let kp = new KeyboardModule.JSKeyboard();
assert.isNotNull(kp);
});
});
describe("Imported `utils`", function () {
it("should include `utils` package's Version class", () => {
let v16 = new KeyboardModule.Version([16, 1]);
assert.equal(v16.toString(), "16.1");
});
});
describe('SyntheticTextStore', () => {
it('basic functionality test', () => {
let target = new Package.Mock("aple", 2); // ap | le
let target = new KeyboardModule.SyntheticTextStore("aple", 2); // ap | le
target.insertTextBeforeCaret('p');
assert.equal(target.getText(), "apple");
});
@ -32,28 +49,12 @@ describe('Bundled ES Module for js-processor', function() {
it('smp test', () => {
KMWString.enableSupplementaryPlane(true); // Declared & defined in web-utils.
try {
let target = new Package.Mock(u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be), 2); // ap | le
let target = new KeyboardModule.SyntheticTextStore(u(0x1d5ba) + u(0x1d5c9) + u(0x1d5c5) + u(0x1d5be), 2); // ap | le
target.insertTextBeforeCaret(u(0x1d5c9));
assert.equal(target.getText(), u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be));
assert.equal(target.getText(), u(0x1d5ba) + u(0x1d5c9) + u(0x1d5c9) + u(0x1d5c5) + u(0x1d5be));
} finally {
KMWString.enableSupplementaryPlane(false);
}
});
});
});
describe('Bundled ES Module for keyboard', function () {
describe('Keyboard', function () {
it('should initialize without errors', function () {
let kp = new Package2.JSKeyboard();
assert.isNotNull(kp);
});
});
describe("Imported `utils`", function () {
it("should include `utils` package's Version class", () => {
let v16 = new Package2.Version([16, 1]);
assert.equal(v16.toString(), "16.1");
});
});
});

View file

@ -3,8 +3,8 @@ import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, JSKeyboardProcessor, Mock } from 'keyman/engine/js-processor';
import { MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, JSKeyboardProcessor } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
import { NodeProctor, RecordedKeystrokeSequence } from '@keymanapp/recorder-core';
@ -61,7 +61,7 @@ function runEngineRuleSet(ruleSet, defaultNoun) {
let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal);
// We want to specify the OutputTarget for this test; our actual concern is the resulting context.
var target = new Mock();
var target = new SyntheticTextStore();
ruleSeq.test(proctor, target);
// Now for the real test!
@ -1118,7 +1118,7 @@ describe('Engine - Context Matching', function() {
let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal);
// We want to specify the OutputTarget for this test; our actual concern is the resulting context.
var target = new Mock();
var target = new SyntheticTextStore();
ruleSeq.test(proctor, target);
// Now for the real test!

View file

@ -3,8 +3,8 @@ import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
import { NodeProctor, RecordedKeystrokeSequence } from '@keymanapp/recorder-core';
@ -23,7 +23,7 @@ function runEngineRuleSet(ruleSet) {
// Prepare the context!
const ruleSeq = new RecordedKeystrokeSequence(ruleDef);
const proctor = new NodeProctor(keyboardWithHarness, device, assert.equal);
const target = new Mock();
const target = new SyntheticTextStore();
ruleSeq.test(proctor, target);
}
}

View file

@ -3,8 +3,8 @@ import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { DeviceSpec, JSKeyboard, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { DeviceSpec, JSKeyboard, MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
describe('Headless keyboard loading', function () {
@ -43,7 +43,7 @@ describe('Headless keyboard loading', function () {
// -- END: Standard Recorder-based unit test loading boilerplate --
// Runs a blank KeyEvent through the keyboard's rule processing.
harness.processKeystroke(new Mock(), (keyboard as JSKeyboard).constructNullKeyEvent(device));
harness.processKeystroke(new SyntheticTextStore(), (keyboard as JSKeyboard).constructNullKeyEvent(device));
});
it('does not change the active kehboard', async function () {

View file

@ -4,8 +4,8 @@ import { ModifierKeyConstants } from '@keymanapp/common-types';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { Codes, KeyEvent, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { Codes, KeyEvent, MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
// Compare and contrast the unit tests here with those for app/browser key-event unit testing
@ -41,7 +41,7 @@ describe('Engine - rule processing', function() {
it('matches rules with mnemonic-specced KeyEvents', () => {
// Note: plain 'n' is produced from default key outputs for sil_ipa, not a keyboard rule.
let mockMnemonic = new Mock('n');
let mockMnemonic = new SyntheticTextStore('n');
let mnemonicEvent = new KeyEvent({
// sil_ipa is a mnenomic keyboard: it expects codes based on the key's standard character output.
Lcode: '>'.charCodeAt(0), // 62
@ -62,7 +62,7 @@ describe('Engine - rule processing', function() {
it('requires correct modifiers', () => {
// Note: plain 'n' is produced from default key outputs for sil_ipa, not a keyboard rule.
let mockMnemonic = new Mock('n');
let mockMnemonic = new SyntheticTextStore('n');
let mnemonicEvent = new KeyEvent({
// sil_ipa is a mnenomic keyboard: it expects codes based on the key's standard character output.
Lcode: '>'.charCodeAt(0), // 62
@ -81,7 +81,7 @@ describe('Engine - rule processing', function() {
});
it('does not match rules with positional-specced KeyEvents', () => {
let mockPositional = new Mock('n');
let mockPositional = new SyntheticTextStore('n');
let positionalEvent = new KeyEvent({
// If it were positional, we'd use this instead:
Lcode: Codes.keyCodes.K_COMMA, // 188
@ -119,7 +119,7 @@ describe('Engine - rule processing', function() {
});
it('matches rules with legacy-specced KeyEvents', () => {
let mockLegacy = new Mock('');
let mockLegacy = new SyntheticTextStore('');
let legacyEvent = new KeyEvent({
// armenian is a KMW 1.0 keyboard: it expects codes based on the key's standard character output.
Lcode: 'a'.charCodeAt(0),
@ -139,7 +139,7 @@ describe('Engine - rule processing', function() {
});
it('ignores current modifiers and states', () => {
let mockLegacy = new Mock('');
let mockLegacy = new SyntheticTextStore('');
let legacyEvent = new KeyEvent({
// armenian is a KMW 1.0 keyboard: it expects codes based on the key's standard character output.
Lcode: 'a'.charCodeAt(0),
@ -159,7 +159,7 @@ describe('Engine - rule processing', function() {
});
it('does not match rules with mnemonic-specced KeyEvents', () => {
let mockMnemonic = new Mock('');
let mockMnemonic = new SyntheticTextStore('');
let mnemonicEvent = new KeyEvent({
// armenian is a KMW 1.0 keyboard: it expects codes based on the key's standard character output.
Lcode: 'a'.charCodeAt(0),
@ -178,7 +178,7 @@ describe('Engine - rule processing', function() {
});
it('does not match rules with positional-specced KeyEvents', () => {
let mockPositional = new Mock('');
let mockPositional = new SyntheticTextStore('');
let positionalEvent = new KeyEvent({
// If it were positional, we'd use this instead:
Lcode: Codes.keyCodes.K_A,

View file

@ -5,8 +5,8 @@ import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { KMWString } from 'keyman/common/web-utils';
import { Codes, KeyEvent, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, JSKeyboardProcessor, Mock } from 'keyman/engine/js-processor';
import { Codes, KeyEvent, MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, JSKeyboardProcessor } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
import { ModifierKeyConstants } from '@keymanapp/common-types';
@ -128,7 +128,7 @@ describe('Engine - specialized backspace handling', function() {
});
it('empty context, positional keyboard', () => {
let contextSource = new Mock('');
let contextSource = new SyntheticTextStore('');
let event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
@ -156,7 +156,7 @@ describe('Engine - specialized backspace handling', function() {
});
it("empty context, positional keyboard, outputless-key that's not BKSP", () => {
let contextSource = new Mock('');
let contextSource = new SyntheticTextStore('');
let event = new KeyEvent({
Lcode: Codes.keyCodes.K_A,
Lmodifiers: 0,
@ -189,7 +189,7 @@ describe('Engine - specialized backspace handling', function() {
});
it('empty context, positional keyboard, but text is selected', () => {
let contextSource = new Mock('selected text', 0);
let contextSource = new SyntheticTextStore('selected text', 0);
contextSource.setSelection(0, KMWString.length(contextSource.getText()));
let event = new KeyEvent({
@ -226,7 +226,7 @@ describe('Engine - specialized backspace handling', function() {
});
it('empty left-context, positional keyboard', () => {
let contextSource = new Mock('post-caret text', 0);
let contextSource = new SyntheticTextStore('post-caret text', 0);
let event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
@ -254,7 +254,7 @@ describe('Engine - specialized backspace handling', function() {
});
it('empty context, mnemonic keyboard', () => {
let contextSource = new Mock('');
let contextSource = new SyntheticTextStore('');
let event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
@ -282,7 +282,7 @@ describe('Engine - specialized backspace handling', function() {
});
it('final empty context, positional keyboard, rule-handled BKSP', () => {
let contextSource = new Mock('abc', 2);
let contextSource = new SyntheticTextStore('abc', 2);
let event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
@ -317,7 +317,7 @@ describe('Engine - specialized backspace handling', function() {
// Special case: BKSP rule-matches with empty left-context.
it("empty context, positional keyboard, outputless BKSP rule", () => {
let contextSource = new Mock('');
let contextSource = new SyntheticTextStore('');
let event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,

View file

@ -1,6 +1,6 @@
import { assert } from 'chai';
import { Mock, findCommonSubstringEndIndex } from 'keyman/engine/js-processor';
import { SyntheticTextStore, findCommonSubstringEndIndex } from 'keyman/engine/keyboard';
import { KMWString } from 'keyman/common/web-utils';
// A unicode-coding like alias for use in constructing non-BMP strings.
@ -157,8 +157,8 @@ describe("Transcriptions and Transforms", function() {
it("does not store an alias for related OutputTargets", function() {
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock("apple");
var original = Mock.from(target);
var target = new SyntheticTextStore("apple");
var original = SyntheticTextStore.from(target);
target.insertTextBeforeCaret("s");
/* It's not exactly black box, but presently we don't NEED the keyEvent object for the method to work.
@ -174,8 +174,8 @@ describe("Transcriptions and Transforms", function() {
it("handles context-free single-char output rules", function() {
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock("apple");
var original = Mock.from(target);
var target = new SyntheticTextStore("apple");
var original = SyntheticTextStore.from(target);
target.insertTextBeforeCaret("s");
/* It's not exactly black box, but presently we don't NEED the keyEvent object for the method to work.
@ -187,8 +187,8 @@ describe("Transcriptions and Transforms", function() {
assert.equal(transcription.transform.deleteLeft, 0, "Incorrectly detected left-of-caret deletions");
assert.equal(transcription.transform.deleteRight, 0, "Incorrectly detected right-of-caret deletions");
target = new Mock("apple", 3);
original = Mock.from(target);
target = new SyntheticTextStore("apple", 3);
original = SyntheticTextStore.from(target);
target.insertTextBeforeCaret("s"); // "appsle"
var transcription = target.buildTranscriptionFrom(original, null);
@ -199,8 +199,8 @@ describe("Transcriptions and Transforms", function() {
});
it("handles operations with moderately long text", function() {
var target = new Mock("The quick brown cat jumped onto the lazy dog.", 19);
var original = Mock.from(target);
var target = new SyntheticTextStore("The quick brown cat jumped onto the lazy dog.", 19);
var original = SyntheticTextStore.from(target);
target.setSelection(30); // 19 + 11: moves it to after "onto".
target.deleteCharsBeforeCaret(14); // delete: "cat jumped onto"
target.insertTextBeforeCaret("fox jumped over");
@ -228,8 +228,8 @@ he did. Unfortunately, he taught his apprentice everything he knew, then his
apprentice killed him in his sleep. It's ironic he could save others from death,
but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
var target = new Mock(text, text.length);
var original = Mock.from(target);
var target = new SyntheticTextStore(text, text.length);
var original = SyntheticTextStore.from(target);
target.deleteCharsBeforeCaret(1);
target.insertTextBeforeCaret("!");
@ -244,8 +244,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
});
it("handles deletions around the caret without text insertion", function() {
var target = new Mock("apple", 2);
var original = Mock.from(target);
var target = new SyntheticTextStore("apple", 2);
var original = SyntheticTextStore.from(target);
target.setSelection(3);
target.deleteCharsBeforeCaret(2); // "ale"
@ -262,8 +262,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
it("handles deletions around the caret without text insertion (non-BMP text)", function() {
try {
KMWString.enableSupplementaryPlane(true);
var target = new Mock(smpApple, 2);
var original = Mock.from(target);
var target = new SyntheticTextStore(smpApple, 2);
var original = SyntheticTextStore.from(target);
target.setSelection(3);
target.deleteCharsBeforeCaret(2); // "ale"
@ -283,8 +283,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
it("handles deletions around the caret with text insertion", function() {
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock("apple", 2);
var original = Mock.from(target);
var target = new SyntheticTextStore("apple", 2);
var original = SyntheticTextStore.from(target);
target.setSelection(3);
target.deleteCharsBeforeCaret(2);
target.insertTextBeforeCaret("PP"); // "aPPle"
@ -300,8 +300,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// CASE 2
var target = new Mock("apple", 2);
var original = Mock.from(target);
var target = new SyntheticTextStore("apple", 2);
var original = SyntheticTextStore.from(target);
target.setSelection(4);
target.deleteCharsBeforeCaret(3);
target.insertTextBeforeCaret("P"); // "aPe"
@ -317,8 +317,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// CASE 3
var target = new Mock("apple", 2);
var original = Mock.from(target);
var target = new SyntheticTextStore("apple", 2);
var original = SyntheticTextStore.from(target);
target.setSelection(4);
target.deleteCharsBeforeCaret(3);
target.insertTextBeforeCaret("aaaaaaaaaaaaaa"); // "aaaaaaaaaaaaaaae"
@ -334,8 +334,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// CASE 4
var target = new Mock("apple", 2);
var original = Mock.from(target);
var target = new SyntheticTextStore("apple", 2);
var original = SyntheticTextStore.from(target);
target.setSelection(5);
target.deleteCharsBeforeCaret(4);
target.insertTextBeforeCaret("les"); // "ales" - since we've appended a letter at the very end, the whole right-hand is indeed an insertion.
@ -357,9 +357,9 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock(smpApple, 2);
var target = new SyntheticTextStore(smpApple, 2);
let smpLE = u(0x1d5c5)+u(0x1d5be);
var original = Mock.from(target);
var original = SyntheticTextStore.from(target);
target.setSelection(3);
target.deleteCharsBeforeCaret(2);
target.insertTextBeforeCaret(smpLE); // "alele"
@ -377,9 +377,9 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// CASE 2
var target = new Mock(smpApple, 2);
var target = new SyntheticTextStore(smpApple, 2);
let smpB = u(0x1d5bb);
var original = Mock.from(target);
var original = SyntheticTextStore.from(target);
target.setSelection(4);
target.deleteCharsBeforeCaret(3);
target.insertTextBeforeCaret(smpB); // "aPe"
@ -395,8 +395,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// CASE 3
var target = new Mock(smpApple, 2);
var original = Mock.from(target);
var target = new SyntheticTextStore(smpApple, 2);
var original = SyntheticTextStore.from(target);
target.setSelection(4);
target.deleteCharsBeforeCaret(3);
target.insertTextBeforeCaret("aaaaaaaaaaaaaa"); // "aaaaaaaaaaaaaaae"
@ -412,9 +412,9 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// CASE 4
var target = new Mock(smpApple, 2);
var target = new SyntheticTextStore(smpApple, 2);
let smpLES = u(0x1d5c5)+u(0x1d5be)+u(0x1d5cb);
var original = Mock.from(target);
var original = SyntheticTextStore.from(target);
target.setSelection(5);
target.deleteCharsBeforeCaret(4);
target.insertTextBeforeCaret(smpLES); // "ales" - since we've appended a letter at the very end, the whole right-hand is indeed an insertion.
@ -435,9 +435,9 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
it('from targets with existing selection', () => {
// | |
const target = new Mock("testing testing one two three");
const target = new SyntheticTextStore("testing testing one two three");
target.setSelection(8, 20)
const original = Mock.from(target);
const original = SyntheticTextStore.from(target);
target.clearSelection();
const transform = target.buildTransformFrom(original);
@ -451,7 +451,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
it('to targets with existing selection', () => {
// | |
const target = new Mock("testing testing one two three");
const target = new SyntheticTextStore("testing testing one two three");
target.setSelection(8, 20)
const transform = {
insert: '',
@ -471,8 +471,8 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock("apple");
var original = Mock.from(target);
var target = new SyntheticTextStore("apple");
var original = SyntheticTextStore.from(target);
target.setSelection(4);
target.insertDeadkeyBeforeCaret(0);
@ -481,7 +481,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
target.setSelection(2);
target.insertDeadkeyBeforeCaret(2); // 'a' dk(1) 'p' dk(2) | 'p' 'l' dk(0) 'e'
var original = Mock.from(target);
var original = SyntheticTextStore.from(target);
target.hasDeadkeyMatch(0, 2);
target.deadkeys().deleteMatched();

View file

@ -3,8 +3,8 @@ import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { KeyboardHarness, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { KeyboardHarness, MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
describe('Headless keyboard loading', function() {
@ -61,7 +61,7 @@ describe('Headless keyboard loading', function() {
let ruleHarness = new JSKeyboardInterface({}, MinimalKeymanGlobal);
ruleHarness.activeKeyboard = keyboard;
try {
ruleHarness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device));
ruleHarness.processKeystroke(new SyntheticTextStore(), keyboard.constructNullKeyEvent(device));
assert.fail();
} catch (err) {
// Drives home an important detail: the 'global' object is effectively

View file

@ -3,8 +3,8 @@ import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { KeyboardHarness, MinimalKeymanGlobal, KeyboardDownloadError, DeviceSpec, InvalidKeyboardError, JSKeyboard } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { KeyboardHarness, MinimalKeymanGlobal, KeyboardDownloadError, DeviceSpec, InvalidKeyboardError, JSKeyboard, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
import { assertThrowsAsync, assertThrows } from 'keyman/tools/testing/test-utils';
@ -84,7 +84,7 @@ describe('Headless keyboard loading', function() {
// 'sandboxed' keyboard loading in the DOM!)
const ruleHarness = new JSKeyboardInterface({}, MinimalKeymanGlobal);
ruleHarness.activeKeyboard = keyboard as JSKeyboard;
assertThrows(() => ruleHarness.processKeystroke(new Mock(), (keyboard as JSKeyboard).constructNullKeyEvent(device)), 'k.KKM is not a function');
assertThrows(() => ruleHarness.processKeystroke(new SyntheticTextStore(), (keyboard as JSKeyboard).constructNullKeyEvent(device)), 'k.KKM is not a function');
});
});
});

View file

@ -1,13 +1,13 @@
import { assert } from 'chai';
import { Mock } from 'keyman/engine/js-processor';
import { SyntheticTextStore } from 'keyman/engine/keyboard';
describe('Mocks', function() {
describe('app|les', () => {
const testMock = new Mock('apples', 3);
const testMock = new SyntheticTextStore('apples', 3);
it('Cloning with .from()', () => {
assert.deepEqual(Mock.from(testMock), testMock);
assert.notStrictEqual(Mock.from(testMock), testMock);
assert.deepEqual(SyntheticTextStore.from(testMock), testMock);
assert.notStrictEqual(SyntheticTextStore.from(testMock), testMock);
});
it('getText', () => {
@ -31,7 +31,7 @@ describe('Mocks', function() {
});
it('clearSelection', () => {
let editMock = Mock.from(testMock);
let editMock = SyntheticTextStore.from(testMock);
editMock.clearSelection();
assert.equal(editMock.getText(), testMock.getTextBeforeCaret() + testMock.getTextAfterCaret());
@ -39,7 +39,7 @@ describe('Mocks', function() {
assert.equal(editMock.getTextAfterCaret(), testMock.getTextAfterCaret());
assert.isTrue(editMock.isSelectionEmpty());
let postClear = Mock.from(editMock);
let postClear = SyntheticTextStore.from(editMock);
editMock.clearSelection(); // on same object; make sure its internal selection stuff updates correctly!
assert.notStrictEqual(postClear, editMock);
assert.deepEqual(postClear, editMock);
@ -47,11 +47,11 @@ describe('Mocks', function() {
});
describe('app|les and ba|nanas', () => { // selection = 'les and ba'
const testMock = new Mock('apples and bananas', 3, 13);
const testMock = new SyntheticTextStore('apples and bananas', 3, 13);
it('Cloning with from()', () => {
assert.deepEqual(Mock.from(testMock), testMock);
assert.notStrictEqual(Mock.from(testMock), testMock);
assert.deepEqual(SyntheticTextStore.from(testMock), testMock);
assert.notStrictEqual(SyntheticTextStore.from(testMock), testMock);
});
it('getText', () => {
@ -75,7 +75,7 @@ describe('Mocks', function() {
});
it('clearSelection', () => {
let editMock = Mock.from(testMock);
let editMock = SyntheticTextStore.from(testMock);
editMock.clearSelection();
assert.equal(editMock.getText(), testMock.getTextBeforeCaret() + testMock.getTextAfterCaret());
@ -83,7 +83,7 @@ describe('Mocks', function() {
assert.equal(editMock.getTextAfterCaret(), testMock.getTextAfterCaret());
assert.isTrue(editMock.isSelectionEmpty());
let postClear = Mock.from(editMock);
let postClear = SyntheticTextStore.from(editMock);
editMock.clearSelection(); // on same object; make sure its internal selection stuff updates correctly!
assert.notStrictEqual(postClear, editMock);
assert.deepEqual(postClear, editMock);

View file

@ -5,8 +5,8 @@ import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { InputProcessor } from 'keyman/engine/main';
import { JSKeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { JSKeyboardInterface } from 'keyman/engine/js-processor';
import { MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { NodeKeyboardLoader } from '../../../resources/loader/nodeKeyboardLoader.js';
import { KeyboardTest } from '@keymanapp/recorder-core';
@ -104,7 +104,7 @@ describe('InputProcessor', function() {
it('with minimal context (no fat-fingers)', function() {
this.timeout(32); // ms
let core = new InputProcessor(device);
let context = new Mock("", 0);
let context = new SyntheticTextStore("", 0);
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
let keyboard = keyboardWithHarness.activeKeyboard;
@ -118,7 +118,7 @@ describe('InputProcessor', function() {
it('with extremely long context (' + KMWString.length(coreSourceCode) + ' chars, no fat-fingers)', function() {
// Assumes no SMP chars in the source, which is fine.
let context = new Mock(coreSourceCode, KMWString.length(coreSourceCode));
let context = new SyntheticTextStore(coreSourceCode, KMWString.length(coreSourceCode));
this.timeout(500); // 500 ms, excluding text import.
// These often run on VMs, so we'll be a bit generous.
@ -141,7 +141,7 @@ describe('InputProcessor', function() {
it('with minimal context (with fat-fingers)', function() {
this.timeout(32); // ms
let core = new InputProcessor(device);
let context = new Mock("", 0);
let context = new SyntheticTextStore("", 0);
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
let keyboard = keyboardWithHarness.activeKeyboard;
@ -156,7 +156,7 @@ describe('InputProcessor', function() {
it('with extremely long context (' + KMWString.length(coreSourceCode) + ' chars, with fat-fingers)', function() {
// Assumes no SMP chars in the source, which is fine.
let context = new Mock(coreSourceCode, KMWString.length(coreSourceCode));
let context = new SyntheticTextStore(coreSourceCode, KMWString.length(coreSourceCode));
this.timeout(500); // 500 ms, excluding text import.
// These often run on VMs, so we'll be a bit generous.
@ -202,7 +202,7 @@ describe('InputProcessor', function() {
it(testSet.msg ?? 'test', function() {
this.timeout(32); // ms
let core = new InputProcessor(device);
let context = new Mock("", 0);
let context = new SyntheticTextStore("", 0);
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
let keyboard = keyboardWithHarness.activeKeyboard;

View file

@ -2,7 +2,7 @@ import { assert } from 'chai';
import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main';
import { SourcemappedWorker as LMWorker } from "@keymanapp/lexical-model-layer/node";
import { Mock } from 'keyman/engine/js-processor';
import { SyntheticTextStore } from 'keyman/engine/keyboard';
/*
* Unit tests for the Dummy prediction model.
@ -110,7 +110,7 @@ describe('LanguageProcessor', function() {
});
it("generates the expected prediction set", function(done) {
let contextSource = new Mock("li", 2);
let contextSource = new SyntheticTextStore("li", 2);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
@ -147,7 +147,7 @@ describe('LanguageProcessor', function() {
describe("does not alter casing when input is lowercased", function() {
it("when input is fully lowercased", function(done) {
let contextSource = new Mock("li", 2);
let contextSource = new SyntheticTextStore("li", 2);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
@ -164,7 +164,7 @@ describe('LanguageProcessor', function() {
});
it("when input has non-initial uppercased letters", function(done) {
let contextSource = new Mock("lI", 2);
let contextSource = new SyntheticTextStore("lI", 2);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
@ -182,7 +182,7 @@ describe('LanguageProcessor', function() {
});
it("unless the suggestion has uppercased letters", function(done) {
let contextSource = new Mock("i", 1);
let contextSource = new SyntheticTextStore("i", 1);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
@ -201,7 +201,7 @@ describe('LanguageProcessor', function() {
describe("uppercases suggestions when input is fully capitalized ", function() {
it("for suggestions with default casing (== 'lower')", function(done) {
let contextSource = new Mock("LI", 2);
let contextSource = new SyntheticTextStore("LI", 2);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
@ -219,7 +219,7 @@ describe('LanguageProcessor', function() {
});
it("for precapitalized suggestions", function(done) {
let contextSource = new Mock("I", 1);
let contextSource = new SyntheticTextStore("I", 1);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
@ -239,7 +239,7 @@ describe('LanguageProcessor', function() {
describe("initial-cases suggestions when input uses initial casing ", function() {
describe("when input is a single capitalized letter", function() {
it("for suggestions with default casing (== 'lower')", function(done) {
let contextSource = new Mock("L", 1);
let contextSource = new SyntheticTextStore("L", 1);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
@ -259,7 +259,7 @@ describe('LanguageProcessor', function() {
describe("input length > 1", function() {
it("for suggestions with default casing (== 'lower')", function(done) {
let contextSource = new Mock("Li", 2);
let contextSource = new SyntheticTextStore("Li", 2);
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {

View file

@ -78,7 +78,7 @@
<div id="floating-mode" class="flex panel inactive">
<div class="group">
<!-- OutputTarget selector (text vs input) -->
<!-- TextStore selector (text vs input) -->
<p class="category">Active Element</p>
<div id="float-target" class="flex">
<button onclick="setTarget('text')">Textarea</button>
@ -102,7 +102,7 @@
<div id="anchored-mode" class="flex panel inactive">
<div class="group">
<!-- OutputTarget selector (text vs input) -->
<!-- TextStore selector (text vs input) -->
<p class="category">Active Element</p>
<div id="anchor-target" class="flex">
<button onclick="setTarget('text')">Textarea</button>

View file

@ -16,6 +16,7 @@ SUBPROJECT_NAME=tools/testing/bulk_rendering
################################ Main script ################################
builder_describe "Builds a 'bulk renderer' that loads all the cloud keyboards from api.keyman.com and renders each of them to a document." \
"@/web/src/common/web-utils build" \
"@/web/src/app/browser build" \
"@/web/src/app/ui build" \
"clean" \

View file

@ -1,5 +1,4 @@
import { Mock } from "keyman/engine/js-processor";
import { KeyDistribution, KeyEvent, type OutputTargetInterface } from "keyman/engine/keyboard";
import { KeyDistribution, KeyEvent, type TextStore, SyntheticTextStore } from "keyman/engine/keyboard";
import Proctor from "./proctor.js";
@ -217,10 +216,10 @@ export abstract class TestSequence<KeyRecord extends RecordedKeystroke | InputEv
abstract hasOSKInteraction(): boolean;
async test(proctor: Proctor, target?: OutputTargetInterface): Promise<{success: boolean, result: string}> {
// Start with an empty OutputTarget and a fresh KeyboardProcessor.
async test(proctor: Proctor, target?: TextStore): Promise<{success: boolean, result: string}> {
// Start with an empty TextStore and a fresh KeyboardProcessor.
if(!target) {
target = new Mock();
target = new SyntheticTextStore();
}
proctor.before();

View file

@ -8,9 +8,7 @@ import {
RecordedSyntheticKeystroke
} from "./index.js";
import { KeyEvent, KeyEventSpec, KeyboardHarness, type OutputTargetInterface } from "keyman/engine/keyboard";
// TODO-web-core: remove usage of OutputTargetBase, use OutputTargetInterface instead
import { Mock, OutputTargetBase } from 'keyman/engine/js-processor';
import { KeyEvent, KeyEventSpec, KeyboardHarness, SyntheticTextStore, TextStore } from "keyman/engine/keyboard";
import { DeviceSpec } from "keyman/common/web-utils";
import { JSKeyboardInterface, JSKeyboardProcessor } from 'keyman/engine/js-processor';
@ -50,19 +48,19 @@ export default class NodeProctor extends Proctor {
return true;
}
async simulateSequence(sequence: TestSequence<any>, target?: OutputTargetInterface): Promise<string> {
// Start with an empty OutputTarget and a fresh KeyboardProcessor.
if(!target) {
target = new Mock();
async simulateSequence(sequence: TestSequence<any>, textStore?: TextStore): Promise<string> {
// Start with an empty TextStore and a fresh KeyboardProcessor.
if(!textStore) {
textStore = new SyntheticTextStore();
}
// Establish a fresh processor, setting its keyboard appropriately for the test.
let processor = new JSKeyboardProcessor(this.device);
const processor = new JSKeyboardProcessor(this.device);
processor.keyboardInterface = this.keyboardWithHarness as JSKeyboardInterface;
const keyboard = processor.activeKeyboard;
if(sequence instanceof RecordedKeystrokeSequence) {
for(let keystroke of sequence.inputs) {
for(const keystroke of sequence.inputs) {
let keyEvent: KeyEventSpec;
if(keystroke instanceof RecordedPhysicalKeystroke) {
// Use the keystroke's stored data to reconstruct the KeyEvent.
@ -78,7 +76,7 @@ export default class NodeProctor extends Proctor {
LisVirtualKey: keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards.
}
} else if(keystroke instanceof RecordedSyntheticKeystroke) {
let key = keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName);
const key = keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName);
keyEvent = keyboard.constructKeyEvent(key, this.device, processor.stateKeys);
}
@ -89,18 +87,17 @@ export default class NodeProctor extends Proctor {
// We don't care too much about particularities of per-keystroke behavior yet.
// ... we _could_ if we wanted to, though. The framework is mostly in place;
// it's a matter of actually adding the feature.
// TODO-web-core
const ruleBehavior = processor.processKeystroke(new KeyEvent(keyEvent), (target as OutputTargetBase));
const ruleBehavior = processor.processKeystroke(new KeyEvent(keyEvent), textStore);
if (this.debugMode) {
console.log("Processing %d:", keyEvent.Lcode);
console.log("target=%s", JSON.stringify(target, null, ' '));
console.log("target=%s", JSON.stringify(textStore, null, ' '));
console.log("ruleBehavior=%s", JSON.stringify(ruleBehavior, null, ' '));
}
}
} else {
throw new Error("NodeProctor only supports RecordedKeystrokeSequences for testing at present.");
}
return target.getText();
return textStore.getText();
}
}

View file

@ -1,5 +1,5 @@
import { type DeviceSpec } from "keyman/common/web-utils";
import { type OutputTargetInterface } from "keyman/engine/keyboard";
import { type TextStore } from "keyman/engine/keyboard";
import type { KeyboardTest, TestSet, TestSequence } from "./index.js";
@ -49,5 +49,5 @@ export default abstract class Proctor {
* Simulates the specified test sequence for use in testing.
* @param sequence The recorded sequence, generally provided by a test set.
*/
abstract simulateSequence(sequence: TestSequence<any>, target?: OutputTargetInterface): Promise<string>;
abstract simulateSequence(sequence: TestSequence<any>, target?: TextStore): Promise<string>;
}

Some files were not shown because too many files have changed in this diff Show more