From f6d79964d657b28bea5f2d66dbfef0d8a54edfdc Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 20 Jan 2023 15:18:09 +0700 Subject: [PATCH] chore(web): removes old namespaced version of osk source files --- web/src/engine/main/osk/anchoredOskView.ts | 307 --- web/src/engine/main/osk/banner.ts | 736 ------- web/src/engine/main/osk/bannerManager.ts | 292 --- web/src/engine/main/osk/browser/keytip.ts | 146 -- web/src/engine/main/osk/browser/oskSubKey.ts | 68 - .../main/osk/browser/pendingLongpress.ts | 73 - .../main/osk/browser/pendingMultiTap.ts | 128 -- .../engine/main/osk/browser/subkeyPopup.ts | 295 --- web/src/engine/main/osk/emptyView.ts | 23 - web/src/engine/main/osk/floatingOskView.ts | 573 ------ .../engine/main/osk/globehint.interface.ts | 10 - web/src/engine/main/osk/helpPageView.ts | 39 - web/src/engine/main/osk/inlinedOskView.ts | 186 -- .../engine/main/osk/inputEventCoordinate.ts | 74 - web/src/engine/main/osk/inputEventEngine.ts | 59 - .../engine/main/osk/keyboardView.interface.ts | 22 - web/src/engine/main/osk/keytip.interface.ts | 9 - .../main/osk/layouts/mouseDragOperation.ts | 171 -- web/src/engine/main/osk/layouts/resizeBar.ts | 89 - .../main/osk/layouts/targetedFloatLayout.ts | 212 -- web/src/engine/main/osk/layouts/titleBar.ts | 205 -- web/src/engine/main/osk/lengthStyle.ts | 87 - web/src/engine/main/osk/mouseEventEngine.ts | 127 -- web/src/engine/main/osk/oskBaseKey.ts | 164 -- web/src/engine/main/osk/oskKey.ts | 519 ----- web/src/engine/main/osk/oskLayer.ts | 122 -- web/src/engine/main/osk/oskLayerGroup.ts | 53 - web/src/engine/main/osk/oskRow.ts | 86 - web/src/engine/main/osk/oskView.ts | 1091 ---------- web/src/engine/main/osk/oskViewComponent.ts | 7 - .../main/osk/pendingGesture.interface.ts | 45 - web/src/engine/main/osk/preProcessor.ts | 71 - .../main/osk/realizedGesture.interface.ts | 34 - web/src/engine/main/osk/touchEventEngine.ts | 90 - web/src/engine/main/osk/uiTouchHandlerBase.ts | 428 ---- web/src/engine/main/osk/utils.ts | 18 - web/src/engine/main/osk/visualKeyboard.ts | 1825 ----------------- 37 files changed, 8484 deletions(-) delete mode 100644 web/src/engine/main/osk/anchoredOskView.ts delete mode 100644 web/src/engine/main/osk/banner.ts delete mode 100644 web/src/engine/main/osk/bannerManager.ts delete mode 100644 web/src/engine/main/osk/browser/keytip.ts delete mode 100644 web/src/engine/main/osk/browser/oskSubKey.ts delete mode 100644 web/src/engine/main/osk/browser/pendingLongpress.ts delete mode 100644 web/src/engine/main/osk/browser/pendingMultiTap.ts delete mode 100644 web/src/engine/main/osk/browser/subkeyPopup.ts delete mode 100644 web/src/engine/main/osk/emptyView.ts delete mode 100644 web/src/engine/main/osk/floatingOskView.ts delete mode 100644 web/src/engine/main/osk/globehint.interface.ts delete mode 100644 web/src/engine/main/osk/helpPageView.ts delete mode 100644 web/src/engine/main/osk/inlinedOskView.ts delete mode 100644 web/src/engine/main/osk/inputEventCoordinate.ts delete mode 100644 web/src/engine/main/osk/inputEventEngine.ts delete mode 100644 web/src/engine/main/osk/keyboardView.interface.ts delete mode 100644 web/src/engine/main/osk/keytip.interface.ts delete mode 100644 web/src/engine/main/osk/layouts/mouseDragOperation.ts delete mode 100644 web/src/engine/main/osk/layouts/resizeBar.ts delete mode 100644 web/src/engine/main/osk/layouts/targetedFloatLayout.ts delete mode 100644 web/src/engine/main/osk/layouts/titleBar.ts delete mode 100644 web/src/engine/main/osk/lengthStyle.ts delete mode 100644 web/src/engine/main/osk/mouseEventEngine.ts delete mode 100644 web/src/engine/main/osk/oskBaseKey.ts delete mode 100644 web/src/engine/main/osk/oskKey.ts delete mode 100644 web/src/engine/main/osk/oskLayer.ts delete mode 100644 web/src/engine/main/osk/oskLayerGroup.ts delete mode 100644 web/src/engine/main/osk/oskRow.ts delete mode 100644 web/src/engine/main/osk/oskView.ts delete mode 100644 web/src/engine/main/osk/oskViewComponent.ts delete mode 100644 web/src/engine/main/osk/pendingGesture.interface.ts delete mode 100644 web/src/engine/main/osk/preProcessor.ts delete mode 100644 web/src/engine/main/osk/realizedGesture.interface.ts delete mode 100644 web/src/engine/main/osk/touchEventEngine.ts delete mode 100644 web/src/engine/main/osk/uiTouchHandlerBase.ts delete mode 100644 web/src/engine/main/osk/utils.ts delete mode 100644 web/src/engine/main/osk/visualKeyboard.ts diff --git a/web/src/engine/main/osk/anchoredOskView.ts b/web/src/engine/main/osk/anchoredOskView.ts deleted file mode 100644 index 4351c47d30..0000000000 --- a/web/src/engine/main/osk/anchoredOskView.ts +++ /dev/null @@ -1,307 +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 OSKPos = {'left'?: number, 'top'?: number}; - - export class AnchoredOSKView extends OSKView { - desktopLayout: layouts.TargetedFloatLayout; - - // OSK positioning fields - x: number; - y: number; - - private isResizing: boolean = false; - - // 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); - - let keymanweb = com.keyman.singleton; - if(keymanweb.isEmbedded) { - this.activationMode == ActivationMode.manual; - } - } - - /** - * 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 = '100%'; - s.position = 'fixed'; - } - - /** - * @override - */ - public refreshLayout(pending?: boolean): void { - // This function is generally triggered whenever the OSK's dimensions change, among other - // things. - if(this.isResizing) { - return; - } - - try { - this.isResizing = true; - // This resizes the OSK to what is appropriate for the device's current orientation, - // which will often trigger a resize event... which in turn triggers a layout refresh. - // - // So, we mark and unmark the `isResizing` flag to prevent triggering a circular - // call-stack chain from this call. - this.doResize(); - } finally { - this.isResizing = false; - } - super.refreshLayout(pending); - } - - protected doResize() { - if(this.vkbd && this.device.touchable) { - let targetOSKHeight = this.getDefaultKeyboardHeight(); - this.setSize(this.getDefaultWidth(), targetOSKHeight + this.banner.height); - } - } - - protected postKeyboardLoad() { - // Initializes the size of a touch keyboard. - this.doResize(); - - this._Visible = false; // I3363 (Build 301) - - this._Box.onmouseover = this._VKbdMouseOver; - this._Box.onmouseout = this._VKbdMouseOut; - - 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: AnchoredOSKView, keepDefaultPosition?: boolean) { - return; - }.bind(this); - - /** - * 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); - - /** - * 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'](); - } - - /* - * We've noticed some fairly inconsistent behavior in the past when attempting to base - * this logic on window.innerWidth/Height, as there can be very unexpected behavior - * on mobile devices during and after rotation. - * - * Online forums (such as https://stackoverflow.com/a/54812656) seem to indicate that - * document.documentElement.clientWidth/Height seem to be the most stable analogues - * to a window's size in the situations where it matters for Keyman Engine for Web. - * - * That said, an important note: this gets the dimensions of the _document element_, - * not the screen or even the window. - */ - let baseWidth = document?.documentElement?.clientWidth; - let baseHeight = document?.documentElement?.clientHeight; - if(typeof baseWidth == 'undefined') { - /* - * Fallback logic. We _shouldn't_ need this, but it's best to have _something_ - * for the sake of robustness. - */ - baseWidth = Math.min(screen.height, screen.width); - baseHeight = Math.max(screen.height, screen.width); - - if(!keymanweb.util.portraitView()) { - let temp = baseWidth; - baseWidth = baseHeight; - baseHeight = temp; - } - } - - var oskHeightLandscapeView=Math.floor(Math.min(baseHeight, baseWidth)/2), - height=oskHeightLandscapeView; - - if(device.formFactor == 'phone') { - /** - * Assuming the first-pass detection of width and height work correctly, note - * that these calculations are based on the document's size, not the device's - * resolution. This _particularly_ matters for height. - * - * - Is the mobile-device browser showing a URL bar? That's not included. - * - The standard signal-strength, battery-strength, etc device status bar? - * Also not included. - */ - if(keymanweb.util.portraitView()) - height=Math.floor(baseHeight/2.4); - else - height=Math.floor(baseHeight/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; - - width = document?.documentElement?.clientWidth; - if(typeof width == 'undefined') { - if(device.OS == 'iOS') { - width = window.innerWidth; - } else if(device.OS == 'Android') { - width=screen.availWidth; - } else { - width=screen.width; - } - } - - return width; - } - - /** - * 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) { - return; - } - - /** - * 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) { - return; // I3363 (Build 301) - } - - protected setDisplayPositioning() { - let Ls = this._Box.style; - - // The following code will always be executed except for externally created OSK such as EuroLatin - if(this.vkbd) { - Ls.position='fixed'; - Ls.left=Ls.bottom='0px'; - Ls.border='none'; - Ls.borderTop='1px solid gray'; - } - } - - /** - * 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); - } - } -} diff --git a/web/src/engine/main/osk/banner.ts b/web/src/engine/main/osk/banner.ts deleted file mode 100644 index c4f28a6e19..0000000000 --- a/web/src/engine/main/osk/banner.ts +++ /dev/null @@ -1,736 +0,0 @@ -/// -/// - -namespace com.keyman.osk { - // Base class for a banner above the keyboard in the OSK - - export abstract class Banner { - private _height: number; // pixels - private div: HTMLDivElement; - - public static DEFAULT_HEIGHT: number = 37; // pixels; embedded apps can modify - - public static readonly BANNER_CLASS: string = 'kmw-banner-bar'; - public static readonly BANNER_ID: string = 'kmw-banner-bar'; - - /** - * Function height - * Scope Public - * @returns {number} height in pixels - * Description Returns the height of the banner in pixels - */ - public get height(): number { - return this._height; - } - - /** - * Function height - * Scope Public - * @param {number} height the height in pixels - * Description Sets the height of the banner in pixels. If a negative - * height is given, set height to 0 pixels. - * Also updates the banner styling. - */ - public set height(height: number) { - this._height = (height > 0) ? height : 0; - this.update(); - } - - /** - * Function update - * @return {boolean} true if the banner styling changed - * Description Update the height and display styling of the banner - */ - private update() : boolean { - let ds = this.div.style; - let currentHeightStyle = ds.height; - let currentDisplayStyle = ds.display; - - if (this._height > 0) { - ds.height = this._height + 'px'; - ds.display = 'block'; - } else { - ds.height = '0px'; - ds.display = 'none'; - } - - return (!(currentHeightStyle === ds.height) || - !(currentDisplayStyle === ds.display)); - } - - public constructor(height?: number) { - let keymanweb = com.keyman.singleton; - let util = keymanweb.util; - - let d = util._CreateElement('div'); - d.id = Banner.BANNER_ID; - d.className = Banner.BANNER_CLASS; - this.div = d; - - this.height = height; - this.update(); - } - - public appendStyleSheet() { - let keymanweb = com.keyman.singleton; - let util = keymanweb.util; - - // TODO: add stylesheets - } - - /** - * Function getDiv - * Scope Public - * @returns {HTMLElement} Base element of the banner - * Description Returns the HTMLElelemnt of the banner - */ - public getDiv(): HTMLElement { - return this.div; - } - - /** - * Function activate - * Scope Public - * Description Adds any relevant event listeners needed by this banner type. - */ - public activate() { - // Default implementation - no listeners. - } - - /** - * Function activate - * Scope Public - * Description Removes any relevant event listeners previously added by this banner. - */ - public deactivate() { - // Default implementation - no listeners. - } - } - - /** - * Function BlankBanner - * Description A banner of height 0 that should not be shown - */ - export class BlankBanner extends Banner { - - constructor() { - super(0); - } - } - - /** - * Function ImageBanner - * @param {string} imagePath Path of image to display in the banner - * @param {number} height If provided, the height of the banner in pixels - * Description Display an image in the banner - */ - export class ImageBanner extends Banner { - private img: HTMLElement; - - constructor(imagePath: string, height?: number) { - if (imagePath.length > 0) { - super(); - if (height) { - this.height = height; - } - } else { - super(0); - } - - if(imagePath.indexOf('base64') >=0) { - console.log("Loading img from base64 data"); - } else { - console.log("Loading img with src '" + imagePath + "'"); - } - this.img = document.createElement('img'); - this.img.setAttribute('src', imagePath); - let ds = this.img.style; - ds.width = '100%'; - ds.height = '100%'; - this.getDiv().appendChild(this.img); - console.log("Image loaded."); - } - - /** - * Function setImagePath - * Scope Public - * @param {string} imagePath Path of image to display in the banner - * Description Update the image in the banner - */ - public setImagePath(imagePath: string) { - if (this.img) { - this.img.setAttribute('src', imagePath); - } - } - } - - export class BannerSuggestion { - div: HTMLDivElement; - private display: HTMLSpanElement; - private fontFamily?: string; - - private _suggestion: Suggestion; - - private index: number; - - static readonly BASE_ID = 'kmw-suggestion-'; - - constructor(index: number) { - let keyman = com.keyman.singleton; - - this.index = index; - - this.constructRoot(); - - // Provides an empty, base SPAN for text display. We'll swap these out regularly; - // `Suggestion`s will have varying length and may need different styling. - let display = this.display = keyman.util._CreateElement('span'); - this.div.appendChild(display); - } - - private constructRoot() { - let keyman = com.keyman.singleton; - - // Add OSK suggestion labels - let div = this.div = keyman.util._CreateElement('div'), ds=div.style; - div.className = "kmw-suggest-option"; - div.id = BannerSuggestion.BASE_ID + this.index; - - let kbdDetails = keyman.keyboardManager.activeStub; - if(kbdDetails) { - if (kbdDetails['KLC']) { - div.lang = kbdDetails['KLC']; - } - - // Establish base font settings - let font = kbdDetails['KFont']; - if(font && font.family && font.family != '') { - ds.fontFamily = this.fontFamily = font.family; - } - } - - // Ensures that a reasonable width % is set. - let usableWidth = 100 - SuggestionBanner.MARGIN * (SuggestionBanner.SUGGESTION_LIMIT - 1); - let widthpc = usableWidth / SuggestionBanner.SUGGESTION_LIMIT; - - ds.width = widthpc + '%'; - - this.div['suggestion'] = this; - } - - get suggestion(): Suggestion { - return this._suggestion; - } - - /** - * Function update - * @param {string} id Element ID for the suggestion span - * @param {Suggestion} suggestion Suggestion from the lexical model - * Description Update the ID and text of the BannerSuggestionSpec - */ - public update(suggestion: Suggestion) { - this._suggestion = suggestion; - this.updateText(); - } - - private updateText() { - let display = this.generateSuggestionText(); - this.div.replaceChild(display, this.display); - this.display = display; - } - - /** - * Function apply - * @param target (Optional) The OutputTarget to which the `Suggestion` ought be applied. - * Description Applies the predictive `Suggestion` represented by this `BannerSuggestion`. - */ - public apply(target?: text.OutputTarget): Promise { - let keyman = com.keyman.singleton; - - if(this.isEmpty()) { - return null; - } - - if(!target) { - /* Assume it's the currently-active `OutputTarget`. We should probably invalidate - * everything if/when the active `OutputTarget` changes, though we haven't gotten that - * far in implementation yet. - */ - target = dom.Utils.getOutputTarget(); - } - - if(this._suggestion.tag == 'revert') { - keyman.core.languageProcessor.applyReversion(this._suggestion as Reversion, target); - return null; - } else { - return keyman.core.languageProcessor.applySuggestion(this.suggestion, target, () => keyman.core.keyboardProcessor.layerId); - } - } - - public isEmpty(): boolean { - return !this._suggestion; - } - - /** - * Function generateSuggestionText - * @return {HTMLSpanElement} Span element of the suggestion - * Description Produces a HTMLSpanElement with the key's actual text. - */ - // - public generateSuggestionText(): HTMLSpanElement { - let keyman = com.keyman.singleton; - let util = keyman.util; - - let suggestion = this._suggestion; - var suggestionText: string; - - var s=util._CreateElement('span'); - s.className = 'kmw-suggestion-text'; - - if(suggestion == null) { - return s; - } - - if(suggestion.displayAs == null || suggestion.displayAs == '') { - suggestionText = '\xa0'; // default: nbsp. - } else { - // Default the LTR ordering to match that of the active keyboard. - let activeKeyboard = keyman.core.activeKeyboard; - let rtl = activeKeyboard && activeKeyboard.isRTL; - let orderCode = rtl ? 0x202e /* RTL */ : 0x202d /* LTR */; - suggestionText = String.fromCharCode(orderCode) + suggestion.displayAs; - } - - // TODO: Dynamic suggestion text resizing. (Refer to OSKKey.getTextWidth in visualKeyboard.ts.) - - // Finalize the suggestion text - s.innerHTML = suggestionText; - return s; - } - } - - /** - * Function SuggestionBanner - * Scope Public - * @param {number} height - If provided, the height of the banner in pixels - * Description Display lexical model suggestions in the banner - */ - export class SuggestionBanner extends Banner { - public static readonly SUGGESTION_LIMIT: number = 3; - public static readonly MARGIN = 1; - - private options : BannerSuggestion[]; - private hostDevice: utils.DeviceSpec; - - private manager: SuggestionManager; - - static readonly TOUCHED_CLASS: string = 'kmw-suggest-touched'; - static readonly BANNER_CLASS: string = 'kmw-suggest-banner'; - - constructor(hostDevice: utils.DeviceSpec, height?: number) { - super(height || Banner.DEFAULT_HEIGHT); - this.hostDevice = hostDevice; - - this.getDiv().className = this.getDiv().className + ' ' + SuggestionBanner.BANNER_CLASS; - - this.options = new Array(); - for (var i=0; i boolean = function(this: SuggestionBanner, outputTarget: text.OutputTarget) { - const keyman = com.keyman.singleton; - // Tell the keyboard that the current layer has not changed - keyman.core.keyboardProcessor.newLayerStore.set(''); - keyman.core.keyboardProcessor.oldLayerStore.set(''); - // Call the keyboard's entry point. - keyman.core.keyboardProcessor.processPostKeystroke(this.hostDevice, outputTarget) - // If we have a RuleBehavior as a result, run it on the target. This should - // only change system store and variable store values. - ?.finalize(keyman.core.keyboardProcessor, outputTarget, true); - - return true; - }.bind(this); - - postConfigure() { - let keyman = com.keyman.singleton; - // Trigger a null-based initial prediction to kick things off. - keyman.core.languageProcessor.predictFromTarget(dom.Utils.getOutputTarget(), keyman.core.keyboardProcessor.layerId); - } - - deactivate() { - let keyman = com.keyman.singleton; - let manager = this.manager; - - keyman.core.languageProcessor.removeListener('invalidatesuggestions', manager.invalidateSuggestions); - keyman.core.languageProcessor.removeListener('suggestionsready', manager.updateSuggestions); - keyman.core.languageProcessor.removeListener('tryaccept', manager.tryAccept); - keyman.core.languageProcessor.removeListener('tryrevert', manager.tryRevert); - keyman.core.languageProcessor.removeListener('suggestionapplied', this.suggestionApplied); - } - } - - export class SuggestionManager extends UITouchHandlerBase { - private selected: BannerSuggestion; - - platformHold: (suggestion: BannerSuggestion, isCustom: boolean) => void; - - //#region Touch handling implementation - findTargetFrom(e: HTMLElement): HTMLDivElement { - let keyman = com.keyman.singleton; - let util = keyman.util; - - try { - if(e) { - if(util.hasClass(e,'kmw-suggest-option')) { - return e as HTMLDivElement; - } - if(e.parentNode && util.hasClass( e.parentNode,'kmw-suggest-option')) { - return e.parentNode as HTMLDivElement; - } - // if(e.firstChild && util.hasClass( e.firstChild,'kmw-suggest-option')) { - // return e.firstChild as HTMLDivElement; - // } - } - } catch(ex) {} - return null; - } - - protected highlight(t: HTMLDivElement, on: boolean): void { - let classes = t.className; - let cs = ' ' + SuggestionBanner.TOUCHED_CLASS; - - if(t.id.indexOf(BannerSuggestion.BASE_ID) == -1) { - console.warn("Cannot find BannerSuggestion object for element to highlight!"); - } else { - // Never highlight an empty suggestion button. - let suggestion = this.selected = t['suggestion'] as BannerSuggestion; - if(suggestion.isEmpty()) { - on = false; - this.selected = null; - } - } - - if(on && classes.indexOf(cs) < 0) { - t.className=classes+cs; - } else { - t.className=classes.replace(cs,''); - } - } - - protected select(t: HTMLDivElement): void { - this.doAccept(t['suggestion'] as BannerSuggestion); - } - - //#region Long-press support - protected hold(t: HTMLDivElement): void { - let suggestionObj = t['suggestion'] as BannerSuggestion; - - // Is this the suggestion? It's never in this.currentSuggestions, so check against that. - let isCustom = this.currentSuggestions.indexOf(suggestionObj.suggestion) == -1; - - if(this.platformHold) { - // Implemented separately for native + embedded mode branches. - // Embedded mode should pass any info needed to show a submenu IMMEDIATELY. - this.platformHold(suggestionObj, isCustom); // No implementation yet for native. - } - } - protected clearHolds(): void { - // Temp, pending implementation of suggestion longpress submenus - // - nothing to clear without them - - - // only really used in native-KMW - } - - protected hasModalPopup(): boolean { - // Utilized by the mobile apps; allows them to 'take over' touch handling, - // blocking it within KMW when the apps are already managing an ongoing touch-hold. - let keyman = com.keyman.singleton; - return keyman['osk'].vkbd.subkeyGesture && keyman.isEmbedded; - } - - protected dealiasSubTarget(target: HTMLDivElement): HTMLDivElement { - return target; - } - - protected hasSubmenu(t: HTMLDivElement): boolean { - // Temp, pending implementation of suggestion longpress submenus - - // Only really used by native-KMW - see kmwnative's highlightSubKeys func. - return false; - } - - protected isSubmenuActive(): boolean { - // Temp, pending implementation of suggestion longpress submenus - - // Utilized only by native-KMW - it parallels hasModalPopup() in purpose. - return false; - } - - protected displaySubmenuFor(target: HTMLDivElement) { - // Utilized only by native-KMW to show submenus. - throw new Error("Method not implemented."); - } - //#endregion - //#endregion - - private options: BannerSuggestion[]; - - private initNewContext: boolean = true; - - private currentSuggestions: Suggestion[] = []; - private keepSuggestion: Keep; - private revertSuggestion: Reversion; - - private recentAccept: boolean = false; - private revertAcceptancePromise: Promise; - - private swallowPrediction: boolean = false; - - private doRevert: boolean = false; - private recentRevert: boolean = false; - - constructor(div: HTMLElement, options: BannerSuggestion[]) { - // TODO: Determine appropriate CSS styling names, etc. - super(div, Banner.BANNER_CLASS, SuggestionBanner.TOUCHED_CLASS); - this.options = options; - } - - private doAccept(suggestion: BannerSuggestion) { - let _this = this; - - // Selecting a suggestion or a reversion should both clear selection - // and clear the reversion-displaying state of the banner. - this.selected = null; - this.doRevert = false; - - this.revertAcceptancePromise = suggestion.apply(); - if(!this.revertAcceptancePromise) { - // We get here either if suggestion acceptance fails or if it was a reversion. - if(suggestion.suggestion && suggestion.suggestion.tag == 'revert') { - // Reversion state management - this.recentAccept = false; - this.recentRevert = true; - - this.doUpdate(); - } - return; - } - - this.revertAcceptancePromise.then(function(suggestion) { - // Always null-check! - if(suggestion) { - _this.revertSuggestion = suggestion; - } - }); - - this.recentAccept = true; - this.recentRevert = false; - - this.swallowPrediction = true; - this.doUpdate(); - } - - private showRevert() { - // Construct a 'revert suggestion' to facilitate a reversion UI component. - this.doRevert = true; - this.doUpdate(); - } - - /** - * Receives messages from the keyboard that the 'accept' keystroke has been entered. - * Should return 'false' if the current state allows accepting a suggestion and act accordingly. - * Otherwise, return true. - */ - tryAccept: (source: string) => boolean = function(this: SuggestionManager, source: string, returnObj: {shouldSwallow: boolean}) { - let keyman = com.keyman.singleton; - - if(!this.recentAccept && this.selected) { - this.doAccept(this.selected); - returnObj.shouldSwallow = true; - } else if(this.recentAccept && source == 'space') { - this.recentAccept = false; - // If the model doesn't insert wordbreaks, don't swallow the space. If it does, - // we consider that insertion to be the results of the first post-accept space. - returnObj.shouldSwallow = !!keyman.core.languageProcessor.wordbreaksAfterSuggestions; - } else { - returnObj.shouldSwallow = false; - } - }.bind(this); - - /** - * Receives messages from the keyboard that the 'revert' keystroke has been entered. - * Should return 'false' if the current state allows reverting a recently-applied suggestion and act accordingly. - * Otherwise, return true. - */ - tryRevert: () => boolean = function(this: SuggestionManager, returnObj: {shouldSwallow: boolean}) { - // Has the revert keystroke (BKSP) already been sent once since the last accept? - if(this.doRevert) { - // If so, clear the 'revert' option and start doing normal predictions again. - this.doRevert = false; - this.recentAccept = false; - // Otherwise, did we just accept something before the revert signal was received? - } else if(this.recentAccept) { - this.showRevert(); - this.swallowPrediction = true; - } - - // We don't yet actually do key-based reversions. - returnObj.shouldSwallow = false; - return; - }.bind(this); - - /** - * Function invalidateSuggestions - * Scope Public - * Description Clears the suggestions in the suggestion banner - */ - public invalidateSuggestions: (this: SuggestionManager, source: text.prediction.InvalidateSourceEnum) => boolean = - function(this: SuggestionManager, source: string) { - - // By default, we assume that the context is the same until we notice otherwise. - this.initNewContext = false; - - if(!this.swallowPrediction || source == 'context') { - this.recentAccept = false; - this.doRevert = false; - this.recentRevert = false; - - if(source == 'context') { - this.swallowPrediction = false; - this.initNewContext = true; - } - } - - this.options.forEach((option: BannerSuggestion) => { - option.update(null); - }); - }.bind(this); - - public activateKeep(): boolean { - return !this.recentAccept && !this.recentRevert && !this.initNewContext; - } - - private doUpdate() { - let suggestions = []; - // Insert 'current text' if/when valid as the leading option. - // Since we don't yet do auto-corrections, we only show 'keep' whenever it's - // a valid word (according to the model). - if(this.activateKeep() && this.keepSuggestion && this.keepSuggestion.matchesModel) { - suggestions.push(this.keepSuggestion); - } else if(this.doRevert) { - suggestions.push(this.revertSuggestion); - } - - suggestions = suggestions.concat(this.currentSuggestions); - - this.options.forEach((option: BannerSuggestion, i: number) => { - if(i < suggestions.length) { - option.update(suggestions[i]); - } else { - option.update(null); - } - }); - } - - /** - * Function updateSuggestions - * Scope Public - * @param {Suggestion[]} suggestions Array of suggestions from the lexical model. - * Description Update the displayed suggestions in the SuggestionBanner - */ - public updateSuggestions: (this: SuggestionManager, prediction: text.prediction.ReadySuggestions) => boolean = - function(this: SuggestionManager, prediction: text.prediction.ReadySuggestions) { - - let suggestions = prediction.suggestions; - - this.currentSuggestions = suggestions; - - // Do we have a keep suggestion? If so, remove it from the list so that we can control its display position - // and prevent it from being hidden after reversion operations. - this.keepSuggestion = null; - for(let s of suggestions) { - if(s.tag == 'keep') { - this.keepSuggestion = s as Keep; - } - } - - if(this.keepSuggestion) { - this.currentSuggestions.splice(this.currentSuggestions.indexOf(this.keepSuggestion), 1); - } - - // If we've gotten an update request like this, it's almost always user-triggered and means the context has shifted. - if(!this.swallowPrediction) { - this.recentAccept = false; - this.doRevert = false; - this.recentRevert = false; - } else { // This prediction was triggered by a recent 'accept.' Now that it's fulfilled, we clear the flag. - this.swallowPrediction = false; - } - - // The rest is the same, whether from input or from "self-updating" after a reversion to provide new suggestions. - this.doUpdate(); - }.bind(this); - } -} diff --git a/web/src/engine/main/osk/bannerManager.ts b/web/src/engine/main/osk/bannerManager.ts deleted file mode 100644 index 6d45d4d2ce..0000000000 --- a/web/src/engine/main/osk/bannerManager.ts +++ /dev/null @@ -1,292 +0,0 @@ -/// -/// - -namespace com.keyman.osk { - /** - * This object is used to specify options by both `BannerManager.getOptions` - * and `BannerManager.setOptions`. Refer to the latter for specification of - * each field. - */ - export interface BannerOptions { - alwaysShow?: boolean; - mayPredict?: boolean; - mayCorrect?: boolean; - imagePath?: string; - } - - export type BannerType = "blank" | "image" | "suggestion"; - - /** - * The `BannerManager` module is designed to serve as a manager for the - * different `Banner` types. - * To facilitate this, it will provide a root element property that serves - * as a container for any active `Banner`, helping KMW to avoid needless - * DOM element shuffling. - * - * Goals for the `BannerManager`: - * - * * It will be exposed as `keyman.osk.banner` and will provide the following API: - * * `getOptions`, `setOptions` - refer to the `BannerOptions` class for details. - * * This provides a persistent point that the web page designers and our - * model apps can utilize and can communicate with. - * * These API functions are designed for live use and will allow - * _hot-swapping_ the `Banner` instance; they're not initialization-only. - * * Disabling the `Banner` (even for suggestions) outright with - * `enablePredictions == false` will auto-unload any loaded predictive model - * from `ModelManager` and setting it to `true` will revert this. - * * This should help to avoid wasting computational resources. - * * It will listen to ModelManager events and automatically swap Banner - * instances as appropriate: - * * The option `persistentBanner == true` is designed to replicate current - * iOS system keyboard behavior. - * * When true, an `ImageBanner` will be displayed. - * * If false, it will be replaced with a `BlankBanner` of zero height, - * corresponding to our current default lack of banner. - * * It will not automatically set `persistentBanner == true`; - * this must be set by the iOS app, and only under the following conditions: - * * `keyman.isEmbedded == true` - * * `device.OS == 'ios'` - * * Keyman is being used as the system keyboard within an app that - * needs to reserve this space (i.e: Keyman for iOS), - * rather than as its standalone app. - */ - export class BannerManager implements OSKViewComponent { - private _activeType: BannerType; - private _options: BannerOptions = {}; - private bannerContainer: HTMLDivElement; - private activeBanner: Banner; - private alwaysShow: boolean; - private imagePath?: string = ""; - - private readonly hostDevice: utils.DeviceSpec; - - public static readonly DEFAULT_OPTIONS: BannerOptions = { - alwaysShow: false, - mayPredict: true, - mayCorrect: true, - imagePath: "" - } - - constructor(hostDevice: utils.DeviceSpec) { - // Step 1 - establish the container element. Must come before this.setOptions. - this.constructContainer(); - this.hostDevice = hostDevice; - - // Initialize with the default options - - // any 'manually set' options come post-construction. - // This will also automatically set the default banner in place. - this.setOptions(BannerManager.DEFAULT_OPTIONS); - } - - /** - * Constructs the
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(); - } - } -}