spiegel-keyman/web/source/osk/oskManager.ts
2019-11-08 09:27:23 +07:00

1572 lines
No EOL
52 KiB
TypeScript

/// <reference path="../kmwexthtml.ts" /> // Includes KMW-added property declaration extensions for HTML elements.
// Includes KMW string extension declarations.
/// <reference path="../text/kmwstring.ts" />
// Includes the default layout specification.
/// <reference path="defaultLayouts.ts" />
// Includes the touch-mode language picker UI.
/// <reference path="languageMenu.ts" />
// Includes the banner
/// <reference path="./bannerManager.ts" />
// Generates the visual keyboard specific to each keyboard. (class="kmw-osk-inner-frame")
/// <reference path="visualKeyboard.ts" />
/***
KeymanWeb 10.0
Copyright 2017 SIL International
***/
namespace com.keyman.osk {
type MouseHandler = (this: GlobalEventHandlers, ev: MouseEvent) => any;
type OSKRect = {'left'?: number, 'top'?: number, 'width'?: number, 'height'?: number,
'nosize'?: boolean, 'nomove'?: boolean};
type OSKPos = {'left'?: number, 'top'?: number};
export class OSKManager {
// Important OSK elements (and container classes)
_Box: HTMLDivElement;
banner: BannerManager;
vkbd: VisualKeyboard;
resizeIcon: HTMLDivElement;
closeButton: HTMLDivElement;
helpImg: HTMLDivElement;
configImg: HTMLDivElement;
pinImg: HTMLDivElement;
ready: boolean = false;
loadRetry: number = 0;
// OSK state fields
_Visible: boolean = false;
_Enabled: boolean = true;
vpScale: number = 1;
// OSK positioning fields
userPositioned: boolean = false;
width: number;
height: number;
x: number;
y: number;
noDrag: boolean = false;
dfltX: string;
dfltY: string;
// OSK resizing-event state fields
resizing: boolean;
_VMoveX: number;
_VMoveY: number;
_ResizeMouseX: number;
_ResizeMouseY: number;
_VOriginalWidth: number;
_VOriginalHeight: number;
// Resize-event temporary storage
_VPreviousMouseMove: MouseHandler;
_VPreviousMouseUp: MouseHandler;
_VPreviousCursor: string;
_VPreviousMouseButton: number;
// 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;
// First time initialization of OSK
prepare() {
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
// Defer loading the OSK until KMW code initialization complete
if(!keymanweb['initialized']) {
window.setTimeout(this.prepare.bind(this), 200);
return;
}
// OSK initialization - create DIV and set default styles
if(!this.ready) {
this._Box = util._CreateElement('div'); // Container for OSK (Help DIV, displayed when user clicks Help icon)
document.body.appendChild(this._Box);
// Install the default OSK stylesheet
util.linkStyleSheet(keymanweb.getStyleSheetPath('kmwosk.css'));
// For mouse click to prevent loss of focus
util.attachDOMEvent(this._Box, 'mousedown', function(obj){
keymanweb.uiManager.setActivatingUI(true);
return false;
});
// And to prevent touch event default behaviour on mobile devices
// TODO: are these needed, or do they interfere with other OSK event handling ????
if(util.device.touchable) { // I3363 (Build 301)
var cancelEventFunc = function(e) {
if(e.cancelable) {
e.preventDefault();
}
e.stopPropagation();
return false;
};
util.attachDOMEvent(this._Box, 'touchstart', function(e) {
keymanweb.uiManager.setActivatingUI(true);
return cancelEventFunc(e);
});
util.attachDOMEvent(this._Box, 'touchend', cancelEventFunc);
util.attachDOMEvent(this._Box, 'touchmove', cancelEventFunc);
util.attachDOMEvent(this._Box, 'touchcancel', cancelEventFunc);
// Can only get (initial) viewport scale factor after page is fully loaded!
this.vpScale=util.getViewportScale();
}
}
this.loadCookie();
this.banner = new BannerManager();
this.ready=true;
}
/**
* Function _Unload
* Scope Private
* Description Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)
*/
_Unload() {
this.vkbd = null;
this.banner = null;
this._Box = null;
}
/**
* Function _Load
* Scope Private
* Description OSK initialization when keyboard selected
*/
_Load() { // Load Help
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
let device = util.device;
var activeKeyboard = keymanweb.keyboardManager.activeKeyboard;
// If _Load called before OSK is ready, must wait and call again
if(this._Box == null) {
if(this.loadRetry >= 99) {
return; // fail silently, but should not happen
}
window.setTimeout(this._Load.bind(this), 100);
this.loadRetry++;
return;
}
this.loadRetry = 0;
if(keymanweb._TitleElement) {
keymanweb._TitleElement.innerHTML = 'KeymanWeb'; // I1972
}
this._Visible = false; // I3363 (Build 301)
var s = this._Box.style;
s.zIndex='9999'; s.display='none'; s.width= device.touchable ? '100%' : 'auto';
s.position = (device.formFactor == 'desktop' ? 'absolute' : 'fixed');
// Use smaller base font size for mobile devices
//if(screen.availHeight < 500) s.fontSize='10pt';
//else if(screen.availHeight < 800) s.fontSize='11pt';
//else s.fontSize='12pt';
// Set scaling for mobile devices here.
if(device.touchable) {
var fontScale: number = 1;
if(device.formFactor == 'phone') {
fontScale = 1.6 * (keymanweb.isEmbedded ? 0.65 : 0.6) * 1.2; // Combines original scaling factor with one previously applied to the layer group.
} else {
// The following is a *temporary* fix for small format tablets, e.g. PendoPad
var pixelRatio = 1;
if(device.OS == 'Android' && 'devicePixelRatio' in window) {
pixelRatio = window.devicePixelRatio;
}
if(device.OS == 'Android' && device.formFactor == 'tablet' && this.getHeight() < 300 * pixelRatio) {
fontScale *= 1.2;
} else {
fontScale *= 2; //'2.5em';
}
}
// Finalize the font size parameter.
s.fontSize = fontScale + 'em';
}
this.vkbd = null;
// TODO: Consider a 'vkbd.release()' method?
// Instantly resets the OSK container, erasing / delinking the previously-loaded keyboard.
this._Box.innerHTML = '';
this._Box.onmouseover = this._VKbdMouseOver;
this._Box.onmouseout = this._VKbdMouseOut;
// TODO: find out and document why this should not be done for touch devices!!
// (Probably to avoid having a null keyboard. But maybe that *is* an option, if there remains a way to get the language menu,
// such as a minimized menu button?)
if(activeKeyboard == null && !device.touchable) {
var Ldiv=util._CreateElement('div');
Ldiv.className = "kmw-title-bar";
Ldiv.appendChild(this._TitleBarInterior());
Ldiv.onmousedown = this._VMoveMouseDown;
this._Box.appendChild(Ldiv);
Ldiv = util._CreateElement('div');
Ldiv.className='kmw-osk-none';
this._Box.appendChild(Ldiv);
} else {
var Lviskbd=null,layouts=null,layout=null,Lhelp='';
this._Box.className = "";
if(activeKeyboard != null) {
Lviskbd=activeKeyboard['KV'];
Lhelp=activeKeyboard['KH'];
// Check if dynamic layout is defined within keyboard
layouts=activeKeyboard['KVKL'];
// If any keyboard layout file is provided, use that to override the generated layout
if(typeof layouts != 'undefined' && layouts != null) {
layout=layouts[device.formFactor];
// Use the layout for the device, if defined, otherwise use the desktop (default) layout
if(typeof layout == 'undefined' || layout == null) {
if(device.formFactor == 'phone') {
layout=layouts['tablet'];
} else if(device.formFactor == 'tablet') {
layout=layouts['phone'];
}
if(typeof layout == 'undefined' || layout == null) {
layout=layouts['desktop'];
}
}
}
}
// Test if Visual keyboard is simply a place holder, set to null if so
if(Lviskbd != null && Lviskbd['BK'] != null) {
var keyCaps=Lviskbd['BK'], noKeyCaps=true;
for(var i=0; i<keyCaps.length; i++) {
if(keyCaps[i].length > 0) {
noKeyCaps = false;
break;
}
}
if(noKeyCaps) {
Lviskbd=null;
}
}
// Generate a visual keyboard from the layout (or layout default)
// TODO: this should probably be unconditional now
if(Lviskbd != null || Lhelp == '' || device.touchable) { // I3363 (Build 301)
// TODO: May want to define a default BK array here as well
if(Lviskbd == null) {
Lviskbd={'F':'Tahoma', 'BK': Layouts.dfltText}; //DDOSK
}
this._GenerateVisualKeyboard(Lviskbd, Lhelp, layout, keymanweb.keyboardManager.getKeyboardModifierBitmask());
} else { //The following code applies only to preformatted 'help' such as European Latin
//osk.ddOSK = false;
Ldiv=util._CreateElement('div');
Ldiv.className = "kmw-title-bar";
Ldiv.appendChild(this._TitleBarInterior());
Ldiv.onmousedown = this._VMoveMouseDown;
this._Box.appendChild(Ldiv);
//Add content
var Ldiv = util._CreateElement('div');
Ldiv.className='kmw-osk-static';
Ldiv.innerHTML = Lhelp;
this._Box.appendChild(Ldiv);
if(activeKeyboard['KHF']) {
activeKeyboard['KHF'](this._Box);
}
}
if(keymanweb._TitleElement)
{
keymanweb._TitleElement.innerHTML = "<span style='font-weight:bold'>"
+ activeKeyboard['KN'] + '</span> - ' + keymanweb._TitleElement.innerHTML; // I1972 // I2186
keymanweb._TitleElement.className=''; keymanweb._TitleElement.style.color='#fff';
}
}
// Correct the classname for the (inner) OSK frame (Build 360)
var innerFrame=<HTMLDivElement> this._Box.firstChild,
kbdClass = ' kmw-keyboard-' + (activeKeyboard ? activeKeyboard['KI'].replace('Keyboard_','') : '');
if(innerFrame.id == 'keymanweb_title_bar') {
// Desktop order is title_bar, banner_container, inner-frame
innerFrame=<HTMLDivElement> innerFrame.nextSibling.nextSibling;
} else if (innerFrame.id == 'keymanweb_banner_container') {
innerFrame=<HTMLDivElement> innerFrame.nextSibling;
}
innerFrame.className = 'kmw-osk-inner-frame' + kbdClass;
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();
}
if(this._Enabled) {
this._Show();
}
}
/**
* Function _GenerateVisualKeyboard
* Scope Private
* @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 visual keyboard element and attaches it to KMW
*/
private _GenerateVisualKeyboard(PVK, Lhelp, layout0, kbdBitmask) {
this.vkbd = new com.keyman.osk.VisualKeyboard(PVK, Lhelp, layout0, kbdBitmask);
let util = com.keyman.singleton.util;
// Set box class - OS and keyboard added for Build 360
this._Box.className=util.device.formFactor+' '+ util.device.OS.toLowerCase() + ' kmw-osk-frame';
// Add header element to OSK only for desktop browsers
if(util.device.formFactor == 'desktop') {
this._Box.appendChild(this.controlBar());
}
// Add suggestion banner bar to OSK
if (this.banner) {
this._Box.appendChild(this.banner.element);
}
// Add primary keyboard element to OSK
this._Box.appendChild(this.vkbd.kbdDiv);
// Add footer element to OSK only for desktop browsers
if(util.device.formFactor == 'desktop') {
this._Box.appendChild(this.resizeBar());
// For other devices, adjust the object heights, allowing for viewport scaling
} else {
this.vkbd.adjustHeights();
}
}
/**
* Create a control bar with title and buttons for the desktop OSK
*/
controlBar(): HTMLDivElement {
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
var bar=util._CreateElement('div'),title='';
bar.id='keymanweb_title_bar';
bar.className='kmw-title-bar';
bar.onmousedown=this._VMoveMouseDown;
if(keymanweb.keyboardManager.activeKeyboard) {
title=keymanweb.keyboardManager.activeKeyboard['KN'];
}
var Ltitle=util._CreateElement('span');
Ltitle.className='kmw-title-bar-caption';
Ltitle.innerHTML=title;
bar.appendChild(Ltitle);
var Limg = this.closeButton = util._CreateElement('div');
Limg.id='kmw-close-button';
Limg.className='kmw-title-bar-image';
Limg.onmousedown=util._CancelMouse;
Limg.onclick=function(this: OSKManager) {
this._Hide(true);
}.bind(this);
bar.appendChild(Limg);
Limg = this.helpImg = util._CreateElement('div');
Limg.id='kmw-help-image';
Limg.className='kmw-title-bar-image';
Limg.title='KeymanWeb Help';
Limg.onclick=function() {
var p={};
util.callEvent('osk.helpclick',p);
if(window.event) {
window.event.returnValue=false;
}
return false;
}
Limg.onmousedown=util._CancelMouse;
bar.appendChild(Limg);
Limg = this.configImg = util._CreateElement('div');
Limg.id='kmw-config-image';
Limg.className='kmw-title-bar-image';
Limg.title='KeymanWeb Configuration Options';
Limg.onclick=function() {
var p={};
util.callEvent('osk.configclick',p);
if(window.event) {
window.event.returnValue=false;
}
return false;
}
Limg.onmousedown=util._CancelMouse;
bar.appendChild(Limg);
Limg = this.pinImg = util._CreateElement('div'); //I2186
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.onclick=function(this: OSKManager) {
this.loadCookie();
this.userPositioned=false;
this.saveCookie();
this._Show();
this.doResizeMove(); //allow the UI to respond to OSK movements
if(this.pinImg) {
this.pinImg.style.display='none';
}
if(window.event) {
window.event.returnValue=false;
}
return false;
}.bind(this);
Limg.onmousedown=util._CancelMouse;
bar.appendChild(Limg);
return bar;
}
/**
* Create a bottom bar with a resizing icon for the desktop OSK
*/
resizeBar(): HTMLDivElement {
let util = com.keyman.singleton.util;
var bar=util._CreateElement('div');
bar.className='kmw-footer';
bar.onmousedown=util._CancelMouse;
// Add caption
var Ltitle=util._CreateElement('div');
Ltitle.className='kmw-footer-caption';
Ltitle.innerHTML='<a href="https://keyman.com/developer/keymanweb/">KeymanWeb</a>';
Ltitle.id='keymanweb-osk-footer-caption';
// Display build number on shift+double click
util.attachDOMEvent(Ltitle,'dblclick', function(e) {
if(e && e.shiftKey) {
this.showBuild();
}
return false;
}.bind(this),false);
// Prevent selection of caption (IE - set by class for other browsers)
if('onselectstart' in Ltitle) Ltitle.onselectstart= util.selectStartHandler; //IE (Build 360)
bar.appendChild(Ltitle);
var Limg = util._CreateElement('div');
Limg.className='kmw-footer-resize';
Limg.onmousedown=this._VResizeMouseDown;
Limg.onmouseover=Limg.onmouseout=this._VResizeMouseOut;
bar.appendChild(Limg);
this.resizeIcon=Limg;
//TODO: the image never appears in IE8, have no idea why!
return bar;
}
/**
* Display build number
*/
showBuild() {
let keymanweb = com.keyman.singleton;
keymanweb.util.alert('KeymanWeb Version '+keymanweb['version']+'.'+keymanweb['build']+'<br /><br />'
+'<span style="font-size:0.8em">Copyright &copy; 2017 SIL International</span>');
}
/**
* Move OSK back to default position
*/
restorePosition: () => void = function(this: OSKManager) {
if(this._Visible) {
com.keyman.singleton.domManager.focusLastActiveElement(); // I2036 - OSK does not unpin to correct location
this.loadCookie();
this.userPositioned=false;
this.saveCookie();
this._Show();
this.doResizeMove(); //allow the UI to respond to OSK movements
}
if(this.pinImg) {
this.pinImg.style.display='none';
}
if(window.event) {
window.event.returnValue=false;
}
}.bind(this);
/**
* Function _TitleBarInterior
* Scope Private
* Description Title bar interior formatting and element event handling
*/
_TitleBarInterior() {
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
var Ldiv = util._CreateElement('div');
var Ls = Ldiv.style;
Ls.paddingLeft='2px';
Ls.cursor='move';
Ls.background='#ad4a28';
Ls.font='8pt Tahoma,Arial,sans-serif'; //I2186
// Add container for buttons, handle mousedown event
var LdivButtons = util._CreateElement('div');
LdivButtons.className = 'kmw-title-bar-actions';
LdivButtons.onmousedown=util._CancelMouse;
// Add close button, handle click and mousedown events
var Limg = util._CreateElement('div');
Limg.className='kmw-close-button';
Limg.onmousedown=util._CancelMouse;
Limg.onclick=function (this: OSKManager) {
this._Hide(true);
}.bind(this);
this.closeButton = Limg;
LdivButtons.appendChild(Limg);
// Add 'Unpin' button for restoring OSK to default location, handle mousedown and click events
Limg = this.pinImg = util._CreateElement('div'); //I2186
Limg.className='kmw-pin-image';
Limg.title='Pin the On Screen Keyboard to its default location on the active text box';
Limg.onclick=this.restorePosition;
Limg.onmousedown=util._CancelMouse;
Limg.style.display='none';
// Do not use Unpin button on touch screens (OSK location fixed)
if(!util.device.touchable) {
LdivButtons.appendChild(Limg); // I3363 (Build 301)
}
// Attach button container to title bar
Ldiv.appendChild(LdivButtons);
// Add title bar caption
var Lcap=keymanweb._TitleElement=util._CreateElement('span'); // I1972
Lcap.className='kmw-title-bar-caption';
Lcap.innerHTML='KeymanWeb';
Ldiv.appendChild(Lcap);
return Ldiv;
}
// End of TitleBarInterior
/**
* Function enabled
* Scope Public
* @return {boolean|number} True if KMW OSK enabled
* Description Test if KMW OSK is enabled
*/
['isEnabled'](): boolean {
return this._Enabled;
}
/**
* 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: OSKManager, 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: OSKManager, e) {
com.keyman.singleton.uiManager.setActivatingUI(false);
}.bind(this);
/**
* Function _VResizeMouseOver, _VResizeMouseOut
* Scope Private
* @param {Object} e event
* Description Process end of resizing of KMW UI
*/
private _VResizeMouseOut = function(this: OSKManager, e: Event) {
e = com.keyman.singleton._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) {
return false;
}
if(e && e.preventDefault) {
e.preventDefault();
}
var r=this.getRect();
this.width = r.width;
this.height = r.height;
e.cancelBubble = true;
return false;
}.bind(this);
private _VResizeMouseOver = this._VResizeMouseOut;
/**
* Function _VResizeMouseDown
* Scope Private
* @param {Object} e event
* Description Process resizing of KMW UI
*/
private _VResizeMouseDown = function(this: OSKManager, e: MouseEvent) {
let keymanweb = com.keyman.singleton;
keymanweb.uiManager.justActivated = true;
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) {
return true;
}
this.resizing = true;
var Lposx,Lposy;
if (e.pageX) {
Lposx = e.pageX;
Lposy = e.pageY;
} else if(e.clientX) {
Lposx = e.clientX + document.body.scrollLeft;
Lposy = e.clientY + document.body.scrollTop;
}
this._ResizeMouseX = Lposx;
this._ResizeMouseY = Lposy;
if(document.onmousemove != this._VResizeMouseMove && document.onmousemove != this._VMoveMouseMove) { // I1472 - Dragging off edge of browser window causes muckup
this._VPreviousMouseMove = document.onmousemove;
this._VPreviousMouseUp = document.onmouseup;
}
this._VPreviousCursor = document.body.style.cursor;
this._VPreviousMouseButton = (typeof(e.which)=='undefined' ? e.button : e.which);
this._VOriginalWidth = this.vkbd.kbdDiv.offsetWidth;
this._VOriginalHeight = this.vkbd.kbdDiv.offsetHeight;
document.onmousemove = this._VResizeMouseMove;
document.onmouseup = this._VResizeMoveMouseUp;
if(document.body.style.cursor) {
document.body.style.cursor = 'se-resize';
}
if(e && e.preventDefault) {
e.preventDefault();
}
e.cancelBubble = true;
return false;
}.bind(this);
/**
* Function _VResizeMouseMove
* Scope Private
* @param {Object} e event
* Description Process mouse movement during resizing of OSK
*/
_VResizeMouseMove = function(this: OSKManager, e: MouseEvent) {
var Lposx,Lposy;
e = com.keyman.singleton._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) {
return true;
}
this.resizing = true;
if(this._VPreviousMouseButton != (typeof(e.which)=='undefined' ? e.button : e.which)) { // I1472 - Dragging off edge of browser window causes muckup
return this._VResizeMoveMouseUp(e);
} else {
if (e.pageX) {
Lposx = e.pageX;
Lposy=e.pageY;
} else if (e.clientX) {
Lposx = e.clientX + document.body.scrollLeft;
Lposy = e.clientY + document.body.scrollTop;
}
var newWidth=(this._VOriginalWidth + Lposx - this._ResizeMouseX),
newHeight=(this._VOriginalHeight + Lposy - this._ResizeMouseY);
// 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) {
newWidth=0.5*screen.height;
}
// Set OSK width
this.vkbd.kbdDiv.style.width=newWidth+'px';
// Explicitly change OSK height and font size - cannot safely rely on scaling from font
this.vkbd.kbdDiv.style.height=newHeight+'px';
this.vkbd.kbdDiv.style.fontSize=(newHeight/8)+'px';
if(e && e.preventDefault) {
e.preventDefault();
}
e.cancelBubble = true;
return false;
}
}.bind(this);
/**
* Function _VMoveMouseDown
* Scope Private
* @param {Object} e event
* Description Process mouse down on OSK
*/
private _VMoveMouseDown = function(this: OSKManager, e: MouseEvent) {
let keymanweb = com.keyman.singleton;
var Lposx, Lposy;
keymanweb.uiManager.justActivated = true;
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) {
return true;
}
this.resizing = true;
if (e.pageX) {
Lposx = e.pageX;
Lposy = e.pageY;
} else if (e.clientX) {
Lposx = e.clientX + document.body.scrollLeft;
Lposy = e.clientY + document.body.scrollTop;
}
if(document.onmousemove != this._VResizeMouseMove && document.onmousemove != this._VMoveMouseMove) { // I1472 - Dragging off edge of browser window causes muckup
this._VPreviousMouseMove = document.onmousemove;
this._VPreviousMouseUp = document.onmouseup;
}
this._VPreviousCursor = document.body.style.cursor;
this._VPreviousMouseButton = (typeof(e.which)=='undefined' ? e.button : e.which);
this._VMoveX = Lposx - this._Box.offsetLeft;
this._VMoveY = Lposy - this._Box.offsetTop;
if(keymanweb.keyboardManager.isCJK()) {
this.pinImg.style.left='15px';
}
document.onmousemove = this._VMoveMouseMove;
document.onmouseup = this._VResizeMoveMouseUp;
if(document.body.style.cursor) {
document.body.style.cursor = 'move';
}
if(e && e.preventDefault) {
e.preventDefault();
}
e.cancelBubble = true;
return false;
}.bind(this);
/**
* Process mouse drag on OSK
*
* @param {Object} e event
*/
private _VMoveMouseMove = function(this: OSKManager, e: MouseEvent) {
let keymanweb = com.keyman.singleton;
var Lposx, Lposy;
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) {
return true;
}
if(this.noDrag) {
return true;
}
this.resizing = true;
this.userPositioned = true;
this.pinImg.style.display='block';
if(this._VPreviousMouseButton != (typeof(e.which)=='undefined' ? e.button : e.which)) { // I1472 - Dragging off edge of browser window causes muckup
return this._VResizeMoveMouseUp(e);
} else {
if (e.pageX) {
Lposx = e.pageX;
Lposy = e.pageY;
} else if (e.clientX) {
Lposx = e.clientX + document.body.scrollLeft;
Lposy = e.clientY + document.body.scrollTop;
}
this._Box.style.left = (Lposx-this._VMoveX)+'px';
this._Box.style.top = (Lposy-this._VMoveY)+'px';
if(e && e.preventDefault) {
e.preventDefault();
}
var r=this.getRect();
this.width=r.width;
this.height=r.height;
e.cancelBubble = true;
return false;
}
}.bind(this);
/**
* Function _VResizeMoveMouseUp
* Scope Private
* @param {Object} e event
* Description Process mouse up during resizing of KMW UI
*/
private _VResizeMoveMouseUp = function(this: OSKManager, e: MouseEvent) {
let keymanweb = com.keyman.singleton;
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) {
return true;
}
this.resizing = false;
if(this.vkbd) {
this.vkbd.currentKey=null;
}
document.onmousemove = this._VPreviousMouseMove;
document.onmouseup = this._VPreviousMouseUp;
if(document.body.style.cursor) {
document.body.style.cursor = this._VPreviousCursor;
}
keymanweb.domManager.focusLastActiveElement();
if(e && e.preventDefault) {
e.preventDefault();
}
keymanweb.uiManager.justActivated = false;
keymanweb.uiManager.setActivatingUI(false);
if(this.vkbd) {
this._VOriginalWidth = this.vkbd.kbdDiv.offsetWidth;
this._VOriginalHeight = this.vkbd.kbdDiv.offsetHeight;
}
this.doResizeMove();
e.cancelBubble = true;
this.saveCookie();
return 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._Enabled ? 1 : 0;
c['userSet'] = this.userPositioned ? 1 : 0;
c['left'] = p.left;
c['top'] = p.top;
if(this.vkbd) {
c['width'] = this.width;
c['height'] = this.height;
}
util.saveCookie('KeymanWeb_OnScreenKeyboard',c);
}
/**
* Restore size, position, font size and visibility of desktop OSK
*
* @return {boolean}
*/
loadCookie() {
let util = com.keyman.singleton.util;
var c = util.loadCookie('KeymanWeb_OnScreenKeyboard');
if(typeof(c) == 'undefined' || c == null) {
this.userPositioned=false;
return false;
}
this._Enabled = 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);
// Restore OSK size - font size now fixed in relation to OSK height, unless overridden (in em) by keyboard
var dfltWidth=0.3*screen.width;
//if(util.toNumber(c['width'],0) == 0) dfltWidth=0.5*screen.width;
var newWidth=util.toNumber(c['width'],dfltWidth),
newHeight=util.toNumber(c['height'],0.15*screen.height);
// 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(this.vkbd) {
this.vkbd.kbdDiv.style.width=newWidth+'px';
this.vkbd.kbdDiv.style.height=newHeight+'px';
this.vkbd.kbdDiv.style.fontSize=(newHeight/8)+'px';
}
// 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});
}
return true;
}
getWidthFromCookie(): number {
let util = com.keyman.singleton.util;
var c = util.loadCookie('KeymanWeb_OnScreenKeyboard');
if(typeof(c) == 'undefined' || c == null) {
return screen.width * 0.3;
}
// Restore OSK size - font size now fixed in relation to OSK height, unless overridden (in em) by keyboard
var newWidth=util.toNumber(c['width'], 0.3 * screen.width); // Default - 30% of screen's width.
if(newWidth < 0.2*screen.width) {
newWidth = 0.2*screen.width;
} else if(newWidth > 0.9*screen.width) {
newWidth=0.9*screen.width;
}
return newWidth;
}
/**
* Get the wanted height of the banner (does not include the keyboard)
* @return {number} height in pixels
*/
getBannerHeight(): number {
return (this.banner != null) ? this.banner.height : 0;
}
/**
* Get the wanted height of the OSK for touch devices (does not include banner height)
* @return {number} height in pixels
**/
getKeyboardHeight(): 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();
}
// Correct for devicePixelratio - needed on Android 4.1.2 phones,
// for Opera, Chrome and Firefox, but not for native browser! Exclude native browser for Build 344.
if(device.OS == 'Android' && device.formFactor == 'phone' && 'devicePixelRatio' in window) {
var bMatch=/Firefox|Chrome|OPR/;
if(bMatch.test(navigator.userAgent)) {
height = height*window.devicePixelRatio;
}
}
return height;
}
/**
* Get the wanted height of the OSK for touch devices (banner height + rows of keys)
* @return {number} height in pixels
**/
getHeight(): number {
return this.getBannerHeight() + this.getKeyboardHeight();
}
/**
* Get the wanted width of the OSK for touch devices
*
* @return {number} height in pixels
**/
getWidth(): 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);
}
/**
* Function getRect //TODO: This is probably not correct, anyway!!!!!
* Scope Public
* @return {Object.<string,number>} Array object with position and size of OSK container
* Description Get rectangle containing KMW Virtual Keyboard
*/
['getRect'](): OSKRect { // I2405
let util = com.keyman.singleton.util;
var p: OSKRect = {};
if(this.vkbd) {
p['left'] = p.left = dom.Utils.getAbsoluteX(this.vkbd.kbdDiv);
p['top'] = p.top = dom.Utils.getAbsoluteY(this.vkbd.kbdDiv);
p['width'] = p.width = dom.Utils.getAbsoluteX(this.vkbd.kbdHelpDiv) -
dom.Utils.getAbsoluteX(this.vkbd.kbdDiv) + this.vkbd.kbdHelpDiv.offsetWidth;
p['height'] = p.height = dom.Utils.getAbsoluteY(this.vkbd.kbdHelpDiv) -
dom.Utils.getAbsoluteY(this.vkbd.kbdDiv) + this.vkbd.kbdHelpDiv.offsetHeight;
} else {
p['left'] = p.left = dom.Utils.getAbsoluteX(this._Box);
p['top'] = p.top = dom.Utils.getAbsoluteY(this._Box);
p['width'] = p.width = dom.Utils.getAbsoluteX(this._Box) + this._Box.offsetWidth;
p['height'] = p.height = dom.Utils.getAbsoluteY(this._Box) + this._Box.offsetHeight;
}
return 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) {
bs.left=(p['left']-dom.Utils.getAbsoluteX(b)+b.offsetLeft)+'px';
this.dfltX=bs.left;
}
if('top' in p) {
bs.top=(p['top']-dom.Utils.getAbsoluteY(b)+b.offsetTop)+'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';
this.width=w;
}
// 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';
this.height=h;
}
// Fix or release user resizing
if('nosize' in p) {
if(this.resizeIcon) {
this.resizeIcon.style.display=(p['nosize'] ? 'none' : 'block');
}
}
}
// Fix or release user dragging
if('nomove' in p) {
this.noDrag=p['nomove'];
if(this.pinImg) {
this.pinImg.style.display=(p['nomove'] || !this.userPositioned) ? 'none' : 'block';
}
}
// 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' || com.keyman.singleton.util.device.touchable) {
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.pinImg) {
this.pinImg.style.display=(this.userPositioned ? 'block' : 'none');
}
}
/**
* Display KMW OSK at specified position (returns nothing)
*
* @param {number=} Px x-coordinate for OSK rectangle
* @param {number=} Py y-coordinate for OSK rectangle
*/
_Show(Px?: number, Py?: number) {
let keymanweb = com.keyman.singleton;
let device = keymanweb.util.device;
// Do not try to display OSK if undefined, or no active element
if(this._Box == null || keymanweb.domManager.getActiveElement() == null) {
return;
}
// Never display the OSK for desktop browsers unless KMW element is focused, and a keyboard selected
if((!device.touchable) && (keymanweb.keyboardManager.activeKeyboard == null || !this._Enabled)) {
return;
}
var Ls = this._Box.style;
// Do not display OSK until it has been positioned correctly
if(device.touchable && Ls.bottom == '') {
Ls.visibility='hidden';
}
if(device.touchable) {
/* In case it's still '0' from a hide() operation.
* Happens when _Show is called before the transitionend events are processed,
* which can happen in bulk-rendering contexts.
*
* (Opacity is only modified when device.touchable = true, though a couple of extra
* conditions may apply.)
*/
Ls.opacity='1';
}
// TODO: Move this into the VisualKeyboard class!
// The following code will always be executed except for externally created OSK such as EuroLatin
if(this.vkbd && this.vkbd.ddOSK) {
// Enable the currently active keyboard layer and update the default nextLayer member
this.vkbd.show();
// Extra style changes and overrides for touch-mode.
if(device.touchable) {
Ls.position='fixed';
Ls.left=Ls.bottom='0px';
let vkbdHeight = (<HTMLElement> this.vkbd.kbdDiv.firstChild).style.height;
vkbdHeight = vkbdHeight.substr(0, vkbdHeight.indexOf('px'));
Ls.height=Ls.maxHeight= (this.getBannerHeight() + parseInt(vkbdHeight, 10) + 5 /* kmw-banner-bar top in css */) + 'px';
Ls.border='none';
Ls.borderTop='1px solid gray';
this._Enabled=true;
this._Visible=true; // I3363 (Build 301)
}
}
//TODO: may need to return here for touch devices??
Ls.display='block'; //Ls.visibility='visible';
if(this.vkbd) {
this.vkbd.showLanguage();
}
if(device.formFactor == 'desktop') {
Ls.position='absolute'; Ls.display='block'; //Ls.visibility='visible';
Ls.left='0px';
this.loadCookie();
if(Px >= 0) { //probably never happens, legacy support only
Ls.left = Px + 'px'; Ls.top = Py + 'px';
} else {
if(this.userPositioned) {
Ls.left=this.x+'px';
Ls.top=this.y+'px';
} else {
var el=keymanweb.domManager.getActiveElement();
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';
}
}
}
this._Enabled=true;
this._Visible=true;
if(this.vkbd && this.vkbd.kbdDiv) {
this.width=this.vkbd.kbdDiv.offsetWidth;
this.height=this.vkbd.kbdDiv.offsetHeight;
}
this.saveCookie();
var pin=this.pinImg;
if(typeof pin != 'undefined' && pin != null)
pin.style.display=this.userPositioned?'block':'none';
}
// If OSK still hidden, make visible only after all calculation finished
if(Ls.visibility == 'hidden') {
window.setTimeout(function(){
this._Box.style.visibility='visible';
}.bind(this), 0);
}
// Allow desktop UI to execute code when showing the OSK
if(!device.touchable) {
var Lpos={};
Lpos['x']=this._Box.offsetLeft;
Lpos['y']=this._Box.offsetTop;
Lpos['userLocated']=this.userPositioned;
this.doShow(Lpos);
}
}
/**
* Hide Keymanweb On Screen Keyboard
*
* @param {boolean} hiddenByUser Distinguish between hiding on loss of focus and explicit hiding by user
*/
_Hide(hiddenByUser: boolean) {
let keymanweb = com.keyman.singleton;
let device = keymanweb.util.device;
// The test for CJK languages is necessary to prevent a picklist (displayed in the OSK) from being hidden by the user
// Once picklist functionality is separated out, this will no longer be needed.
// Logic is: execute always if hidden on lost focus, but if requested by user, only if not CJK
// Save current size if visible
if(this._Box && this._Box.style.display == 'block' && this.vkbd && this.vkbd.kbdDiv) {
this.width = this.vkbd.kbdDiv.offsetWidth;
this.height = this.vkbd.kbdDiv.offsetHeight;
}
if(hiddenByUser) {
//osk.loadCookie(); // preserve current offset and userlocated state
this._Enabled = ((keymanweb.keyboardManager.isCJK() || device.touchable)? true : false); // I3363 (Build 301)
this.saveCookie(); // Save current OSK state, size and position (desktop only)
} else if(device.formFactor == 'desktop') {
//Allow desktop OSK to remain visible on blur if body class set
if(document.body.className.indexOf('osk-always-visible') >= 0) {
return;
}
}
this._Visible = false;
if(this._Box && device.touchable && this._Box.offsetHeight > 0) { // I3363 (Build 301)
var os=this._Box.style;
//Firefox doesn't transition opacity if start delay is explicitly set to 0!
if(typeof(os.MozBoxSizing) == 'string') {
os.transition='opacity 0.8s linear';
} else {
os.transition=os.msTransition=os.WebkitTransition='opacity 0.5s linear 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)
window.setTimeout(function(this: OSKManager) {
var os=this._Box.style;
if(this._Visible) {
// Leave opacity alone and clear transition if another element activated
os.transition=os.msTransition=os.MozTransition=os.WebkitTransition='';
} else {
// Set opacity to zero, should decrease smoothly
os.opacity='0';
// Actually hide the OSK at the end of the transition
this._Box.addEventListener('transitionend', this.hideNow, false);
this._Box.addEventListener('webkitTransitionEnd', this.hideNow, false);
}
}.bind(this), 200); // Wait a bit before starting, to allow for moving to another element
} else {
if(this._Box) {
this._Box.style.display = 'none';
}
}
// 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) {
keymanweb.domManager.focusLastActiveElement();
}
}
/**
* Function hideNow
* Scope Private
* Description Hide the OSK unconditionally and immediately, cancel any pending transition
*/
hideNow: () => void = function(this: OSKManager) { // I3363 (Build 301)
this._Box.removeEventListener('transitionend', this.hideNow, false);
this._Box.removeEventListener('webkitTransitionEnd', this.hideNow, false);
if(document.body.className.indexOf('osk-always-visible') >= 0) {
return;
}
var os=this._Box.style;
os.display='none';
os.opacity='1';
this._Visible=false;
os.transition=os.msTransition=os.MozTransition=os.WebkitTransition='';
if(this.vkbd) {
this.vkbd.onHide();
}
}.bind(this);
/**
* Function hide
* Scope Public
* Description Prevent display of OSK window on focus
*/
['hide']() {
this._Enabled = false;
this._Hide(true);
}
/**
* 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);
}
/**
* Display list of installed keyboards in pop-up menu
**/
showLanguageMenu() {
let menu = new LanguageMenu(com.keyman.singleton);
menu.show();
}
/**
* 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;
}
/**
* 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._Enabled=bShow;
if(bShow) {
this._Show();
} else {
this._Hide(true);
}
} else {
if(this._Visible) {
this._Hide(true);
} else {
this._Show();
}
}
}
/**
* 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);
}
['shutdown']() {
// 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);
}
}
}
}