element used to contain hot-swapped `Banner` instances.
- */
- private constructContainer(): HTMLDivElement {
- let keymanweb = com.keyman.singleton;
- let util = keymanweb.util;
- let d = util._CreateElement('div');
- d.id = "keymanweb_banner_container";
- d.className = "kmw-banner-container";
- return this.bannerContainer = d;
- }
-
- /**
- * Returns the `Banner`-containing div element used to facilitate hot-swapping.
- */
- public get element(): HTMLDivElement {
- return this.bannerContainer;
- }
-
- /**
- * This function corresponds to `keyman.osk.banner.getOptions`.
- *
- * Gets the current control settings in use by `BannerManager`.
- */
- public getOptions(): BannerOptions {
- let retObj = {};
-
- for(let key in this._options) {
- retObj[key] = this._options[key];
- }
-
- return retObj;
- }
-
- /**
- * This function corresponds to `keyman.osk.banner.setOptions`.
- *
- * Sets options used to tweak the automatic `Banner`
- * control logic used by `BannerManager`.
- * @param optionSpec An object specifying one or more of the following options:
- * * `persistentBanner` (boolean) When `true`, ensures that a `Banner`
- * is always displayed, even when no predictive model exists
- * for the active language.
- *
- * Default: `false`
- * * `imagePath` (URL string) Specifies the file path to use for an
- * `ImageBanner` when `persistentBanner` is `true` and no predictive model exists.
- *
- * Default: `''`.
- * * `enablePredictions` (boolean) Turns KMW predictions
- * on (when `true`) and off (when `false`).
- *
- * Default: `true`.
- */
- public setOptions(optionSpec: BannerOptions) {
- let keyman = com.keyman.singleton;
-
- for(let key in optionSpec) {
- switch(key) {
- // Each defined option may require specialized handling.
- case 'alwaysShow':
- // Determines the banner type to activate.
- this.alwaysShow = optionSpec[key];
- break;
- case 'mayPredict':
- // If this toggles our internal flag, it will generate events
- // that reconfigures the banner (and internal engine state) appropriately.
- keyman.core.languageProcessor.mayPredict = optionSpec[key]
- break;
- case 'mayCorrect':
- keyman.core.languageProcessor.mayCorrect = optionSpec[key];
- break;
- case 'imagePath':
- // Determines the image file to use for ImageBanners.
- this.imagePath = optionSpec[key];
- break;
- default:
- // Invalid option specified!
- }
- this._options[key] = optionSpec[key];
-
- // If no banner instance exists yet, go with a safe, blank initialization.
- if(!this.activeBanner) {
- this.selectBanner('inactive');
- }
- }
- }
-
- /**
- * Applies any stylesheets needed by specific `Banner` instances.
- */
- public appendStyles() {
- if(this.activeBanner) {
- this.activeBanner.appendStyleSheet();
- }
- }
-
- /**
- * Sets the active `Banner` to the specified type, regardless of
- * existing management logic settings.
- *
- * @param type `'blank' | 'image' | 'suggestion'` - A plain-text string
- * representing the type of `Banner` to set active.
- * @param height - Optional banner height in pixels.
- */
- public setBanner(type: BannerType, height?: number) {
- var banner: Banner;
-
- switch(type) {
- case 'blank':
- banner = new BlankBanner();
- break;
- case 'image':
- banner = new ImageBanner(this.imagePath, Banner.DEFAULT_HEIGHT);
- break;
- case 'suggestion':
- banner = new SuggestionBanner(this.hostDevice, height);
- break;
- default:
- throw new Error("Invalid type specified for the banner!");
- }
-
- this._activeType = type;
-
- if(banner) {
- this._setBanner(banner);
- banner.activate();
- }
- }
-
- /**
- * Handles `LanguageProcessor`'s `'statechange'` events,
- * allowing logic to automatically hot-swap `Banner`s as needed.
- * @param state
- */
- selectBanner(state: text.prediction.StateChangeEnum) {
- // Only display a SuggestionBanner when LanguageProcessor states it is active.
- if(state == 'active') {
- this.setBanner('suggestion');
- } else if(state == 'inactive') {
- if(this.alwaysShow) {
- this.setBanner('image');
- } else {
- this.setBanner('blank');
- }
- } else if(state == 'configured') {
- let suggestionBanner = this.activeBanner as SuggestionBanner;
- if(suggestionBanner.postConfigure) {
- // Triggers the initially-displayed suggestions.
- suggestionBanner.postConfigure();
- }
- }
- }
-
- /**
- * Internal method used by the public API `setBanner`. `setBanner`
- * translates the string parameter into a new instance consumed by this method.
- * @param banner The `Banner` instance to set as active.
- */
- private _setBanner(banner: Banner) {
- if(this.activeBanner) {
- if(banner == this.activeBanner) {
- return;
- } else {
- let prevBanner = this.activeBanner;
- prevBanner.deactivate();
- this.bannerContainer.replaceChild(banner.getDiv(), prevBanner.getDiv());
- }
- }
-
- this.activeBanner = banner;
- this.bannerContainer.appendChild(banner.getDiv());
-
- // Don't forget to adjust the OSK in case we're now using a blank Banner!
- // Null guard b/c this function can be trigggered during OSK initialization.
- let keyman = com.keyman.singleton;
- if(keyman['osk']) {
- keyman['osk'].refreshLayout();
- }
- }
-
- public get activeType(): BannerType {
- return this._activeType;
- }
-
- /**
- * Gets the height (in pixels) of the active `Banner` instance.
- */
- public get height(): number {
- if(this.activeBanner) {
- return this.activeBanner.height;
- } else {
- return 0;
- }
- }
-
- /**
- * Sets the height (in pixels) of the active 'Banner' instance.
- */
- public set height(h: number) {
- if (this.activeBanner) {
- this.activeBanner.height = h;
- }
- }
-
- public get layoutHeight(): ParsedLengthStyle {
- return ParsedLengthStyle.inPixels(this.height);
- }
-
- public refreshLayout() {};
- }
-}
diff --git a/web/src/engine/main/osk/browser/keytip.ts b/web/src/engine/main/osk/browser/keytip.ts
deleted file mode 100644
index 4f647ee63c..0000000000
--- a/web/src/engine/main/osk/browser/keytip.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-namespace com.keyman.osk.browser {
- export class KeyTip implements com.keyman.osk.KeyTip {
- public readonly element: HTMLDivElement;
- public key: KeyElement;
- public state: boolean = false;
-
- // -----
- // | | <-- tip
- // | x | <-- label
- // |_ _|
- // | |
- // | | <-- cap
- // |___|
-
- private readonly cap: HTMLDivElement;
- private readonly tip: HTMLDivElement;
- private readonly label: HTMLSpanElement;
-
- private readonly constrain: boolean;
-
- /**
- *
- * @param constrain keep the keytip within the bounds of the overall OSK.
- * Will probably be handled via function in a later pass.
- */
- constructor(constrain: boolean) {
- let tipElement = this.element=document.createElement('div');
- tipElement.className='kmw-keytip';
- tipElement.id = 'kmw-keytip';
-
- // The following style is critical, so do not rely on external CSS
- tipElement.style.pointerEvents='none';
- tipElement.style.display='none';
-
- tipElement.appendChild(this.tip = document.createElement('div'));
- tipElement.appendChild(this.cap = document.createElement('div'));
- this.tip.appendChild(this.label = document.createElement('span'));
-
- this.tip.className = 'kmw-keytip-tip';
- this.cap.className = 'kmw-keytip-cap';
- this.label.className = 'kmw-keytip-label';
-
- this.constrain = constrain;
- }
-
- show(key: KeyElement, on: boolean, vkbd: VisualKeyboard) {
- let keyman = com.keyman.singleton;
- let util = keyman.util;
-
- // Create and display the preview
- // If !key.offsetParent, the OSK is probably hidden. Either way, it's a half-
- // decent null-guard check.
- if(on && key.offsetParent) {
- // The key element is positioned relative to its key-square, which is,
- // in turn, relative to its row. Rows take 100% width, so this is sufficient.
- //
- let rowElement = (key.key as OSKBaseKey).row.element;
-
- // May need adjustment for borders if ever enabled for the desktop form-factor target.
- let rkey = key.getClientRects()[0], rrow = rowElement.getClientRects()[0];
- let xLeft = rkey.left - rrow.left,
- xWidth = rkey.width,
- xHeight = rkey.height,
- kc = key.key.label,
- previewFontScale = 1.8;
-
- let kts = this.element.style;
-
- // Roughly matches how the subkey positioning is set.
- const _Box = vkbd.element.parentNode as HTMLDivElement;
- const _BoxRect = _Box.getBoundingClientRect();
- const keyRect = key.getBoundingClientRect();
- let y = (keyRect.bottom - _BoxRect.top + 1);
- let ySubPixelPadding = y - Math.floor(y);
-
- // Canvas dimensions must be set explicitly to prevent clipping
- // This gives us exactly the same number of pixels on left and right
- let canvasWidth = xWidth + Math.ceil(xWidth * 0.3) * 2;
- let canvasHeight = Math.ceil(2.3 * xHeight) + (ySubPixelPadding); //
-
- kts.top = 'auto';
- kts.bottom = Math.floor(keyman.osk.computedHeight - y) + 'px';
- kts.textAlign = 'center';
- kts.overflow = 'visible';
- kts.fontFamily = util.getStyleValue(kc,'font-family');
- kts.width = canvasWidth+'px';
- kts.height = canvasHeight+'px';
-
- var px=util.getStyleInt(kc, 'font-size');
- if(px != 0) {
- let popupFS = previewFontScale * px;
- let scaleStyle = {
- fontFamily: kts.fontFamily,
- fontSize: popupFS + 'px',
- height: 1.6 * xHeight + 'px' // as opposed to the canvas height of 2.3 * xHeight.
- };
-
- kts.fontSize = key.key.getIdealFontSize(vkbd, key.key.keyText, scaleStyle, true);
- }
-
- this.label.textContent = kc.textContent;
-
- // Adjust shape if at edges
- var xOverflow = (canvasWidth - xWidth) / 2;
- if(xLeft < xOverflow) {
- this.cap.style.left = '1px';
- xLeft += xOverflow - 1;
- } else if(xLeft > window.innerWidth - xWidth - xOverflow) {
- this.cap.style.left = (canvasWidth - xWidth - 1) + 'px';
- xLeft -= xOverflow - 1;
- } else {
- this.cap.style.left = xOverflow + 'px';
- }
-
- kts.left=(xLeft - xOverflow) + 'px';
-
- let cs = getComputedStyle(this.element);
- let oskHeight = keyman.osk.computedHeight;
- let bottomY = parseFloat(cs.bottom);
- let tipHeight = parseFloat(cs.height);
- let halfHeight = Math.ceil(canvasHeight / 2);
-
- this.cap.style.width = xWidth + 'px';
- this.tip.style.height = halfHeight + 'px';
-
- this.cap.style.top = (halfHeight - 3) + 'px';
- this.cap.style.height = (keyRect.bottom - _BoxRect.top - Math.floor(y - canvasHeight) - (halfHeight)) + 'px'; //(halfHeight + 3 + ySubPixelPadding) + 'px';
-
- if(this.constrain && tipHeight + bottomY > oskHeight) {
- const delta = tipHeight + bottomY - oskHeight;
- kts.height = (canvasHeight-delta) + 'px';
- const hx = Math.max(0, (canvasHeight-delta)-(canvasHeight/2) + 2);
- this.cap.style.height = hx + 'px';
- }
-
- kts.display = 'block';
- } else { // Hide the key preview
- this.element.style.display = 'none';
- }
-
- // Save the key preview state
- this.key = key;
- this.state = on;
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/browser/oskSubKey.ts b/web/src/engine/main/osk/browser/oskSubKey.ts
deleted file mode 100644
index 9a8879f0c5..0000000000
--- a/web/src/engine/main/osk/browser/oskSubKey.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-///
-
-namespace com.keyman.osk.browser {
- export class OSKSubKey extends OSKKey {
- constructor(spec: OSKKeySpec, layer: string) {
- if(typeof(layer) != 'string' || layer == '') {
- throw "The 'layer' parameter for subkey construction must be properly defined.";
- }
-
- super(spec, layer);
- }
-
- getId(): string {
- // Create (temporarily) unique ID by prefixing 'popup-' to actual key ID
- return 'popup-'+this.layer+'-'+this.spec['id'];
- }
-
- construct(osk: VisualKeyboard, baseKey: KeyElement, topMargin: boolean): HTMLDivElement {
- let spec = this.spec;
-
- let kDiv=document.createElement('div');
- let tKey = osk.getDefaultKeyObject();
- let ks=kDiv.style;
-
- for(var tp in tKey) {
- if(typeof spec[tp] != 'string') {
- spec[tp]=tKey[tp];
- }
- }
-
- kDiv.className='kmw-key-square-ex';
- if(topMargin) {
- ks.marginTop='5px';
- }
-
- if(typeof spec['width'] != 'undefined') {
- ks.width=(spec['width']*baseKey.offsetWidth/100)+'px';
- } else {
- ks.width=baseKey.offsetWidth+'px';
- }
- ks.height=baseKey.offsetHeight+'px';
-
- let btnEle=document.createElement('div');
- let btn = this.btn = link(btnEle, new KeyData(this, spec['id']));
-
- this.setButtonClass();
- btn.id = this.getId();
-
- // Must set button size (in px) dynamically, not from CSS
- let bs=btn.style;
- bs.height=ks.height;
- bs.lineHeight=baseKey.style.lineHeight;
- bs.width=ks.width;
-
- // Must set position explicitly, at least for Android
- bs.position='absolute';
-
- btn.appendChild(this.label = this.generateKeyText(osk));
- kDiv.appendChild(btn);
-
- return this.square = kDiv;
- }
-
- public allowsKeyTip(): boolean {
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/browser/pendingLongpress.ts b/web/src/engine/main/osk/browser/pendingLongpress.ts
deleted file mode 100644
index 08f227fc39..0000000000
--- a/web/src/engine/main/osk/browser/pendingLongpress.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-///
-///
-
-namespace com.keyman.osk.browser {
- /**
- * (Conceptually) represents a finite-state-machine that determines
- * whether or not a series of touch events corresponds to a longpress
- * touch input. The `resolve` method may be used to trigger the
- * subkey menu early, as with the upward quick-display shortcut.
- *
- * This is the default implementation of longpress behavior for KMW.
- * Alterate implementations are modeled through the `embedded`
- * namespace's equivalent, which is designed to facilitate custom
- * modeling for such gestures.
- *
- * Once the conditions to recognize a longpress gesture have been
- * fulfilled, this class's `promise` will resolve with a `SubkeyPopup`
- * matching the gesture's 'base' key, which itself provides a
- * `promise` field that will resolve to a `KeyEvent` once the touch
- * sequence is completed.
- */
- export class PendingLongpress implements PendingGesture {
- public readonly baseKey: KeyElement;
- public readonly promise: Promise
;
-
- public readonly subkeyUI: SubkeyPopup;
-
- private readonly vkbd: VisualKeyboard;
- private resolver: (subkeyPopup: SubkeyPopup) => void;
-
- private timerId: number;
- private popupDelay: number = 500;
-
- constructor(vkbd: VisualKeyboard, baseKey: KeyElement) {
- this.vkbd = vkbd;
- this.baseKey = baseKey;
-
- let _this = this;
- this.promise = new Promise(function(resolve, reject) {
- _this.resolver = resolve;
- // After the timeout, it's no longer deferred; it's being fulfilled.
- // Even if the actual subkey itself is still async.
- _this.timerId = window.setTimeout(_this.resolve.bind(_this), _this.popupDelay);
- });
- }
-
- public cancel() {
- if(this.timerId) {
- window.clearTimeout(this.timerId);
- this.timerId = null;
- }
-
- if(this.resolver) {
- this.resolver(null);
- this.resolver = null;
- }
- }
-
- public resolve() {
- // User has flicked up to get to the longpress, before
- // the timeout has expired. We need to cancel the timeout.
- // See #5950
- if(this.timerId) {
- window.clearTimeout(this.timerId);
- this.timerId = null;
- }
-
- if(this.resolver) {
- this.resolver(new SubkeyPopup(this.vkbd, this.baseKey));
- }
- }
- }
-}
diff --git a/web/src/engine/main/osk/browser/pendingMultiTap.ts b/web/src/engine/main/osk/browser/pendingMultiTap.ts
deleted file mode 100644
index 90bf97c438..0000000000
--- a/web/src/engine/main/osk/browser/pendingMultiTap.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-///
-///
-
-namespace com.keyman.osk {
-
- export enum PendingMultiTapState { Waiting, Realized, Cancelled };
- /**
- * Implements the multi-tap gesture, which is a series of taps on a single key
- * (based on key id substring in the case of the shift key), within a
- * specified timeout period.
- */
- export class PendingMultiTap implements PendingGesture {
- public readonly vkbd: VisualKeyboard;
- public readonly baseKey: KeyElement;
- public readonly count: number;
- private timerId;
- private _touches = 1; // we start the multitap with a single touch
- private _state: PendingMultiTapState = PendingMultiTapState.Waiting;
- private _timeout: Promise;
- private cancelDelayFactor = 125; // 125msec * count
- private _destinationLayerId;
-
- public get timeout() {
- return this._timeout;
- }
- public get realized() {
- return this._state == PendingMultiTapState.Realized;
- }
- public get cancelled() {
- return this._state == PendingMultiTapState.Cancelled;
- }
-
- /**
- * Construct a record of a potential multitap gesture
- * @param vkbd
- * @param baseKey key which is being tapped
- * @param count number of taps required to finalize this gesture
- */
- constructor(vkbd: VisualKeyboard, baseKey: KeyElement, count: number) {
- this.vkbd = vkbd;
- this.count = count;
- this.baseKey = baseKey;
-
- this._destinationLayerId = 'caps';
- let multitap = baseKey?.key?.spec?.['multitap'];
- if(multitap?.length && multitap[0]?.['nextlayer']) {
- this._destinationLayerId = multitap[0]['nextlayer'];
- }
-
- const _this = this;
- this._timeout = new Promise(function(resolve) {
- // If multiple taps do not occur within the timeout window,
- // then we will abandon the gesture
- _this.timerId = window.setTimeout(() => {
- _this.cancel();
- resolve();
- }, _this.cancelDelayFactor * _this.count);
- });
- }
-
- public static isValidTarget(vkbd: VisualKeyboard, baseKey: KeyElement) {
- // Could use String.includes, but Chrome for Android must be version 41+.
- // We support down to version 37.
- return (
- baseKey['keyId'].indexOf('K_SHIFT') >= 0 &&
- vkbd.layerGroup.layers['caps'] &&
- !baseKey['subKeys'] &&
- vkbd.touchCount == 1
- );
- }
-
- private cleanup(): void {
- if(this.timerId) {
- window.clearTimeout(this.timerId);
- }
- this.timerId = null;
- }
-
- /**
- * Cancel a pending multitap gesture
- */
- public cancel(): void {
- this._state = PendingMultiTapState.Cancelled;
- this.cleanup();
- }
-
- /**
- * Increments the touch counter for the gesture, and
- * if the touch count is reached, realize the gesture
- * @returns new state of the gesture
- */
- public incrementTouch(newKey: KeyElement): PendingMultiTapState {
- // TODO: support for any key
- if(this._state == PendingMultiTapState.Waiting) {
- if(!newKey?.['keyId']?.includes('K_SHIFT')) {
- this.cancel();
- }
- else if(++this._touches == this.count) {
- this.realize();
- }
- }
- return this._state;
- }
-
- /**
- * Realize the gesture. In Keyman 15, this supports only
- * the Caps double-tap gesture on the Shift key.
- */
- public realize(): void {
- if(this._state != PendingMultiTapState.Waiting) {
- return;
- }
- this._state = PendingMultiTapState.Realized;
- this.cleanup();
-
- // In Keyman 15, only the K_SHIFT key supports multi-tap, so we can hack
- // in the switch to the caps layer.
- //
- // TODO: generalize this with double-tap key properties in touch layout
- // description.
- let e = text.KeyEvent.constructNullKeyEvent(this.vkbd.device);
- e.kNextLayer = this._destinationLayerId;
- e.Lstates = text.Codes.stateBitmasks.CAPS;
- e.LmodifierChange = true;
- PreProcessor.raiseKeyEvent(e);
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/browser/subkeyPopup.ts b/web/src/engine/main/osk/browser/subkeyPopup.ts
deleted file mode 100644
index 7a5d1c3506..0000000000
--- a/web/src/engine/main/osk/browser/subkeyPopup.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-///
-///
-
-namespace com.keyman.osk.browser {
- /**
- * Represents a 'realized' longpress gesture's default implementation
- * within KeymanWeb. Once a touch sequence has been confirmed to
- * correspond to a longpress gesture, implementations of this class
- * provide the following:
- * * The UI needed to present a subkey menu
- * * The state management needed to present feedback about the
- * currently-selected subkey to the user
- * * A `Promise` that will resolve to the user's selected subkey
- * once the longpress operation is complete.
- *
- * As selection of the subkey occurs after the subkey popup is
- * displayed, selection of the subkey is inherently asynchronous.
- * The `Promise` may also resolve to `null` if the user indicates
- * the desire to cancel subkey selection.
- */
- export class SubkeyPopup implements RealizedGesture {
- public readonly element: HTMLDivElement;
- public readonly shim: HTMLDivElement;
-
- private vkbd: VisualKeyboard;
- private currentSelection: KeyElement;
-
- private callout: HTMLDivElement;
-
- public readonly baseKey: KeyElement;
- public readonly promise: Promise;
-
- // Resolves the promise that generated this SubkeyPopup.
- private resolver: (keyEvent: text.KeyEvent) => void;
-
- constructor(vkbd: VisualKeyboard, e: KeyElement) {
- let keyman = com.keyman.singleton;
- let _this = this;
-
- this.promise = new Promise(function(resolve) {
- _this.resolver = resolve;
- })
-
- this.vkbd = vkbd;
- this.baseKey = e;
-
- // If the user doesn't move their finger and releases, we'll output the base key
- // by default.
- this.currentSelection = e;
- e.key.highlight(true);
-
- // A tag we directly set on a key element during its construction.
- let subKeySpec: OSKKeySpec[] = e['subKeys'];
-
- // The holder is position:fixed, but the keys do not need to be, as no scrolling
- // is possible while the array is visible. So it is simplest to let the keys have
- // position:static and display:inline-block
- var subKeys = this.element = document.createElement('div');
-
- var i;
- subKeys.id='kmw-popup-keys';
-
- // #3718: No longer prepend base key to popup array
-
- // Must set position dynamically, not in CSS
- var ss=subKeys.style;
-
- // Set key font according to layout, or defaulting to OSK font
- // (copied, not inherited, since OSK is not a parent of popup keys)
- ss.fontFamily=vkbd.fontFamily;
-
- // Copy the font size from the parent key, allowing for style inheritance
- ss.fontSize=keyman.util.getStyleValue(e,'font-size');
- ss.visibility='hidden';
-
- var nKeys=subKeySpec.length,nRows,nCols;
- nRows=Math.min(Math.ceil(nKeys/9),2);
- nCols=Math.ceil(nKeys/nRows);
- ss.width=(nCols*e.offsetWidth+nCols*5)+'px';
-
- // Add nested button elements for each sub-key
- for(i=0; i 1 && nRow > 0) {
- needsTopMargin = true;
- }
-
- let layer = e['key'].layer;
- if(typeof(layer) != 'string' || layer == '') {
- // Use the currently-active layer.
- layer = vkbd.layerId;
- }
- let keyGenerator = new OSKSubKey(subKeySpec[i], layer);
- let kDiv = keyGenerator.construct(vkbd, e, needsTopMargin);
-
- subKeys.appendChild(kDiv);
- }
-
- // And add a filter to fade main keyboard
- this.shim = document.createElement('div');
- this.shim.id = 'kmw-popup-shim';
-
- // Highlight the duplicated base key or ideal subkey (if a phone)
- if(vkbd.device.formFactor == utils.FormFactor.Phone) {
- this.selectDefaultSubkey(vkbd, e, subKeys /* == this.element */);
- }
- }
-
- finalize(input: InputEventCoordinate) {
- if(this.resolver) {
- let keyEvent: text.KeyEvent = null;
- if(this.currentSelection) {
- keyEvent = this.vkbd.initKeyEvent(this.currentSelection, input);
- this.currentSelection.key.highlight(false);
- }
- this.resolver(keyEvent);
- }
- this.resolver = null;
- }
-
- reposition(vkbd: VisualKeyboard) {
- let keyman = com.keyman.singleton;
-
- let subKeys = this.element;
- let e = this.baseKey;
-
- // And correct its position with respect to that element
- const _Box = vkbd.element.offsetParent as HTMLDivElement;
- let rowElement = (e.key as OSKBaseKey).row.element;
- let ss=subKeys.style;
- var x = e.offsetLeft + (e.offsetParent).offsetLeft + 0.5*(e.offsetWidth-subKeys.offsetWidth);
- var xMax = keyman.osk.computedWidth - subKeys.offsetWidth;
-
- if(x > xMax) {
- x=xMax;
- }
- if(x < 0) {
- x=0;
- }
- ss.left=x+'px';
-
- let _BoxRect = _Box.getBoundingClientRect();
- let rowElementRect = rowElement.getBoundingClientRect();
- ss.top = (rowElementRect.top - _BoxRect.top - subKeys.offsetHeight - 3) + 'px';
-
- // Make the popup keys visible
- ss.visibility='visible';
-
- // For now, should only be true (in production) when keyman.isEmbedded == true.
- let constrainPopup = keyman.isEmbedded;
-
- let cs = getComputedStyle(subKeys);
- let topY = parseFloat(cs.top);
-
- // Adjust the vertical position of the popup to keep it within the
- // bounds of the keyboard rectangle, when on iPhone (system keyboard)
- const topOffset = 0; // Set this when testing constrainPopup, e.g. to -80px
- let delta = 0;
- if(topY < topOffset && constrainPopup) {
- delta = topOffset - topY;
- ss.top = topOffset + 'px';
- }
-
- // Add the callout
- if(vkbd.device.formFactor == utils.FormFactor.Phone && vkbd.device.OS == utils.OperatingSystem.iOS) {
- this.callout = this.addCallout(e, delta);
- }
- }
-
- /**
- * Add a callout for popup keys (if KeymanWeb on a phone device)
- *
- * @param {Object} key HTML key element
- * @return {Object} callout object
- */
- addCallout(key: KeyElement, delta?: number): HTMLDivElement {
- let keyman = com.keyman.singleton;
-
- delta = delta || 0;
-
- let calloutHeight = key.offsetHeight - delta + 6;
-
- if(calloutHeight > 0) {
- var cc = document.createElement('div'), ccs = cc.style;
- cc.id = 'kmw-popup-callout';
- keyman.osk._Box.appendChild(cc);
-
- // Create the callout
- let keyRect = key.getBoundingClientRect();
- let _BoxRect = keyman.osk._Box.getBoundingClientRect();
-
- // Set position and style
- // We're going to adjust the top of the box to ensure it stays
- // pixel aligned, otherwise we can get antialiasing artifacts
- // that look ugly
- let top = Math.floor(keyRect.top - _BoxRect.top - 9 + delta);
- ccs.top = top + 'px';
- ccs.left = (keyRect.left - _BoxRect.left) + 'px';
- ccs.width = keyRect.width + 'px';
- ccs.height = (keyRect.bottom - _BoxRect.top - top - 1) + 'px'; //(height - 1) + 'px';
-
- // Return callout element, to allow removal later
- return cc;
- } else {
- return null;
- }
- }
-
- selectDefaultSubkey(vkbd: VisualKeyboard, baseKey: KeyElement, popupBase: HTMLElement) {
- var bk: KeyElement;
- let subkeys = baseKey['subKeys'];
- for(let i=0; i < subkeys.length; i++) {
- let skSpec = subkeys[i];
- let skElement = popupBase.childNodes[i].firstChild;
-
- // Preference order:
- // #1: if a default subkey has been specified, select it. (pending, for 15.0+)
- // #2: if no default subkey is specified, default to a subkey with the same
- // key ID and layer / modifier spec.
- //if(skSpec.isDefault) { TODO for 15.0
- // bk = skElement;
- // break;
- //} else
- if(!baseKey.key || !baseKey.key.spec) {
- continue;
- }
-
- if(skSpec.elementID == baseKey.key.spec.elementID) {
- bk = skElement;
- break; // Best possible match has been found. (Disable 'break' once above block is implemented.)
- }
- }
-
- if(bk) {
- vkbd.keyPending = bk;
- // Subkeys never get key previews, so we can directly highlight the subkey.
- bk.key.highlight(true);
- }
- }
-
- isVisible(): boolean {
- return this.element.style.visibility == 'visible';
- }
-
- clear() {
- // Discard the reference to the Promise's resolve method, allowing
- // GC to clean it up. The corresponding Promise's contract allows
- // passive cancellation.
- this.resolver = null;
-
- // Remove the displayed subkey array, if any
- if(this.element.parentNode) {
- this.element.parentNode.removeChild(this.element);
- }
-
- if(this.shim.parentNode) {
- this.shim.parentNode.removeChild(this.shim);
- }
-
- if(this.callout && this.callout.parentNode) {
- this.callout.parentNode.removeChild(this.callout);
- }
- }
-
- updateTouch(input: InputEventCoordinate) {
- this.currentSelection = null;
- this.baseKey.key.highlight(false);
-
- for(let i=0; i < this.baseKey['subKeys'].length; i++) {
- try {
- let sk = this.element.childNodes[i].firstChild as KeyElement;
-
- let onKey = sk.key.isUnderTouch(input);
- if(onKey) {
- this.currentSelection = sk;
- }
- sk.key.highlight(onKey);
- } catch(ex) {
- if(ex.message) {
- console.error("Unexpected error when attempting to update selected subkey:" + ex.message);
- } else {
- console.error("Unexpected error (and error type) when attempting to update selected subkey.");
- }
- }
- }
-
- // Use the popup duplicate of the base key if a phone with a visible popup array
- if(!this.currentSelection && this.baseKey.key.isUnderTouch(input)) {
- this.baseKey.key.highlight(true);
- this.currentSelection = this.baseKey;
- }
- }
- }
-}
diff --git a/web/src/engine/main/osk/emptyView.ts b/web/src/engine/main/osk/emptyView.ts
deleted file mode 100644
index 8f2e56638d..0000000000
--- a/web/src/engine/main/osk/emptyView.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-///
-
-namespace com.keyman.osk {
- export class EmptyView implements KeyboardView {
- readonly element: HTMLDivElement;
-
- constructor() {
- let Ldiv = this.element = document.createElement('div');
- Ldiv.style.userSelect = 'none';
- Ldiv.className='kmw-osk-none';
- }
-
- // No operations needed; this is a stand-in for the desktop OSK when no keyboard is active.
- public postInsert() { }
- public updateState() { }
-
- public refreshLayout() { }
-
- public get layoutHeight(): ParsedLengthStyle {
- return ParsedLengthStyle.inPixels(0);
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/floatingOskView.ts b/web/src/engine/main/osk/floatingOskView.ts
deleted file mode 100644
index d4d24ef77e..0000000000
--- a/web/src/engine/main/osk/floatingOskView.ts
+++ /dev/null
@@ -1,573 +0,0 @@
-// Includes KMW-added property declaration extensions for HTML elements.
-///
-// Includes the touch-mode language picker UI.
-///
-///
-// Defines desktop-centric OSK positioning + sizing behavior
-///
-///
-
-/***
- KeymanWeb 10.0
- Copyright 2017 SIL International
-***/
-
-namespace com.keyman.osk {
- type OSKRect = {'left'?: number, 'top'?: number, 'width'?: number, 'height'?: number,
- 'nosize'?: boolean, 'nomove'?: boolean};
- type OSKPos = {'left'?: number, 'top'?: number};
-
- export class FloatingOSKView extends OSKView {
- readonly desktopLayout: layouts.TargetedFloatLayout;
-
- // OSK positioning fields
- userPositioned: boolean = false;
- specifiedPosition: boolean = false;
- x: number;
- y: number;
- noDrag: boolean = false;
- dfltX: string;
- dfltY: string;
-
- // Key code definition aliases for legacy keyboards (They expect window['keyman']['osk'].___)
- modifierCodes = text.Codes.modifierCodes;
- modifierBitmasks = text.Codes.modifierBitmasks;
- stateBitmasks = text.Codes.stateBitmasks;
- keyCodes = text.Codes.keyCodes;
-
- public constructor(modeledDevice: utils.DeviceSpec) {
- super(modeledDevice);
-
- document.body.appendChild(this._Box);
-
- this.loadCookie();
-
- // Add header element to OSK only for desktop browsers
- const layout = this.desktopLayout = new layouts.TargetedFloatLayout();
- this.headerView = layout.titleBar;
- layout.titleBar.attachHandlers(this);
- }
-
- /**
- * Function _Unload
- * Scope Private
- * Description Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)
- */
- _Unload() {
- this.keyboardView = null;
- this.bannerView = null;
- this._Box = null;
- }
-
- protected setBoxStyling() {
- const s = this._Box.style;
-
- s.zIndex = '9999';
- s.display = 'none';
- s.width = 'auto';
- s.position = 'absolute';
- }
-
- protected postKeyboardLoad() {
- this._Visible = false; // I3363 (Build 301)
-
- this._Box.onmouseover = this._VKbdMouseOver;
- this._Box.onmouseout = this._VKbdMouseOut;
-
- // Add header element to OSK only for desktop browsers
- const layout = this.desktopLayout;
- layout.attachToView(this);
- if(this.activeKeyboard) {
- this.desktopLayout.titleBar.setTitleFromKeyboard(this.activeKeyboard);
- }
-
- if(this.vkbd) {
- this.footerView = layout.resizeBar;
- this._Box.appendChild(this.footerView.element);
- } else {
- if(this.footerView) {
- this._Box.removeChild(this.footerView.element);
- }
- this.footerView = null;
- }
-
- this.loadCookie();
- this.setNeedsLayout();
-
- if(this.displayIfActive) {
- this.present();
- }
- }
-
- /**
- * Function restorePosition
- * Scope Public
- * @param {boolean?} keepDefaultPosition If true, does not reset the default x,y set by `setRect`.
- * If false or omitted, resets the default x,y as well.
- * Description Move OSK back to default position, floating under active input element
- */
- ['restorePosition']: (keepDefaultPosition?: boolean) => void = function(this: FloatingOSKView, keepDefaultPosition?: boolean) {
- let isVisible = this._Visible;
- if(isVisible && this.activeTarget instanceof dom.targets.OutputTarget) {
- this.activeTarget?.focus(); // I2036 - OSK does not unpin to correct location
- }
-
- this.loadCookie();
- this.userPositioned=false;
- if(!keepDefaultPosition) {
- delete this.dfltX;
- delete this.dfltY;
- }
- this.saveCookie();
-
- if(isVisible) {
- this.present();
- }
-
- this.doResizeMove(); //allow the UI to respond to OSK movements
- this.desktopLayout.titleBar.showPin(false);
- }.bind(this);
-
- /**
- * Function enabled
- * Scope Public
- * @return {boolean|number} True if KMW OSK enabled
- * Description Test if KMW OSK is enabled
- */
- ['isEnabled'](): boolean {
- return this.displayIfActive;
- }
-
- /**
- * Function isVisible
- * Scope Public
- * @return {boolean|number} True if KMW OSK visible
- * Description Test if KMW OSK is actually visible
- * Note that this will usually return false after any UI event that results in (temporary) loss of input focus
- */
- ['isVisible'](): boolean {
- return this._Visible;
- }
-
- /**
- * Function _VKbdMouseOver
- * Scope Private
- * @param {Object} e event
- * Description Activate the KMW UI on mouse over
- */
- private _VKbdMouseOver = function(this: AnchoredOSKView, e) {
- com.keyman.singleton.uiManager.setActivatingUI(true);
- }.bind(this);
-
- /**
- * Function _VKbdMouseOut
- * Scope Private
- * @param {Object} e event
- * Description Cancel activation of KMW UI on mouse out
- */
- private _VKbdMouseOut = function(this: AnchoredOSKView, e) {
- com.keyman.singleton.uiManager.setActivatingUI(false);
- }.bind(this);
-
- /**
- * Save size, position, font size and visibility of OSK
- */
- saveCookie() {
- let util = com.keyman.singleton.util;
-
- var c = util.loadCookie('KeymanWeb_OnScreenKeyboard');
- var p = this.getPos();
-
- c['visible'] = this.displayIfActive ? 1 : 0;
- c['userSet'] = this.userPositioned ? 1 : 0;
- c['left'] = p.left;
- c['top'] = p.top;
- c['_version'] = utils.Version.CURRENT.toString();
-
- if(this.vkbd) {
- c['width'] = this.width.val;
- c['height'] = this.height.val;
- }
-
- util.saveCookie('KeymanWeb_OnScreenKeyboard',c);
- }
-
- /**
- * Restore size, position, font size and visibility of desktop OSK
- *
- * @return {boolean}
- */
- loadCookie(): void {
- let util = com.keyman.singleton.util;
-
- var c = util.loadCookie('KeymanWeb_OnScreenKeyboard');
-
- this.displayIfActive = util.toNumber(c['visible'], 1) == 1;
- this.userPositioned = util.toNumber(c['userSet'], 0) == 1;
- this.x = util.toNumber(c['left'],-1);
- this.y = util.toNumber(c['top'],-1);
- let cookieVersionString = c['_version'];
-
- // Restore OSK size - font size now fixed in relation to OSK height, unless overridden (in em) by keyboard
- let dfltWidth=0.3*screen.width;
- let dfltHeight=0.15*screen.height;
- //if(util.toNumber(c['width'],0) == 0) dfltWidth=0.5*screen.width;
- let newWidth = parseInt(c['width'], 10);
- let newHeight = parseInt(c['height'], 10);
- let isNewCookie = isNaN(newHeight);
- newWidth = isNaN(newWidth) ? dfltWidth : newWidth;
- newHeight = isNaN(newHeight) ? dfltHeight : newHeight;
-
- // Limit the OSK dimensions to reasonable values
- if(newWidth < 0.2*screen.width) {
- newWidth = 0.2*screen.width;
- }
- if(newHeight < 0.1*screen.height) {
- newHeight = 0.1*screen.height;
- }
- if(newWidth > 0.9*screen.width) {
- newWidth=0.9*screen.width;
- }
- if(newHeight > 0.5*screen.height) {
- newHeight=0.5*screen.height;
- }
-
- // if(!cookieVersionString) - this component was not tracked until 15.0.
- // Before that point, the OSK's title bar and resize bar heights were not included
- // in the OSK's cookie-persisted height.
- if(isNewCookie || !cookieVersionString) {
- // Adds some space to account for the OSK's header and footer, should they exist.
- if(this.headerView && this.headerView.layoutHeight.absolute) {
- newHeight += this.headerView.layoutHeight.val;
- }
-
- if(this.footerView && this.footerView.layoutHeight.absolute) {
- newHeight += this.footerView.layoutHeight.val;
- }
- }
-
- this.setSize(newWidth, newHeight);
-
- // and OSK position if user located
- if(this.x == -1 || this.y == -1 || (!this._Box)) {
- this.userPositioned = false;
- }
-
- if(this.x < window.pageXOffset-0.8*newWidth) {
- this.x=window.pageXOffset-0.8*newWidth;
- }
- if(this.y < 0) {
- this.x=-1;
- this.y=-1;
- this.userPositioned=false;
- }
-
- if(this.userPositioned && this._Box) {
- this.setPos({'left': this.x, 'top': this.y});
- }
- }
-
- /**
- * Get the wanted height of the OSK for touch devices (does not include banner height)
- * @return {number} height in pixels
- **/
- getDefaultKeyboardHeight(): number {
- let keymanweb = com.keyman.singleton;
- let device = keymanweb.util.device;
-
- // KeymanTouch - get OSK height from device
- if(typeof(keymanweb['getOskHeight']) == 'function') {
- return keymanweb['getOskHeight']();
- }
-
- var oskHeightLandscapeView=Math.floor(Math.min(screen.availHeight,screen.availWidth)/2),
- height=oskHeightLandscapeView;
-
- if(device.formFactor == 'phone') {
- var sx=Math.min(screen.height,screen.width),
- sy=Math.max(screen.height,screen.width);
-
- if(keymanweb.util.portraitView())
- height=Math.floor(Math.max(screen.availHeight,screen.availWidth)/3);
- else
- height=height*(sy/sx)/1.6; //adjust for aspect ratio, increase slightly for iPhone 5
- }
-
- // Correct for viewport scaling (iOS - Android 4.2 does not want this, at least on Galaxy Tab 3))
- if(device.OS == 'iOS') {
- height=height/keymanweb.util.getViewportScale();
- }
-
- return height;
- }
-
- /**
- * Get the wanted width of the OSK for touch devices
- *
- * @return {number} height in pixels
- **/
- getDefaultWidth(): number {
- let keymanweb = com.keyman.singleton;
- let device = keymanweb.util.device;
-
- // KeymanTouch - get OSK height from device
- if(typeof(keymanweb['getOskWidth']) == 'function') {
- return keymanweb['getOskWidth']();
- }
-
- var width: number;
- if(device.OS == 'iOS') {
- // iOS does not interchange these values when the orientation changes!
- //width = util.portraitView() ? screen.width : screen.height;
- width = window.innerWidth;
- } else if(device.OS == 'Android') {
- try {
- width=document.documentElement.clientWidth;
- } catch(ex) {
- width=screen.availWidth;
- }
- } else {
- width=screen.width;
- }
-
- return width;
- }
-
- /**
- * Allow UI to update OSK position and properties
- *
- * @param {Object=} p object with coordinates and userdefined flag
- *
- */
- doResizeMove(p?) {
- return com.keyman.singleton.util.callEvent('osk.resizemove',p);
- }
-
- /**
- * Allow the UI or page to set the position and size of the OSK
- * and (optionally) override user repositioning or sizing
- *
- * @param {Object.} p Array object with position and size of OSK container
- **/
- ['setRect'](p: OSKRect) {
- let util = com.keyman.singleton.util;
- if(this._Box == null || util.device.formFactor != 'desktop') {
- return;
- }
-
- var b = this._Box, bs = b.style;
- if('left' in p) {
- this.x = p['left'] - dom.Utils.getAbsoluteX(b) + b.offsetLeft;
- bs.left= this.x + 'px';
- this.dfltX=bs.left;
- }
-
- if('top' in p) {
- this.y = p['top'] - dom.Utils.getAbsoluteY(b) + b.offsetTop;
- bs.top = this.y + 'px';
- this.dfltY=bs.top;
- }
-
- //Do not allow user resizing for non-standard keyboards (e.g. EuroLatin)
- if(this.vkbd != null) {
- var d=this.vkbd.kbdDiv, ds=d.style;
-
- // Set width, but limit to reasonable value
- if('width' in p) {
- var w=(p['width']-(b.offsetWidth-d.offsetWidth));
- if(w < 0.2*screen.width) {
- w=0.2*screen.width;
- }
- if(w > 0.9*screen.width) {
- w=0.9*screen.width;
- }
- ds.width=w+'px';
- // Use of the `computed` variant is here temporary.
- // Shouldn't use `setSize` for this in the long-term.
- this.setSize(w, this.computedHeight, true);
- }
-
- // Set height, but limit to reasonable value
- // This sets the default font size for the OSK in px, but that
- // can be modified at the key text level by setting
- // the font size in em in the kmw-key-text class
- if('height' in p) {
- var h=(p['height']-(b.offsetHeight-d.offsetHeight));
- if(h < 0.1*screen.height) {
- h=0.1*screen.height;
- }
- if(h > 0.5*screen.height) {
- h=0.5*screen.height;
- }
- ds.height=h+'px'; ds.fontSize=(h/8)+'px';
- // Use of the `computed` variant is here temporary.
- // Shouldn't use `setSize` for this in the long-term.
- this.setSize(this.computedWidth, h, true);
- }
-
- // Fix or release user resizing
- if('nosize' in p) {
- this.desktopLayout.resizingEnabled = !p['nosize'];
- }
-
- }
- // Fix or release user dragging
- if('nomove' in p) {
- this.noDrag=p['nomove'];
- this.desktopLayout.movementEnabled = !this.noDrag;
- }
- // Save the user-defined OSK size
- this.saveCookie();
- }
-
- /**
- * Get position of OSK window
- *
- * @return {Object.} Array object with OSK window position
- **/
- getPos(): OSKPos {
- var Lkbd=this._Box, p={
- left: this._Visible ? Lkbd.offsetLeft : this.x,
- top: this._Visible ? Lkbd.offsetTop : this.y
- };
-
- return p;
- }
-
- /**
- * Function setPos
- * Scope Private
- * @param {Object.} p Array object with OSK left, top
- * Description Set position of OSK window, but limit to screen, and ignore if a touch input device
- */
- ['setPos'](p: OSKPos) {
- if(typeof(this._Box) == 'undefined') {
- return; // I3363 (Build 301)
- }
-
- if(this.userPositioned) {
- var Px=p['left'], Py=p['top'];
-
- if(typeof(Px) != 'undefined') {
- if(Px < -0.8*this._Box.offsetWidth) {
- Px = -0.8*this._Box.offsetWidth;
- }
- if(this.userPositioned) {
- this._Box.style.left=Px+'px';
- this.x = Px;
- }
- }
- // May not be needed - vertical positioning is handled differently and defaults to input field if off screen
- if(typeof(Py) != 'undefined') {
- if(Py < 0) {
- Py = 0;
- }
-
- if(this.userPositioned) {
- this._Box.style.top=Py+'px';
- this.y = Py;
- }
- }
- }
-
- if(this.desktopLayout) {
- this.desktopLayout.titleBar.showPin(this.userPositioned);
- }
- }
-
- public setDisplayPositioning() {
- var Ls = this._Box.style;
-
- Ls.position='absolute'; Ls.display='block'; //Ls.visibility='visible';
- Ls.left='0px';
- if(this.specifiedPosition || this.userPositioned) {
- Ls.left = this.x+'px';
- Ls.top = this.y+'px';
- } else {
- let el: HTMLElement = null;
- if(this.activeTarget instanceof dom.targets.OutputTarget) {
- el = this.activeTarget?.getElement();
- }
-
- if(this.dfltX) {
- Ls.left=this.dfltX;
- } else if(typeof el != 'undefined' && el != null) {
- Ls.left=dom.Utils.getAbsoluteX(el) + 'px';
- }
-
- if(this.dfltY) {
- Ls.top=this.dfltY;
- } else if(typeof el != 'undefined' && el != null) {
- Ls.top=(dom.Utils.getAbsoluteY(el) + el.offsetHeight)+'px';
- }
- }
-
- // Unset the flag, keeping 'specified position' specific to single
- // presentAtPosition calls.
- this.specifiedPosition = false;
- }
-
- /**
- * Display KMW OSK at specified position (returns nothing)
- *
- * @param {number=} Px x-coordinate for OSK rectangle
- * @param {number=} Py y-coordinate for OSK rectangle
- */
- presentAtPosition(Px?: number, Py?: number) {
- if(!this.mayShow()) {
- return;
- }
-
- this.specifiedPosition = Px >= 0 || Py >= 0; //probably never happens, legacy support only
- if(this.specifiedPosition) {
- this.x = Px;
- this.y = Py;
- }
-
- // Combines the two paths with set positioning.
- this.specifiedPosition = this.specifiedPosition || this.userPositioned;
-
- this.present();
- }
-
- present() {
- if(!this.mayShow()) {
- return;
- }
-
- this.desktopLayout.titleBar.showPin(this.userPositioned);
-
- super.present();
-
- // Allow desktop UI to execute code when showing the OSK
- var Lpos={};
- Lpos['x']=this._Box.offsetLeft;
- Lpos['y']=this._Box.offsetTop;
- Lpos['userLocated']=this.userPositioned;
- this.doShow(Lpos);
- }
-
- public startHide(hiddenByUser: boolean) {
- super.startHide(hiddenByUser);
-
- if(hiddenByUser) {
- this.saveCookie(); // Save current OSK state, size and position (desktop only)
- }
- }
-
- ['show'](bShow: boolean) {
- super['show'](bShow);
- this.saveCookie();
- }
-
- /**
- * Function userPositioned
- * Scope Public
- * @return {(boolean|number)} true if user located
- * Description Test if OSK window has been repositioned by user
- */
- ['userLocated']() {
- return this.userPositioned;
- }
- }
-}
diff --git a/web/src/engine/main/osk/globehint.interface.ts b/web/src/engine/main/osk/globehint.interface.ts
deleted file mode 100644
index 6bf39371b4..0000000000
--- a/web/src/engine/main/osk/globehint.interface.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace com.keyman.osk {
- export interface GlobeHint {
- text: string;
- state: boolean;
- element?: HTMLDivElement;
-
- show(key: KeyElement, onDismiss?: () => void);
- hide(key: KeyElement);
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/helpPageView.ts b/web/src/engine/main/osk/helpPageView.ts
deleted file mode 100644
index 28ffe38462..0000000000
--- a/web/src/engine/main/osk/helpPageView.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-///
-
-namespace com.keyman.osk {
- export class HelpPageView implements KeyboardView {
- private readonly kbd: keyboards.Keyboard;
- public readonly element: HTMLDivElement;
-
- private static readonly ID = 'kmw-osk-help-page';
-
- constructor(keyboard: keyboards.Keyboard) {
- this.kbd = keyboard;
-
- var Ldiv = this.element = document.createElement('div');
- Ldiv.style.userSelect = "none";
- Ldiv.className = 'kmw-osk-static';
- Ldiv.id = HelpPageView.ID;
- Ldiv.innerHTML = keyboard.helpText;
- }
-
- public postInsert() {
- if(!this.element.parentElement || !document.getElementById(HelpPageView.ID)) {
- throw new Error("The HelpPage root element has not yet been inserted into the DOM.");
- }
-
- if(this.kbd.hasScript) {
- // .parentElement: ensure this matches the _Box element from OSKManager / OSKView
- // Not a hard requirement for any known keyboards, but is asserted by legacy docs.
- this.kbd.embedScript(this.element.parentElement);
- }
- }
-
- public updateState() { }
- public refreshLayout() { }
-
- public get layoutHeight(): ParsedLengthStyle {
- return ParsedLengthStyle.inPercent(100);
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/inlinedOskView.ts b/web/src/engine/main/osk/inlinedOskView.ts
deleted file mode 100644
index 12dad81035..0000000000
--- a/web/src/engine/main/osk/inlinedOskView.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-// Includes KMW-added property declaration extensions for HTML elements.
-///
-// Includes the touch-mode language picker UI.
-///
-///
-// Defines desktop-centric OSK positioning + sizing behavior
-///
-///
-
-/*
- * Keyman is copyright (c) SIL International. MIT License.
- */
-
-namespace com.keyman.osk {
- type OSKPos = {'left'?: number, 'top'?: number};
-
- /**
- * Defines a version of the OSK that produces an element designed for site-controlled
- * insertion into the DOM. Rather than "floating" over the page, this version is inlined
- * as part of the host page's layout.
- */
- export class InlinedOSKView extends OSKView {
- // Key code definition aliases for legacy keyboards (They expect window['keyman']['osk'].___)
- modifierCodes = text.Codes.modifierCodes;
- modifierBitmasks = text.Codes.modifierBitmasks;
- stateBitmasks = text.Codes.stateBitmasks;
- keyCodes = text.Codes.keyCodes;
-
- public constructor(modeledDevice: utils.DeviceSpec, hostDevice?: utils.DeviceSpec) {
- super(modeledDevice, hostDevice);
-
- this.activationMode = ActivationMode.manual;
- }
-
- public get element(): HTMLDivElement {
- return this._Box;
- }
-
- /**
- * Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)
- *
- * This should probably be merged or incorporated into the `shutdown` method at some point.
- */
- _Unload() {
- this.keyboardView = null;
- this.bannerView = null;
- this._Box = null;
- }
-
- protected setBoxStyling() {
- const s = this._Box.style;
- s.display = 'none';
- // Positioned with no relative offset from its default position.
- // This allows _Box to still serve as an offsetParent for keytip & subkey menu positioning.
- s.position = 'relative';
- }
-
- protected postKeyboardLoad() {
- this._Visible = false; // I3363 (Build 301)
-
- this._Box.onmouseover = this._VKbdMouseOver;
- this._Box.onmouseout = this._VKbdMouseOut;
-
- if(this.displayIfActive) {
- this.present();
- }
- }
-
- /**
- * Moves the OSK back to default position, floating under active input element
- *
- * Is a long-published API intended solely for use with the FloatingOSKView use pattern.
- * @param keepDefaultPosition If true, does not reset the default x,y set by `setRect`.
- * If false or omitted, resets the default x,y as well.
- */
- ['restorePosition']: (keepDefaultPosition?: boolean) => void = function(this: AnchoredOSKView, keepDefaultPosition?: boolean) {
- return;
- }.bind(this);
-
- /**
- * Activates the KMW UI on mouse over, allowing DOMManager to preserve the
- * active element's (conceptual) focus during OSK interactions.
- */
- private _VKbdMouseOver = function(this: AnchoredOSKView, e) {
- com.keyman.singleton.uiManager.setActivatingUI(true);
- }.bind(this);
-
- /**
- * Cancels activation of the KMW UI on mouse out, which is used to disable
- * DOMManager's focus-preservation mode.
- *
- * @see _VKbdMouseOver
- */
- private _VKbdMouseOut = function(this: AnchoredOSKView, e) {
- com.keyman.singleton.uiManager.setActivatingUI(false);
- }.bind(this);
-
- /**
- * Get the default height for the OSK
- * @return height in pixels
- **/
- getDefaultKeyboardHeight(): number {
- if(this.keyboardView instanceof VisualKeyboard) {
- return this.keyboardView.height;
- } else {
- // Should probably refine, but it's a decent stopgap.
- return this.computedHeight;
- }
- }
-
- /**
- * Get the default width for the OSK
- * @return width in pixels
- **/
- getDefaultWidth(): number {
- return this.computedWidth;
- }
-
- /**
- * Allow the UI or page to set the position and size of the OSK
- * and (optionally) override user repositioning or sizing
- *
- * Designed solely for use with the FloatingOSKView use pattern, but is a
- * long-standing API endpoint that needs preservation.
- *
- * @param p Array object with position and size of OSK container
- **/
- ['setRect'](p: OSKRect) {
- return;
- }
-
- /**
- * Get position of OSK window
- *
- * @return Array object with OSK window position
- **/
- getPos(): OSKPos {
- var Lkbd=this._Box, p={
- left: this._Visible ? Lkbd.offsetLeft : undefined,
- top: this._Visible ? Lkbd.offsetTop : undefined
- };
-
- return p;
- }
-
- /**
- * Set position of OSK window, but limited to the screen.
- *
- * Designed solely for use with the FloatingOSKView use pattern, but is a
- * long-standing API endpoint that needs preservation.
- * @param p Array object with OSK left, top
- */
- ['setPos'](p: OSKPos) {
- return; // I3363 (Build 301)
- }
-
- protected setDisplayPositioning() {
- // no-op; an inlined OSK cannot control its own positioning.
- }
-
- /**
- * Allow UI to respond to OSK being shown (passing position and properties)
- *
- * @param p object with coordinates and userdefined flag
- * @return
- *
- */
- doShow(p) {
- return com.keyman.singleton.util.callEvent('osk.show',p);
- }
-
- /**
- * Allows UI modules to update state when the OSK is being hidden
- *
- * @param p object with coordinates and userdefined flag
- * @return
- */
- doHide(p) {
- return com.keyman.singleton.util.callEvent('osk.hide',p);
- }
-
- protected allowsDeviceChange(newSpec: com.keyman.utils.DeviceSpec): boolean {
- return true;
- }
- }
-}
diff --git a/web/src/engine/main/osk/inputEventCoordinate.ts b/web/src/engine/main/osk/inputEventCoordinate.ts
deleted file mode 100644
index 95cac39e04..0000000000
--- a/web/src/engine/main/osk/inputEventCoordinate.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-namespace com.keyman.osk {
- /**
- * Represents the current location of the current cursor / touchpoint during
- * an ongoing OSK input event. This class standardizes to .pageX (document)
- * coordinates, rather than .clientX (viewport) coordinates.
- */
- export class InputEventCoordinate {
- public readonly x: number;
- public readonly y: number;
-
- private readonly source: MouseEvent | TouchEvent;
-
- public constructor(x: number, y: number, source?: MouseEvent | TouchEvent) {
- this.x = x;
- this.y = y;
-
- if(source) {
- this.source = source;
- }
- }
-
- // Converts a MouseEvent or TouchEvent into the base coordinates needed
- // by the mouse-dragging operations.
- public static fromEvent(e: MouseEvent | TouchEvent) {
- let coordSource: MouseEvent | Touch;
-
- // Desktop Safari versions as recent as 14.1 do not support TouchEvents.
- // So, just in case, a two-fold conditional check to avoid issues with a direct
- // 'instanceof' against the type.
- if(window['TouchEvent'] && e instanceof TouchEvent) {
- coordSource = e.changedTouches[0];
- } else if(e['changedTouches']) {
- coordSource = e['changedTouches'][0] as Touch;
- } else {
- coordSource = e as MouseEvent;
- }
-
- // For MouseEvents, .pageX is slightly less supported in older browsers when
- // compared to .clientX. They're about equally supported for TouchEvents.
- if (coordSource.pageX) {
- return new InputEventCoordinate(coordSource.pageX, coordSource.pageY, e);
- } else if (coordSource.clientX) {
- const x = coordSource.clientX + document.body.scrollLeft;
- const y = coordSource.clientY + document.body.scrollTop;
-
- return new InputEventCoordinate(x, y, e);
- } else {
- return new InputEventCoordinate(null, null, e);
- }
- }
-
- public get activeInputCount(): number {
- // May not be an ACTUAL touch event during unit tests.
- if(window['TouchEvent'] && this.source['touches'] !== undefined && this.source['touches'] !== null) {
- return this.source['touches'].length;
- } else {
- const event = this.source as MouseEvent;
- return event.buttons > 0 ? 1 : 0;
- }
- }
-
- public get target() {
- return this.source?.target;
- }
-
- public get isFromTouch(): boolean {
- return !this.isFromMouse;
- }
-
- public get isFromMouse(): boolean {
- return this.source instanceof MouseEvent;
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/inputEventEngine.ts b/web/src/engine/main/osk/inputEventEngine.ts
deleted file mode 100644
index 82e4e501c2..0000000000
--- a/web/src/engine/main/osk/inputEventEngine.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-///
-
-namespace com.keyman.osk {
- export type InputHandler = (coord: InputEventCoordinate) => void;
-
- export interface InputEventEngineConfig {
- /**
- * Specifies the element that input listeners should be attached to.
- */
- readonly eventRoot: HTMLElement;
- /**
- * Specifies the most specific common ancestor element of any event target
- * that the InputEventEngine should consider.
- */
- readonly targetRoot: HTMLElement;
-
- readonly coordConstrainedWithinInteractiveBounds: (coord: InputEventCoordinate) => boolean;
-
- readonly inputStartHandler?: InputHandler;
- readonly inputMoveHandler?: InputHandler;
- readonly inputMoveCancelHandler?: InputHandler;
- readonly inputEndHandler?: InputHandler;
- }
-
- export abstract class InputEventEngine {
- protected readonly config: InputEventEngineConfig;
-
- public constructor(config: InputEventEngineConfig) {
- this.config = config;
- }
-
- abstract registerEventHandlers();
- abstract unregisterEventHandlers();
-
- onInputStart(coord: InputEventCoordinate) {
- if(this.config.inputStartHandler) {
- this.config.inputStartHandler(coord);
- }
- }
-
- onInputMove(coord: InputEventCoordinate) {
- if(this.config.inputMoveHandler) {
- this.config.inputMoveHandler(coord);
- }
- }
-
- onInputMoveCancel(coord: InputEventCoordinate) {
- if(this.config.inputMoveCancelHandler) {
- this.config.inputMoveCancelHandler(coord);
- }
- }
-
- onInputEnd(coord: InputEventCoordinate) {
- if(this.config.inputEndHandler) {
- this.config.inputEndHandler(coord);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/keyboardView.interface.ts b/web/src/engine/main/osk/keyboardView.interface.ts
deleted file mode 100644
index 5384cfa9dd..0000000000
--- a/web/src/engine/main/osk/keyboardView.interface.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace com.keyman.osk {
- /**
- * An abstract representation for visualizations of the active keyboard within an
- * OSKManager / OSKView. Most keyboards will default to use of a VisualKeyboard,
- * though some will use HelpPage for certain form factors.
- */
- export interface KeyboardView extends OSKViewComponent {
- readonly element: HTMLDivElement;
-
- /**
- * Evaluates code that must be run _after_ the KeyboardView has been inserted into
- * the DOM hierarchy.
- */
- postInsert(): void;
-
- /**
- * Code that updates the state of the KeyboardView whenever the OSK itself needs to be
- * refreshed or updated with new state information.
- */
- updateState(): void;
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/keytip.interface.ts b/web/src/engine/main/osk/keytip.interface.ts
deleted file mode 100644
index 1e8468b712..0000000000
--- a/web/src/engine/main/osk/keytip.interface.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace com.keyman.osk {
- export interface KeyTip {
- key: KeyElement;
- state: boolean;
- element?: HTMLDivElement;
-
- show(key: KeyElement, on: boolean, vkbd: VisualKeyboard);
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/layouts/mouseDragOperation.ts b/web/src/engine/main/osk/layouts/mouseDragOperation.ts
deleted file mode 100644
index 01777ffe9f..0000000000
--- a/web/src/engine/main/osk/layouts/mouseDragOperation.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-///
-
-namespace com.keyman.osk.layouts {
-
-
- type MouseHandler = (this: GlobalEventHandlers, ev: MouseEvent) => any;
-
- /**
- * Used to store the page's original mouse handlers and properties
- * when temporarily overridden by OSK moving or resizing handlers due
- * to user interaction.
- */
- class MouseStartSnapshot {
- private readonly _VPreviousMouseMove: MouseHandler;
- private readonly _VPreviousMouseUp: MouseHandler;
- private readonly _VPreviousCursor: string;
- private readonly _VPreviousMouseButton: number;
-
- constructor(e: MouseEvent) {
- this._VPreviousMouseMove = document.onmousemove;
- this._VPreviousMouseUp = document.onmouseup;
-
- this._VPreviousCursor = document.body.style.cursor;
- this._VPreviousMouseButton = (typeof(e.which)=='undefined' ? e.button : e.which);
- }
-
- restore() {
- document.onmousemove = this._VPreviousMouseMove;
- document.onmouseup = this._VPreviousMouseUp;
-
- if(document.body.style.cursor) {
- document.body.style.cursor = this._VPreviousCursor;
- }
- }
-
- matchesCausingClick(e: MouseEvent): boolean {
- return this._VPreviousMouseButton == (typeof(e.which)=='undefined' ? e.button : e.which);
- }
- }
-
- export abstract class MouseDragOperation {
- private _enabled: boolean;
- private _startCoord: InputEventCoordinate;
- private _mouseStartSnapshot: MouseStartSnapshot;
-
- private startHandler: (e: MouseEvent) => void;
- private cursorType: string;
-
- public constructor(cursorType?: string) {
- this.startHandler = this._VMoveMouseDown.bind(this);
- this.cursorType = cursorType;
- }
-
- /**
- * Denotes whether or not this object should handle incoming events.
- */
- public get enabled(): boolean {
- return this._enabled;
- }
-
- public set enabled(flag: boolean) {
- this._enabled = flag;
- }
-
- /**
- * Denotes whether or not this object is currently handling an ongoing drag event.
- */
- public get isActive(): boolean {
- return !!this._mouseStartSnapshot;
- }
-
- public get mouseDownHandler(): (e: MouseEvent) => void {
- return this.startHandler;
- }
-
- /**
- * Function _VMoveMouseDown
- * Scope Private
- * @param {Object} e event
- * Description Process mouse down on OSK
- */
- private _VMoveMouseDown(e: MouseEvent) {
- if(!e) {
- return true;
- }
-
- if(!this._enabled) {
- return true;
- }
-
- if(!this._mouseStartSnapshot) { // I1472 - Dragging off edge of browser window causes muckup
- this._mouseStartSnapshot = new MouseStartSnapshot(e);
- }
-
- this._startCoord = InputEventCoordinate.fromEvent(e);
-
- document.onmousemove = this._VMoveMouseMove.bind(this);
- document.onmouseup = this._VMoveMouseUp.bind(this);
- if(document.body.style.cursor) {
- document.body.style.cursor = this.cursorType;
- }
-
- e.preventDefault();
- e.cancelBubble = true;
-
- this.onDragStart();
- return false;
- }
-
- protected abstract onDragStart();
-
- /**
- * Process mouse drag on OSK
- *
- * @param {Object} e event
- */
- private _VMoveMouseMove(e: MouseEvent) {
- if(!e) {
- return true;
- }
-
- if(!this.enabled) {
- return true;
- }
-
- e.preventDefault();
- e.cancelBubble = true;
-
- if(!this._mouseStartSnapshot.matchesCausingClick(e)) { // I1472 - Dragging off edge of browser window causes muckup
- return this._VMoveMouseUp(e);
- } else {
- const coord = InputEventCoordinate.fromEvent(e);
- const deltaX = coord.x - this._startCoord.x;
- const deltaY = coord.y - this._startCoord.y;
-
- this.onDragMove(deltaX, deltaY);
- return false;
- }
- }
-
- /**
- *
- * @param deltaX The total horizontal distance moved, in pixels, since the start of the drag
- * @param deltaY The total vertical distance moved, in pixels, since the start of the drag
- */
- protected abstract onDragMove(deltaX: number, deltaY: number);
-
- /**
- * Function _VMoveMouseUp
- * Scope Private
- * @param {Object} e event
- * Description Process mouse up during movement of KMW OSK UI
- */
- private _VMoveMouseUp(e: MouseEvent) {
- if(!e) {
- return true;
- }
-
- this._mouseStartSnapshot.restore();
- this._mouseStartSnapshot = null;
-
- e.preventDefault();
- e.cancelBubble = true;
-
- this.onDragRelease();
- return false;
- }
-
- protected abstract onDragRelease();
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/layouts/resizeBar.ts b/web/src/engine/main/osk/layouts/resizeBar.ts
deleted file mode 100644
index 8d64cc3793..0000000000
--- a/web/src/engine/main/osk/layouts/resizeBar.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-///
-
-namespace com.keyman.osk.layouts {
- export class ResizeBar implements OSKViewComponent {
- private _element: HTMLDivElement;
- private _resizeHandle: HTMLDivElement;
-
- private static readonly DISPLAY_HEIGHT = ParsedLengthStyle.inPixels(16); // As set in kmwosk.css
-
- private mouseCancellingHandler: (ev: MouseEvent) => boolean = function(ev: MouseEvent) {
- ev.preventDefault();
- ev.cancelBubble = true;
- return false;
- };
-
- public constructor(dragHandler?: MouseDragOperation) {
- this._element = this.buildResizeBar();
-
- if(dragHandler) {
- this._resizeHandle.onmousedown = dragHandler.mouseDownHandler;
- }
- }
-
- public get layoutHeight(): ParsedLengthStyle {
- return ResizeBar.DISPLAY_HEIGHT;
- }
-
- public get element(): HTMLDivElement {
- return this._element;
- }
-
- public get handle(): HTMLDivElement {
- return this._resizeHandle;
- }
-
- public allowResizing(flag: boolean) {
- this._resizeHandle.style.display = flag ? 'block' : 'none';
- }
-
- private markUnselectable(e: HTMLElement) {
- e.style.MozUserSelect="none";
- e.style.KhtmlUserSelect="none";
- e.style.UserSelect="none";
- e.style.WebkitUserSelect="none";
- }
-
- /**
- * Create a bottom bar with a resizing icon for the desktop OSK
- */
- buildResizeBar(): HTMLDivElement {
- let util = com.keyman.singleton.util;
- let osk = com.keyman.singleton.osk;
-
- var bar = document.createElement('div');
- this.markUnselectable(bar);
- bar.className='kmw-footer';
- bar.onmousedown = this.mouseCancellingHandler;
-
- // Add caption
- var Ltitle=document.createElement('div');
- this.markUnselectable(Ltitle);
- Ltitle.className='kmw-footer-caption';
- Ltitle.innerHTML='KeymanWeb';
- Ltitle.id='keymanweb-osk-footer-caption';
-
- // Display build number on shift+double click
- util.attachDOMEvent(Ltitle,'dblclick', function(e) {
- if(e && e.shiftKey) {
- osk.showBuild();
- }
- return false;
- }.bind(this),false);
-
- bar.appendChild(Ltitle);
-
- var Limg = document.createElement('div');
- this.markUnselectable(Limg);
- Limg.className='kmw-footer-resize';
- bar.appendChild(Limg);
- this._resizeHandle=Limg;
-
- return bar;
- }
-
- public refreshLayout() {
- // The title bar is adaptable as it is and needs no adjustments.
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/layouts/targetedFloatLayout.ts b/web/src/engine/main/osk/layouts/targetedFloatLayout.ts
deleted file mode 100644
index df3fe9b08f..0000000000
--- a/web/src/engine/main/osk/layouts/targetedFloatLayout.ts
+++ /dev/null
@@ -1,212 +0,0 @@
-///
-///
-///
-
-namespace com.keyman.osk.layouts {
- export class TargetedFloatLayout {
- titleBar: layouts.TitleBar;
- resizeBar: layouts.ResizeBar;
-
- private oskView: FloatingOSKView;
-
- // Encapsulations of the drag behaviors for OSK movement & resizing
- private _moveHandler: MouseDragOperation;
- private _resizeHandler: MouseDragOperation;
-
- public constructor() {
- this.titleBar = new layouts.TitleBar(this.titleDragHandler);
- this.resizeBar = new layouts.ResizeBar(this.resizeDragHandler);
- }
-
- public get movementEnabled(): boolean {
- return this.titleDragHandler.enabled;
- }
-
- public set movementEnabled(flag: boolean) {
- this.titleDragHandler.enabled = flag;
- this.titleBar.showPin(flag && this.oskView.userPositioned);
- }
-
- public get resizingEnabled(): boolean {
- return this.resizeDragHandler.enabled;
- }
-
- public set resizingEnabled(flag: boolean) {
- this.resizeDragHandler.enabled = flag;
- this.resizeBar.allowResizing(flag);
- }
-
- public get isBeingMoved(): boolean {
- return this.titleDragHandler.isActive;
- }
-
- public get isBeingResized(): boolean {
- return this.resizeDragHandler.isActive;
- }
-
- attachToView(view: FloatingOSKView) {
- this.oskView = view;
- this.titleBar.attachHandlers(view);
- this.titleDragHandler.enabled = !view.noDrag;
- this.resizeDragHandler.enabled = true; // by default.
- }
-
- private get titleDragHandler(): MouseDragOperation {
- const layout = this;
-
- if(this._moveHandler) {
- return this._moveHandler;
- }
-
- this._moveHandler = new class extends MouseDragOperation {
- startX: number;
- startY: number;
-
- constructor() {
- super('move'); // The type of cursor to use while 'active'.
- }
-
- onDragStart() {
- if(!layout.oskView) {
- return;
- }
-
- this.startX = layout.oskView._Box.offsetLeft;
- this.startY = layout.oskView._Box.offsetTop;
-
- let keymanweb = com.keyman.singleton;
- if(keymanweb.isCJK()) {
- layout.titleBar.setPinCJKOffset();
- }
-
- keymanweb.uiManager.justActivated = true;
- }
-
- // Note: _this.oskView may not be initialized yet.
- onDragMove(cumulativeX: number, cumulativeY: number) {
- if(!layout.oskView) {
- return;
- }
-
- layout.titleBar.showPin(true);
- layout.oskView.userPositioned = true;
-
- layout.oskView._Box.style.left = (this.startX + cumulativeX) + 'px';
- layout.oskView._Box.style.top = (this.startY + cumulativeY) + 'px';
-
- var r=layout.oskView.getRect();
- layout.oskView.setSize(r.width, r.height, true);
- layout.oskView.x = r.left;
- layout.oskView.y = r.top;
- }
-
- onDragRelease() {
- if(!layout.oskView) {
- return;
- }
-
- let keymanweb = com.keyman.singleton;
-
- keymanweb.domManager.focusLastActiveElement();
-
- keymanweb.uiManager.justActivated = false;
- keymanweb.uiManager.setActivatingUI(false);
-
- if(layout.oskView.vkbd) {
- layout.oskView.vkbd.currentKey=null;
- }
-
- layout.oskView.userPositioned = true;
- layout.oskView.doResizeMove();
- layout.oskView.saveCookie();
- }
- }
-
- return this._moveHandler;
- }
-
- private get resizeDragHandler(): MouseDragOperation {
- const layout = this;
-
- if(this._resizeHandler) {
- return this._resizeHandler;
- }
-
- this._resizeHandler = new class extends MouseDragOperation {
- startWidth: number;
- startHeight: number;
-
- constructor() {
- super('se-resize'); // The type of cursor to use while 'active'.
- }
-
- onDragStart() {
- if(!layout.oskView) {
- return;
- }
-
- this.startWidth = layout.oskView.computedWidth;
- this.startHeight = layout.oskView.computedHeight;
-
- let keymanweb = com.keyman.singleton;
-
- keymanweb.uiManager.justActivated = true;
- }
-
- // Note: _this.oskView may not be initialized yet.
- onDragMove(cumulativeX: number, cumulativeY: number) {
- if(!layout.oskView) {
- return;
- }
-
- let newWidth = this.startWidth + cumulativeX;
- let newHeight = this.startHeight + cumulativeY;
-
- // Set the smallest and largest OSK size
- if(newWidth < 0.2*screen.width) {
- newWidth = 0.2*screen.width;
- }
- if(newHeight < 0.1*screen.height) {
- newHeight = 0.1*screen.height;
- }
- if(newWidth > 0.9*screen.width) {
- newWidth = 0.9*screen.width;
- }
- if(newHeight > 0.5*screen.height) {
- newHeight = 0.5*screen.height;
- }
-
- // Explicitly set OSK width, height, and font size - cannot safely rely on scaling from font
- layout.oskView.setSize(newWidth, newHeight, true);
- }
-
- onDragRelease() {
- if(!layout.oskView) {
- return;
- }
-
- let keymanweb = com.keyman.singleton;
-
- keymanweb.domManager.focusLastActiveElement();
-
- keymanweb.uiManager.justActivated = false;
- keymanweb.uiManager.setActivatingUI(false);
-
- if(layout.oskView.vkbd) {
- layout.oskView.vkbd.currentKey=null;
- }
-
- if(layout.oskView.vkbd) {
- this.startWidth = layout.oskView.computedWidth;
- this.startHeight = layout.oskView.computedHeight;
- }
- layout.oskView.refreshLayout(); // Finalize the resize.
- layout.oskView.doResizeMove();
- layout.oskView.saveCookie();
- }
- }
-
- return this._resizeHandler;
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/layouts/titleBar.ts b/web/src/engine/main/osk/layouts/titleBar.ts
deleted file mode 100644
index 31daeaddbf..0000000000
--- a/web/src/engine/main/osk/layouts/titleBar.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-///
-///
-
-namespace com.keyman.osk.layouts {
- export class TitleBar implements OSKViewComponent {
- private _element: HTMLDivElement;
- private _unpinButton: HTMLDivElement;
- private _closeButton: HTMLDivElement;
- private _helpButton: HTMLDivElement;
- private _configButton: HTMLDivElement;
- private _caption: HTMLSpanElement;
-
- private _helpEnabled: boolean;
- private _configEnabled: boolean;
-
- public get helpEnabled(): boolean {
- return this._helpEnabled;
- }
-
- public set helpEnabled(val) {
- this._helpEnabled = val;
-
- this._helpButton.style.display = val ? 'inline' : 'none';
- }
-
- public get configEnabled(): boolean {
- return this._configEnabled;
- }
-
- public set configEnabled(val) {
- this._configEnabled = val;
-
- this._configButton.style.display = val ? 'inline' : 'none';
- }
-
- private static readonly DISPLAY_HEIGHT = ParsedLengthStyle.inPixels(20); // As set in kmwosk.css
-
- public constructor(dragHandler?: MouseDragOperation) {
- this._element = this.buildTitleBar();
-
- this.helpEnabled = false;
- this.configEnabled = false;
-
- if(dragHandler) {
- this.element.onmousedown = dragHandler.mouseDownHandler;
- }
- }
-
- public get layoutHeight(): ParsedLengthStyle {
- return TitleBar.DISPLAY_HEIGHT;
- }
-
- private mouseCancellingHandler: (ev: MouseEvent) => boolean = function(ev: MouseEvent) {
- ev.preventDefault();
- ev.cancelBubble = true;
- return false;
- };
-
- public get element(): HTMLDivElement {
- return this._element;
- }
-
- public setPinCJKOffset() {
- this._unpinButton.style.left = '15px';
- }
-
- public showPin(visible: boolean) {
- this._unpinButton.style.display = visible ? 'block' : 'none';
- }
-
- public setTitle(str: string) {
- this._caption.innerHTML = str;
- }
-
- public setTitleFromKeyboard(keyboard: keyboards.Keyboard) {
- let title = "" + keyboard?.name + ''; // I1972 // I2186
- this._caption.innerHTML = title;
- }
-
- public attachHandlers(osk: OSKView) {
- let util = com.keyman.singleton.util;
-
- this._helpButton.onclick = function() {
- var p={};
- util.callEvent('osk.helpclick',p);
- if(window.event) {
- window.event.returnValue=false;
- }
- return false;
- }
-
- this._configButton.onclick = function() {
- var p={};
- util.callEvent('osk.configclick',p);
- if(window.event) {
- window.event.returnValue=false;
- }
- return false;
- }
-
- this._closeButton.onclick = function () {
- osk.startHide(true);
- return false;
- };
-
- if(osk instanceof FloatingOSKView) {
- const _osk = osk as FloatingOSKView;
- this._unpinButton.onclick = function () {
- _osk.restorePosition(true);
- return false;
- }
- }
- }
-
- /**
- * Create a control bar with title and buttons for the desktop OSK
- */
- buildTitleBar(): HTMLDivElement {
- let bar = document.createElement('div');
- this.markUnselectable(bar);
- bar.id='keymanweb_title_bar';
- bar.className='kmw-title-bar';
-
- var Ltitle = this._caption = document.createElement('span');
- this.markUnselectable(Ltitle);
- Ltitle.className='kmw-title-bar-caption';
- Ltitle.style.color='#fff';
- bar.appendChild(Ltitle);
-
- var Limg = this._closeButton = this.buildCloseButton();
- bar.appendChild(Limg);
-
- Limg = this._helpButton = this.buildHelpButton()
- bar.appendChild(Limg);
-
- Limg = this._configButton = this.buildConfigButton();
- bar.appendChild(Limg);
-
- Limg = this._unpinButton = this.buildUnpinButton();
- bar.appendChild(Limg);
-
- return bar;
- }
-
- private markUnselectable(e: HTMLElement) {
- e.style.MozUserSelect="none";
- e.style.KhtmlUserSelect="none";
- e.style.UserSelect="none";
- e.style.WebkitUserSelect="none";
- }
-
- private buildCloseButton(): HTMLDivElement {
- var Limg = document.createElement('div');
- this.markUnselectable(Limg);
-
- Limg.id='kmw-close-button';
- Limg.className='kmw-title-bar-image';
- Limg.onmousedown = this.mouseCancellingHandler;
-
- return Limg;
- }
-
- private buildHelpButton(): HTMLDivElement {
- let Limg = document.createElement('div');
- this.markUnselectable(Limg);
- Limg.id='kmw-help-image';
- Limg.className='kmw-title-bar-image';
- Limg.title='KeymanWeb Help';
- Limg.onmousedown = this.mouseCancellingHandler;
- return Limg;
- }
-
- private buildConfigButton(): HTMLDivElement {
- let Limg = document.createElement('div');
- this.markUnselectable(Limg);
-
- Limg.id='kmw-config-image';
- Limg.className='kmw-title-bar-image';
- Limg.title='KeymanWeb Configuration Options';
- Limg.onmousedown = this.mouseCancellingHandler;
-
- return Limg;
- }
-
- /**
- * Builds an 'unpin' button for restoring OSK to default location, handle mousedown and click events
- */
- private buildUnpinButton(): HTMLDivElement {
- let Limg = document.createElement('div'); //I2186
- this.markUnselectable(Limg);
-
- Limg.id = 'kmw-pin-image';
- Limg.className = 'kmw-title-bar-image';
- Limg.title='Pin the On Screen Keyboard to its default location on the active text box';
-
- Limg.onmousedown = this.mouseCancellingHandler;
-
- return Limg;
- }
-
- public refreshLayout() {
- // The title bar is adaptable as it is and needs no adjustments.
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/lengthStyle.ts b/web/src/engine/main/osk/lengthStyle.ts
deleted file mode 100644
index 2d45dcffd9..0000000000
--- a/web/src/engine/main/osk/lengthStyle.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-namespace com.keyman.osk {
- export interface LengthStyle {
- val: number,
- absolute: boolean,
- special?: 'em' | 'rem';
- };
-
- export class ParsedLengthStyle implements LengthStyle {
- public readonly val: number;
- public readonly absolute: boolean;
- public readonly special: 'em' | 'rem';
-
- public constructor(style: LengthStyle | string) {
- let parsed: LengthStyle = (typeof style == 'string') ? ParsedLengthStyle.parseLengthStyle(style) : style;
-
- // While Object.assign would be nice (and previously, was used), it will break
- // on old but still supported versions of Android if their Chrome isn't updated.
- // Requires mobile Chrome 45+, but API 21 (5.0) launches with an older browser.
-
- // Object.assign(this, parsed);
- this.val = parsed.val;
- this.absolute = parsed.absolute;
- if(parsed.special) {
- this.special = parsed.special;
- }
- }
-
- public get styleString(): string {
- if(this.absolute) {
- return this.val + 'px';
- } else if(this.special) {
- // Only 'em' and 'rem' are allowed, and both may be treated similarly.
- // Both relate to font sizes, though the path to the reference element
- // differs between them.
- return this.val + this.special;
- } else {
- return (this.val * 100) + '%';
- }
- }
-
- public scaledBy(scalar: number): ParsedLengthStyle {
- return new ParsedLengthStyle({
- val: scalar * this.val,
- absolute: this.absolute
- });
- }
-
- public static inPixels(val: number): ParsedLengthStyle {
- return new ParsedLengthStyle({val: val, absolute: true});
- }
-
- public static inPercent(val: number): ParsedLengthStyle {
- return new ParsedLengthStyle({val: val/100, absolute: false});
- }
-
- public static forScalar(val: number): ParsedLengthStyle {
- return new ParsedLengthStyle({val: val, absolute: false});
- }
-
- public static special(val: number, suffix: 'em' | 'rem'): ParsedLengthStyle {
- return new ParsedLengthStyle({val: val, absolute: false, special: suffix});
- }
-
- private static parseLengthStyle(spec: string): LengthStyle {
- const val = parseFloat(spec);
-
- if(isNaN(val)) {
- // Cannot parse.
- console.error("Could not properly parse specified length style info: '" + spec + "'.");
- return null;
- }
-
- return spec.indexOf('px') != -1 ? {val: val, absolute: true} :
- // 16 px ~= 12 pt.
- // Reference: https://kyleschaeffer.com/css-font-size-em-vs-px-vs-pt-vs-percent
- spec.indexOf('pt') != -1 ? {val: (4 * val / 3), absolute: true} :
- spec.indexOf('%') != -1 ? {val: val/100, absolute: false} :
- spec.indexOf('rem') != -1 ? {val: val, absolute: false, special: 'rem'} :
- spec.indexOf('em') != -1 ? {val: val, absolute: false, special: 'em'} :
- // At this point, assuming either Number or number in a string without units
- // Note: this one is NOT natively handled by browsers!
- // We'll treat it as if it were 'pt', since that's likely the user's
- // most familiar font size unit.
- {val: (4 * val / 3), absolute: true};
- }
- }
-}
diff --git a/web/src/engine/main/osk/mouseEventEngine.ts b/web/src/engine/main/osk/mouseEventEngine.ts
deleted file mode 100644
index ee77db7844..0000000000
--- a/web/src/engine/main/osk/mouseEventEngine.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-///
-
-namespace com.keyman.osk {
- export class MouseEventEngine extends InputEventEngine {
- private readonly _mouseStart: typeof MouseEventEngine.prototype.onMouseStart;
- private readonly _mouseMove: typeof MouseEventEngine.prototype.onMouseMove;
- private readonly _mouseEnd: typeof MouseEventEngine.prototype.onMouseEnd;
-
- private hasActiveClick: boolean = false;
- private ignoreSequence: boolean = false;
-
- public constructor(config: InputEventEngineConfig) {
- super(config);
-
- this._mouseStart = this.onMouseStart.bind(this);
- this._mouseMove = this.onMouseMove.bind(this);
- this._mouseEnd = this.onMouseEnd.bind(this);
- }
-
- public static forVisualKeyboard(vkbd: VisualKeyboard) {
- const config: InputEventEngineConfig = {
- targetRoot: vkbd.element,
- // document.body is the event root b/c we need to track the mouse if it leaves
- // the VisualKeyboard's hierarchy.
- eventRoot: document.body,
- inputStartHandler: vkbd.touch.bind(vkbd),
- inputMoveHandler: vkbd.moveOver.bind(vkbd),
- inputMoveCancelHandler: vkbd.moveCancel.bind(vkbd),
- inputEndHandler: vkbd.release.bind(vkbd),
- coordConstrainedWithinInteractiveBounds: vkbd.detectWithinInteractiveBounds.bind(vkbd)
- };
-
- return new MouseEventEngine(config);
- }
-
- public static forPredictiveBanner(banner: SuggestionBanner, handlerRoot: SuggestionManager) {
- const config: InputEventEngineConfig = {
- targetRoot: banner.getDiv(),
- // document.body is the event root b/c we need to track the mouse if it leaves
- // the VisualKeyboard's hierarchy.
- eventRoot: document.body,
- inputStartHandler: handlerRoot.touchStart.bind(handlerRoot),
- inputMoveHandler: handlerRoot.touchMove.bind(handlerRoot),
- inputEndHandler: handlerRoot.touchEnd.bind(handlerRoot),
- coordConstrainedWithinInteractiveBounds: function() { return true; }
- };
-
- return new MouseEventEngine(config);
- }
-
- registerEventHandlers() {
- this.config.eventRoot.addEventListener('mousedown', this._mouseStart, true);
- this.config.eventRoot.addEventListener('mousemove', this._mouseMove, false);
- // The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'.
- this.config.eventRoot.addEventListener('mouseup', this._mouseEnd, true);
- }
-
- unregisterEventHandlers() {
- this.config.eventRoot.removeEventListener('mousedown', this._mouseStart, true);
- this.config.eventRoot.removeEventListener('mousemove', this._mouseMove, false);
- this.config.eventRoot.removeEventListener('mouseup', this._mouseEnd, true);
- }
-
- private preventPropagation(e: MouseEvent) {
- // Standard event maintenance
- e.preventDefault();
- e.cancelBubble=true;
- e.returnValue=false; // I2409 - Avoid focus loss for visual keyboard events
-
- if(typeof e.stopImmediatePropagation == 'function') {
- e.stopImmediatePropagation();
- } else if(typeof e.stopPropagation == 'function') {
- e.stopPropagation();
- }
- }
-
- onMouseStart(event: MouseEvent) {
- if(!this.config.targetRoot.contains(event.target as Node)) {
- this.ignoreSequence = true;
- return;
- }
-
- this.preventPropagation(event);
- this.onInputStart(InputEventCoordinate.fromEvent(event));
- this.hasActiveClick = true;
- }
-
- onMouseMove(event: MouseEvent) {
- if(this.ignoreSequence) {
- return;
- }
-
- const coord = InputEventCoordinate.fromEvent(event);
-
- if(!event.buttons) {
- if(this.hasActiveClick) {
- this.hasActiveClick = false;
- this.onInputMoveCancel(coord);
- }
- return;
- } else if(!this.hasActiveClick) {
- // Can interfere with OSK drag-handlers (title bar, resize bar) otherwise.
- return;
- }
-
- this.preventPropagation(event);
-
- if(this.config.coordConstrainedWithinInteractiveBounds(coord)) {
- this.onInputMove(coord);
- } else {
- this.onInputMoveCancel(coord);
- }
- }
-
- onMouseEnd(event: MouseEvent) {
- if(this.ignoreSequence) {
- this.ignoreSequence = false;
- return;
- }
-
- if(!event.buttons) {
- this.hasActiveClick = false;
- }
- this.onInputEnd(InputEventCoordinate.fromEvent(event));
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/oskBaseKey.ts b/web/src/engine/main/osk/oskBaseKey.ts
deleted file mode 100644
index 9cf89e643d..0000000000
--- a/web/src/engine/main/osk/oskBaseKey.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-///
-
-namespace com.keyman.osk {
- let Codes = com.keyman.text.Codes;
-
- export class OSKBaseKey extends OSKKey {
- private capLabel: HTMLDivElement;
- public readonly row: OSKRow;
-
- constructor(spec: OSKKeySpec, layer: string, row: OSKRow) {
- super(spec, layer);
- this.row = row;
- }
-
- getId(): string {
- // Define each key element id by layer id and key id (duplicate possible for SHIFT - does it matter?)
- return this.spec.elementID;
- }
-
- getCoreId(): string {
- return this.spec.coreID;
- }
-
- getBaseId(): string {
- return this.spec.baseKeyID;
- }
-
- // Produces a small reference label for the corresponding physical key on a US keyboard.
- private generateKeyCapLabel(): HTMLDivElement {
- // Create the default key cap labels (letter keys, etc.)
- var x = Codes.keyCodes[this.spec.baseKeyID];
- switch(x) {
- // Converts the keyman key id code for common symbol keys into its representative ASCII code.
- // K_COLON -> K_BKQUOTE
- case 186: x=59; break;
- case 187: x=61; break;
- case 188: x=44; break;
- case 189: x=45; break;
- case 190: x=46; break;
- case 191: x=47; break;
- case 192: x=96; break;
- // K_LBRKT -> K_QUOTE
- case 219: x=91; break;
- case 220: x=92; break;
- case 221: x=93; break;
- case 222: x=39; break;
- default:
- // No other symbol character represents a base key on the standard QWERTY English layout.
- if(x < 48 || x > 90) {
- x=0;
- }
- }
-
- let q = document.createElement('div');
- q.className='kmw-key-label';
- if(x > 0) {
- q.innerText=String.fromCharCode(x);
- } else {
- // Keyman-only virtual keys have no corresponding physical key.
- // So, no text for the key-cap.
- }
- return q;
- }
-
- private processSubkeys(btn: KeyElement, vkbd: VisualKeyboard) {
- // Add reference to subkey array if defined
- var bsn: number, bsk=btn['subKeys'] = this.spec['sk'];
- // Transform any special keys into their PUA representations.
- for(bsn=0; bsn elem;
-
- // Merges all properties and methods of KeyData onto the underlying HTMLDivElement, creating a merged class.
- for(let id in data) {
- if(!e.hasOwnProperty(id)) {
- (e)[id] = (data)[id];
- }
- }
-
- return e;
- }
-
- export function isKey(elem: Node): boolean {
- return elem && ('key' in elem) && (( elem['key']) instanceof OSKKey);
- }
-
- export function getKeyFrom(elem: Node): KeyElement {
- if(isKey(elem)) {
- return elem;
- } else {
- return null;
- }
- }
-
- export class OSKKeySpec implements keyboards.LayoutKey {
- id: string;
-
- // Only set (within @keymanapp/keyboard-processor) for keys actually specified in a loaded layout
- baseKeyID?: string;
- coreID?: string;
- elementID?: string;
-
- text?: string;
- sp?: keyboards.ButtonClass;
- width: number;
- layer?: string; // The key will derive its base modifiers from this property - may not equal the layer on which it is displayed.
- nextlayer?: string;
- pad?: number;
- sk?: OSKKeySpec[];
-
- constructor(id: string, text?: string, width?: number, sp?: keyboards.ButtonClass, nextlayer?: string, pad?: number) {
- this.id = id;
- this.text = text;
- this.width = width ? width : 50;
- this.sp = sp;
- this.nextlayer = nextlayer;
- this.pad = pad;
- }
- }
-
- export abstract class OSKKey {
- // Defines the PUA code mapping for the various 'special' modifier/control keys on keyboards.
- // `specialCharacters` must be kept in sync with the same variable in builder.js. See also CompileKeymanWeb.pas: CSpecialText10
- static readonly specialCharacters = {
- '*Shift*': 8,
- '*Enter*': 5,
- '*Tab*': 6,
- '*BkSp*': 4,
- '*Menu*': 11,
- '*Hide*': 10,
- '*Alt*': 25,
- '*Ctrl*': 1,
- '*Caps*': 3,
- '*ABC*': 16,
- '*abc*': 17,
- '*123*': 19,
- '*Symbol*': 21,
- '*Currency*': 20,
- '*Shifted*': 9,
- '*AltGr*': 2,
- '*TabLeft*': 7,
- '*LAlt*': 0x56,
- '*RAlt*': 0x57,
- '*LCtrl*': 0x58,
- '*RCtrl*': 0x59,
- '*LAltCtrl*': 0x60,
- '*RAltCtrl*': 0x61,
- '*LAltCtrlShift*': 0x62,
- '*RAltCtrlShift*': 0x63,
- '*AltShift*': 0x64,
- '*CtrlShift*': 0x65,
- '*AltCtrlShift*': 0x66,
- '*LAltShift*': 0x67,
- '*RAltShift*': 0x68,
- '*LCtrlShift*': 0x69,
- '*RCtrlShift*': 0x70,
- // Added in Keyman 14.0.
- '*LTREnter*': 0x05, // Default alias of '*Enter*'.
- '*LTRBkSp*': 0x04, // Default alias of '*BkSp*'.
- '*RTLEnter*': 0x71,
- '*RTLBkSp*': 0x72,
- '*ShiftLock*': 0x73,
- '*ShiftedLock*': 0x74,
- '*ZWNJ*': 0x75, // If this one is specified, auto-detection will kick in.
- '*ZWNJiOS*': 0x75, // The iOS version will be used by default, but the
- '*ZWNJAndroid*': 0x76, // Android platform has its own default glyph.
- };
-
- static readonly BUTTON_CLASSES = [
- 'default',
- 'shift',
- 'shift-on',
- 'special',
- 'special-on',
- '', // Key classes 5 through 7 are reserved for future use.
- '',
- '',
- 'deadkey',
- 'blank',
- 'hidden'
- ];
-
- static readonly HIGHLIGHT_CLASS = 'kmw-key-touched';
- readonly spec: OSKKeySpec;
-
- btn: KeyElement;
- label: HTMLSpanElement;
- square: HTMLDivElement;
-
- /**
- * The layer of the OSK on which the key is displayed.
- */
- readonly layer: string;
-
- constructor(spec: OSKKeySpec, layer: string) {
- this.spec = spec;
- this.layer = layer;
- }
-
- abstract getId(): string;
-
- /**
- * Attach appropriate class to each key button, according to the layout
- *
- * @param {Object=} layout source layout description (optional, sometimes)
- */
- public setButtonClass() {
- let key = this.spec;
- let btn = this.btn;
-
- var n=0;
- if(typeof key['dk'] == 'string' && key['dk'] == '1') {
- n=8;
- }
-
- n = key['sp'] ?? n;
-
- if(n < 0 || n > 10) {
- n=0;
- }
-
- btn.className='kmw-key kmw-key-'+OSKKey.BUTTON_CLASSES[n];
- }
-
- /**
- * For keys with button classes that support toggle states, this method
- * may be used to toggle which state the key's button class is in.
- * - shift <=> shift-on
- * - special <=> special-on
- * @param {boolean=} flag The new toggle state
- */
- public setToggleState(flag?: boolean) {
- let btnClassId: number;
-
- btnClassId = this.spec['sp'];
-
- // 1 + 2: shift + shift-on
- // 3 + 4: special + special-on
- switch(OSKKey.BUTTON_CLASSES[btnClassId]) {
- case 'shift':
- case 'shift-on':
- if(flag === undefined) {
- flag = OSKKey.BUTTON_CLASSES[btnClassId] == 'shift';
- }
-
- this.spec['sp'] = 1 + (flag ? 1 : 0) as keyboards.ButtonClass;
- break;
- // Added in 15.0: special key highlight toggling.
- // Was _intended_ in earlier versions, but not actually implemented.
- case 'special':
- case 'special-on':
- if(flag === undefined) {
- flag = OSKKey.BUTTON_CLASSES[btnClassId] == 'special';
- }
-
- this.spec['sp'] = 3 + (flag ? 1 : 0) as keyboards.ButtonClass;
- break;
- default:
- return;
- }
-
- this.setButtonClass();
- }
-
- // "Frame key" - generally refers to non-linguistic keys on the keyboard
- public isFrameKey(): boolean {
- let classIndex = this.spec['sp'] || 0;
- switch(OSKKey.BUTTON_CLASSES[classIndex]) {
- case 'default':
- case 'deadkey':
- // Note: will (generally) include the spacebar.
- return false;
- default:
- return true;
- }
- }
-
- public allowsKeyTip(): boolean {
- if(this.isFrameKey()) {
- return false;
- } else {
- return !this.btn.classList.contains('kmw-spacebar');
- }
- }
-
- public highlight(on: boolean) {
- var classes=this.btn.classList;
-
- if(on) {
- if(!classes.contains(OSKKey.HIGHLIGHT_CLASS)) {
- classes.add(OSKKey.HIGHLIGHT_CLASS);
- }
- } else {
- classes.remove(OSKKey.HIGHLIGHT_CLASS);
- }
- }
-
- /**
- * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
- *
- * @param {String} text The text to be rendered.
- * @param {String} style The CSSStyleDeclaration for an element to measure against, without modification.
- *
- * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
- * This version has been substantially modified to work for this particular application.
- */
- static getTextMetrics(text: string, emScale: number, style: {fontFamily?: string, fontSize: string}): TextMetrics {
- // Since we may mutate the incoming style, let's make sure to copy it first.
- // Only the relevant properties, though.
- style = {
- fontFamily: style.fontFamily,
- fontSize: style.fontSize
- };
-
- // A final fallback - having the right font selected makes a world of difference.
- if(!style.fontFamily) {
- style.fontFamily = getComputedStyle(document.body).fontFamily;
- }
-
- if(!style.fontSize || style.fontSize == "") {
- style.fontSize = '1em';
- }
-
- let fontFamily = style.fontFamily;
- let fontSpec = getFontSizeStyle(style.fontSize);
-
- var fontSize: string;
- if(fontSpec.absolute) {
- // We've already got an exact size - use it!
- fontSize = fontSpec.val + 'px';
- } else {
- fontSize = fontSpec.val * emScale + 'px';
- }
-
- // re-use canvas object for better performance
- var canvas: HTMLCanvasElement = OSKKey.getTextMetrics['canvas'] ||
- (OSKKey.getTextMetrics['canvas'] = document.createElement("canvas"));
- var context = canvas.getContext("2d");
- context.font = fontSize + " " + fontFamily;
- var metrics = context.measureText(text);
-
- return metrics;
- }
-
- /**
- * Calculate the font size required for a key cap, scaling to fit longer text
- * @param vkbd
- * @param style specification for the desired base font size
- * @param override if true, don't use the font spec from the button, just use the passed in spec
- * @returns font size as a style string
- */
- getIdealFontSize(vkbd: VisualKeyboard, text: string, style: {height?: string, fontFamily?: string, fontSize: string}, override?: boolean): string {
- let buttonStyle = getComputedStyle(this.btn);
- let keyWidth = parseFloat(buttonStyle.width);
- let emScale = 1;
-
- const originalSize = getFontSizeStyle(style.fontSize || '1em');
-
- // Not yet available; it'll be handled in a later layout pass.
- if(!buttonStyle.fontSize) {
- // NOTE: preserves old behavior for use in documentation keyboards, for now.
- // Once we no longer need to maintain this code block, we can drop all current
- // method parameters safely.
- //
- // Recompute the new width for use in autoscaling calculations below, just in case.
- emScale = vkbd.getKeyEmFontSize();
- keyWidth = this.getKeyWidth(vkbd);
- } else if(!override) {
- // When available, just use computedStyle instead.
- style = buttonStyle;
- }
-
- let fontSpec = getFontSizeStyle(style.fontSize || '1em');
- let metrics = OSKKey.getTextMetrics(text, emScale, style);
-
- const MAX_X_PROPORTION = 0.90;
- const MAX_Y_PROPORTION = 0.90;
- const X_PADDING = 2;
- const Y_PADDING = 2;
-
- var fontHeight: number, keyHeight: number;
- if(metrics.fontBoundingBoxAscent) {
- fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
- }
-
- let textHeight = fontHeight ? fontHeight + Y_PADDING : 0;
- if(style.height && style.height.indexOf('px') != -1) {
- keyHeight = Number.parseFloat(style.height.substring(0, style.height.indexOf('px')));
- }
-
- let xProportion = (keyWidth * MAX_X_PROPORTION) / (metrics.width + X_PADDING); // How much of the key does the text want to take?
- let yProportion = textHeight && keyHeight ? (keyHeight * MAX_Y_PROPORTION) / textHeight : undefined;
-
- var proportion: number = xProportion;
- if(yProportion && yProportion < xProportion) {
- proportion = yProportion;
- }
-
- // Never upscale keys past the default - only downscale them.
- // Proportion < 1: ratio of key width to (padded [loosely speaking]) text width
- // maxProportion determines the 'padding' involved.
- //
- if(proportion < 1) {
- if(originalSize.absolute) {
- return proportion * fontSpec.val + 'px';
- } else {
- return proportion * originalSize.val + 'em';
- }
- } else {
- if(originalSize.absolute) {
- return fontSpec.val + 'px';
- } else {
- return originalSize.val + 'em';
- }
- }
- }
-
- getKeyWidth(vkbd: VisualKeyboard): number {
- let key = this.spec as keyboards.ActiveKey;
- return key.proportionalWidth * vkbd.width;
- }
-
- /**
- * Replace default key names by special font codes for modifier keys
- *
- * @param {string} oldText
- * @return {string}
- **/
- protected renameSpecialKey(oldText: string, vkbd: VisualKeyboard): string {
- // If a 'special key' mapping exists for the text, replace it with its corresponding special OSK character.
- switch(oldText) {
- case '*ZWNJ*':
- // Default ZWNJ symbol comes from iOS. We'd rather match the system defaults where
- // possible / available though, and there's a different standard symbol on Android.
- oldText = vkbd.device.OS == com.keyman.utils.OperatingSystem.Android ?
- '*ZWNJAndroid*' :
- '*ZWNJiOS*';
- break;
- case '*Enter*':
- oldText = vkbd.isRTL ? '*RTLEnter*' : '*LTREnter*';
- break;
- case '*BkSp*':
- oldText = vkbd.isRTL ? '*RTLBkSp*' : '*LTRBkSp*';
- break;
- default:
- // do nothing.
- }
-
- let specialCodePUA = 0XE000 + VisualKeyboard.specialCharacters[oldText];
-
- return VisualKeyboard.specialCharacters[oldText] ?
- String.fromCharCode(specialCodePUA) :
- oldText;
- }
-
- public get keyText(): string {
- const spec = this.spec;
- const DEFAULT_BLANK = '\xa0';
-
- // Add OSK key labels
- let keyText = null;
- if(spec['text'] == null || spec['text'] == '') {
- if(typeof spec['id'] == 'string') {
- // If the ID's Unicode-based, just use that code.
- keyText = keyboards.ActiveKey.unicodeIDToText(spec['id']);
- }
-
- keyText = keyText || DEFAULT_BLANK;
- } else {
- keyText=spec['text'];
-
- // Unique layer-based transformation: SHIFT-TAB uses a different glyph.
- if(keyText == '*Tab*' && this.layer == 'shift') {
- keyText = '*TabLeft*';
- }
- }
-
- return keyText;
- }
-
- // Produces a HTMLSpanElement with the key's actual text.
- protected generateKeyText(vkbd: VisualKeyboard): HTMLSpanElement {
- const spec = this.spec;
-
- let t = document.createElement('span'), ts=t.style;
- t.className='kmw-key-text';
-
- // Add OSK key labels
- let keyText = this.keyText;
- let specialText = this.renameSpecialKey(keyText, vkbd);
- if(specialText != keyText) {
- // The keyboard wants to use the code for a special glyph defined by the SpecialOSK font.
- keyText = specialText;
- spec['font'] = "SpecialOSK";
- }
-
- //Override font spec if set for this key in the layout
- if(typeof spec['font'] == 'string' && spec['font'] != '') {
- ts.fontFamily=spec['font'];
- }
-
- if(typeof spec['fontsize'] == 'string' && spec['fontsize'] != '') {
- ts.fontSize=spec['fontsize'];
- }
-
- // For some reason, fonts will sometimes 'bug out' for the embedded iOS page if we
- // instead assign fontFamily to the existing style 'ts'. (Occurs in iOS 12.)
- let styleSpec: {fontFamily?: string, fontSize: string} = {fontSize: ts.fontSize};
-
- if(ts.fontFamily) {
- styleSpec.fontFamily = ts.fontFamily;
- } else {
- styleSpec.fontFamily = vkbd.fontFamily; // Helps with style sheet calculations.
- }
-
- // Check the key's display width - does the key visualize well?
- let emScale = vkbd.getKeyEmFontSize();
- var width: number = OSKKey.getTextMetrics(keyText, emScale, styleSpec).width;
- if(width == 0 && keyText != '' && keyText != '\xa0') {
- // Add the Unicode 'empty circle' as a base support for needy diacritics.
-
- // Disabled by mcdurdin 2020-10-19; dotted circle display is inconsistent on iOS/Safari
- // at least and doesn't combine with diacritic marks. For consistent display, it may be
- // necessary to build a custom font that does not depend on renderer choices for base
- // mark display -- e.g. create marks with custom base included, potentially even on PUA
- // code points and use those in rendering the OSK. See #3039 for more details.
- // keyText = '\u25cc' + keyText;
-
- if(vkbd.isRTL) {
- // Add the RTL marker to ensure it displays properly.
- keyText = '\u200f' + keyText;
- }
- }
-
- ts.fontSize = this.getIdealFontSize(vkbd, keyText, styleSpec);
-
- // Finalize the key's text.
- t.innerText = keyText;
-
- return t;
- }
-
- public isUnderTouch(input: InputEventCoordinate): boolean {
- let x = input.x;
- let y = input.y;
-
- let btn = this.btn;
- let x0 = dom.Utils.getAbsoluteX(btn);
- let y0 = dom.Utils.getAbsoluteY(btn);
- let x1 = x0 + btn.offsetWidth;
- let y1 = y0 + btn.offsetHeight;
-
- return (x > x0 && x < x1 && y > y0 && y < y1);
- }
-
- public refreshLayout(vkbd: VisualKeyboard) {
- // space bar may not define the text span!
- if(this.label) {
- if(!this.label.classList.contains('kmw-spacebar-caption')) {
- this.label.style.fontSize = this.getIdealFontSize(vkbd, this.keyText, this.btn.style);
- } else {
- // Remove any custom setting placed on it before recomputing its inherited style info.
- this.label.style.fontSize = '';
- const fontSize = this.getIdealFontSize(vkbd, this.label.textContent, getComputedStyle(this.label), true);
-
- // Since the kmw-spacebar-caption version uses !important, we must specify
- // it directly on the element too; otherwise, scaling gets ignored.
- this.label.style.setProperty("font-size", fontSize, "important");
- }
- }
- }
- }
-}
diff --git a/web/src/engine/main/osk/oskLayer.ts b/web/src/engine/main/osk/oskLayer.ts
deleted file mode 100644
index 172ee70dd3..0000000000
--- a/web/src/engine/main/osk/oskLayer.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-///
-
-namespace com.keyman.osk {
- export class OSKLayer {
- public readonly element: HTMLDivElement;
- public readonly rows: OSKRow[];
- public readonly spec: keyboards.ActiveLayer;
- public readonly nextlayer: string;
-
- public readonly globeKey: OSKBaseKey;
- public readonly spaceBarKey: OSKBaseKey;
- public readonly hideKey: OSKBaseKey;
- public readonly capsKey: OSKBaseKey;
- public readonly numKey: OSKBaseKey;
- public readonly scrollKey: OSKBaseKey;
-
- private _rowHeight: number;
-
- public get rowHeight(): number {
- return this._rowHeight;
- }
-
- public get id(): string {
- return this.spec.id;
- }
-
- public constructor(vkbd: VisualKeyboard,
- layout: keyboards.ActiveLayout,
- layer: keyboards.ActiveLayer) {
- this.spec = layer;
-
- const gDiv = this.element = document.createElement('div');
- const gs=gDiv.style;
- gDiv.className='kmw-key-layer';
-
- var nRows=layer['row'].length;
- if(nRows > 4 && vkbd.device.formFactor == 'phone') {
- gDiv.className = gDiv.className + ' kmw-5rows';
- }
-
- // Set font for layer if defined in layout
- gs.fontFamily = 'font' in layout ? layout['font'] : '';
-
- this.nextlayer = gDiv['layer'] = layer['id'];
- if(typeof layer['nextlayer'] == 'string') {
- // The gDiv['nextLayer'] is no longer referenced in KMW 15.0+, but is
- // maintained for partial back-compat in case any site devs actually
- // relied on its value from prior versions.
- //
- // We won't pay attention to any mutations to the gDiv copy, though.
- gDiv['nextLayer'] = this.nextlayer = layer['nextlayer'];
- }
-
- // Create a DIV for each row of the group
- let rows=layer['row'];
- this.rows = [];
-
- for(let i=0; i
-
-namespace com.keyman.osk {
- export class OSKLayerGroup {
- public readonly element: HTMLDivElement;
- public readonly layers: {[layerID: string]: OSKLayer} = {};
-
- public constructor(vkbd: VisualKeyboard, keyboard: keyboards.Keyboard, formFactor: utils.FormFactor) {
- let layout = keyboard.layout(formFactor);
-
- const lDiv = this.element = document.createElement('div');
- const ls=lDiv.style;
-
- // Set OSK box default style
- lDiv.className='kmw-key-layer-group';
-
- // Return empty DIV if no layout defined
- if(layout == null) {
- return;
- }
-
- // Set default OSK font size (Build 344, KMEW-90)
- let layoutFS = layout['fontsize'];
- if(typeof layoutFS == 'undefined' || layoutFS == null || layoutFS == '') {
- ls.fontSize='1em';
- } else {
- ls.fontSize=layout['fontsize'];
- }
-
- // Create a separate OSK div for each OSK layer, only one of which will ever be visible
- var n: number, i: number, j: number;
- var layers: keyboards.LayoutLayer[];
-
- layers=layout['layer'];
-
- // Set key default attributes (must use exportable names!)
- var tKey=vkbd.getDefaultKeyObject();
- tKey['fontsize']=ls.fontSize;
-
- for(n=0; n
-
-namespace com.keyman.osk {
- /**
- * Models one row of one layer of the OSK (`VisualKeyboard`) for a keyboard.
- */
- export class OSKRow {
- public readonly element: HTMLDivElement;
- public readonly keys: OSKBaseKey[];
- public readonly heightFraction: number;
-
- public constructor(vkbd: VisualKeyboard,
- layerSpec: keyboards.ActiveLayer,
- rowSpec: keyboards.ActiveRow) {
- const rDiv = this.element = document.createElement('div');
- rDiv.className='kmw-key-row';
-
- // Calculate default row height
- this.heightFraction = 1 / layerSpec.row.length;
-
- // Apply defaults, setting the width and other undefined properties for each key
- const keys=rowSpec.key;
- this.keys = [];
-
- // Calculate actual key widths by multiplying by the OSK's width and rounding appropriately,
- // adjusting the width of the last key to make the total exactly 100%.
- // Overwrite the previously-computed percent.
- // NB: the 'percent' suffix is historical, units are percent on desktop devices, but pixels on touch devices
- // All key widths and paddings are rounded for uniformity
- for(let j=0; j 0) {
- return this.keys[0].displaysKeyCap;
- } else {
- return undefined;
- }
- }
-
- public set displaysKeyCaps(flag: boolean) {
- for(const key of this.keys) {
- key.displaysKeyCap = flag;
- }
- }
-
- public refreshLayout(vkbd: VisualKeyboard) {
- const rs = this.element.style;
-
- const rowHeight = vkbd.internalHeight.scaledBy(this.heightFraction);
- rs.maxHeight=rs.lineHeight=rs.height=rowHeight.styleString;
-
- // Only used for fixed-height scales at present.
- const padRatio = 0.15;
-
- const keyHeightBase = vkbd.usesFixedHeightScaling ? rowHeight : ParsedLengthStyle.forScalar(1);
- const padTop = keyHeightBase.scaledBy(padRatio / 2);
- const keyHeight = keyHeightBase.scaledBy(1 - padRatio);
-
- for(const key of this.keys) {
- const keySquare = key.btn.parentElement;
- const keyElement = key.btn;
-
- // Set the kmw-key-square position
- const kss = keySquare.style;
- kss.height=kss.minHeight=keyHeightBase.styleString;
-
- const kes = keyElement.style;
- kes.top = padTop.styleString;
- kes.height=kes.lineHeight=kes.minHeight=keyHeight.styleString;
-
- if(keyElement.key) {
- keyElement.key.refreshLayout(vkbd);
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/oskView.ts b/web/src/engine/main/osk/oskView.ts
deleted file mode 100644
index b1848d1d36..0000000000
--- a/web/src/engine/main/osk/oskView.ts
+++ /dev/null
@@ -1,1091 +0,0 @@
-// Includes the banner
-///
-
-// Generates the visual keyboard specific to each keyboard. (class="kmw-osk-inner-frame")
-///
-// Models keyboards that present a help page, rather than a standard OSK.
-///
-///
-
-namespace com.keyman.osk {
- export type OSKRect = {
- 'left'?: number,
- 'top'?: number,
- 'width'?: number,
- 'height'?: number,
- 'nosize'?: boolean,
- 'nomove'?: boolean
- };
-
- export enum ActivationMode {
- static = "static", // For use by documentation keyboards, eventually.
- manual = "manual",
- automatic = "automatic"
- }
-
- export abstract class OSKView {
- _Box: HTMLDivElement;
-
- headerView: OSKViewComponent;
- bannerView: BannerManager; // Which implements OSKViewComponent
- keyboardView: KeyboardView; // Which implements OSKViewComponent
- footerView: OSKViewComponent;
-
- protected device: com.keyman.utils.DeviceSpec;
- protected readonly hostDevice: com.keyman.utils.DeviceSpec;
-
- private _boxBaseMouseDown: (e: MouseEvent) => boolean;
- private _boxBaseTouchStart: (e: TouchEvent) => boolean;
- private _boxBaseTouchEventCancel: (e: TouchEvent) => boolean;
-
- private keyboard: keyboards.Keyboard;
- private lgMenu?: LanguageMenu; // only used on non-embedded paths.
-
- private _target: text.OutputTarget;
-
- /**
- * The configured width for this OSKManager. May be `undefined` or `null`
- * to allow automatic width scaling.
- */
- private _width: ParsedLengthStyle;
-
- /**
- * The configured height for this OSKManager. May be `undefined` or `null`
- * to allow automatic height scaling.
- */
- private _height: ParsedLengthStyle;
-
- /**
- * The computed width for this OSKManager. May be null if auto sizing
- * is allowed and the OSKManager is not currently in the DOM hierarchy.
- */
- private _computedWidth: number;
-
- /**
- * The computed height for this OSKManager. May be null if auto sizing
- * is allowed and the OSKManager is not currently in the DOM hierarchy.
- */
- private _computedHeight: number;
-
- /**
- * The base font size to use for hosted `Banner`s and `VisualKeyboard`
- * instances.
- */
- private _baseFontSize: ParsedLengthStyle;
-
- private needsLayout: boolean = true;
-
- //
- private _activationMode: ActivationMode = ActivationMode.automatic;
- private _displayIfActive: boolean = true;
-
- private _animatedHideTimeout: number;
-
- constructor(deviceSpec: com.keyman.utils.DeviceSpec, hostDevice?: com.keyman.utils.DeviceSpec) {
- this.device = deviceSpec;
-
- if(!hostDevice) {
- hostDevice = deviceSpec;
- }
- this.hostDevice = hostDevice;
-
- // OSK initialization - create DIV and set default styles
- this._Box = document.createElement('div'); // Container for OSK (Help DIV, displayed when user clicks Help icon)
- this._Box.style.userSelect = 'none';
-
- // Initializes the two constant OSKComponentView fields.
- this.bannerView = new BannerManager(this.hostDevice);
- this.keyboardView = null;
-
- let keymanweb = com.keyman.singleton;
- let util = keymanweb.util;
-
- // Install the default OSK stylesheet
- util.linkStyleSheet(keymanweb.getStyleSheetPath('kmwosk.css'));
-
- this.setBaseMouseEventListeners();
- if(hostDevice.touchable) {
- this.setBaseTouchEventListeners();
- }
-
- // Register a listener for model change events so that we can hot-swap the banner as needed.
- // Handled here b/c banner changes may trigger a need to re-layout the OSK.
- const _this = this;
- keymanweb.core.languageProcessor.on('statechange',
- function(state: text.prediction.StateChangeEnum) {
- let currentType = _this.bannerView.activeType;
- _this.bannerView.selectBanner(state);
-
- if(currentType != _this.bannerView.activeType) {
- _this.refreshLayout();
- }
-
- return true;
- });
- }
-
- private setBaseMouseEventListeners() {
- let keymanweb = com.keyman.singleton;
-
- this._boxBaseMouseDown = function(e) {
- keymanweb.uiManager.setActivatingUI(true);
- return false;
- }
-
- this._Box.addEventListener('mousedown', this._boxBaseMouseDown, false);
- }
-
- private removeBaseMouseEventListeners() {
- if(this._boxBaseMouseDown) {
- this._Box.removeEventListener('mousedown', this._boxBaseMouseDown, false);
- this._boxBaseMouseDown = null;
- }
- }
-
- private setBaseTouchEventListeners() {
- // And to prevent touch event default behaviour on mobile devices
- let keymanweb = com.keyman.singleton;
-
- var cancelEventFunc = this._boxBaseTouchEventCancel = function(e) {
- if(e.cancelable) {
- e.preventDefault();
- }
- e.stopPropagation();
- return false;
- };
-
- this._boxBaseTouchStart = function(e) {
- keymanweb.uiManager.setActivatingUI(true);
- return cancelEventFunc(e);
- }
-
- this._Box.addEventListener('touchstart', this._boxBaseTouchStart, false);
- this._Box.addEventListener('touchmove', this._boxBaseTouchEventCancel, false);
- this._Box.addEventListener('touchend', this._boxBaseTouchEventCancel, false);
- this._Box.addEventListener('touchcancel', this._boxBaseTouchEventCancel, false);
- }
-
- private removeBaseTouchEventListeners() {
- if(!this._boxBaseTouchEventCancel) {
- return;
- }
-
- this._Box.removeEventListener('touchstart', this._boxBaseTouchStart, false);
- this._Box.removeEventListener('touchmove', this._boxBaseTouchEventCancel, false);
- this._Box.removeEventListener('touchend', this._boxBaseTouchEventCancel, false);
- this._Box.removeEventListener('touchcancel', this._boxBaseTouchEventCancel, false);
-
- this._boxBaseTouchEventCancel = null;
- this._boxBaseTouchStart = null;
- }
-
- /**
- * Gets and sets the IME-like interface (`OutputTarget`) to be affected by events from
- * the OSK.
- *
- * If `activationMode` is `'conditional'`, this property's state controls the visibility
- * of the OSKView.
- */
- public get activeTarget(): text.OutputTarget {
- return this._target;
- }
-
- public set activeTarget(targ: text.OutputTarget) {
- // If already null & set to null again, take no action.
- if(this._target == null && targ == null) {
- return;
- }
-
- this._target = targ;
- this.commonCheckAndDisplay();
- }
-
-
- public get targetDevice(): com.keyman.utils.DeviceSpec {
- return this.device;
- }
-
- public set targetDevice(spec: com.keyman.utils.DeviceSpec) {
- if(this.allowsDeviceChange(spec)) {
- this.device = spec;
- this.loadActiveKeyboard();
- } else {
- console.error("May not change target device for this OSKView type.");
- }
- }
-
- protected allowsDeviceChange(newSpec: com.keyman.utils.DeviceSpec): boolean {
- return false;
- }
-
- /**
- * Determines the activation state model used to control presentation of the OSK.
- * - `'conditional'`: Only displays if `activeTarget` is non-null - if there is an active
- * target that can receive the OSK's context-manipulation events.
- * - `'manual'`: Display is directly controlled by manipulating the value of `displayIfActive`.
- * It may be displayed while `activeTarget` is `null`.
- * - `'static'`: The OSK should be permanently displayed and may never be hidden.
- */
- get activationMode(): ActivationMode {
- if(!this._activationMode) {
- this._activationMode = ActivationMode.automatic;
- }
-
- return this._activationMode;
- }
-
- set activationMode(mode: ActivationMode) {
- this._activationMode = mode;
- this.commonCheckAndDisplay();
- }
-
- /**
- * Implementation of the activation modeling described in the documentation for
- * `activationMode`.
- */
- protected get activationConditionsMet(): boolean {
- switch(this.activationMode) {
- case 'manual':
- return true;
- case 'static':
- return true;
- case 'automatic':
- return !!this.activeTarget;
- default:
- console.error("Unexpected activation mode set for the OSK.");
- return false;
- }
- }
-
- /**
- * A property denoting whether or not the OSK should be presented if it meets its
- * activation conditions.
- *
- * When `activationMode == 'manual'`, `displayIfActive == true` is the lone
- * activation condition.
- *
- * Note: cannot be set to `false` if `activationMode == 'static'`.
- */
- get displayIfActive(): boolean {
- return this._displayIfActive;
- }
-
- set displayIfActive(flag: boolean) {
- if(this.displayIfActive == flag) {
- return;
- }
-
- // if is touch device or is CJK keyboard, this.displayIfActive must remain true.
- if(this.keyboard?.isCJK && !flag) {
- console.warn("Cannot hide display of OSK for CJK keyboards.");
- flag = true;
- } else if(this.hostDevice.touchable && !flag) {
- console.warn("Cannot hide display of OSK when hosted on touch-based devices.");
- flag = true;
- } else if(this.activationMode == 'static') {
- // Silently fail; it's a documentation keyboard.
- // This is the primary difference between 'manual' and 'static'.
- flag = true;
- }
-
- this._displayIfActive = flag;
- this.commonCheckAndDisplay();
- }
-
- /**
- * Used by the activation & visibility properties as a common helper; all of their
- * setters rely on this function to manage presentation (showing / hiding) of the OSK.
- */
- private commonCheckAndDisplay() {
- if(this.activationConditionsMet && this.displayIfActive) {
- this.present();
- } else {
- this.startHide(false);
- }
- }
-
- public get vkbd(): VisualKeyboard {
- if(this.keyboardView instanceof VisualKeyboard) {
- return this.keyboardView;
- } else {
- return null;
- }
- }
-
- public get banner(): BannerManager { // Maintains old reference point used by embedding apps.
- return this.bannerView;
- }
-
- /**
- * The configured width for this VisualKeyboard. May be `undefined` or `null`
- * to allow automatic width scaling.
- */
- get width(): ParsedLengthStyle {
- return this._width;
- }
-
- /**
- * The configured height for this VisualKeyboard. May be `undefined` or `null`
- * to allow automatic height scaling.
- */
- get height(): ParsedLengthStyle {
- return this._height;
- }
-
- /**
- * The computed width for this VisualKeyboard. May be null if auto sizing
- * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.
- */
- get computedWidth(): number {
- // Computed during layout operations; allows caching instead of continuous recomputation.
- if(this.needsLayout) {
- this.refreshLayout();
- }
- return this._computedWidth;
- }
-
- /**
- * The computed height for this VisualKeyboard. May be null if auto sizing
- * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.
- */
- get computedHeight(): number {
- // Computed during layout operations; allows caching instead of continuous recomputation.
- if(this.needsLayout) {
- this.refreshLayout();
- }
- return this._computedHeight;
- }
-
- /**
- * The top-level style string for the font size used by the predictive banner
- * and the primary keyboard visualization elements.
- */
- get baseFontSize(): string {
- return this.parsedBaseFontSize?.styleString || '';
- }
-
- protected get parsedBaseFontSize(): ParsedLengthStyle {
- if(!this._baseFontSize) {
- let keymanweb = com.keyman.singleton;
- this._baseFontSize = OSKView.defaultFontSize(this.device, this.computedHeight, keymanweb.isEmbedded);
- }
-
- return this._baseFontSize;
- }
-
- public static defaultFontSize(device: utils.DeviceSpec, computedHeight: number, isEmbedded: boolean): ParsedLengthStyle {
- if(device.touchable) {
- const fontScale = device.formFactor == 'phone'
- ? 1.6 * (isEmbedded ? 0.65 : 0.6) * 1.2 // Combines original scaling factor with one previously applied to the layer group.
- : 2; // iPad or Android tablet
- return ParsedLengthStyle.special(fontScale, 'em');
- } else {
- return computedHeight ? ParsedLengthStyle.inPixels(computedHeight / 8) : undefined;
- }
- }
-
- public get activeKeyboard(): keyboards.Keyboard {
- return this.keyboard;
- }
-
- public set activeKeyboard(keyboard: keyboards.Keyboard) {
- this.keyboard = keyboard;
- this.loadActiveKeyboard();
-
- if(this.keyboard?.isCJK) {
- this.displayIfActive = true;
- }
- }
-
- private computeFrameHeight(): number {
- return (this.headerView?.layoutHeight.val || 0) + (this.footerView?.layoutHeight.val || 0);
- }
-
- setSize(width?: number | LengthStyle, height?: number | LengthStyle, pending?: boolean) {
- let mutatedFlag = false;
-
- let parsedWidth: ParsedLengthStyle;
- let parsedHeight: ParsedLengthStyle;
-
- if(!width && width !== 0) {
- return;
- }
-
- if(!height && height !== 0) {
- return;
- }
-
- if(Number.isFinite(width as number)) {
- parsedWidth = ParsedLengthStyle.inPixels(width as number);
- } else {
- parsedWidth = new ParsedLengthStyle(width as LengthStyle);
- }
-
- if(Number.isFinite(height as number)) {
- parsedHeight = ParsedLengthStyle.inPixels(height as number);
- } else {
- parsedHeight = new ParsedLengthStyle(height as LengthStyle);
- }
-
- if(width && height) {
- mutatedFlag = !this._width || !this._height;
-
- mutatedFlag = mutatedFlag || parsedWidth.styleString != this._width.styleString;
- mutatedFlag = mutatedFlag || parsedHeight.styleString != this._height.styleString;
-
- this._width = parsedWidth;
- this._height = parsedHeight;
- }
-
- this.needsLayout = this.needsLayout || mutatedFlag;
- this.refreshLayoutIfNeeded(pending);
- }
-
- public setNeedsLayout() {
- this.needsLayout = true;
- }
-
- public refreshLayout(pending?: boolean): void {
- if(!this.keyboardView) {
- return;
- }
-
- // Step 1: have the necessary conditions been met?
- const hasDimensions = this.width && this.height;
- const fixedSize = hasDimensions && this.width.absolute && this.height.absolute;
- const computedStyle = getComputedStyle(this._Box);
- const isInDOM = computedStyle.height != '' && computedStyle.height != 'auto';
-
- // Step 2: determine basic layout geometry
- if(fixedSize) {
- this._computedWidth = this.width.val;
- this._computedHeight = this.height.val;
- } else if(isInDOM && hasDimensions) {
- const parent = this._Box.offsetParent as HTMLElement;
- this._computedWidth = this.width.val * (this.width.absolute ? 1 : parent.offsetWidth);
- this._computedHeight = this.height.val * (this.height.absolute ? 1 : parent.offsetHeight);
- } else {
- // Cannot perform layout operations!
- return;
- }
-
- // Must be set before any references to the .computedWidth and .computedHeight properties!
- this.needsLayout = false;
-
- // Step 3: perform layout operations.
- this.banner.element.style.fontSize = this.baseFontSize;
- if(this.vkbd) {
- this.vkbd.fontSize = this.parsedBaseFontSize;
- }
-
- if(!pending) {
- this.headerView?.refreshLayout();
- this.bannerView.refreshLayout();
- this.footerView?.refreshLayout();
- }
-
- if(this.vkbd) {
- let availableHeight = this.computedHeight - this.computeFrameHeight();
-
- // +5: from kmw-banner-bar's 'top' attribute when active
- if(this.bannerView.height > 0) {
- availableHeight -= this.bannerView.height + 5;
- }
- this.vkbd.setSize(this.computedWidth, availableHeight, pending);
-
- const bs = this._Box.style;
- // OSK size settings can only be reliably applied to standard VisualKeyboard
- // visualizations, not to help text or empty views.
- bs.width = bs.maxWidth = this.computedWidth + 'px';
- bs.height = bs.maxHeight = this.computedHeight + 'px';
-
- // Ensure that the layer's spacebar is properly captioned.
- this.vkbd.showLanguage();
- } else {
- const bs = this._Box.style;
- bs.width = 'auto';
- bs.height = 'auto';
- bs.maxWidth = bs.maxHeight = '';
- }
- }
-
- public refreshLayoutIfNeeded(pending?: boolean) {
- if(this.needsLayout) {
- this.refreshLayout(pending);
- }
- }
-
- public abstract getDefaultWidth(): number;
- public abstract getDefaultKeyboardHeight(): number;
-
- /**
- * Function _Load
- * Scope Private
- * Description OSK initialization when keyboard selected
- */
- _Load() { // Load Help - maintained only temporarily.
- let keymanweb = com.keyman.singleton;
- this.activeKeyboard = keymanweb.core.activeKeyboard;
- }
-
- protected abstract postKeyboardLoad(): void;
-
- protected abstract setBoxStyling(): void;
-
- private loadActiveKeyboard() {
- this.setBoxStyling();
-
- if(this.vkbd) {
- this.vkbd.shutdown();
- }
- this.keyboardView = null;
- this.needsLayout = true;
-
- // Instantly resets the OSK container, erasing / delinking the previously-loaded keyboard.
- this._Box.innerHTML = '';
-
- // Any event-cancelers would go here, after the innerHTML reset.
-
- // Add header element to OSK only for desktop browsers
- if(this.headerView) {
- this._Box.appendChild(this.headerView.element);
- }
-
- // Add suggestion banner bar to OSK
- this._Box.appendChild(this.banner.element);
-
- let kbdView: KeyboardView = this.keyboardView = this._GenerateKeyboardView(this.activeKeyboard);
- this._Box.appendChild(kbdView.element);
- kbdView.postInsert();
-
- // Add footer element to OSK only for desktop browsers
- if(this.footerView) {
- this._Box.appendChild(this.footerView.element);
- }
- // END: construction of the actual internal layout for the overall OSK
-
- this.banner.appendStyles();
-
- if(this.vkbd) {
- // Create the key preview (for phones)
- this.vkbd.createKeyTip();
- // Create the globe hint (for embedded contexts; has a stub for other contexts)
- this.vkbd.createGlobeHint();
-
- // Append a stylesheet for this keyboard for keyboard specific styles
- // or if needed to specify an embedded font
- this.vkbd.appendStyleSheet();
- }
-
- this.postKeyboardLoad();
- }
-
- private layerChangeHandler: text.SystemStoreMutationHandler = function(this: OSKView,
- source: text.MutableSystemStore,
- newValue: string) {
- // This handler is also triggered on state-key state changes (K_CAPS) that
- // may not actually change the layer.
- if(this.vkbd) {
- this.vkbd._UpdateVKShiftStyle(newValue);
- }
-
- if((this.vkbd && this.vkbd.layerId != newValue) || source.value != newValue) {
- // Prevents console errors when a keyboard only displays help.
- // Can occur when using SHIFT with sil_euro_latin on a desktop form-factor.
- //
- // Also, only change the layer ID itself if there is an actual corresponding layer
- // in the OSK.
- if(this.vkbd?.layerGroup.layers[newValue]) {
- this.vkbd.layerId = newValue;
- // Ensure that the layer's spacebar is properly captioned.
- this.vkbd.showLanguage();
- }
-
- // Ensure the keyboard view is modeling the correct state. (Correct layer, etc.)
- this.keyboardView.updateState();
- // We need to recalc the font size here because the layer did not have
- // calculated dimensions available before it was visible
- this.refreshLayout();
- }
- }.bind(this);
-
- private _GenerateKeyboardView(keyboard: keyboards.Keyboard): KeyboardView {
- let device = this.device;
-
- if(this.vkbd) {
- this.vkbd.shutdown();
- }
-
- this._Box.className = "";
-
- // Case 1: since we hide the system keyboard on touch devices, we need
- // to display SOMETHING that can accept input.
- if(keyboard == null && !device.touchable) {
- // We do not (currently) allow selecting the default system keyboard on
- // touch form-factors. Likely b/c mnemonic difficulties.
- return new EmptyView();
- } else {
- // Generate a visual keyboard from the layout (or layout default)
- // Condition is false if no key definitions exist, formFactor == desktop, AND help text exists. All three.
- if(keyboard && keyboard.layout(device.formFactor as utils.FormFactor)) {
- return this._GenerateVisualKeyboard(keyboard);
- } else if(!keyboard /* && device.touchable (implied) */) {
- // Show a basic, "hollow" OSK that at least allows input, since we're
- // on a touch device and hiding the system keyboard
- return this._GenerateVisualKeyboard(null);
- } else {
- // A keyboard help-page or help-text is still a visualization, even not a standard OSK.
- return new HelpPageView(keyboard);
- }
- }
- }
-
- /**
- * Function _GenerateVisualKeyboard
- * Scope Private
- * @param {Object} keyboard The keyboard to visualize
- * Description Generates the visual keyboard element and attaches it to KMW
- */
- private _GenerateVisualKeyboard(keyboard: keyboards.Keyboard): VisualKeyboard {
- let device = this.device;
-
- // Root element sets its own classes, one of which is 'kmw-osk-inner-frame'.
- let vkbd = new VisualKeyboard(keyboard, device, this.hostDevice);
-
- // Ensure the OSK's current layer is kept up to date.
- let core = com.keyman.singleton.core; // Note: will eventually be a class field.
- core.keyboardProcessor.layerStore.handler = this.layerChangeHandler;
-
- // Set box class - OS and keyboard added for Build 360
- this._Box.className=device.formFactor+' '+ device.OS.toLowerCase() + ' kmw-osk-frame';
-
- // Add primary keyboard element to OSK
- return vkbd;
- }
-
- /**
- * The main function for presenting the OSKView.
- *
- * This includes:
- * - refreshing its layout
- * - displaying it
- * - positioning it
- */
- public present(): void {
- // Do not try to display OSK if no active element
- if(!this.mayShow()) {
- return;
- }
-
- // Ensure the keyboard view is modeling the correct state. (Correct layer, etc.)
- this.keyboardView.updateState();
-
- this._Box.style.display='block'; // Is 'none' when hidden.
-
- // First thing after it's made visible.
- this.refreshLayoutIfNeeded();
-
- if(this.keyboardView instanceof VisualKeyboard) {
- this.keyboardView.showLanguage();
- }
-
- this._Visible=true;
-
- /* In case it's still '0' from a hide() operation.
- *
- * (Opacity is only modified when device.touchable = true,
- * though a couple of extra conditions may apply.)
- */
- this._Box.style.opacity = '1';
-
- // If OSK still hidden, make visible only after all calculation finished
- if(this._Box.style.visibility == 'hidden') {
- let _this = this;
- window.setTimeout(function() {
- _this._Box.style.visibility = 'visible';
- }, 0);
- }
-
- this.setDisplayPositioning();
- }
-
- /**
- * Method usable by subclasses of OSKView to control that OSKView type's
- * positioning behavior when needed by the present() method.
- */
- protected abstract setDisplayPositioning();
-
- /**
- * Method used to start a potentially-asynchronous hide of the OSK.
- * @param hiddenByUser `true` if this hide operation was directly requested by the user.
- */
- public startHide(hiddenByUser: boolean): void {
- if(!this.mayHide(hiddenByUser)) {
- return;
- }
-
- if(hiddenByUser) {
- // The one location outside of the `displayIfActive` property that bypasses the setter.
- // Avoids needless recursion that could be triggered by it, as we're already in the
- // process of hiding the OSK anyway.
- this._displayIfActive = ((this.keyboard.isCJK || this.hostDevice.touchable)? true : false); // I3363 (Build 301)
- }
-
- let promise: Promise = null;
- if(this._Box && this.hostDevice.touchable && !(this.keyboardView instanceof EmptyView)) {
- /**
- * Note: this refactored code appears to reflect a currently-dead code path. 14.0's
- * equivalent is either extremely niche or is actually inaccessible.
- */
- promise = this.useHideAnimation();
- } else {
- promise = Promise.resolve(true);
- }
-
- const _this = this;
- promise.then(function(shouldHide: boolean) {
- if(shouldHide) {
- _this.finalizeHide();
- }
- });
-
- // Allow UI to execute code when hiding the OSK
- var p={};
- p['HiddenByUser']=hiddenByUser;
- this.doHide(p);
-
- // If hidden by the UI, be sure to restore the focus
- if(hiddenByUser && this.activeTarget instanceof dom.targets.OutputTarget) {
- this.activeTarget?.focus();
- }
- }
-
- /**
- * Performs the _actual_ logic and functionality involved in hiding the OSK.
- */
- protected finalizeHide() {
- if(document.body.className.indexOf('osk-always-visible') >= 0) {
- return;
- }
-
- if(this._Box) {
- let bs=this._Box.style;
- bs.display = 'none';
- bs.transition = '';
- bs.opacity = '1';
- this._Visible=false;
- }
-
- if(this.vkbd) {
- this.vkbd.onHide();
- }
- }
-
- /**
- *
- * @returns `false` if the OSK is in an invalid state for being presented to the user.
- */
- protected mayShow(): boolean {
- if(!this.activationConditionsMet) {
- return false;
- }
-
- // Never display the OSK for desktop browsers unless KMW element is focused, and a keyboard selected
- if(!this.keyboardView || this.keyboardView instanceof EmptyView || !this.displayIfActive) {
- return false;
- }
-
- if(!this._Box) {
- return false;
- }
-
- return true;
- }
-
- /**
- *
- * @param hiddenByUser
- * @returns `false` if the OSK is in an invalid state for being hidden from the user.
- */
- protected mayHide(hiddenByUser: boolean): boolean {
- if(this.activationMode != 'automatic' && this.displayIfActive) {
- return false;
- }
-
- if(!hiddenByUser && this.hostDevice.formFactor == 'desktop') {
- //Allow desktop OSK to remain visible on blur if body class set
- if(document.body.className.indexOf('osk-always-visible') >= 0) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Applies CSS styling and handling needed to perform a fade animation when
- * hiding the OSK.
- *
- * Note: currently reflects an effectively-dead code path, though this is
- * likely not intentional. Other parts of the KMW engine seem to call hideNow()
- * synchronously after each and every part of the engine that calls this function,
- * cancelling the Promise.
- *
- * @returns A Promise denoting either cancellation of the hide (`false`) or
- * completion of the hide & its animation (`true`)
- */
-
- protected useHideAnimation(): Promise {
- const os = this._Box.style;
- const _this = this;
-
- return new Promise(function(resolve) {
- const cleanup = function() {
- // TODO(lowpri): attach event listeners on create and leave them there
- _this._Box.removeEventListener('transitionend', cleanup, false);
- _this._Box.removeEventListener('webkitTransitionEnd', cleanup, false);
- _this._Box.removeEventListener('transitioncancel', cleanup, false);
- _this._Box.removeEventListener('webkitTransitionCancel', cleanup, false);
- if(_this._animatedHideTimeout != 0) {
- window.clearTimeout(_this._animatedHideTimeout);
- }
- _this._animatedHideTimeout = 0;
-
- if(_this._Visible && _this.activationConditionsMet) {
- // Leave opacity alone and clear transition if another element activated
- os.transition='';
- os.opacity='1';
- resolve(false);
- return false;
- } else {
- resolve(true);
- return true;
- }
- }, startup = function() {
- _this._Box.removeEventListener('transitionrun', startup, false);
- _this._Box.removeEventListener('webkitTransitionRun', startup, false);
- _this._Box.addEventListener('transitionend', cleanup, false);
- _this._Box.addEventListener('webkitTransitionEnd', cleanup, false);
- _this._Box.addEventListener('transitioncancel', cleanup, false);
- _this._Box.addEventListener('webkitTransitionCancel', cleanup, false);
- };
-
- _this._Box.addEventListener('transitionrun', startup, false);
- _this._Box.addEventListener('webkitTransitionRun', startup, false);
-
- os.transition='opacity 0.5s linear 0';
- os.opacity='0';
-
- // Cannot hide the OSK smoothly using a transitioned drop, since for
- // position:fixed elements transitioning is incompatible with translate3d(),
- // and also does not work with top, bottom or height styles.
- // Opacity can be transitioned and is probably the simplest alternative.
- // We must condition on osk._Visible in case focus has since been moved to another
- // input (in which case osk._Visible will be non-zero)
- _this._animatedHideTimeout = window.setTimeout(cleanup,
- 200); // Wait a bit before starting, to allow for moving to another element
- });
- }
-
- /**
- * Used to synchronously hide the OSK, cancelling any async hide animations that have
- * not started and immediately completing the hide of any hide ops pending completion
- * of their animation.
- */
- public hideNow() {
- if(!this.mayHide(false) || !this._Box) {
- return;
- }
-
- // Two possible uses for _animatedHideResolver:
- // - _animatedHideTimeout is set: animation is waiting to start
- // - _animatedHideTimeout is null: animation has already started.
-
- // Was an animated hide waiting to start? Just cancel it.
- if(this._animatedHideTimeout) {
- window.clearTimeout(this._animatedHideTimeout);
- this._animatedHideTimeout = 0;
- }
-
- // Was an animated hide already in progress? If so, just trigger it early.
- const os = this._Box.style;
- os.transition='';
- os.opacity='0';
- this.finalizeHide();
- }
-
- ['shutdown']() {
- // Disable the OSK's event handlers.
- this.removeBaseMouseEventListeners();
- this.removeBaseTouchEventListeners();
-
- // Remove the OSK's elements from the document, allowing them to be properly cleaned up.
- // Necessary for clean engine testing.
- var _box = this._Box;
- if(_box.parentElement) {
- _box.parentElement.removeChild(_box);
- }
- }
-
- /**
- * Function getRect
- * Scope Public
- * @return {Object.} Array object with position and size of OSK container
- * Description Get rectangle containing KMW Virtual Keyboard
- */
- ['getRect'](): OSKRect { // I2405
- var p: OSKRect = {};
-
- // Always return these based upon _Box; using this.vkbd will fail to account for banner and/or
- // the desktop OSK border.
- p['left'] = p.left = dom.Utils.getAbsoluteX(this._Box);
- p['top'] = p.top = dom.Utils.getAbsoluteY(this._Box);
-
- p['width'] = this.computedWidth;
- p['height'] = this.computedHeight;
- return p;
- }
-
- /* ---- Legacy interfacing methods and fields ----
- *
- * The endgoal is to eliminate the need for these entirely, but extra work and care
- * will be necessary to achieve said endgoal for these methods.
- *
- * The simplest way forward is to maintain them, then resolve them independently,
- * one at a time.
- */
-
- /**
- * Display build number
- *
- * In the future, this should raise an event that the consuming KeymanWeb
- * engine may listen for & respond to, rather than having it integrated
- * as part of the OSK itself.
- */
- showBuild() {
- let keymanweb = com.keyman.singleton;
- keymanweb.util.internalAlert('KeymanWeb Version '+keymanweb['version']+'.'+keymanweb['build']+'
'
- +'Copyright © 2021 SIL International');
- }
-
- /**
- * Display list of installed keyboards in pop-up menu
- *
- * In the future, this language menu should be defined as a UI module like the standard
- * desktop UI modules. The globe key should then trigger an event to _request_ that the
- * consuming engine display the active UI module's menu.
- *
- **/
- showLanguageMenu() {
- if(this.hostDevice.touchable) {
- this.lgMenu = new LanguageMenu(com.keyman.singleton);
- this.lgMenu.show();
- }
- }
-
- hideLanguageMenu() {
- this.lgMenu?.hide();
- this.lgMenu = null;
- }
-
- // OSK state fields & events
- //
- // These are relatively stable and may be preserved as they are.
- _Visible: boolean = false;
-
- /**
- * Function enabled
- * Scope Public
- * @return {boolean|number} True if KMW OSK enabled
- * Description Test if KMW OSK is enabled
- */
- ['isEnabled'](): boolean {
- return this.displayIfActive;
- }
-
- /**
- * Function isVisible
- * Scope Public
- * @return {boolean|number} True if KMW OSK visible
- * Description Test if KMW OSK is actually visible
- * Note that this will usually return false after any UI event that results in (temporary) loss of input focus
- */
- ['isVisible'](): boolean {
- return this._Visible;
- }
-
- /**
- * Function hide
- * Scope Public
- * Description Prevent display of OSK window on focus
- */
- ['hide']() {
- this.displayIfActive = false;
- this.startHide(true);
- }
-
- /**
- * Description Display KMW OSK (at position set in callback to UI)
- * Function show
- * Scope Public
- * @param {(boolean|number)=} bShow True to display, False to hide, omitted to toggle
- */
- ['show'](bShow?: boolean) {
- if(arguments.length > 0) {
- this.displayIfActive = bShow;
- } else {
- if(this.activationConditionsMet) {
- this.displayIfActive = !this.displayIfActive;
- }
- }
- }
-
- /**
- * Allow UI to respond to OSK being shown (passing position and properties)
- *
- * @param {Object=} p object with coordinates and userdefined flag
- * @return {boolean}
- *
- */
- doShow(p) {
- return com.keyman.singleton.util.callEvent('osk.show',p);
- }
-
- /**
- * Allow UI to update respond to OSK being hidden
- *
- * @param {Object=} p object with coordinates and userdefined flag
- * @return {boolean}
- *
- */
- doHide(p) {
- return com.keyman.singleton.util.callEvent('osk.hide',p);
- }
-
- /**
- * Function addEventListener
- * Scope Public
- * @param {string} event event name
- * @param {function(Object)} func event handler
- * @return {boolean}
- * Description Wrapper function to add and identify OSK-specific event handlers
- */
- ['addEventListener'](event: string, func: (obj) => boolean) {
- // As the following title bar buttons (for desktop / FloatingOSKView) do nothing unless
- // a site designer uses these events, we disable / hide them until an event is attached.
- let titleBar = this.headerView;
- if(titleBar && titleBar instanceof layouts.TitleBar) {
- switch(event) {
- case 'configclick':
- titleBar.configEnabled = true;
- break;
- case 'helpclick':
- titleBar.helpEnabled = true;
- break;
- }
- }
-
- return com.keyman.singleton.util.addEventListener('osk.'+event, func);
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/oskViewComponent.ts b/web/src/engine/main/osk/oskViewComponent.ts
deleted file mode 100644
index fb93acd53e..0000000000
--- a/web/src/engine/main/osk/oskViewComponent.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace com.keyman.osk {
- export interface OSKViewComponent {
- readonly element: HTMLElement;
- readonly layoutHeight: ParsedLengthStyle;
- refreshLayout(): void;
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/pendingGesture.interface.ts b/web/src/engine/main/osk/pendingGesture.interface.ts
deleted file mode 100644
index 1e2d3a1138..0000000000
--- a/web/src/engine/main/osk/pendingGesture.interface.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-namespace com.keyman.osk {
- /**
- * Used for evaluating potential gestures. Classes adhering to this interface
- * should be instantiated whenever the (implied) state-machine allows a new
- * touch event to mark the start of a potential new gesture.
- *
- * For example, whenever a user touches a base key and there are no "realized"
- * (fully-completed, but as-of-yet unresolved) gestures, that state allows the
- * start of a potential new longpress event.
- *
- * The role of the `PendingGesture` is complete whenever all touch-events and
- * conditions necessary for a modeled gesture have been met. As this point,
- * it should be `resolve`d, fulfilling its `promise`. This results in a
- * `RealizedGesture` appropriate for the gesture type that is used to obtain
- * the final `KeyEvent` for the overall gesture sequence.
- *
- * For example, a "longpress" is considered resolved once the user has maintained
- * an active, stationary touch point on the same key for a sufficiently long
- * period without releasing it.
- * * Were it released earlier, that would result in selection of a base key.
- *
- * Alternatively, a "flick" might be considered resolved if:
- * * a user has rapidly moved a touch point in a consistent direction
- * * for a long enough distance
- * * and _then_ releases that touch point within a short timeframe.
- *
- * The pending gesture should only `resolve` to a realized gesture once
- * _all_ such conditions are met, confirming that this specific gesture,
- * and _only_ this specific gesture, could have resulted from the active
- * touch sequence.
- *
- * The `RealizedGesture` that results and is 'returned' via the Promise will
- * be handled by the `VisualKeyboard` class, which will retrieve and forward
- * any `KeyEvent` that results from the overall gesture input sequence.
- *
- * @see `RealizedGesture`
- */
- export interface PendingGesture {
- readonly baseKey: KeyElement;
- readonly promise?: Promise;
-
- cancel(): void;
- resolve?(): void;
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/preProcessor.ts b/web/src/engine/main/osk/preProcessor.ts
deleted file mode 100644
index 40bab7ade0..0000000000
--- a/web/src/engine/main/osk/preProcessor.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-namespace com.keyman.osk {
- export class PreProcessor {
- /**
- * Simulate a keystroke according to the touched keyboard button element
- *
- * Note that the test-case oriented 'recorder' stubs this method to facilitate OSK-based input
- * recording for use in test cases. If changing this function, please ensure the recorder is
- * not affected.
- *
- * @param {Object} e element touched (or clicked)
- */
- static clickKey(e: osk.KeyElement, input?: InputEventCoordinate) {
- let keyman = com.keyman.singleton;
- let Lkc = keyman['osk'].vkbd.initKeyEvent(e, input);
- if(!Lkc) {
- return true;
- }
-
- return this.raiseKeyEvent(Lkc);
- }
-
- static raiseKeyEvent(keyEvent: text.KeyEvent) {
- let keyman = com.keyman.singleton;
- var Lelem = keyman.domManager.lastActiveElement;
-
- if(Lelem != null) {
- // Handle any DOM state management related to click inputs.
- let outputTarget = dom.Utils.getOutputTarget(Lelem);
- keyman.domManager.initActiveElement(Lelem);
-
- // Clear any cached codepoint data; we can rebuild it if it's unchanged.
- outputTarget.invalidateSelection();
- // Deadkey matching continues to be troublesome.
- // Deleting matched deadkeys here seems to correct some of the issues. (JD 6/6/14)
- outputTarget.deadkeys().deleteMatched(); // Delete any matched deadkeys before continuing
-
- if(!keyman.isEmbedded) {
- keyman.uiManager.setActivatingUI(true);
- com.keyman.dom.DOMEventHandlers.states._IgnoreNextSelChange = 100;
- keyman.domManager.focusLastActiveElement();
- com.keyman.dom.DOMEventHandlers.states._IgnoreNextSelChange = 0;
- }
-
- let retVal = PreProcessor.handleClick(keyEvent, outputTarget, null);
-
- // Now that processing is done, we can do a bit of post-processing, too.
- keyman.uiManager.setActivatingUI(false); // I2498 - KeymanWeb OSK does not accept clicks in FF when using automatic UI
- return retVal;
- } else {
- return true;
- }
- }
-
- // Serves to hold DOM-dependent code that affects both 'native' and 'embedded' mode OSK use
- // after the KeyEvent object has been properly instantiated. This should help catch any
- // mutual last-minute DOM-side interactions before passing control to the processor... such as
- // the UI-control command keys as seen below.
- static handleClick(Lkc: text.KeyEvent, outputTarget: text.OutputTarget, e: osk.KeyElement) {
- let keyman = com.keyman.singleton;
- // Exclude menu and OSK hide keys from normal click processing
- if(Lkc.kName == 'K_LOPT' || Lkc.kName == 'K_ROPT') {
- keyman['osk'].vkbd.optionKey(e, Lkc.kName, true);
- return true;
- }
-
- let retVal = !!keyman.core.processKeyEvent(Lkc, outputTarget);
-
- return retVal;
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/realizedGesture.interface.ts b/web/src/engine/main/osk/realizedGesture.interface.ts
deleted file mode 100644
index 390a6abf23..0000000000
--- a/web/src/engine/main/osk/realizedGesture.interface.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-namespace com.keyman.osk {
- /**
- * Implementations of this interface allow individual types of gestures to
- * specify any additional user interaction and functionality (which may
- * include UI elements) appropriate for obtaining a key event that may be
- * produced by the modeled gesture type. These should only be instantiated
- * once the associated `PendingLongpress` is no longer 'pending' - once it
- * has become clear that the input touch-event sequence could only correspond
- * to the modeled gesture.
- *
- * For example, when a longpress gesture completes - and hence, the user has
- * kept their finger stationary on the same key for a long enough period -
- * we display a popup view presenting subkeys corresponding to the gesture's
- * underlying element. This popup view accepts touch input and completes only
- * upon release of the ongoing touch sequence.
- *
- * Gestures are events that occur over intervals of time, and since some of them
- * will require time and user interaction after becoming 'realized', these cases
- * will be inherently async. The simplest way to model this is with `Promise`s.
- *
- * If appropriate for the modeled gesture type, an implementation may supply an
- * instantly-resolving `Promise`. This may be appropriate for modeling "flick"
- * or "swipe" gestures in the future, which may require no additional input once
- * such a gesture is fully realized.
- */
- export interface RealizedGesture {
- readonly baseKey: KeyElement;
- readonly promise: Promise;
-
- clear(): void;
- isVisible(): boolean;
- updateTouch(input: InputEventCoordinate): void;
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/touchEventEngine.ts b/web/src/engine/main/osk/touchEventEngine.ts
deleted file mode 100644
index 4e141d20ed..0000000000
--- a/web/src/engine/main/osk/touchEventEngine.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-///
-
-namespace com.keyman.osk {
- export class TouchEventEngine extends InputEventEngine {
- private readonly _touchStart: typeof TouchEventEngine.prototype.onTouchStart;
- private readonly _touchMove: typeof TouchEventEngine.prototype.onTouchMove;
- private readonly _touchEnd: typeof TouchEventEngine.prototype.onTouchEnd;
-
- public constructor(config: InputEventEngineConfig) {
- super(config);
-
- this._touchStart = this.onTouchStart.bind(this);
- this._touchMove = this.onTouchMove.bind(this);
- this._touchEnd = this.onTouchEnd.bind(this);
- }
-
- public static forVisualKeyboard(vkbd: VisualKeyboard) {
- let config: InputEventEngineConfig = {
- targetRoot: vkbd.element,
- eventRoot: vkbd.element,
- inputStartHandler: vkbd.touch.bind(vkbd),
- inputMoveHandler: vkbd.moveOver.bind(vkbd),
- inputMoveCancelHandler: vkbd.moveCancel.bind(vkbd),
- inputEndHandler: vkbd.release.bind(vkbd),
- coordConstrainedWithinInteractiveBounds: vkbd.detectWithinInteractiveBounds.bind(vkbd)
- };
-
- return new TouchEventEngine(config);
- }
-
- public static forPredictiveBanner(banner: SuggestionBanner, handlerRoot: SuggestionManager) {
- const config: InputEventEngineConfig = {
- targetRoot: banner.getDiv(),
- // document.body is the event root b/c we need to track the mouse if it leaves
- // the VisualKeyboard's hierarchy.
- eventRoot: banner.getDiv(),
- inputStartHandler: handlerRoot.touchStart.bind(handlerRoot),
- inputMoveHandler: handlerRoot.touchMove.bind(handlerRoot),
- inputEndHandler: handlerRoot.touchEnd.bind(handlerRoot),
- coordConstrainedWithinInteractiveBounds: function() { return true; }
- };
-
- return new TouchEventEngine(config);
- }
-
- registerEventHandlers() {
- this.config.eventRoot.addEventListener('touchstart', this._touchStart, true);
- this.config.eventRoot.addEventListener('touchmove', this._touchMove, false);
- // The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'.
- this.config.eventRoot.addEventListener('touchend', this._touchEnd, true);
- }
-
- unregisterEventHandlers() {
- this.config.eventRoot.removeEventListener('touchstart', this._touchStart, true);
- this.config.eventRoot.removeEventListener('touchmove', this._touchMove, false);
- this.config.eventRoot.removeEventListener('touchend', this._touchEnd, true);
- }
-
- private preventPropagation(e: TouchEvent) {
- // Standard event maintenance
- e.preventDefault();
- e.cancelBubble=true;
-
- if(typeof e.stopImmediatePropagation == 'function') {
- e.stopImmediatePropagation();
- } else if(typeof e.stopPropagation == 'function') {
- e.stopPropagation();
- }
- }
-
- onTouchStart(event: TouchEvent) {
- this.onInputStart(InputEventCoordinate.fromEvent(event));
- }
-
- onTouchMove(event: TouchEvent) {
- this.preventPropagation(event);
- const coord = InputEventCoordinate.fromEvent(event);
-
- if(this.config.coordConstrainedWithinInteractiveBounds(coord)) {
- this.onInputMove(coord);
- } else {
- this.onInputMoveCancel(coord);
- }
- }
-
- onTouchEnd(event: TouchEvent) {
- this.onInputEnd(InputEventCoordinate.fromEvent(event));
- }
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/uiTouchHandlerBase.ts b/web/src/engine/main/osk/uiTouchHandlerBase.ts
deleted file mode 100644
index 84cdaae234..0000000000
--- a/web/src/engine/main/osk/uiTouchHandlerBase.ts
+++ /dev/null
@@ -1,428 +0,0 @@
-namespace com.keyman.osk {
- /**
- * This class was added to facilitate scroll handling for overflow-x elements, though it could
- * be extended in the future to accept overflow-y if needed.
- *
- * This is necessary because of the OSK's need to use `.preventDefault()` for stability; that
- * same method blocks native handling of overflow scrolling for touch browsers.
- */
- class ScrollState {
- // While we don't currently track y-coordinates here, the class is designed
- // to permit tracking them with minimal extra effort if we ever decide to do so.
- x: number;
- totalLength = 0;
-
- // The amount of coordinate 'noise' allowed during a scroll-enabled touch allowed
- // before interpreting the currently-ongoing touch command as having scrolled.
- static readonly HAS_SCROLLED_FUDGE_FACTOR = 10;
-
- constructor(coord: InputEventCoordinate) {
- this.x = coord.x;
-
- this.totalLength = 0;
- }
-
- updateTo(coord: InputEventCoordinate): {deltaX: number} {
- let x = this.x;
- this.x = coord.x;
-
- let deltas = {deltaX: this.x - x};
- this.totalLength += Math.abs(deltas.deltaX);
-
- return deltas;
- }
-
- public get hasScrolled(): boolean {
- // Allow an accidental fudge-factor for overflow element noise during a touch, but not much.
- return this.totalLength > ScrollState.HAS_SCROLLED_FUDGE_FACTOR;
- }
- }
-
- export abstract class UITouchHandlerBase {
- private rowClassMatch: string;
- private selectedTargetMatch: string;
- private baseElement: HTMLElement;
-
- private touchX: number;
- private touchY: number;
- private touchCount: number;
-
- private currentTarget: Target;
-
- private scrollTouchState: ScrollState;
- private pendingTarget: Target;
-
- constructor(baseElement: HTMLElement, rowClassMatch: string, selectedTargetMatch: string) {
- this.baseElement = baseElement;
- this.rowClassMatch = rowClassMatch;
- this.selectedTargetMatch = selectedTargetMatch;
- }
-
- /**
- * Finds the internally-preferred target element or submenu target element.
- * @param e The DOM element that actually received the touch event.
- * May be parent, child, or the actually-desired element itself.
- */
- abstract findTargetFrom(e: HTMLElement): Target;
-
- /**
- * Highlights the target element as visual feedback representing
- * a pending touch.
- * @param t The `Target` to highlight
- * @param state `true` to apply highlighting, `false` to remove it.
- */
- protected abstract highlight(t: Target, state: boolean): void;
-
- /**
- * Called whenever the touch-handling analysis determines that the Target has been selected
- * @param t The `Target` to activate/execute.
- */
- protected abstract select(t: Target): void;
-
- /**
- * Requests info on whether or not the indicated `Target` has subkeys or a submenu.
- * @param t A `Target`.
- */
- protected abstract hasSubmenu(t: Target): boolean;
-
- /**
- * Indicates that the user is maintaining a `Touch` on the specified `Target`.
- * Popups and-or longpress menus may be appropriate.
- * @param t The `Target` being held.
- */
- protected abstract hold(t: Target): void;
-
- /**
- * Signals that any popup elements (previews, subkey views, etc) should be cancelled.
- */
- protected abstract clearHolds(): void;
-
- /**
- * Requests a boolean indicating whether or not the UI is currently displaying any input-blocking popup elements.
- * Embedded mode should return `true` when the app is displaying popup menus.
- */
- protected abstract hasModalPopup(): boolean;
-
- /**
- * Designed to support highlighting of prepended base keys on phone form-factor subkey menus.
- * @param target The base element with a potential subkey menu alias.
- * @returns The aliased submenu version of the `Target`, or the original `Target` if no alias exists.
- */
- protected abstract dealiasSubTarget(target: Target): Target;
-
- /**
- * Should return true whenever a 'native'-mode submenu (or subkey) display is active.
- */
- protected abstract isSubmenuActive(): boolean;
-
- /**
- * For 'native' mode - requests that the submenu for the indicated `Target` be instantly displayed.
- * @param target The base element with a potential submenu
- */
- protected abstract displaySubmenuFor(target: Target);
-
- /**
- * Identify the key nearest to (but NOT under) the touch point if at the end of a key row,
- * but return null more than about 0.6 key width from the nearest key.
- *
- * @param {Object} coord A pre-analyzed input coordinate
- * @param {Object} t HTML object at touch point
- * @param {boolean} omitCurrent Omits any target directly under the touch point.
- * @return {Object} nearest key to touch point
- *
- **/
- private findTargetFromTouch(coord: InputEventCoordinate, t: HTMLElement, forMove: boolean): Target {
- var x = coord.x;
-
- // Get the UI row beneath touch point (SuggestionBanner div, 'kmw-key-row' if OSK, ...)
- while(t && t.className !== undefined && t.className.indexOf(this.rowClassMatch) < 0) {
- t = t.parentNode;
- }
- if(!t) {
- return null;
- }
-
- // Find minimum distance from any key
- var k: number, bestMatch=0, dxMax=24, dxMin=100000, x1: number, x2: number;
- for(k = 0; k < t.childNodes.length; k++) {
- let childNode = t.childNodes[k] as HTMLElement;
-
- if(this.isInvalidTarget(this.findTargetFrom(childNode))) {
- continue;
- }
-
- x1 = childNode.offsetLeft;
- x2 = x1 + childNode.offsetWidth;
-
- // If it lies completely to the right and is the closest so far
- let dxRight = x1 - x;
- if(dxRight >= 0 && dxRight < dxMin) {
- bestMatch = k;
- dxMin = dxRight;
- }
-
- // If it lies completely to the left and is the closest so far
- let dxLeft = x - x2;
- if(dxLeft >= 0 && dxLeft < dxMin) {
- bestMatch = k;
- dxMin = dxLeft;
- }
-
- // If it is neither completely to the left nor completely to the right,
- // it's under the cursor. Stop the search!
- if(dxLeft < 0 && dxRight < 0) {
- return this.findTargetFrom(childNode);
- }
- }
-
- if(dxMin < 100000) {
- t = t.childNodes[bestMatch];
- x1 = t.offsetLeft;
- x2 = x1 + t.offsetWidth;
-
- // Limit extended touch area to the larger of 0.6 of the potential target's width and 24 px
- if(t.offsetWidth > 40) {
- dxMax = 0.6 * t.offsetWidth;
- }
-
- if(((x1 - x) >= 0 && (x1 - x) < dxMax) || ((x - x2) >= 0 && (x - x2) < dxMax)) {
- return this.findTargetFrom(t);
- }
- }
- return null;
- }
-
- findBestTarget(coord: InputEventCoordinate, forMove?: boolean) {
- var eventTarget: HTMLElement;
-
- if(forMove) {
- const clientX = coord.x + document.body.scrollLeft;
- const clientY = coord.y + document.body.scrollTop;
- eventTarget = document.elementFromPoint(clientX, clientY) as HTMLElement;
- } else {
- eventTarget = coord.target as HTMLElement;
- }
-
- let target = this.findTargetFrom(eventTarget);
-
- // Should refactor this multi-check a bit for more overall reliability.
- if(!target) {
- // We didn't find a direct target, so we should look for the closest possible one.
- // Filters out invalid targets.
- target = this.findTargetFromTouch(coord, eventTarget, forMove);
- }
-
- return target;
- }
-
- /**
- * Reports whether or not a `Target` should be considered invalid. Needed by the OSK for
- * hidden keys.
- * @param target A `Target` element to be validated.
- */
- protected isInvalidTarget(target: Target): boolean {
- return false;
- }
-
- touchStart(coord: InputEventCoordinate) {
- // Determine the selected Target, manage state.
- this.currentTarget = this.findBestTarget(coord);
- this.touchX = coord.x;
- this.touchY = coord.y;
-
- // If popup stuff, immediately return.
-
- this.touchCount = coord.activeInputCount;
-
- if(!this.currentTarget) {
- return;
- }
-
- // Establish scroll tracking.
- let shouldScroll = (this.currentTarget.clientWidth < this.currentTarget.scrollWidth);
- this.scrollTouchState = shouldScroll ? new ScrollState(coord) : null;
-
- // Alright, Target acquired! Now to use it:
-
- // Highlight the touched key
- this.highlight(this.currentTarget,true);
-
- // If used by the OSK, the special function keys need immediate action
- // Add a `checkForImmediates()` to facilitate this.
- if(this.pendingTarget) {
- this.highlight(this.pendingTarget, false);
- this.select(this.pendingTarget);
- this.clearHolds();
- // Decrement the number of unreleased touch points to prevent
- // sending the keystroke again when the key is actually released
- this.touchCount--;
- } else {
- // If this key has subkey, start timer to display subkeys after delay, set up release
- this.hold(this.currentTarget);
- }
- this.pendingTarget = this.currentTarget;
- }
-
- touchEnd(coord: InputEventCoordinate): void {
- // Prevent incorrect multi-touch behaviour if native or device popup visible
- let t = this.currentTarget;
-
- if(this.isSubmenuActive() || this.hasModalPopup()) {
- // Ignore release if a multiple touch
- if(coord.activeInputCount > 0) {
- return;
- }
-
- // Cancel (but do not execute) pending key if neither a popup key or the base key
- if(t == null || t.id.indexOf('popup') < 0) {
- if (this.pendingTarget) {
- this.highlight(this.pendingTarget,false);
- }
- this.clearHolds();
- this.pendingTarget = null;
- }
- }
-
- // Test if moved off screen (effective release point must be corrected for touch point horizontal speed)
- // This is not completely effective and needs some tweaking, especially on Android
- var x = coord.x;
- var beyondEdge = ((x < 2 && this.touchX > 5) || (x > window.innerWidth - 2 && this.touchX < window.innerWidth - 5));
-
- if(this.scrollTouchState) {
- beyondEdge = beyondEdge || this.scrollTouchState.hasScrolled;
- }
-
- // Save then decrement current touch count
- var tc=this.touchCount;
- if(this.touchCount > 0) {
- this.touchCount--;
- }
-
- // Process and clear highlighting of pending target
- if(this.pendingTarget) {
- this.highlight(this.pendingTarget,false);
-
- // Output character unless moved off key
- if(this.pendingTarget.className.indexOf('hidden') < 0 && tc > 0 && !beyondEdge) {
- this.select(this.pendingTarget);
- }
- this.clearHolds();
- this.pendingTarget = null;
- // Always clear highlighting of current target on release (multi-touch)
- } else {
- t = this.findBestTarget(coord);
-
- if(t) {
- this.highlight(t,false);
- }
- }
- }
-
- /**
- * OSK touch move event handler
- *
- * @param {Object} coord A pre-analyzed input coordinate
- *
- **/
- touchMove(coord: InputEventCoordinate) : void {
- let keyman = com.keyman.singleton;
- let util = keyman.util;
-
- // Do not attempt to support reselection of target key for overlapped keystrokes
- if(coord.activeInputCount > 1 || this.touchCount == 0) {
- return;
- }
-
- if(this.currentTarget && this.scrollTouchState != null) {
- let deltaX = this.scrollTouchState.updateTo(coord).deltaX;
- this.currentTarget.scrollLeft -= window.devicePixelRatio * deltaX;
-
- return;
- }
-
- // Get touch position
- var y = coord.y;
-
- // Move target key and highlighting
- var key0 = this.pendingTarget,
- key1 = this.findBestTarget(coord, true); // For the OSK, this ALSO gets subkeys.
-
- // If option should not be selectable, how do we re-target?
-
-
- // Do not move over keys if device popup visible
- if(this.hasModalPopup()) {
- if(key0) {
- this.highlight(key0,false);
- }
- this.pendingTarget=null;
- return;
- }
-
- // Use the popup duplicate of the base key if a phone with a visible popup array
- key1 = this.dealiasSubTarget(key1);
-
- // Identify current touch position (to manage off-key release)
- this.currentTarget = key1;
-
- // Clear previous key highlighting
- if(key0 && key1 && key1 !== key0) {
- this.highlight(key0,false);
- }
-
- // Code below directly related to subkeys should only be triggered within 'native' mode.
- // The embedded version instead passes info to the apps to produce their own subkeys in-app.
-
- // If popup is visible, need to move over popup, not over main keyboard
- if(key1 && this.hasSubmenu(key1)) {
- //this.highlightSubKeys(key1,x,y);
-
- // Native-mode: show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353)
- if(!keyman.isEmbedded && (this.touchY-y > 5) && !this.isSubmenuActive()) {
- // Instantly show the submenu.
- this.displaySubmenuFor(key1);
- }
-
- // Once a subkey array is displayed, do not allow changing the base key.
- // Keep that array visible and accept no other options until the touch ends.
- if(key1 && key1.id.indexOf('popup') < 0) { // TODO: reliant on 'popup' in .id
- return;
- }
-
- // Highlight the base key on devices that do not append it to the subkey array.
- if(key1 && key1.className.indexOf(this.selectedTargetMatch) < 0) {
- this.highlight(key1,true);
- }
- // Cancel touch if moved up and off keyboard, unless popup keys visible
- } else {
- let base = this.baseElement;
- let top = dom.Utils.getAbsoluteY(base);
- let height = base.offsetHeight;
- let yMin = Math.max(5, top - 0.25 * height);
- let yMax = (top + height) + 0.25 * height;
- if(key0 && (coord.y < yMin || coord.y > yMax)) {
- this.highlight(key0,false);
- this.clearHolds();
- this.pendingTarget = null;
- }
- }
-
- // Replace the target key, if any, by the new target key
- // Do not replace a null target, as that indicates the key has already been released
- if(key1 && this.pendingTarget) {
- this.pendingTarget = key1;
- }
-
- if(this.pendingTarget) {
- if(key1 && (key0 != key1 || key1.className.indexOf(this.selectedTargetMatch) < 0)) {
- this.highlight(key1,true);
- }
- }
-
- if(key0 && key1 && (key1 != key0) && (key1.id != '')) {
- // Display the touch-hold keys (after a pause)
- this.hold(key1);
- }
- }
- }
-}
diff --git a/web/src/engine/main/osk/utils.ts b/web/src/engine/main/osk/utils.ts
deleted file mode 100644
index 1e3a622eff..0000000000
--- a/web/src/engine/main/osk/utils.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-///
-
-namespace com.keyman.osk {
- export function getFontSizeStyle(e: HTMLElement|string): {val: number, absolute: boolean} {
- var fs: string;
-
- if(typeof e == 'string') {
- fs = e;
- } else {
- fs = e.style.fontSize;
- if(!fs) {
- fs = getComputedStyle(e).fontSize;
- }
- }
-
- return new ParsedLengthStyle(fs);
- }
-}
\ No newline at end of file
diff --git a/web/src/engine/main/osk/visualKeyboard.ts b/web/src/engine/main/osk/visualKeyboard.ts
deleted file mode 100644
index 9f98cc5f6c..0000000000
--- a/web/src/engine/main/osk/visualKeyboard.ts
+++ /dev/null
@@ -1,1825 +0,0 @@
-///
-///
-///
-///
-///
-///
-///
-///
-///
-///
-///
-///
-
-namespace com.keyman.osk {
- interface BoundingRect {
- left: number,
- right: number,
- top: number,
- bottom: number
- };
-
- export class VisualKeyboard implements KeyboardView {
- // Legacy alias, maintaining a reference for code built against older
- // versions of KMW.
- static specialCharacters = OSKKey.specialCharacters;
-
- /**
- * Contains layout properties corresponding to the OSK's layout. Needs to be public
- * so that its geometry may be updated on rotations and keyboard resize events, as
- * said geometry needs to be accurate for fat-finger probability calculations.
- */
- kbdLayout: keyboards.ActiveLayout;
- layerGroup: OSKLayerGroup;
-
- private _layerId: string = "default";
- layerIndex: number = 0; // the index of the default layer
- readonly isRTL: boolean;
-
- device: com.keyman.utils.DeviceSpec;
- hostDevice: com.keyman.utils.DeviceSpec;
-
- inputEngine: InputEventEngine;
-
- isStatic: boolean = false;
- _fixedWidthScaling: boolean = false;
- _fixedHeightScaling: boolean = true;
-
- // Stores the base element for this instance of the visual keyboard.
- // Formerly known as osk._DivVKbd
- kbdDiv: HTMLDivElement;
- styleSheet: HTMLStyleElement;
-
- /**
- * The configured width for this VisualKeyboard. May be `undefined` or `null`
- * to allow automatic width scaling.
- */
- private _width: number;
-
- /**
- * The configured height for this VisualKeyboard. May be `undefined` or `null`
- * to allow automatic height scaling.
- */
- private _height: number;
-
- /**
- * The computed width for this VisualKeyboard. May be null if auto sizing
- * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.
- */
- private _computedWidth: number;
-
- /**
- * The computed height for this VisualKeyboard. May be null if auto sizing
- * is allowed and the VisualKeyboard is not currently in the DOM hierarchy.
- */
- private _computedHeight: number;
-
- // Style-related properties
- fontFamily: string;
- private _fontSize: ParsedLengthStyle;
- // fontSize: string;
-
- // State-related properties
- keyPending: KeyElement;
- touchPending: InputEventCoordinate;
- deleteKey: KeyElement;
- deleting: number; // Tracks a timer id for repeated deletions.
- nextLayer: string;
- currentKey: string;
-
- // Touch-tracking properties
- initTouchCoord: InputEventCoordinate;
- touchCount: number;
- currentTarget: KeyElement;
-
- // Used by embedded-mode's globe key
- menuEvent: KeyElement; // Used by embedded-mode.
-
- // Popup key management
- keytip: KeyTip;
- globeHint: GlobeHint;
- pendingSubkey: PendingGesture;
- subkeyGesture: RealizedGesture;
-
- // Multi-tap gesture management
- pendingMultiTap: PendingMultiTap;
-
- // The keyboard object corresponding to this VisualKeyboard.
- private layoutKeyboard: keyboards.Keyboard;
-
- get layerId(): string {
- return this._layerId;
- }
-
- set layerId(value: string) {
- const changedLayer = value != this._layerId;
- if(!this.layerGroup.layers[value]) {
- throw new Error(`Keyboard ${this.layoutKeyboard.id} does not have a layer with id ${value}`);
- } else {
- this._layerId = value;
- }
-
- if(changedLayer) {
- this.updateState();
- }
- }
-
- get currentLayer(): OSKLayer {
- return this.layerId ? this.layerGroup?.layers[this.layerId] : null;
- }
-
- // Special keys (for the currently-visible layer)
- get lgKey(): KeyElement { // currently, must be visible for the touch language menu.
- return this.currentLayer?.globeKey?.btn;
- }
-
- private get hkKey(): KeyElement { // hide keyboard key
- return this.currentLayer?.hideKey?.btn;
- }
-
- public get spaceBar(): KeyElement { // also referenced by the touch language menu.
- return this.currentLayer?.spaceBarKey?.btn;
- }
-
- //#region OSK constructor and helpers
-
- /**
- * @param {Object} PVK Visual keyboard name
- * @param {Object} Lhelp true if OSK defined for this keyboard
- * @param {Object} layout0
- * @param {Number} kbdBitmask Keyboard modifier bitmask
- * Description Generates the base visual keyboard element, prepping for attachment to KMW
- */
- constructor(keyboard: keyboards.Keyboard, device: com.keyman.utils.DeviceSpec, hostDevice?: com.keyman.utils.DeviceSpec, isStatic?: boolean) {
- this.device = device;
- this.hostDevice = hostDevice || device;
- if (isStatic) {
- this.isStatic = isStatic;
- }
-
- this._fixedWidthScaling = this.device.touchable && !this.isStatic;
- this._fixedHeightScaling = this.device.touchable && !this.isStatic;
-
- // Create the collection of HTML elements from the device-dependent layout object
- var Lkbd = document.createElement('div');
- let layout: keyboards.ActiveLayout;
- if (keyboard) {
- layout = this.kbdLayout = keyboard.layout(device.formFactor as utils.FormFactor);
- this.isRTL = keyboard.isRTL;
- } else {
- // This COULD be called with no backing keyboard; KMW will try to force-show the OSK even without
- // a backing keyboard on mobile, using the most generic default layout as the OSK's base.
- //
- // In KMW's current state, it'd take a major break, though - Processor always has an activeKeyboard,
- // even if it's "hollow".
- let rawLayout = keyboards.Layouts.buildDefaultLayout(null, null, device.formFactor);
- layout = this.kbdLayout = keyboards.ActiveLayout.polyfill(rawLayout, null, device.formFactor as utils.FormFactor);
- this.isRTL = false;
- }
-
- // Override font if specified by keyboard
- if ('font' in layout) {
- this.fontFamily = layout['font'];
- } else {
- this.fontFamily = '';
- }
-
- // Now to build the actual layout.
- const formFactor = device.formFactor as utils.FormFactor;
- this.layoutKeyboard = keyboard;
- if (!this.layoutKeyboard) {
- // May occasionally be null in embedded contexts; have seen this when iOS engine sets
- // keyboard height during change of keyboards.
- this.layoutKeyboard = new keyboards.Keyboard(null);
- }
-
- this.layerGroup = new OSKLayerGroup(this, this.layoutKeyboard, formFactor);
-
- // Now that we've properly processed the keyboard's layout, mark it as calibrated.
- // TODO: drop the whole 'calibration' thing. The newer layout system supersedes the
- // need for it. (Is no longer really used, so the drop ought be clean.)
- this.layoutKeyboard.markLayoutCalibrated(formFactor);
-
- // Append the OSK layer group container element to the containing element
- //osk.keyMap = divLayerContainer;
- Lkbd.appendChild(this.layerGroup.element);
-
- // Set base class - OS and keyboard added for Build 360
- this.kbdDiv = Lkbd;
-
- // For 'live' touch keyboards, attach touch-based event handling.
- // Needs to occur AFTER this.kbdDiv is initialized.
- if (!this.isStatic) {
- if (this.hostDevice.touchable) {
- this.inputEngine = TouchEventEngine.forVisualKeyboard(this);
- } else {
- this.inputEngine = MouseEventEngine.forVisualKeyboard(this);
- }
- this.inputEngine.registerEventHandlers();
- }
-
- Lkbd.classList.add(device.formFactor, 'kmw-osk-inner-frame');
-
- // Tag the VisualKeyboard with a CSS class corresponding to its ID.
- let kbdID: string = this.layoutKeyboard?.id.replace('Keyboard_','') ?? '';
-
- const separatorIndex = kbdID.indexOf('::');
- if(separatorIndex != -1) { // We used to also test if we were in embedded mode, but... whatever.
- // De-namespaces the ID for use with CSS classes.
- // Assumes that keyboard IDs may not contain the ':' symbol.
- kbdID = kbdID.substring(separatorIndex + 2);
- }
-
- const kbdClassSuffix = 'kmw-keyboard-' + kbdID;
- this.element.classList.add(kbdClassSuffix);
- }
-
- public get element(): HTMLDivElement {
- return this.kbdDiv;
- }
-
- public postInsert(): void { }
-
- /**
- * The configured width for this VisualKeyboard. May be `undefined` or `null`
- * to allow automatic width scaling.
- */
- get width(): number {
- return this._width;
- }
-
- /**
- * The configured height for this VisualKeyboard. May be `undefined` or `null`
- * to allow automatic height scaling.
- */
- get height(): number {
- return this._height;
- }
-
- get layoutWidth(): ParsedLengthStyle {
- if (this.usesFixedWidthScaling) {
- let baseWidth = this.width;
- let cs = getComputedStyle(this.element);
- if (cs.border) {
- let borderWidth = new ParsedLengthStyle(cs.borderWidth).val;
- baseWidth -= borderWidth * 2;
- }
- return ParsedLengthStyle.inPixels(baseWidth);
- } else {
- return ParsedLengthStyle.forScalar(1);
- }
- }
-
- get layoutHeight(): ParsedLengthStyle {
- if (this.usesFixedHeightScaling) {
- let baseHeight = this.height;
- let cs = getComputedStyle(this.element);
- if (cs.border) {
- let borderHeight = new ParsedLengthStyle(cs.borderWidth).val;
- baseHeight -= borderHeight * 2;
- }
- return ParsedLengthStyle.inPixels(baseHeight);
- } else {
- return ParsedLengthStyle.forScalar(1);
- }
- }
-
- get internalHeight(): ParsedLengthStyle {
- if (this.usesFixedHeightScaling) {
- // Touch OSKs may apply internal padding to prevent row cropping at the edges.
- return ParsedLengthStyle.inPixels(this.layoutHeight.val - this.getVerticalLayerGroupPadding());
- } else {
- return ParsedLengthStyle.forScalar(1);
- }
- }
-
- get fontSize(): ParsedLengthStyle {
- if (!this._fontSize) {
- this._fontSize = new ParsedLengthStyle('1em');
- }
- return this._fontSize;
- }
-
- set fontSize(value: ParsedLengthStyle) {
- this._fontSize = value;
- this.kbdDiv.style.fontSize = value.styleString;
- }
-
- /**
- * Uses fixed scaling for widths of internal elements, rather than relative,
- * percent-based scaling.
- */
- public get usesFixedWidthScaling(): boolean {
- return this._fixedWidthScaling;
- }
-
- public set usesFixedWidthScaling(val: boolean) {
- this._fixedWidthScaling = val;
- }
-
- /**
- * Uses fixed scaling for heights of internal elements, rather than relative,
- * percent-based scaling.
- */
- public get usesFixedHeightScaling(): boolean {
- return this._fixedHeightScaling;
- }
-
- public set usesFixedHeightScaling(val: boolean) {
- this._fixedHeightScaling = val;
- }
-
- /**
- * Denotes if the VisualKeyboard or its containing OSKView / OSKManager uses
- * fixed positioning.
- */
- public get usesFixedPositioning(): boolean {
- let node: HTMLElement = this.element;
- while (node) {
- if (getComputedStyle(node).position == 'fixed') {
- return true;
- } else {
- node = node.offsetParent as HTMLElement;
- }
- }
-
- return false;
- }
-
- /**
- * Sets & tracks the size of the VisualKeyboard's primary element.
- * @param width
- * @param height
- * @param pending Set to `true` if called during a resizing interaction
- */
- public setSize(width?: number, height?: number, pending?: boolean) {
- this._width = width;
- this._height = height;
-
- if (this.kbdDiv) {
- this.kbdDiv.style.width = width ? this._width + 'px' : '';
- this.kbdDiv.style.height = height ? this._height + 'px' : '';
-
- if (!this.device.touchable && height) {
- this.fontSize = new ParsedLengthStyle((this._height / 8) + 'px');
- }
-
- if (!pending) {
- this.refreshLayout();
- }
- }
- }
-
- /**
- * Returns the default properties for a key object, used to construct
- * both a base keyboard key and popup keys
- *
- * @return {Object} An object that contains default key properties
- */
- getDefaultKeyObject(): OSKKeySpec {
- return new OSKKeySpec(undefined, '', keyboards.ActiveKey.DEFAULT_KEY.width, keyboards.ActiveKey.DEFAULT_KEY.sp as keyboards.ButtonClass,
- null, keyboards.ActiveKey.DEFAULT_KEY.pad);
- };
- //#endregion
-
- //#region OSK touch handlers
- getTouchCoordinatesOnKeyboard(input: InputEventCoordinate) {
- let keyman = com.keyman.singleton;
-
- // We need to compute the 'local', keyboard-based coordinates for the touch.
- let kbdCoords = keyman.util.getAbsolute(this.kbdDiv as HTMLElement);
- let offsetCoords = { x: input.x - kbdCoords.x, y: input.y - kbdCoords.y };
-
- // The layer group's element always has the proper width setting, unlike kbdDiv itself.
- offsetCoords.x /= this.layerGroup.element.offsetWidth;
- offsetCoords.y /= this.kbdDiv.offsetHeight;
-
- return offsetCoords;
- }
-
- /**
- * Builds the fat-finger distribution used by predictive text as its source for likelihood
- * of alternate keystroke sequences.
- * @param input The input coordinate of the event that led to use of this function
- * @param keySpec The spec of the key directly triggered by the input event. May be for a subkey.
- * @returns
- */
- getTouchProbabilities(input: InputEventCoordinate, keySpec?: keyboards.ActiveKey): text.KeyDistribution {
- let keyman = com.keyman.singleton;
- if (!keyman.core.languageProcessor.mayCorrect) {
- return null;
- }
-
- // Note: if subkeys are active, they will still be displayed at this time.
- // TODO: In such cases, we should build an ActiveLayout (of sorts) for subkey displays,
- // update their geometries to the actual display values, and use the results here.
- let touchKbdPos = this.getTouchCoordinatesOnKeyboard(input);
- let layerGroup = this.layerGroup.element; // Always has proper dimensions, unlike kbdDiv itself.
- let width = layerGroup.offsetWidth, height = this.kbdDiv.offsetHeight;
- // Prevent NaN breakages.
- if (!width || !height) {
- return null;
- }
-
- let kbdAspectRatio = layerGroup.offsetWidth / this.kbdDiv.offsetHeight;
- let baseKeyProbabilities = this.kbdLayout.getLayer(this.layerId).getTouchProbabilities(touchKbdPos, kbdAspectRatio);
-
- if (!keySpec || !this.subkeyGesture || !this.subkeyGesture.baseKey.key) {
- return baseKeyProbabilities;
- } else {
- // A temp-hack, as this was noted just before 14.0's release.
- // Since a more... comprehensive solution would be way too complex this late in the game,
- // this provides a half-decent stopgap measure.
- //
- // Will not correct to nearby subkeys; only includes the selected subkey and its base keys.
- // Still, better than ignoring them both for whatever base key is beneath the final cursor location.
- let baseMass = 1.0;
-
- let baseKeyMass = 1.0;
- let baseKeyID = this.subkeyGesture.baseKey.key.spec.coreID;
-
- let popupKeyMass = 0.0;
- let popupKeyID: string = null;
-
- popupKeyMass = 3.0;
- popupKeyID = keySpec.coreID;
-
- // If the base key appears in the subkey array and was selected, merge the probability masses.
- if (popupKeyID == baseKeyID) {
- baseKeyMass += popupKeyMass;
- popupKeyMass = 0;
- } else {
- // We namespace it so that lookup operations know to find it via its base key.
- popupKeyID = `${baseKeyID}::${popupKeyID}`;
- }
-
- // Compute the normalization factor
- let totalMass = baseMass + baseKeyMass + popupKeyMass;
- let scalar = 1.0 / totalMass;
-
- // Prevent duplicate entries in the final map & normalize the remaining entries!
- for (let i = 0; i < baseKeyProbabilities.length; i++) {
- let entry = baseKeyProbabilities[i];
- if (entry.keyId == baseKeyID) {
- baseKeyMass += entry.p * scalar;
- baseKeyProbabilities.splice(i, 1);
- i--;
- } else if (entry.keyId == popupKeyID) {
- popupKeyMass = + entry.p * scalar;
- baseKeyProbabilities.splice(i, 1);
- i--;
- } else {
- entry.p *= scalar;
- }
- }
-
- let finalArray: { keyId: string, p: number }[] = [];
-
- if (popupKeyMass > 0) {
- finalArray.push({ keyId: popupKeyID, p: popupKeyMass * scalar });
- }
-
- finalArray.push({ keyId: baseKeyID, p: baseKeyMass * scalar });
-
- finalArray = finalArray.concat(baseKeyProbabilities);
- return finalArray;
- }
- }
-
- //#region Input handling start
-
- /**
- * Determines a "fuzzy boundary" area around the OSK within which active mouse and
- * touch events will be maintained, even if their coordinates lie outside of the OSK's
- * true visual bounds.
- * @returns A `BoundingRect`, in `.pageX` / `.pageY` coordinates.
- */
- private getInteractiveBoundingRect(): BoundingRect {
- // Determine the important geometric values involved
- let oskX = dom.Utils.getAbsoluteX(this.element);
- let oskY = dom.Utils.getAbsoluteY(this.element);
-
- // Determine the out-of-bounds threshold at which touch-cancellation should automatically occur.
- // Assuming square key-squares, we'll use 1/3 the height of a row for bounds detection
- // for both dimensions.
- const rowCount = this.currentLayer.rows.length;
- const buffer = (0.333 * this.height / rowCount);
-
- // Determine the OSK's boundaries and the boundaries of the page / view.
- // These values are needed in .pageX / .pageY coordinates for the final calcs.
- let boundingRect: BoundingRect = {
- left: oskX - buffer,
- right: oskX + this.width + buffer,
- top: oskY - buffer,
- bottom: oskY + this.height + buffer
- };
-
- return boundingRect;
- }
-
- /**
- * Adjusts a potential "interactive boundary" definition by enforcing an
- * "event cancellation zone" near screen boundaries that are not directly adjacent
- * to the ongoing input event's initial coordinate.
- *
- * This facilitates modeling of conventional cancellation gestures where a user would
- * drag the mouse or touch point off the OSK, as mouse and touch event handlers receive
- * no input beyond screen boundaries.
- *
- * @param baseBounds The baseline interactive bounding area to be adjusted
- * @param startCoord The initial coordinate of a currently-ongoing input event
- * @returns
- */
- private applyScreenMarginBoundsThresholding(baseBounds: BoundingRect,
- startCoord: InputEventCoordinate): BoundingRect {
- // Determine the needed linear translation to screen coordinates.
- const xDelta = window.screenLeft - window.pageXOffset;
- const yDelta = window.screenTop - window.pageYOffset;
-
- let adjustedBounds: BoundingRect = { ...baseBounds };
-
- // Also translate the initial touch's screen coord, as it affects our bounding box logic.
- const initScreenCoord = new InputEventCoordinate(startCoord.x + xDelta, startCoord.y + yDelta);
-
- // Detection: is the OSK aligned with any screen boundaries?
- // If so, create a 'fuzzy' zone around the edges not near the initial touch point that allow
- // move-based cancellation.
-
- // If the initial input screen-coord is at least 5 pixels from the screen's left AND
- // the OSK's left boundary is within 2 pixels from the screen's left...
- if (initScreenCoord.x >= 5 && baseBounds.left + xDelta <= 2) {
- adjustedBounds.left = 2 - xDelta; // new `leftBound` is set to 2 pixels from the screen's left.
- }
-
- if (initScreenCoord.x <= screen.width - 5 && baseBounds.right + xDelta >= screen.width - 2) {
- adjustedBounds.right = (screen.width - 2) - xDelta; // new `rightBound` 2px from screen's right.
- }
-
- if (initScreenCoord.y >= 5 && baseBounds.top + yDelta <= 2) {
- adjustedBounds.top = 2 - yDelta;
- }
-
- if (initScreenCoord.y <= screen.height - 5 && baseBounds.bottom + yDelta >= screen.height - 2) {
- adjustedBounds.bottom = (screen.height - 2) - yDelta;
- }
-
- return adjustedBounds;
- }
-
- detectWithinInteractiveBounds(coord: InputEventCoordinate): boolean {
- // Shortcuts the method during unit testing, as we don't currently
- // provide coordinate values in its synthetic events.
- if (coord.x === null && coord.y === null) {
- return true;
- }
-
- const baseBoundingRect = this.getInteractiveBoundingRect();
- let adjustedBoundingRect = baseBoundingRect;
- if(this.initTouchCoord) {
- this.applyScreenMarginBoundsThresholding(baseBoundingRect, this.initTouchCoord);
- }
-
- // Now to check where the input coordinate lies in relation to the final bounding box!
-
- if (coord.x < adjustedBoundingRect.left || coord.x > adjustedBoundingRect.right) {
- return false;
- } else if (coord.y < adjustedBoundingRect.top || coord.y > adjustedBoundingRect.bottom) {
- return false;
- } else {
- return true;
- }
- }
-
- /**
- * The main OSK touch start event handler
- *
- * @param {Event} e touch start event object
- *
- */
- touch(input: InputEventCoordinate) {
- // Identify the key touched
- var t = input.target, key = this.keyTarget(t);
-
- // Save the touch point, which is used for quick-display of popup keys (defined in highlightSubKeys)
- this.initTouchCoord = input;
-
- // Set the key for the new touch point to be current target, if defined
- this.currentTarget = key;
-
- // Clear repeated backspace if active, preventing 'sticky' behavior.
- this.cancelDelete();
-
- // Prevent multi-touch if popup displayed
- if (this.subkeyGesture && this.subkeyGesture.isVisible()) {
- return;
- }
-
- // Keep track of number of active (unreleased) touch points
- this.touchCount = input.activeInputCount;
-
- // Get nearest key if touching a hidden key or the end of a key row
- if ((key && ((key.className.indexOf('key-hidden') >= 0) || (key.className.indexOf('key-blank') >= 0)))
- || t.className.indexOf('kmw-key-row') >= 0) {
-
- // Perform "fudged" selection ops if and only if we're not sure about the precision of the
- // input source. Mouse-based selection IS precise, so no need for "fudging" there.
- if (!input.isFromMouse) {
- key = this.findNearestKey(input, t);
- }
- }
- // Do not do anything if no key identified!
- if (key == null) {
- return;
- }
-
- // Get key name (K_...) from element ID
- let keyName = key['keyId'];
-
- // Highlight the touched key
- this.highlightKey(key, true);
-
- // Special function keys need immediate action
- if (keyName == 'K_LOPT' || keyName == 'K_ROPT') {
- window.setTimeout(function (this: VisualKeyboard) {
- this.modelKeyClick(key);
- // Because we immediately process the key, we need to re-highlight it after the click.
- this.highlightKey(key, true);
- // Highlighting'll be cleared automatically later.
- }.bind(this), 0);
- this.keyPending = null;
- this.touchPending = null;
-
- // Also backspace, to allow delete to repeat while key held
- } else if (keyName == 'K_BKSP') {
- // While we could inline the execution of the delete key here, we lose the ability to
- // record the backspace key if we do so.
- this.modelKeyClick(key, input);
- this.deleteKey = key;
- this.deleting = window.setTimeout(this.repeatDelete, 500);
- this.keyPending = null;
- this.touchPending = null;
- } else {
- if (this.keyPending) {
- this.highlightKey(this.keyPending, false);
-
- if (this.subkeyGesture && this.subkeyGesture instanceof browser.SubkeyPopup) {
- let subkeyPopup = this.subkeyGesture as browser.SubkeyPopup;
- subkeyPopup.updateTouch(input);
- subkeyPopup.finalize(input);
- } else {
- this.modelKeyClick(this.keyPending, this.touchPending);
- }
- // Decrement the number of unreleased touch points to prevent
- // sending the keystroke again when the key is actually released
- this.touchCount--;
- } else {
- this.initGestures(key, input);
- }
- this.keyPending = key;
- this.touchPending = input;
- }
- }
-
- /**
- * OSK touch release event handler
- *
- * @param {Event} e touch release event object
- *
- **/
- release(input: InputEventCoordinate): void {
- // Prevent incorrect multi-touch behaviour if native or device popup visible
- var t = this.currentTarget;
-
- // Clear repeated backspace if active, preventing 'sticky' behavior.
- this.cancelDelete();
-
- // Multi-Tap
- if (this.pendingMultiTap && this.pendingMultiTap.realized) {
- // Ignore pending key if we've just handled a multitap
- this.pendingMultiTap = null;
-
- this.highlightKey(this.keyPending, false);
- this.keyPending = null;
- this.touchPending = null;
-
- return;
- }
-
- if (this.pendingMultiTap && this.pendingMultiTap.cancelled) {
- this.pendingMultiTap = null;
- }
-
- // Longpress
- if ((this.subkeyGesture && this.subkeyGesture.isVisible())) {
- // Ignore release if a multiple touch
- if (input.activeInputCount > 0) {
- return;
- }
-
- if (this.subkeyGesture instanceof browser.SubkeyPopup) {
- let subkeyPopup = this.subkeyGesture as browser.SubkeyPopup;
- subkeyPopup.finalize(input);
- }
- this.highlightKey(this.keyPending, false);
- this.keyPending = null;
- this.touchPending = null;
-
- return;
- }
-
- // Handle menu key release event
- if (t && t.id) {
- this.optionKey(t, t.id, false);
- }
-
- // Test if moved off screen (effective release point must be corrected for touch point horizontal speed)
- // This is not completely effective and needs some tweaking, especially on Android
- if (!this.detectWithinInteractiveBounds(input)) {
- this.moveCancel(input);
- this.touchCount--;
- return;
- }
-
- // Save then decrement current touch count
- var tc = this.touchCount;
- if (this.touchCount > 0) {
- this.touchCount--;
- }
-
- // Process and clear highlighting of pending target
- if (this.keyPending) {
- this.highlightKey(this.keyPending, false);
- // Output character unless moved off key
- if (this.keyPending.className.indexOf('hidden') < 0 && tc > 0) {
- this.modelKeyClick(this.keyPending, input);
- }
- this.clearPopup();
- this.keyPending = null;
- this.touchPending = null;
- // Always clear highlighting of current target on release (multi-touch)
- } else {
- var tt = input;
- t = this.keyTarget(tt.target);
- if (!t) {
- // Operates relative to the viewport, not based on the actual coordinate on the page.
- var t1 = document.elementFromPoint(input.x - window.pageXOffset, input.y - window.pageYOffset);
- t = this.findNearestKey(input, t1);
- }
-
- this.highlightKey(t, false);
- }
- }
-
- moveCancel(input: InputEventCoordinate): void {
- // Do not attempt to support reselection of target key for overlapped keystrokes.
- // Perform _after_ ensuring possible sticky keys have been cancelled.
- if (input.activeInputCount > 1) {
- return;
- }
-
- // Update all gesture tracking. The function returns true if further input processing
- // should be blocked. (Keeps the subkey array operating when the input coordinate has
- // moved outside the OSK's boundaries.)
- if (this.updateGestures(null, this.keyPending, input)) {
- return;
- }
-
- this.cancelDelete();
-
- this.highlightKey(this.keyPending, false);
- this.showKeyTip(null, false);
- this.clearPopup();
- this.keyPending = null;
- this.touchPending = null;
- }
-
- /**
- * OSK touch move event handler
- *
- * @param {Event} e touch move event object
- *
- **/
- moveOver(input: InputEventCoordinate): void {
- // Shouldn't be possible, but just in case.
- if (this.touchCount == 0) {
- this.cancelDelete();
- return;
- }
-
- // Get touch position
- const x = input.x - window.pageXOffset;
- const y = input.y - window.pageYOffset;
-
- // Move target key and highlighting
- this.touchPending = input;
- // Operates on viewport-based coordinates, not page-based.
- var t1 = document.elementFromPoint(x, y);
- const key0 = this.keyPending;
- let key1 = this.keyTarget(t1); // Not only gets base keys, but also gets popup keys!
-
- // Find the nearest key to the touch point if not on a visible key
- if ((key1 && key1.className.indexOf('key-hidden') >= 0) ||
- (t1 && (!key1) && t1.className.indexOf('key-row') >= 0)) {
- key1 = this.findNearestKey(input, t1);
- }
-
- // Cancels BKSP if it's not the key. (Note... could also cancel BKSP if the ongoing
- // input is cancelled, regardless of key, just to be safe.)
-
- // Stop repeat if no longer on BKSP key
- if (key1 && (typeof key1.id == 'string') && (key1.id.indexOf('-K_BKSP') < 0)) {
- this.cancelDelete();
- }
-
- // Cancels if it's a multitouch attempt.
-
- // Do not attempt to support reselection of target key for overlapped keystrokes.
- // Perform _after_ ensuring possible sticky keys have been cancelled.
- if (input.activeInputCount > 1) {
- return;
- }
-
- // Gesture-updates should probably be a separate call from other touch-move aspects.
-
- // Update all gesture tracking. The function returns true if further input processing
- // should be blocked.
- if (this.updateGestures(key1, key0, input)) {
- return;
- }
-
- // Identify current touch position (to manage off-key release)
- this.currentTarget = key1;
-
- // Only NOW do we denote the newly-selected key as the currently-focused key.
-
- // Replace the target key, if any, by the new target key
- // Do not replace a null target, as that indicates the key has already been released
- if (key1 && this.keyPending) {
- this.highlightKey(key0, false);
- this.keyPending = key1;
- this.touchPending = input;
- }
-
- if (key0 && key1 && (key1 != key0) && (key1.id != '')) {
- // While there may not be an active subkey menu, we should probably update which base key
- // is being highlighted by the current touch & start a pending longpress for it.
- this.clearPopup();
- this.initGestures(key1, input);
- }
-
- if (this.keyPending) {
- if (key0 != key1 || key1.className.indexOf(OSKKey.HIGHLIGHT_CLASS) < 0) {
- this.highlightKey(key1, true);
- }
- }
- }
-
- //#endregion
-
- /**
- * Get the current key target from the touch point element within the key
- *
- * @param {Object} t element at touch point
- * @return {Object} the key element (or null)
- **/
- keyTarget(target: HTMLElement | EventTarget): KeyElement {
- let t = target;
-
- try {
- if (t) {
- if (t.classList.contains('kmw-key')) {
- return getKeyFrom(t);
- }
- if (t.parentNode && (t.parentNode as HTMLElement).classList.contains('kmw-key')) {
- return getKeyFrom(t.parentNode);
- }
- if (t.firstChild && (t.firstChild as HTMLElement).classList.contains('kmw-key')) {
- return getKeyFrom(t.firstChild);
- }
- }
- } catch (ex) { }
- return null;
- }
-
- /**
- * Identify the key nearest to the touch point if at the end of a key row,
- * but return null more than about 0.6 key width from the nearest key.
- *
- * @param {Event} e touch event
- * @param {Object} t HTML object at touch point
- * @return {Object} nearest key to touch point
- *
- **/
- findNearestKey(input: InputEventCoordinate, t: HTMLElement): KeyElement {
- if (!input) {
- return null;
- }
-
- // Get touch point on screen
- var x = input.x;
-
- // Get key-row beneath touch point
- while (t && t.className !== undefined && t.className.indexOf('key-row') < 0) {
- t = t.parentNode;
- }
- if (!t) {
- return null;
- }
-
- // Find minimum distance from any key
- var k, k0 = 0, dx, dxMax = 24, dxMin = 100000, x1, x2;
- for (k = 0; k < t.childNodes.length; k++) {
- let keySquare = t.childNodes[k] as HTMLElement; // gets the .kmw-key-square containing a key
- // Find the actual key element.
- let childNode = keySquare.firstChild ? keySquare.firstChild as HTMLElement : keySquare;
-
- if (childNode.className !== undefined
- && (childNode.className.indexOf('key-hidden') >= 0
- || childNode.className.indexOf('key-blank') >= 0)) {
- continue;
- }
- x1 = keySquare.offsetLeft;
- x2 = x1 + keySquare.offsetWidth;
- if (x >= x1 && x <= x2) {
- // Within the key square
- return childNode;
- }
- dx = x1 - x;
- if (dx >= 0 && dx < dxMin) {
- // To right of key
- k0 = k; dxMin = dx;
- }
- dx = x - x2;
- if (dx >= 0 && dx < dxMin) {
- // To left of key
- k0 = k; dxMin = dx;
- }
- }
-
- if (dxMin < 100000) {
- t = t.childNodes[k0];
- x1 = t.offsetLeft;
- x2 = x1 + t.offsetWidth;
-
- // Limit extended touch area to the larger of 0.6 of key width and 24 px
- if (t.offsetWidth > 40) {
- dxMax = 0.6 * t.offsetWidth;
- }
-
- if (((x1 - x) >= 0 && (x1 - x) < dxMax) || ((x - x2) >= 0 && (x - x2) < dxMax)) {
- return t.firstChild;
- }
- }
- return null;
- }
-
- /**
- * Repeat backspace as long as the backspace key is held down
- **/
- repeatDelete: () => void = function (this: VisualKeyboard) {
- if (this.deleting) {
- this.modelKeyClick(this.deleteKey);
- this.deleting = window.setTimeout(this.repeatDelete, 100);
- }
- }.bind(this);
-
- /**
- * Cancels any active repeatDelete() timeouts, ensuring that
- * repeating backspace operations are properly terminated.
- */
- cancelDelete() {
- // Clears the delete-repeating timeout.
- if (this.deleting) {
- window.clearTimeout(this.deleting);
- }
- this.deleting = 0;
- }
- //#endregion
-
- modelKeyClick(e: osk.KeyElement, input?: InputEventCoordinate) {
- let keyEvent = this.initKeyEvent(e, input);
-
- // TODO: convert into an actual event, raised by the VisualKeyboard.
- // Its code is intended to lie outside of the OSK-Core library/module.
- PreProcessor.raiseKeyEvent(keyEvent);
- }
-
- initKeyEvent(e: osk.KeyElement, input?: InputEventCoordinate) {
- // Turn off key highlighting (or preview)
- this.highlightKey(e, false);
-
- // Future note: we need to refactor osk.OSKKeySpec to instead be a 'tag field' for
- // keyboards.ActiveKey. (Prob with generics, allowing the Web-only parts to
- // be fully specified within the tag.)
- //
- // Would avoid the type shenanigans needed here because of our current type-abuse setup
- // for key spec tracking.
- let keySpec = (e['key'] ? e['key'].spec : null) as unknown as keyboards.ActiveKey;
- if (!keySpec) {
- console.error("OSK key with ID '" + e.id + "', keyID '" + e.keyId + "' missing needed specification");
- return null;
- }
-
- // Return the event object.
- return this.keyEventFromSpec(keySpec, input);
- }
-
- keyEventFromSpec(keySpec: keyboards.ActiveKey, input?: InputEventCoordinate) {
- let core = com.keyman.singleton.core; // only singleton-based ref currently needed here.
-
- // Start: mirrors _GetKeyEventProperties
-
- // First check the virtual key, and process shift, control, alt or function keys
- let Lkc = keySpec.constructKeyEvent(core.keyboardProcessor, this.device);
-
- /* In case of "fun" edge cases caused by JS's single-threadedness & event processing queue.
- *
- * Should a touch occur on an OSK key during active JS execution that results in a change
- * of the active keyboard, it's possible for an OSK key to be evaluated against an
- * unexpected, non-matching keyboard - one that could even be `null`!
- *
- * So, we mark the keyboard backing the OSK as the 'correct' keyboard for this key.
- */
- Lkc.srcKeyboard = this.layoutKeyboard;
-
- // End - mirrors _GetKeyEventProperties
-
- if (core.languageProcessor.isActive && input) {
- Lkc.source = input;
- Lkc.keyDistribution = this.getTouchProbabilities(input, keySpec);
- }
-
- // Return the event object.
- return Lkc;
- }
-
- // cancel = function(e) {} //cancel event is never generated by iOS
-
- /**
- * Function _UpdateVKShiftStyle
- * Scope Private
- * @param {string=} layerId
- * Description Updates the OSK's visual style for any toggled state keys
- */
- _UpdateVKShiftStyle(layerId?: string) {
- var i;
- let core = com.keyman.singleton.core;
-
- if (!layerId) {
- layerId = this.layerId;
- }
-
- const layer = this.layerGroup.layers[layerId];
- if (!layer) {
- return;
- }
-
- // So... through KMW 14, we actually never tracked the capsKey, numKey, and scrollKey
- // properly for keyboard-defined layouts - only _default_, desktop-style layouts.
- //
- // We _could_ remedy this, but then... touch keyboards like khmer_angkor actually
- // repurpose certain state keys, and in an inconsistent manner at that.
- // Considering the potential complexity of touch layouts, with multiple possible
- // layer-shift keys, it's likely best to just leave things as they are for now.
- if (!core.activeKeyboard?.usesDesktopLayoutOnDevice(this.device)) {
- return;
- }
-
- // Set the on/off state of any visible state keys.
- const states = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'];
- const keys = [layer.capsKey, layer.numKey, layer.scrollKey];
-
- for (i = 0; i < keys.length; i++) {
- // Skip any keys not in the OSK!
- if (keys[i] == null) {
- continue;
- }
-
- keys[i].setToggleState(core.keyboardProcessor.stateKeys[states[i]]);
- }
- }
-
- clearPopup() {
- // Remove the displayed subkey array, if any, and cancel popup request
- if (this.subkeyGesture) {
- this.subkeyGesture.clear();
- this.subkeyGesture = null;
- }
-
- if (this.pendingSubkey) {
- this.pendingSubkey.cancel();
- this.pendingSubkey = null;
- }
- }
-
- //#endregion
-
- /**
- * Indicate the current language and keyboard on the space bar
- **/
- showLanguage() {
- let keyman = com.keyman.singleton;
-
- let displayName: string = undefined;
- let activeStub = keyman.keyboardManager.activeStub;
-
- if (activeStub) {
- if (activeStub['displayName'] != null) {
- displayName = activeStub['displayName'];
- } else {
- let
- lgName: string = activeStub['KL'],
- kbdName: string = activeStub['KN'];
- kbdName = kbdName.replace(/\s*keyboard\s*/i, '');
- switch (keyman.options['spacebarText']) {
- case SpacebarText.KEYBOARD:
- displayName = kbdName;
- break;
- case SpacebarText.LANGUAGE:
- displayName = lgName;
- break;
- case SpacebarText.LANGUAGE_KEYBOARD:
- displayName = (kbdName == lgName) ? lgName : lgName + ' - ' + kbdName;
- break;
- case SpacebarText.BLANK:
- displayName = '';
- break;
- default:
- displayName = kbdName;
- }
- }
- } else {
- displayName = '(System keyboard)';
- }
-
- try {
- var t = this.spaceBar.key.label;
- let tParent = t.parentNode;
- if (typeof (tParent.className) == 'undefined' || tParent.className == '') {
- tParent.className = 'kmw-spacebar';
- } else if (tParent.className.indexOf('kmw-spacebar') == -1) {
- tParent.className += ' kmw-spacebar';
- }
-
- if (t.className != 'kmw-spacebar-caption') {
- t.className = 'kmw-spacebar-caption';
- }
-
- // It sounds redundant, but this dramatically cuts down on browser DOM processing;
- // but sometimes innerText is reported empty when it actually isn't, so set it
- // anyway in that case (Safari, iOS 14.4)
- if (t.innerText != displayName || displayName == '') {
- t.innerText = displayName;
- }
-
- this.spaceBar.key.refreshLayout(this);
- }
- catch (ex) { }
- }
-
- /**
- * Add or remove a class from a keyboard key (when touched or clicked)
- * or add a key preview for phone devices
- *
- * @param {Object} key key affected
- * @param {boolean} on add or remove highlighting
- **/
- highlightKey(key: KeyElement, on: boolean) {
- // Do not change element class unless a key
- if (!key || !key.key || (key.className == '') || (key.className.indexOf('kmw-key-row') >= 0)) return;
-
- // For phones, use key preview rather than highlighting the key,
- var usePreview = (this.keytip != null) && key.key.allowsKeyTip();
-
- if (usePreview) {
- this.showKeyTip(key, on);
- } else {
- // No key tip should be shown. In some cases (e.g. multitap), we
- // may still have a tip visible so let's always hide in that case
- this.showKeyTip(null, false);
- key.key.highlight(on);
- }
- }
-
- /**
- * Use of `getComputedStyle` is ideal, but in many of our use cases its preconditions are not met.
- * This function allows us to calculate the font size in those situations.
- */
- getKeyEmFontSize(): number {
- if (!this.fontSize) {
- return 0;
- }
-
- if (this.device.formFactor == 'desktop') {
- let keySquareScale = 0.8; // Set in kmwosk.css, is relative.
- return this.fontSize.scaledBy(keySquareScale).val;
- } else {
- let emSizeStr = getComputedStyle(document.body).fontSize;
- let emSize = getFontSizeStyle(emSizeStr).val;
-
- var emScale = 1;
- if (!this.isStatic) {
- // Double-check against the font scaling applied to the _Box element.
- if (this.fontSize.absolute) {
- return this.fontSize.val;
- } else {
- emScale = this.fontSize.val;
- }
- }
- return emSize * emScale;
- }
- }
-
- updateState() {
- // May happen for desktop-oriented keyboards that neglect to specify a touch layout.
- // See `test_chirality.js` from the unit-test keyboard suite, which tests keystrokes
- // using modifiers that lack corresponding visual-layout representation.
- if (!this.currentLayer) {
- return;
- }
-
- var n, b = this.kbdDiv.childNodes[0].childNodes;
- this.nextLayer = this.layerId;
-
- if (this.currentLayer.nextlayer) {
- this.nextLayer = this.currentLayer.nextlayer;
- }
-
- for (n = 0; n < b.length; n++) {
- let layerElement = b[n];
- if (layerElement['layer'] == this.layerId) {
- layerElement.style.display = 'block';
- //b[n].style.visibility='visible';
-
- // Most functions that call this one often indicate a change in modifier
- // or state key state. Keep it updated!
- this._UpdateVKShiftStyle();
- } else {
- layerElement.style.display = 'none';
- //layerElement.style.visibility='hidden';
- }
- }
- }
-
- /**
- * Used to refresh the VisualKeyboard's geometric layout and key sizes
- * when needed.
- */
- refreshLayout() {
- let keyman = com.keyman.singleton;
- let device = this.device;
-
- var fs = 1.0;
- // TODO: Logically, this should be needed for Android, too - may need to be changed for the next version!
- if (device.OS == utils.OperatingSystem.iOS && !keyman.isEmbedded) {
- fs = fs / keyman.util.getViewportScale();
- }
-
- let paddedHeight: number;
- if (this.height) {
- paddedHeight = this.computedAdjustedOskHeight(this.height);
- }
-
- let b = this.layerGroup.element as HTMLElement;
- let gs = this.kbdDiv.style;
- let bs = b.style;
- if (this.usesFixedHeightScaling && this.height) {
- // Sets the layer group to the correct height.
- gs.height = gs.maxHeight = this.height + 'px';
- }
-
- // The font-scaling applied on the layer group.
- gs.fontSize = this.fontSize.styleString;
- bs.fontSize = ParsedLengthStyle.forScalar(fs).styleString;
-
- // NEW CODE ------
-
- // Step 1: have the necessary conditions been met?
- const fixedSize = this.width && this.height;
- const computedStyle = getComputedStyle(this.kbdDiv);
- const isInDOM = computedStyle.height != '' && computedStyle.height != 'auto';
-
- // Step 2: determine basic layout geometry
- if (fixedSize) {
- this._computedWidth = this.width;
- this._computedHeight = this.height;
- } else if (isInDOM) {
- this._computedWidth = parseInt(computedStyle.width, 10);
- if (!this._computedWidth) {
- // For touch keyboards, the width _was_ specified on the layer group,
- // not the root element (`kbdDiv`).
- const groupStyle = getComputedStyle(this.kbdDiv.firstElementChild);
- this._computedWidth = parseInt(groupStyle.width, 10);
- }
- this._computedHeight = parseInt(computedStyle.height, 10);
- } else {
- // Cannot perform layout operations!
- return;
- }
-
- // Step 3: perform layout operations. (Handled by 'old code' section below.)
-
- // END NEW CODE -----------
-
- // Needs the refreshed layout info to work correctly.
- if(this.currentLayer) {
- this.currentLayer.refreshLayout(this, this._computedHeight - this.getVerticalLayerGroupPadding());
- }
- }
-
- private getVerticalLayerGroupPadding(): number {
- // For touch-based OSK layouts, kmwosk.css may include top & bottom padding on the layer-group element.
- const computedGroupStyle = getComputedStyle(this.layerGroup.element);
-
- // parseInt('') => NaN, which is falsy; we want to fallback to zero.
- let pt = parseInt(computedGroupStyle.paddingTop, 10) || 0;
- let pb = parseInt(computedGroupStyle.paddingBottom, 10) || 0;
- return pt + pb;
- }
-
- /*private*/ computedAdjustedOskHeight(allottedHeight: number): number {
- if (!this.layerGroup) {
- return allottedHeight;
- }
-
- const layers = this.layerGroup.layers;
- let oskHeight = 0;
-
- // In case the keyboard's layers have differing row counts, we check them all for the maximum needed oskHeight.
- for (const layerID in layers) {
- const layer = layers[layerID];
- let nRows = layer.rows.length;
- let rowHeight = Math.floor(allottedHeight / (nRows == 0 ? 1 : nRows));
- let layerHeight = nRows * rowHeight;
-
- if (layerHeight > oskHeight) {
- oskHeight = layerHeight;
- }
- }
-
- // This isn't set anywhere else; it's a legacy part of the original methods.
- const oskPad = 0;
- let oskPaddedHeight = oskHeight + oskPad;
-
- return oskPaddedHeight;
- }
-
- /**
- * Append a style sheet for the current keyboard if needed for specifying an embedded font
- * or to re-apply the default element font
- *
- **/
- appendStyleSheet() {
- let keymanweb = com.keyman.singleton;
- let util = keymanweb.util;
-
- var activeKeyboard = keymanweb.core.activeKeyboard;
- var activeStub: com.keyman.keyboards.KeyboardStub = keymanweb.keyboardManager.activeStub;
-
- // Do not do anything if a null stub
- if (activeStub == null) {
- return;
- }
-
- // First remove any existing keyboard style sheet
- if (this.styleSheet) {
- util.removeStyleSheet(this.styleSheet);
- }
-
- var i, kfd = activeStub['KFont'], ofd = activeStub['KOskFont'];
-
- // Add style sheets for embedded fonts if necessary (each font-face style will only be added once)
- util.addFontFaceStyleSheet(kfd);
- util.addFontFaceStyleSheet(ofd);
-
- // Build the style string and append (or replace) the font style sheet
- // Note: Some browsers do not download the font-face font until it is applied,
- // so must apply style before testing for font availability
- // Extended to allow keyboard-specific custom styles for Build 360
- var customStyle = this.addFontStyle(kfd, ofd);
- if (activeKeyboard != null && typeof (activeKeyboard.oskStyling) == 'string') // KMEW-129
- customStyle = customStyle + activeKeyboard.oskStyling;
-
- this.styleSheet = util.addStyleSheet(customStyle); //Build 360
- }
-
- /**
- * Add or replace the style sheet used to set the font for input elements and OSK
- *
- * @param {Object} kfd KFont font descriptor
- * @param {Object} ofd OSK font descriptor (if any)
- * @return {string}
- *
- **/
- addFontStyle(kfd, ofd): string {
- let keymanweb = com.keyman.singleton;
-
- // Get name of font to be applied
- var fn = keymanweb.baseFont;
- if (typeof (kfd) != 'undefined' && typeof (kfd['family']) != 'undefined') {
- fn = kfd['family'];
- }
-
- // Unquote font name in base font (if quoted)
- fn = fn.replace(/\u0022/g, '');
-
- // Set font family chain for mapped elements and remove any double quotes
- var rx = new RegExp('\\s?' + fn + ',?'), ff = keymanweb.appliedFont.replace(/\u0022/g, '');
-
- // Remove base font name from chain if present
- ff = ff.replace(rx, '');
- ff = ff.replace(/,$/, '');
-
- // Then replace it at the head of the chain
- if (ff == '') {
- ff = fn;
- } else {
- ff = fn + ',' + ff;
- }
-
- // Re-insert quotes around individual font names
- ff = '"' + ff.replace(/\,\s?/g, '","') + '"';
-
- // Add to the stylesheet, quoted, and with !important to override any explicit style
- var s = '.keymanweb-font{\nfont-family:' + ff + ' !important;\n}\n';
-
- // Set font family for OSK text
- if (typeof (ofd) != 'undefined') {
- s = s + '.kmw-key-text{\nfont-family:"' + ofd['family'].replace(/\u0022/g, '').replace(/,/g, '","') + '";\n}\n';
- } else if (typeof (kfd) != 'undefined') {
- s = s + '.kmw-key-text{\nfont-family:"' + kfd['family'].replace(/\u0022/g, '').replace(/,/g, '","') + '";\n}\n';
- }
-
- // Store the current font chain (with quote-delimited font names)
- keymanweb.appliedFont = ff;
-
- // Return the style string
- return s;
- }
-
- /**
- * Create copy of the OSK that can be used for embedding in documentation or help
- * The currently active keyboard will be returned if PInternalName is null
- *
- * @param {Object} PKbd the keyboard object to be displayed
- * @param {string=} argFormFactor layout form factor, defaulting to 'desktop'
- * @param {(string|number)=} argLayerId name or index of layer to show, defaulting to 'default'
- * @param {number} height Target height for the rendered keyboard
- * (currently required for legacy reasons)
- * @return {Object} DIV object with filled keyboard layer content
- */
- static buildDocumentationKeyboard(PKbd: com.keyman.keyboards.Keyboard, argFormFactor, argLayerId, height: number): HTMLElement { // I777
- if (!PKbd) {
- return null;
- }
-
- var formFactor = (typeof (argFormFactor) == 'undefined' ? 'desktop' : argFormFactor),
- layerId = (typeof (argLayerId) == 'undefined' ? 'default' : argLayerId),
- device = new Device();
-
- // Device emulation for target documentation.
- device.formFactor = formFactor;
- if (formFactor != 'desktop') {
- device.OS = 'iOS';
- device.touchable = true;
- } else {
- device.OS = 'windows';
- device.touchable = false;
- }
-
- let layout = PKbd.layout(formFactor);
-
- let kbdObj = new VisualKeyboard(PKbd, device.coreSpec, device.coreSpec, true);
-
- kbdObj.layerGroup.element.className = kbdObj.kbdDiv.className; // may contain multiple classes
- kbdObj.layerGroup.element.classList.add(device.formFactor + '-static');
-
- let kbd = kbdObj.kbdDiv.childNodes[0] as HTMLDivElement; // Gets the layer group.
-
- // Models CSS classes hosted on the OSKView in normal operation. We can't do this on the main
- // layer-group element because of the CSS rule structure for keyboard styling.
- //
- // For example, `.ios .kmw-keyboard-sil_cameroon_azerty` requires the element with the keyboard
- // ID to be in a child of an element with the .ios class.
- let classWrapper = document.createElement('div');
- classWrapper.classList.add(device.OS.toLowerCase(), device.formFactor);
-
- // Select the layer to display, and adjust sizes
- if (layout != null) {
- kbdObj.layerId = layerId;
-
- // This still feels fairly hacky... but something IS needed to constrain the height.
- // There are plans to address related concerns through some of the later aspects of
- // the Web OSK-Core design.
- kbdObj.setSize(800, height); // Probably need something for width, too, rather than
- kbdObj.fontSize = OSKView.defaultFontSize(device.coreSpec, height, false);
-
- // assuming 100%.
- kbdObj.refreshLayout(); // Necessary for the row heights to be properly set!
- // Relocates the font size definition from the main VisualKeyboard wrapper, since we don't return the whole thing.
- kbd.style.fontSize = kbdObj.kbdDiv.style.fontSize;
- kbd.style.height = kbdObj.kbdDiv.style.height;
- kbd.style.maxHeight = kbdObj.kbdDiv.style.maxHeight;
- } else {
- kbd.innerHTML = "No " + formFactor + " layout is defined for " + PKbd.name + ".
";
- }
- // Add a faint border
- kbd.style.border = '1px solid #ccc';
-
- // Once the element is inserted into the DOM, refresh the layout so that proper text scaling may apply.
- const detectAndHandleInsertion = () => {
- if(document.contains(kbd)) {
- // Yay, insertion!
-
- try {
- // Are there font-size attributes we may safely adjust? If so, do that!
- if(getComputedStyle(kbd) && kbd.style.fontSize) {
- kbdObj.fontSize = new ParsedLengthStyle(kbd.style.fontSize);
- }
-
- // Make sure that the stylesheet is attached, now that the keyboard-doc's been inserted.
- // The stylesheet is currently built + constructed in the same code that attaches it to
- // the page.
- kbdObj.appendStyleSheet();
-
- // Grab a reference to the stylesheet.
- const stylesheet = kbdObj.styleSheet;
- const stylesheetParentElement = stylesheet.parentElement;
-
- // Don't reset top-level stuff; just the visible layer.
- // kbdObj.currentLayer.refreshLayout(kbdObj, kbdObj.height);
-
- // We refresh the full layout so that font-size is properly detected & stored
- // on the documentation keyboard.
- kbdObj.refreshLayout();
- kbd.style.fontSize = kbdObj.kbdDiv.style.fontSize;
-
- // We no longer need a reference to the constructing VisualKeyboard, so we should let
- // it clean up its stylesheet links. This detaches the stylesheet, though.
- kbdObj.shutdown();
-
- // Now that shutdown is done, re-attach the stylesheet.
- stylesheetParentElement.appendChild(stylesheet);
- } finally {
- insertionObserver.disconnect();
- }
- }
- }
-
- const insertionObserver = new MutationObserver(detectAndHandleInsertion);
- insertionObserver.observe(document.body, {
- childList: true,
- subtree: true
- });
-
- classWrapper.append(kbd);
- return classWrapper;
- }
-
- onHide() {
- // Remove highlighting from hide keyboard key, if applied
- if (this.hkKey) {
- this.highlightKey(this.hkKey, false);
- }
- }
-
- /**
- * Starts an implementation-specific longpress gesture. Separately implemented for
- * in-browser and embedded modes.
- * @param key The base key of the longpress.
- * @returns
- */
- startLongpress(key: KeyElement): PendingGesture {
- let _this = this;
-
- // First-level object/Promise: will produce a subkey popup when the longpress gesture completes.
- // 'Returns' a second-level object/Promise: resolves when a subkey is selected or is cancelled.
- let pendingLongpress = new browser.PendingLongpress(this, key);
- pendingLongpress.promise.then(function (subkeyPopup) {
- // In-browser-specific handling.
- if (subkeyPopup) {
- // Append the touch-hold (subkey) array to the OSK
- let keyman = com.keyman.singleton;
- keyman.osk._Box.appendChild(subkeyPopup.element);
- keyman.osk._Box.appendChild(subkeyPopup.shim);
-
- // Must be placed after its `.element` has been inserted into the DOM.
- subkeyPopup.reposition(_this);
- }
- });
-
- return pendingLongpress;
- }
-
- /**
- * Initializes all supported gestures given a base key and the triggering touch coordinates.
- * @param key The gesture's base key
- * @param touch The starting touch coordinates for the gesture
- * @returns
- */
- initGestures(key: KeyElement, input: InputEventCoordinate) {
-
- if (this.pendingMultiTap) {
- switch (this.pendingMultiTap.incrementTouch(key)) {
- case PendingMultiTapState.Cancelled:
- this.pendingMultiTap = null;
- break;
- case PendingMultiTapState.Realized:
- // Don't initialize any other gestures if the
- // multi tap is realized; we cleanup on touch
- // release because we need to cancel the base
- // key action
- return;
- }
- }
-
- if (!this.pendingMultiTap && PendingMultiTap.isValidTarget(this, key)) {
- // We are only going to support double-tap on Shift
- // in Keyman 15, so we pass in the constant count = 2
- this.pendingMultiTap = new PendingMultiTap(this, key, 2);
- this.pendingMultiTap.timeout.then(() => {
- this.pendingMultiTap = null;
- });
- }
-
-
- if (key['subKeys']) {
- let _this = this;
-
- let pendingLongpress = this.startLongpress(key);
- if (pendingLongpress == null) {
- return;
- }
- this.pendingSubkey = pendingLongpress;
-
- pendingLongpress.promise.then(function (subkeyPopup) {
- if (_this.pendingSubkey == pendingLongpress) {
- _this.pendingSubkey = null;
- }
-
- if (subkeyPopup) {
- // Clear key preview if any
- _this.showKeyTip(null, false);
-
- _this.subkeyGesture = subkeyPopup;
- subkeyPopup.promise.then(function (keyEvent: text.KeyEvent) {
- // Allow active cancellation, even if the source should allow passive.
- // It's an easy and cheap null guard.
- if (keyEvent) {
- PreProcessor.raiseKeyEvent(keyEvent);
- }
- _this.clearPopup();
- });
- }
- });
- }
- }
-
- /**
- * Updates all currently-pending and activated gestures.
- *
- * @param currentKey The key currently underneath the most recent touch coordinate
- * @param previousKey The previously-selected key
- * @param input The current mouse or touch coordinate for the gesture
- * @returns true if should fully capture input, false if input should 'fall through'.
- */
- updateGestures(currentKey: KeyElement, previousKey: KeyElement, input: InputEventCoordinate): boolean {
- let key0 = previousKey;
- let key1 = currentKey;
-
- if(!currentKey && this.pendingMultiTap) {
- this.pendingMultiTap.cancel();
- this.pendingMultiTap = null;
- }
-
- // Clear previous key highlighting, allow subkey controller to highlight as appropriate.
- if (this.subkeyGesture) {
- if (key0) {
- key0.key.highlight(false);
- }
- this.subkeyGesture.updateTouch(input);
-
- this.keyPending = null;
- this.touchPending = null;
-
- return true;
- }
-
- this.currentTarget = null;
-
- // If popup is visible, need to move over popup, not over main keyboard
- // Could be turned into a browser-longpress specific implementation within browser.PendingLongpress?
- if (key1 && key1['subKeys'] != null && this.initTouchCoord) {
- if(this.pendingSubkey && this.pendingSubkey instanceof browser.PendingLongpress) {
- // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353)
- if (this.initTouchCoord.y - input.y > this.getLongpressFlickThreshold()) {
- this.pendingSubkey.resolve();
- }
- }
- }
-
- // If there is an active popup menu (which can occur from the previous block),
- // a subkey popup exists; do not allow base key output.
- if (this.subkeyGesture || this.pendingSubkey) {
- return true;
- }
-
- return false;
- }
-
- private getLongpressFlickThreshold(): number {
- const rowHeight = this.currentLayer.rowHeight;
-
- // If larger than 5 (and it likely is), new threshold = 1/4 the std. key height.
- const proportionalThreshold = rowHeight / 4;
-
- // 5 - the longpress-flick triggering threshold before 15.0.
- return Math.max(proportionalThreshold, 5);
- }
-
- optionKey(e: KeyElement, keyName: string, keyDown: boolean) {
- let keyman = com.keyman.singleton;
- let oskManager = keyman.osk;
- if (keyDown) {
- if (keyName.indexOf('K_LOPT') >= 0) {
- oskManager.showLanguageMenu();
- } else if (keyName.indexOf('K_ROPT') >= 0) {
- keyman.uiManager.setActivatingUI(false);
- oskManager.startHide(true);
- keyman.domManager.lastActiveElement = null;
- }
- }
- };
-
- /**
- * Add (or remove) the keytip preview (if KeymanWeb on a phone device)
- *
- * @param {Object} key HTML key element
- * @param {boolean} on show or hide
- */
- showKeyTip(key: KeyElement, on: boolean) {
- var tip = this.keytip;
-
- if (tip == null) {
- return;
- }
-
- let sk = this.subkeyGesture;
- let popup = (sk && sk.isVisible());
-
- // If popup keys are active, do not show the key tip.
- on = popup ? false : on;
-
- tip.show(key, on, this);
- };
-
- /**
- * Create a key preview element for phone devices
- */
- createKeyTip() {
- let keyman = com.keyman.singleton;
-
- if (this.device.formFactor == 'phone') {
- if (this.keytip == null) {
- // For now, should only be true (in production) when keyman.isEmbedded == true.
- let constrainPopup = keyman.isEmbedded;
- this.keytip = new browser.KeyTip(constrainPopup);
- }
-
- // Always append to _Box (since cleared during OSK Load)
- if (this.keytip && this.keytip.element) {
- keyman.osk._Box.appendChild(this.keytip.element);
- }
- }
- };
-
- createGlobeHint() {
- // A no-op for standard, non-app-embedded use cases.
- }
-
- shutdown() {
- let keyman = com.keyman.singleton;
-
- // Prevents style-sheet pollution from multiple keyboard swaps.
- if (this.styleSheet) {
- keyman.util.removeStyleSheet(this.styleSheet);
- }
-
- if(this.inputEngine) {
- this.inputEngine.unregisterEventHandlers();
- }
-
- if(this.deleting) {
- window.clearTimeout(this.deleting);
- }
-
- this.keyPending = null;
- this.touchPending = null;
-
- this.keytip?.show(null, false, this);
- this.subkeyGesture?.clear();
- this.pendingMultiTap?.cancel();
- this.pendingSubkey?.cancel();
- }
- }
-}