// 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) } } /** * 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; } } }