spiegel-keyman/web/source/osk/floatingOskView.ts
Marc Durdin b5615e3a4b fix(web): error in floating osk view when no keyboard active
This fixes an issue that arises when loading the Keyman Developer Server
home page and there are registered keyboards, but no keyboards active.
The floating osk view would throw an exception because it would attempt
to set the title of the view from the active keyboard, which is `null`.
2022-03-02 08:46:10 +11:00

568 lines
17 KiB
TypeScript

// Includes KMW-added property declaration extensions for HTML elements.
/// <reference path="../kmwexthtml.ts" />
// Includes the touch-mode language picker UI.
/// <reference path="languageMenu.ts" />
/// <reference path="lengthStyle.ts" />
// Defines desktop-centric OSK positioning + sizing behavior
/// <reference path="layouts/targetedFloatLayout.ts" />
/// <reference path="oskView.ts" />
/***
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.<string,number>} 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.<string,number>} 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.<string,number>} 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;
}
}
}