// 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 _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 = this.defaultFontSize(this.device, keymanweb.isEmbedded); } return this._baseFontSize; } public defaultFontSize(device: utils.DeviceSpec, 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 this.computedHeight ? ParsedLengthStyle.inPixels(this.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); } protected setNeedsLayout() { this.needsLayout = true; } public refreshLayout(pending?: boolean): void { if(!this.keyboardView) { return; } // Step 1: have the necessary conditions been met? const fixedSize = this.width && this.height && 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) { 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'; } else { const bs = this._Box.style; bs.width = 'auto'; bs.height = 'auto'; bs.maxWidth = bs.maxHeight = ''; } let keyman = com.keyman.singleton; keyman.alignInputs(); } 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 // Correct the classname for the (inner) OSK frame (Build 360) var kbdID: string = (this.activeKeyboard ? this.activeKeyboard.id.replace('Keyboard_','') : ''); if(kbdID.indexOf('::') != -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(kbdID.indexOf('::') + 2); } const kbdClassSuffix = ' kmw-keyboard-' + kbdID; kbdView.element.className = kbdView.element.className + kbdClassSuffix; this.banner.appendStyles(); if(this.vkbd) { // Create the key preview (for phones) this.vkbd.createKeyTip(); // 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. if(this.vkbd) { 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) { let menu = new LanguageMenu(com.keyman.singleton); menu.show(); } } // 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) { return com.keyman.singleton.util.addEventListener('osk.'+event, func); } } }