spiegel-keyman/web/source/osk/visualKeyboard.ts

2680 lines
No EOL
97 KiB
TypeScript

namespace com.keyman.osk {
//#region Definition of the KeyElement merger type
class KeyData {
['key']: OSKKey;
['keyId']: string;
['subKeys']?: OSKKeySpec[];
constructor(keyData: OSKKey, keyId: string) {
this['key'] = keyData;
this['keyId'] = keyId;
}
}
export type KeyElement = HTMLDivElement & KeyData;
// Many thanks to https://www.typescriptlang.org/docs/handbook/advanced-types.html for this.
function link(elem: HTMLDivElement, data: KeyData): KeyElement {
let e = <KeyElement> elem;
// Merges all properties and methods of KeyData onto the underlying HTMLDivElement, creating a merged class.
for(let id in data) {
if(!e.hasOwnProperty(id)) {
(<any>e)[id] = (<any>data)[id];
}
}
return e;
}
export function isKey(elem: Node): boolean {
return elem && ('key' in elem) && ((<any> elem['key']) instanceof OSKKey);
}
export function getKeyFrom(elem: Node): KeyElement {
if(isKey(elem)) {
return <KeyElement> elem;
} else {
return null;
}
}
//#endregion
//#region OSK key objects and construction
export class OSKKeySpec {
id: string;
text?: string;
sp?: number | ButtonClass;
width: string;
layer?: string; // Added during OSK construction.
nextlayer?: string;
pad?: string;
widthpc?: number; // Added during OSK construction.
padpc?: number; // Added during OSK construction.
sk?: OSKKeySpec[];
constructor(id: string, text?: string, width?: string, sp?: number | ButtonClass, nextlayer?: string, pad?: string) {
this.id = id;
this.text = text;
this.width = width ? width : "50";
this.sp = sp;
this.nextlayer = nextlayer;
this.pad = pad;
}
}
export abstract class OSKKey {
spec: OSKKeySpec;
constructor(spec: OSKKeySpec) {
this.spec = spec;
}
abstract getId(osk: VisualKeyboard): string;
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} style The CSSStyleDeclaration for an element to measure against, without modification.
*
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
* This version has been substantially modified to work for this particular application.
*/
static getTextWidth(osk: VisualKeyboard, text: string, style: {fontFamily?: string, fontSize: string}) {
// A final fallback - having the right font selected makes a world of difference.
if(!style.fontFamily) {
style.fontFamily = getComputedStyle(document.body).fontFamily;
}
if(!style.fontSize || style.fontSize == "") {
style.fontSize = '1em';
}
let fontFamily = style.fontFamily;
// Use of `getComputedStyle` is ideal, but in many of our use cases its preconditions are not met.
// The following allows us to calculate the font size in those situations.
let emScale = osk.getKeyEmFontSize();
let fontSpec = (<KeymanBase> window['keyman']).util.getFontSizeStyle(style.fontSize);
var fontSize: string;
if(fontSpec.absolute) {
// We've already got an exact size - use it!
fontSize = fontSpec.val + 'px';
} else {
fontSize = fontSpec.val * emScale + 'px';
}
// re-use canvas object for better performance
var canvas = OSKKey.getTextWidth['canvas'] || (OSKKey.getTextWidth['canvas'] = document.createElement("canvas"));
var context = canvas.getContext("2d");
context.font = fontSize + " " + fontFamily;
var metrics = context.measureText(text);
return metrics.width;
}
getKeyWidth(): number {
let units = this.objectUnits();
if(units == 'px') {
// For mobile devices, we presently specify width directly in pixels. Just use that!
return this.spec['widthpc'];
} else if(units == '%') {
// For desktop devices, each key is given a %age of the total OSK width. We'll need to compute an
// approximation for that. `this.kbdDiv` is the element controlling the OSK's width, set in px.
// ... and since it's null whenever this method would be called during key construction, we simply
// grab it from the cookie (or its default values) instead.
let oskWidth = com.keyman.singleton.osk.getWidthFromCookie();
// This is an approximation that tends to be a bit too large, but it's close enough to be useful.
return Math.floor(oskWidth * this.spec['widthpc'] / 100);
}
}
objectUnits(): string {
// Returns a unit string corresponding to how the width for each key is specified.
if((<KeymanBase>window['keyman']).util.device.formFactor == 'desktop') {
return '%';
} else {
return 'px';
}
}
/**
* Replace default key names by special font codes for modifier keys
*
* @param {string} oldText
* @return {string}
**/
protected renameSpecialKey(oldText: string): string {
let keyman = (<KeymanBase>window['keyman'])
// If a 'special key' mapping exists for the text, replace it with its corresponding special OSK character.
return VisualKeyboard.specialCharacters[oldText] ?
String.fromCharCode(0XE000 + VisualKeyboard.specialCharacters[oldText]) :
oldText;
}
// Produces a HTMLSpanElement with the key's actual text.
protected generateKeyText(osk: VisualKeyboard): HTMLSpanElement {
let util = (<KeymanBase>window['keyman']).util;
let spec = this.spec;
// Add OSK key labels
var keyText;
var t=util._CreateElement('span'), ts=t.style;
if(spec['text'] == null || spec['text'] == '') {
keyText='\xa0'; // default: nbsp.
if(typeof spec['id'] == 'string') {
// If the ID's Unicode-based, just use that code.
if(/^U_[0-9A-F]{4}$/i.test(spec['id'])) {
keyText=String.fromCharCode(parseInt(spec['id'].substr(2),16));
}
}
} else {
keyText=spec['text'];
}
t.className='kmw-key-text';
// Use special case lookup for modifier keys
if(spec['sp'] == '1' || spec['sp'] == '2') {
// Unique layer-based transformation.
var tId=((spec['text'] == '*Tab*' && spec.layer == 'shift') ? '*TabLeft*' : spec['text']);
// Transforms our *___* special key codes into their corresponding PUA character codes for keyboard display.
keyText=this.renameSpecialKey(tId);
}
// Grab our default for the key's font and font size.
ts.fontSize=osk.fontSize; //Build 344, KMEW-90
//Override font spec if set for this key in the layout
if(typeof spec['font'] == 'string' && spec['font'] != '') {
ts.fontFamily=spec['font'];
}
if(typeof spec['fontsize'] == 'string' && spec['fontsize'] != 0) {
ts.fontSize=spec['fontsize'];
}
let keyboardManager = (<KeymanBase>window['keyman']).keyboardManager;
// For some reason, fonts will sometimes 'bug out' for the embedded iOS page if we
// instead assign fontFamily to the existing style 'ts'. (Occurs in iOS 12.)
let styleSpec: {fontFamily?: string, fontSize: string} = {fontSize: ts.fontSize};
if(ts.fontFamily) {
styleSpec.fontFamily = ts.fontFamily;
} else {
styleSpec.fontFamily = osk.fontFamily; // Helps with style sheet calculations.
}
// Check the key's display width - does the key visualize well?
var width: number = OSKKey.getTextWidth(osk, keyText, styleSpec);
if(width == 0 && keyText != '' && keyText != '\xa0') {
// Add the Unicode 'empty circle' as a base support for needy diacritics.
keyText = '\u25cc' + keyText;
if(keyboardManager.isRTL()) {
// Add the RTL marker to ensure it displays properly.
keyText = '\u200f' + keyText;
}
// Recompute the new width for use in autoscaling calculations below, just in case.
width = OSKKey.getTextWidth(osk, keyText, styleSpec);
}
let fontSpec = util.getFontSizeStyle(ts.fontSize);
let keyWidth = this.getKeyWidth();
let maxProportion = 0.90;
let proportion = (keyWidth * maxProportion) / width; // How much of the key does the text want to take?
// Never upscale keys past the default - only downscale them.
if(proportion < 1) {
if(fontSpec.absolute) {
ts.fontSize = proportion * fontSpec.val + 'px';
} else {
ts.fontSize = proportion * fontSpec.val + 'em';
}
}
// Finalize the key's text.
t.innerHTML = keyText;
return t;
}
}
export class OSKBaseKey extends OSKKey {
constructor(spec: OSKKeySpec) {
super(spec);
}
getId(osk: VisualKeyboard): string {
// Define each key element id by layer id and key id (duplicate possible for SHIFT - does it matter?)
return this.spec.layer+'-'+this.spec.id;
}
// Produces a small reference label for the corresponding physical key on a US keyboard.
private generateKeyCapLabel(): HTMLDivElement {
// Create the default key cap labels (letter keys, etc.)
var x = VisualKeyboard.keyCodes[this.spec.id];
switch(x) {
// Converts the keyman key id code for common symbol keys into its representative ASCII code.
// K_COLON -> K_BKQUOTE
case 186: x=59; break;
case 187: x=61; break;
case 188: x=44; break;
case 189: x=45; break;
case 190: x=46; break;
case 191: x=47; break;
case 192: x=96; break;
// K_LBRKT -> K_QUOTE
case 219: x=91; break;
case 220: x=92; break;
case 221: x=93; break;
case 222: x=39; break;
default:
// No other symbol character represents a base key on the standard QWERTY English layout.
if(x < 48 || x > 90) {
x=0;
}
}
if(x > 0) {
let q = (<KeymanBase>window['keyman']).util._CreateElement('div');
q.className='kmw-key-label';
q.innerHTML=String.fromCharCode(x);
return q;
} else {
// Keyman-only virtual keys have no corresponding physical key.
return null;
}
}
private processSubkeys(btn: KeyElement) {
// Add reference to subkey array if defined
var bsn: number, bsk=btn['subKeys'] = this.spec['sk'];
// Transform any special keys into their PUA representations.
for(bsn=0; bsn<bsk.length; bsn++) {
if(bsk[bsn]['sp'] == '1' || bsk[bsn]['sp'] == '2') {
var oldText=bsk[bsn]['text'];
bsk[bsn]['text']=this.renameSpecialKey(oldText);
}
}
// If a subkey array is defined, add an icon
var skIcon=(<KeymanBase>window['keyman']).util._CreateElement('div');
skIcon.className='kmw-key-popup-icon';
//kDiv.appendChild(skIcon);
btn.appendChild(skIcon);
}
construct(osk: VisualKeyboard, layout: LayoutFormFactor, layerId: string, rowStyle: CSSStyleDeclaration, totalPercent: number): {element: HTMLDivElement, percent: number} {
let util = (<KeymanBase>window['keyman']).util;
let spec = this.spec;
let isDesktop = util.device.formFactor == 'desktop'
spec.layer = layerId;
let kDiv=util._CreateElement('div');
kDiv.className='kmw-key-square';
let ks=kDiv.style;
ks.width=this.objectGeometry(spec['widthpc']);
let originalPercent = totalPercent;
let btnEle=util._CreateElement('div');
let btn = link(btnEle, new KeyData(this, spec['id']));
// Set button class
osk.setButtonClass(spec,btn,layout);
// Set key and button positioning properties.
if(!isDesktop) {
// Regularize interkey spacing by rounding key width and padding (Build 390)
ks.left=this.objectGeometry(totalPercent+spec['padpc']);
ks.bottom=rowStyle.bottom;
ks.height=rowStyle.height; //must be specified in px for rest of layout to work correctly
// Set distinct phone and tablet button position properties
btn.style.left=ks.left;
btn.style.width=ks.width;
} else {
ks.marginLeft=this.objectGeometry(spec['padpc']);
}
totalPercent=totalPercent+spec['padpc']+spec['widthpc'];
// Add the (US English) keycap label for desktop OSK or if KDU flag is non-zero
if(layout.keyLabels || isDesktop) {
let keyCap = this.generateKeyCapLabel();
if(keyCap) {
btn.appendChild(keyCap);
}
}
// Define each key element id by layer id and key id (duplicate possible for SHIFT - does it matter?)
btn.id=this.getId(osk);
// Define callbacks to handle key touches: iOS and Android tablets and phones
// TODO: replace inline function calls??
if(!util.device.touchable) {
// Highlight key while mouse down or if moving back over originally selected key
btn.onmouseover=btn.onmousedown=osk.mouseOverMouseDownHandler; // Build 360
// Remove highlighting when key released or moving off selected element
btn.onmouseup=btn.onmouseout=osk.mouseUpMouseOutHandler; //Build 360
}
// Handle subkey-related tasks.
if(typeof(spec['sk']) != 'undefined' && spec['sk'] != null) {
this.processSubkeys(btn);
} else {
btn['subKeys']=null;
}
// Add text to button and button to placeholder div
btn.appendChild(this.generateKeyText(osk));
kDiv.appendChild(btn);
// Prevent user selection of key captions
//t.style.webkitUserSelect='none';
// The 'return value' of this process.
return {element: kDiv, percent: totalPercent - originalPercent};
}
objectGeometry(v: number): string {
let unit = this.objectUnits();
if(unit == '%') {
return v + unit;
} else { // unit == 'px'
return Math.round(v)+unit;
}
}
}
export class OSKSubKey extends OSKKey {
constructor(spec: OSKKeySpec) {
super(spec);
}
getId(osk: VisualKeyboard): string {
let spec = this.spec;
// Create (temporarily) unique ID by prefixing 'popup-' to actual key ID
if(typeof(spec['layer']) == 'string' && spec['layer'] != '') {
return 'popup-'+spec['layer']+'-'+spec['id'];
} else {
// We only create subkeys when they're needed - the currently-active layer should be fine.
return 'popup-' + osk.layerId + '-'+spec['id'];
}
}
construct(osk: VisualKeyboard, baseKey: HTMLDivElement, topMargin: boolean): HTMLDivElement {
let spec = this.spec;
let kDiv=document.createElement('div');
let tKey = osk.getDefaultKeyObject();
let ks=kDiv.style;
for(var tp in tKey) {
if(typeof spec[tp] != 'string') {
spec[tp]=tKey[tp];
}
}
kDiv.className='kmw-key-square-ex';
if(topMargin) {
ks.marginTop='5px';
}
if(typeof spec['width'] != 'undefined') {
ks.width=(parseInt(spec['width'],10)*baseKey.offsetWidth/100)+'px';
} else {
ks.width=baseKey.offsetWidth+'px';
}
ks.height=baseKey.offsetHeight+'px';
let btnEle=document.createElement('div');
let btn = link(btnEle, new KeyData(this, spec['id']));
osk.setButtonClass(spec,btn);
btn.id = this.getId(osk);
// Must set button size (in px) dynamically, not from CSS
let bs=btn.style;
bs.height=ks.height;
bs.width=ks.width;
// Must set position explicitly, at least for Android
bs.position='absolute';
btn.appendChild(this.generateKeyText(osk));
kDiv.appendChild(btn);
return kDiv;
}
}
//#endregion
export class VisualKeyboard {
//#region Keyboard-related static constants
// Define Keyman Developer modifier bit-flags (exposed for use by other modules)
static modifierCodes = {
"LCTRL":0x0001,
"RCTRL":0x0002,
"LALT":0x0004,
"RALT":0x0008,
"SHIFT":0x0010,
"CTRL":0x0020,
"ALT":0x0040,
"CAPS":0x0100,
"NO_CAPS":0x0200,
"NUM_LOCK":0x0400,
"NO_NUM_LOCK":0x0800,
"SCROLL_LOCK":0x1000,
"NO_SCROLL_LOCK":0x2000,
"VIRTUAL_KEY":0x4000
};
static modifierBitmasks = {
"ALL":0x007F,
"ALT_GR_SIM": (VisualKeyboard.modifierCodes['LCTRL'] | VisualKeyboard.modifierCodes['LALT']),
"CHIRAL":0x001F, // The base bitmask for chiral keyboards. Includes SHIFT, which is non-chiral.
"IS_CHIRAL":0x000F, // Used to test if a bitmask uses a chiral modifier.
"NON_CHIRAL":0x0070 // The default bitmask, for non-chiral keyboards
};
static stateBitmasks = {
"ALL":0x3F00,
"CAPS":0x0300,
"NUM_LOCK":0x0C00,
"SCROLL_LOCK":0x3000
};
// Define standard keycode numbers (exposed for use by other modules)
static keyCodes = {
"K_BKSP":8,"K_TAB":9,"K_ENTER":13,
"K_SHIFT":16,"K_CONTROL":17,"K_ALT":18,"K_PAUSE":19,"K_CAPS":20,
"K_ESC":27,"K_SPACE":32,"K_PGUP":33,
"K_PGDN":34,"K_END":35,"K_HOME":36,"K_LEFT":37,"K_UP":38,
"K_RIGHT":39,"K_DOWN":40,"K_SEL":41,"K_PRINT":42,"K_EXEC":43,
"K_INS":45,"K_DEL":46,"K_HELP":47,"K_0":48,
"K_1":49,"K_2":50,"K_3":51,"K_4":52,"K_5":53,"K_6":54,"K_7":55,
"K_8":56,"K_9":57,"K_A":65,"K_B":66,"K_C":67,"K_D":68,"K_E":69,
"K_F":70,"K_G":71,"K_H":72,"K_I":73,"K_J":74,"K_K":75,"K_L":76,
"K_M":77,"K_N":78,"K_O":79,"K_P":80,"K_Q":81,"K_R":82,"K_S":83,
"K_T":84,"K_U":85,"K_V":86,"K_W":87,"K_X":88,"K_Y":89,"K_Z":90,
"K_NP0":96,"K_NP1":97,"K_NP2":98,
"K_NP3":99,"K_NP4":100,"K_NP5":101,"K_NP6":102,
"K_NP7":103,"K_NP8":104,"K_NP9":105,"K_NPSTAR":106,
"K_NPPLUS":107,"K_SEPARATOR":108,"K_NPMINUS":109,"K_NPDOT":110,
"K_NPSLASH":111,"K_F1":112,"K_F2":113,"K_F3":114,"K_F4":115,
"K_F5":116,"K_F6":117,"K_F7":118,"K_F8":119,"K_F9":120,
"K_F10":121,"K_F11":122,"K_F12":123,"K_NUMLOCK":144,"K_SCROLL":145,
"K_LSHIFT":160,"K_RSHIFT":161,"K_LCONTROL":162,"K_RCONTROL":163,
"K_LALT":164,"K_RALT":165,
"K_COLON":186,"K_EQUAL":187,"K_COMMA":188,"K_HYPHEN":189,
"K_PERIOD":190,"K_SLASH":191,"K_BKQUOTE":192,
"K_LBRKT":219,"K_BKSLASH":220,"K_RBRKT":221,
"K_QUOTE":222,"K_oE2":226,
"K_LOPT":50001,"K_ROPT":50002,
"K_NUMERALS":50003,"K_SYMBOLS":50004,"K_CURRENCIES":50005,
"K_UPPER":50006,"K_LOWER":50007,"K_ALPHA":50008,
"K_SHIFTED":50009,"K_ALTGR":50010,
"K_TABBACK":50011,"K_TABFWD":50012
};
// Defines the PUA code mapping for the various 'special' modifier/control keys on keyboards.
static specialCharacters = {
'*Shift*': 8,
'*Enter*': 5,
'*Tab*': 6,
'*BkSp*': 4,
'*Menu*': 11,
'*Hide*': 10,
'*Alt*': 25,
'*Ctrl*': 1,
'*Caps*': 3,
'*ABC*': 16,
'*abc*': 17,
'*123*': 19,
'*Symbol*': 21,
'*Currency*': 20,
'*Shifted*': 8, // set SHIFTED->9 for filled arrow icon
'*AltGr*': 2,
'*TabLeft*': 7,
'*LAlt*': 0x56,
'*RAlt*': 0x57,
'*LCtrl*': 0x58,
'*RCtrl*': 0x59,
'*LAltCtrl*': 0x60,
'*RAltCtrl*': 0x61,
'*LAltCtrlShift*': 0x62,
'*RAltCtrlShift*': 0x63,
'*AltShift*': 0x64,
'*CtrlShift*': 0x65,
'*AltCtrlShift*': 0x66,
'*LAltShift*': 0x67,
'*RAltShift*': 0x68,
'*LCtrlShift*': 0x69,
'*RCtrlShift*': 0x70
};
static codesUS = [
['0123456789',';=,-./`', '[\\]\''],
[')!@#$%^&*(',':+<_>?~', '{|}"']
];
//#endregion
// Tracks the OSK-based state of supported state keys.
// Using the exact keyCode name from above allows for certain optimizations elsewhere in the code.
stateKeys = {
"K_CAPS":false,
"K_NUMLOCK":false,
"K_SCROLL":false
};
private layout: LayoutFormFactor;
layers: LayoutLayer[];
layerId: string = "default";
layerIndex: number;
// Stores the base element for this instance of the visual keyboard.
// Formerly known as osk._DivVKbd
kbdDiv: HTMLDivElement;
kbdHelpDiv: HTMLDivElement;
styleSheet: HTMLStyleElement;
// Style-related properties
fontFamily: string;
fontSize: string;
// State-related properties
ddOSK: boolean = false;
popupVisible: boolean;
keyPending: KeyElement;
deleteKey: KeyElement;
deleting: number; // Tracks a timer id for repeated deletions.
nextLayer: string;
currentKey: string;
// Special keys (for the currently-visible layer)
lgKey: KeyElement;
hkKey: KeyElement; // hide keyboard key
spaceBar: KeyElement;
// Touch-tracking properties
touchX: number;
touchY: number;
touchCount: number;
currentTarget: KeyElement;
// Popup key management
popupBaseKey: KeyElement;
popupPending: boolean = false;
subkeyDelayTimer: number;
popupDelay: number = 500;
menuEvent: KeyElement; // Used by embedded-mode.
keytip: {key: KeyElement, state: boolean, element?: HTMLDivElement};
popupCallout: HTMLDivElement;
// Function fields (fleshed out by kmwnative.ts and/or kmwembedded.ts)
touchHold: (key: KeyElement) => void;
optionKey: (e: KeyElement, keyName: string, keyDown: boolean) => void;
highlightSubKeys: (key: KeyElement, x: number, y: number) => void = this.highlightSubKeys || function(k,x,y) {};
showKeyTip: (key: KeyElement, on: boolean) => void;
drawPreview: (canvas: HTMLCanvasElement, w: number, h: number, edge: number) => void = this.drawPreview || function(c,w,h,e) {};
createKeyTip: () => void;
addCallout: (key: KeyElement) => HTMLDivElement = this.addCallout || function(key) {return null};
waitForFonts: (kfd,ofd) => boolean = this.waitForFonts || function(kfd,ofd){return true;}; // Default is used by embedded.
adjustHeights: () => boolean;
//#region OSK constructor and helpers
/**
* @param {Object} PVK Visual keyboard name
* @param {Object} Lhelp true if OSK defined for this keyboard
* @param {Object} layout0
* @param {Number} kbdBitmask Keyboard modifier bitmask
* Description Generates the base visual keyboard element, prepping for attachment to KMW
*/
constructor(PVK, Lhelp, layout0: LayoutFormFactor, kbdBitmask: number) {
let keyman = com.keyman.singleton;
let util = keyman.util;
let layout=layout0;
var Lkbd=util._CreateElement('div'), oskWidth;//s=Lkbd.style,
var activeKeyboard = keyman.keyboardManager.activeKeyboard;
// Build a layout using the default for the device
if(typeof layout != 'object' || layout == null) {
layout=Layouts.buildDefaultLayout(PVK,kbdBitmask,util.device.formFactor);
}
// Create the collection of HTML elements from the device-dependent layout object
this.layout=layout;
this.layers=layout['layer'];
// Override font if specified by keyboard
if('font' in layout) {
this.fontFamily=layout['font'];
} else {
this.fontFamily='';
}
// Set flag to add default (US English) key label if specified by keyboard
layout.keyLabels = activeKeyboard && ((typeof(activeKeyboard['KDU']) != 'undefined') && activeKeyboard['KDU']);
let divLayerContainer = this.deviceDependentLayout(layout, util.device.formFactor);
this.ddOSK = true;
// Append the OSK layer group container element to the containing element
//osk.keyMap = divLayerContainer;
Lkbd.appendChild(divLayerContainer);
// Set base class - OS and keyboard added for Build 360
this.kbdHelpDiv = this.kbdDiv = Lkbd;
Lkbd.className=util.device.formFactor+' kmw-osk-inner-frame';
}
/**
* Returns the default properties for a key object, used to construct
* both a base keyboard key and popup keys
*
* @return {Object} An object that contains default key properties
*/
getDefaultKeyObject(): OSKKeySpec {
return new OSKKeySpec(undefined, '', '100', '0', null, '15');
};
/**
* Create the OSK for a particular keyboard and device
*
* @param {Object} layout OSK layout definition
* @param {string} formFactor layout form factor
* @return {Object} fully formatted OSK object
*/
deviceDependentLayout(layout, formFactor: string): HTMLDivElement {
let util = com.keyman.singleton.util;
let oskManager = com.keyman.singleton.osk;
var lDiv=util._CreateElement('div'), ls=lDiv.style, actualHeight=0;
// Set OSK box default style
lDiv.className='kmw-key-layer-group';
// Adjust OSK height for mobile and tablet devices TODO move outside this function???
switch(formFactor) {
case 'phone':
case 'tablet':
actualHeight=oskManager.getHeight();
ls.height=actualHeight+'px';
break;
}
// Return empty DIV if no layout defined
if(layout == null) {
return lDiv;
}
// Set default OSK font size (Build 344, KMEW-90)
let layoutFS = layout['fontsize'];
if(typeof layoutFS == 'undefined' || layoutFS == null || layoutFS == '') {
ls.fontSize='1em';
} else {
ls.fontSize=layout['fontsize'];
}
this.fontSize=ls.fontSize; //TODO: move outside function*********
// Create a separate OSK div for each OSK layer, only one of which will ever be visible
var n,i,j,layers,layer,gDiv: HTMLDivElement;
var rows,row,rowHeight,rDiv,keys,key,rs,gs;
layers=layout['layer'];
// Set key default attributes (must use exportable names!)
var tKey=this.getDefaultKeyObject();
tKey['fontsize']=ls.fontSize;
// Identify key labels (e.g. *Shift*) that require the special OSK font
var specialLabel=/\*\w+\*/;
// ***Delete any empty rows at the end added by compiler bug...
for(n=0; n<layers.length; n++) {
layer=layers[n]; rows=layer['row'];
for(i=rows.length; i>0; i--) {
if(rows[i-1]['key'].length > 0) {
break;
}
}
if(i < rows.length) {
rows.splice(i-rows.length,rows.length-i);
}
}
// ...remove to here when compiler bug fixed ***
// Set the OSK row height, **assuming all layers have the same number of rows**
// Calculate default row height
rowHeight=100/rows.length;
// For desktop OSK, use a percentage of the OSK height
if(formFactor == 'desktop') {
rowHeight=100/rows.length;
}
// Get the actual available document width and scale factor according to device type
var objectWidth;
if(formFactor == 'desktop') {
objectWidth = 100;
} else {
objectWidth = oskManager.getWidth();
}
if(util.device.touchable) { // /*&& ('ontouchstart' in window)*/ // Except Chrome emulation doesn't set this.
// Not to mention, it's rather redundant.
lDiv.addEventListener('touchstart', this.touch, true);
// The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'.
lDiv.addEventListener('touchend', this.release,true);
lDiv.addEventListener('touchmove', this.moveOver,false);
//lDiv.addEventListener('touchcancel', osk.cancel,false); //event never generated by iOS
}
for(n=0; n<layers.length; n++) {
layer=layers[n];
layer.aligned=false;
gDiv=util._CreateElement('div'), gs=gDiv.style;
gDiv.className='kmw-key-layer';
// Always make the first layer visible
gs.display=(n==0?'block':'none');
gs.height=ls.height;
// Set font for layer if defined in layout
if('font' in layout) gs.fontFamily=layout['font']; else gs.fontFamily='';
gDiv['layer']=gDiv['nextLayer']=layer['id'];
if(typeof layer['nextlayer'] == 'string') gDiv['nextLayer']=layer['nextlayer'];
// Create a DIV for each row of the group
rows=layer['row'];
// Calculate the maximum row width (in layout units)
var totalWidth=0;
for(i=0; i<rows.length; i++) {
var width=0;
row=rows[i]; keys=row['key'];
for(j=0; j<keys.length; j++) {
key=keys[j];
// Test for a trailing comma included in spec, added as null object by IE
if(key == null) {
keys.length = keys.length-1;
} else {
var kw, kp;
kw = (typeof key['width'] == 'string' && key['width'] != '') ? parseInt(key['width'],10) : 100;
if(isNaN(kw) || kw == 0) kw = 100;
key['width'] = kw.toString();
kp = (typeof key['pad'] == 'string' && key['pad'] != '') ? parseInt(key['pad'],10) : 15;
if(isNaN(kp) || kp == 0) kp = 15; // KMEW-119
key['pad'] = kp.toString();
width += kw + kp;
//if(typeof key['width'] == 'string' && key['width'] != '') width += parseInt(key['width'],10); else width += 100;
//if(typeof key['pad'] == 'string' && key['pad'] != '') width += parseInt(key['pad'],10); else width += 5;
}
}
if(width > totalWidth) {
totalWidth = width;
}
}
// Add default right margin
if(formFactor == 'desktop') {
totalWidth += 5; // KMEW-117
} else {
// TODO: Not entirely clear why this needs to be 15 instead of 5 on touch layouts. We probably have
// a miscalculation somewhere
totalWidth += 15;
}
for(i=0; i<rows.length; i++) {
rDiv=util._CreateElement('div');
rDiv.className='kmw-key-row';
// The following event trap is needed to prevent loss of focus in IE9 when clicking on a key gap.
// Unclear why normal _CreateElement prevention of loss of focus does not seem to work here.
// Appending handler to event handler chain does not work (other event handling remains active).
rDiv.onmousedown=util.mouseDownPreventDefaultHandler; // Build 360
//util.attachDOMEvent(rDiv,'mousedown',function(e){if(e)e.preventDefault();
row=rows[i];
rs=rDiv.style;
// Set row height. (Phone and tablet heights are later recalculated
// and set in px, allowing for viewport scaling.)
rs.maxHeight=rs.height=rowHeight+'%';
// Apply defaults, setting the width and other undefined properties for each key
keys=row['key'];
for(j=0; j<keys.length; j++) {
key=keys[j];
for(var tp in tKey) { // tKey = osk.getDefaultKeyObject();
if(typeof key[tp] != 'string') key[tp]=tKey[tp];
}
// Modify the key type for special keys with non-standard labels
// to allow the keyboard font to ovveride the SpecialOSK font.
// Blank keys are no longer reclassed - can use before/after CSS to add text
switch(key['sp']) {
case '1':
if(!specialLabel.test(key['text']) && key['text'] != '') key['sp']='3';
break;
case '2':
if(!specialLabel.test(key['text']) && key['text'] != '') key['sp']='4';
break;
}
}
// Calculate actual key widths by summing defined widths and scaling each key to %,
// adjusting the width of the last key to make the total exactly 100%
// Save each percentage key width as a separate member (do *not* overwrite layout specified width!)
// NB: the 'percent' suffix is historical, units are percent on desktop devices, but pixels on touch devices
// All key widths and paddings are rounded for uniformity
var keyPercent,padPercent,totalPercent=0;
for(j=0; j<keys.length-1; j++) {
keyPercent=Math.round(parseInt(keys[j]['width'],10)*objectWidth/totalWidth);
keys[j]['widthpc']=keyPercent;
padPercent=Math.round(parseInt(keys[j]['pad'],10)*objectWidth/totalWidth);
keys[j]['padpc']=padPercent;
totalPercent += padPercent+keyPercent;
}
// Allow for right OSK margin (15 layout units)
totalPercent += Math.round(15*objectWidth/totalWidth);
// If a single key, and padding is negative, add padding to right align the key
if(keys.length == 1 && parseInt(keys[0]['pad'],10) < 0) {
keyPercent=Math.round(parseInt(keys[0]['width'],10)*objectWidth/totalWidth);
keys[0]['widthpc']=keyPercent;
totalPercent += keyPercent;
keys[0]['padpc']=(objectWidth-totalPercent);
} else if(keys.length > 0) {
j=keys.length-1;
padPercent=Math.round(parseInt(keys[j]['pad'],10)*objectWidth/totalWidth);
keys[j]['padpc']=padPercent;
totalPercent += padPercent;
keys[j]['widthpc']=(objectWidth-totalPercent);
}
//Create the key square (an outer DIV) for each key element with padding, and an inner DIV for the button (btn)
totalPercent=0;
for(j=0; j<keys.length; j++) {
key=keys[j];
var keyGenerator = new OSKBaseKey(key);
var keyTuple = keyGenerator.construct(this, layout, layer['id'], rs, totalPercent);
rDiv.appendChild(keyTuple.element);
totalPercent += keyTuple.percent;
}
// Add row to layer
gDiv.appendChild(rDiv);
}
// Add layer to group
lDiv.appendChild(gDiv);
}
return lDiv;
}
//#endregion
//#region OSK touch handlers
/**
* The main OSK touch start event handler
*
* @param {Event} e touch start event object
*
*/
touch: (e: TouchEvent) => void = function(this: VisualKeyboard, e: TouchEvent) {
// Identify the key touched
var t = <HTMLElement> e.changedTouches[0].target, key = this.keyTarget(t);
// Save the touch point
this.touchX = e.changedTouches[0].pageX;
// Set the key for the new touch point to be current target, if defined
this.currentTarget = key;
// Prevent multi-touch if popup displayed
var sk = document.getElementById('kmw-popup-keys');
if((sk && sk.style.visibility == 'visible') || this.popupVisible) {
return;
}
// Keep track of number of active (unreleased) touch points
this.touchCount = e.touches.length;
// Get nearest key if touching a hidden key or the end of a key row
if((key && (key.className.indexOf('key-hidden') >= 0))
|| t.className.indexOf('kmw-key-row') >= 0) {
key = this.findNearestKey(e,t);
}
// Do not do anything if no key identified!
if(key == null) {
return;
}
// Get key name (K_...) from element ID
let keyName = key['keyId'];
// Highlight the touched key
this.highlightKey(key,true);
// Special function keys need immediate action
if(keyName == 'K_LOPT' || keyName == 'K_ROPT') {
window.setTimeout(function(this: VisualKeyboard){
this.clickKey(key);
}.bind(this),0);
this.keyPending = null;
// Also backspace, to allow delete to repeat while key held
} else if(keyName == 'K_BKSP') {
// While we could inline the execution of the delete key here, we lose the ability to
// record the backspace key if we do so.
this.clickKey(key);
this.deleteKey = key;
this.deleting = window.setTimeout(this.repeatDelete,500);
this.keyPending = null;
} else {
if(this.keyPending) {
this.highlightKey(this.keyPending, false);
this.clickKey(this.keyPending);
this.clearPopup();
// Decrement the number of unreleased touch points to prevent
// sending the keystroke again when the key is actually released
this.touchCount--;
} else {
// If this key has subkey, start timer to display subkeys after delay, set up release
this.touchHold(key);
//if(key.subKeys != null) this.subkeyDelayTimer=window.setTimeout(function(){this.showSubKeys(key);},this.popupDelay);
}
this.keyPending = key;
}
}.bind(this);
/**
* OSK touch release event handler
*
* @param {Event} e touch release event object
*
**/
release: (e: TouchEvent) => void = function(this: VisualKeyboard, e: TouchEvent) {
// Prevent incorrect multi-touch behaviour if native or device popup visible
var sk = document.getElementById('kmw-popup-keys'), t = this.currentTarget;
if((sk && sk.style.visibility == 'visible') || this.popupVisible) {
// Ignore release if a multiple touch
if(e.touches.length > 0) {
return;
}
// Cancel (but do not execute) pending key if neither a popup key or the base key
if((t == null) || ((t.id.indexOf('popup') < 0) && (t.id != this.popupBaseKey.id))) {
this.highlightKey(this.keyPending,false);
this.clearPopup();
this.keyPending = null;
}
}
// Handle menu key release event
if(t && t.id) {
this.optionKey(t, t.id, false);
}
// Test if moved off screen (effective release point must be corrected for touch point horizontal speed)
// This is not completely effective and needs some tweaking, especially on Android
var x = e.changedTouches[0].pageX;
var beyondEdge = ((x < 2 && this.touchX > 5) || (x > window.innerWidth - 2 && this.touchX < window.innerWidth - 5));
// Save then decrement current touch count
var tc=this.touchCount;
if(this.touchCount > 0) {
this.touchCount--;
}
// Process and clear highlighting of pending target
if(this.keyPending) {
this.highlightKey(this.keyPending,false);
// Output character unless moved off key
if(this.keyPending.className.indexOf('hidden') < 0 && tc > 0 && !beyondEdge) {
this.clickKey(this.keyPending);
}
this.clearPopup();
this.keyPending = null;
// Always clear highlighting of current target on release (multi-touch)
} else {
var tt = e.changedTouches[0];
t = this.keyTarget(tt.target);
if(!t) {
var t1 = document.elementFromPoint(tt.clientX,tt.clientY);
t = this.findNearestKey(e, <HTMLElement> t1);
}
this.highlightKey(t,false);
}
// Clear repeated backspace if active
if(this.deleting) {
window.clearTimeout(this.deleting);
}
this.deleting = 0;
}.bind(this);
/**
* OSK touch move event handler
*
* @param {Event} e touch move event object
*
**/
moveOver: (e: TouchEvent) => void = function(this: VisualKeyboard, e: TouchEvent) {
let util = com.keyman.singleton.util;
e.preventDefault();
e.cancelBubble=true;
if(typeof e.stopImmediatePropagation == 'function') {
e.stopImmediatePropagation();
} else if(typeof e.stopPropagation == 'function') {
e.stopPropagation();
}
// Do not attempt to support reselection of target key for overlapped keystrokes
if(e.touches.length > 1 || this.touchCount == 0) {
return;
}
// Get touch position
var x=typeof e.touches == 'object' ? e.touches[0].clientX : e.clientX,
y=typeof e.touches == 'object' ? e.touches[0].clientY : e.clientY;
// Move target key and highlighting
var t = e.changedTouches[0],
t1 = <HTMLElement> document.elementFromPoint(x,y),
key0 = this.keyPending,
key1 = this.keyTarget(t1);
// Find the nearest key to the touch point if not on a visible key
if((key1 && key1.className.indexOf('key-hidden') >= 0) ||
(t1 && (!key1) && t1.className.indexOf('key-row') >= 0)) {
key1 = this.findNearestKey(e,t1);
}
// Stop repeat if no longer on BKSP key
if(key1 && (typeof key1.id == 'string') && (key1.id.indexOf('BKSP') < 0)) {
if(this.deleting) {
window.clearTimeout(this.deleting);
}
this.deleting = 0;
}
// Do not move over keys if device popup visible
if(this.popupVisible) {
if(key1 == null) {
if(key0) {
this.highlightKey(key0,false);
}
this.keyPending=null;
} else {
if(key1 == this.popupBaseKey) {
if(!util.hasClass(key1,'kmw-key-touched')) {
this.highlightKey(key1,true);
}
this.keyPending = key1;
} else {
if(key0) {
this.highlightKey(key0,false);
}
this.keyPending = null;
}
}
return;
}
// Use the popup duplicate of the base key if a phone with a visible popup array
var sk=document.getElementById('kmw-popup-keys');
if(sk && sk.style.visibility == 'visible' && util.device.formFactor == 'phone' && key1 == this.popupBaseKey) {
key1 = <KeyElement> sk.childNodes[0].firstChild;
}
// Identify current touch position (to manage off-key release)
this.currentTarget = key1;
// Clear previous key highlighting
if(key0 && key1 && (key1.id != key0.id)) {
this.highlightKey(key0,false);
}
// If popup is visible, need to move over popup, not over main keyboard
this.highlightSubKeys(key1,x,y);
if(sk && sk.style.visibility == 'visible') {
if(key1 && key1.id.indexOf('popup') < 0 && key1 != this.popupBaseKey) {
return;
}
if(key1 && key1 == this.popupBaseKey && key1.className.indexOf('kmw-key-touched') < 0) {
this.highlightKey(key1,true);
}
// Cancel touch if moved up and off keyboard, unless popup keys visible
} else {
let _Box = com.keyman.singleton.osk._Box;
var yMin = Math.max(5, _Box.offsetTop - 0.25*_Box.offsetHeight);
if(key0 && e.touches[0].pageY < Math.max(5,_Box.offsetTop - 0.25*_Box.offsetHeight)) {
this.highlightKey(key0,false);
this.showKeyTip(null,false);
this.keyPending = null;
}
}
// Replace the target key, if any, by the new target key
// Do not replace a null target, as that indicates the key has already been released
if(key1 && this.keyPending) {
this.keyPending = key1;
}
if(this.keyPending) {
if(key0 != key1 || key1.className.indexOf('kmw-key-touched') < 0) {
this.highlightKey(key1,true);
}
}
if(key0 && key1 && (key1 != key0) && (key1.id != '')) {
// Display the touch-hold keys (after a pause)
this.touchHold(key1);
/*
// Clear and restart the popup timer
if(this.subkeyDelayTimer)
{
window.clearTimeout(this.subkeyDelayTimer);
this.subkeyDelayTimer = null;
}
if(key1.subKeys != null)
{
this.subkeyDelayTimer = window.setTimeout(
function()
{
this.clearPopup();
this.showSubKeys(key1);
}.bind(this),
this.popupDelay);
}
*/
}
}.bind(this);
/**
* Get the current key target from the touch point element within the key
*
* @param {Object} t element at touch point
* @return {Object} the key element (or null)
**/
keyTarget(target: HTMLElement | EventTarget): KeyElement {
let keyman = com.keyman.singleton;
let util = keyman.util;
let t = <HTMLElement> target;
try {
if(t) {
if(util.hasClass(t,'kmw-key')) {
return getKeyFrom(t);
}
if(t.parentNode && util.hasClass(<HTMLElement> t.parentNode,'kmw-key')) {
return getKeyFrom(t.parentNode);
}
if(t.firstChild && util.hasClass(<HTMLElement> t.firstChild,'kmw-key')) {
return getKeyFrom(t.firstChild);
}
}
} catch(ex) {}
return null;
}
/**
* Identify the key nearest to the touch point if at the end of a key row,
* but return null more than about 0.6 key width from the nearest key.
*
* @param {Event} e touch event
* @param {Object} t HTML object at touch point
* @return {Object} nearest key to touch point
*
**/
findNearestKey(e: TouchEvent, t: HTMLElement): KeyElement {
if((!e) || (typeof e.changedTouches == 'undefined')
|| (e.changedTouches.length == 0)) {
return null;
}
// Get touch point on screen
var x = e.changedTouches[0].pageX;
// Get key-row beneath touch point
while(t && t.className.indexOf('key-row') < 0) {
t = <HTMLElement> t.parentNode;
}
if(!t) {
return null;
}
// Find minimum distance from any key
var k, k0=0, dx, dxMax=24, dxMin=100000, x1, x2;
for(k = 0; k < t.childNodes.length; k++) {
if((<HTMLElement> t.childNodes[k].firstChild).className.indexOf('key-hidden') >= 0) {
continue;
}
x1 = (<HTMLElement> t.childNodes[k]).offsetLeft;
x2 = x1 + (<HTMLElement> t.childNodes[k]).offsetWidth;
dx =x1 - x;
if(dx >= 0 && dx < dxMin) {
k0 = k; dxMin = dx;
}
dx = x - x2;
if(dx >= 0 && dx < dxMin) {
k0 = k; dxMin = dx;
}
}
if(dxMin < 100000) {
t = <HTMLElement> t.childNodes[k0];
x1 = t.offsetLeft;
x2 = x1 + t.offsetWidth;
// Limit extended touch area to the larger of 0.6 of key width and 24 px
if(t.offsetWidth > 40) {
dxMax = 0.6 * t.offsetWidth;
}
if(((x1 - x) >= 0 && (x1 - x) < dxMax) || ((x - x2) >= 0 && (x - x2) < dxMax)) {
return <KeyElement> t.firstChild;
}
}
return null;
}
/**
* Repeat backspace as long as the backspace key is held down
**/
repeatDelete: () => void = function(this: VisualKeyboard) {
if(this.deleting) {
this.clickKey(this.deleteKey);
this.deleting = window.setTimeout(this.repeatDelete,100);
}
}.bind(this);
//#endregion
/**
* Simulate a keystroke according to the touched keyboard button element
*
* Note that the test-case oriented 'recorder' stubs this method to facilitate OSK-based input
* recording for use in test cases. If changing this function, please ensure the recorder is
* not affected.
*
* @param {Object} e element touched (or clicked)
*/
clickKey(e: KeyElement) {
let keyman = com.keyman.singleton;
var Lelem = keyman.domManager.getLastActiveElement(), Ls, Le, Lkc;
var activeKeyboard = keyman.keyboardManager.activeKeyboard;
let kbdInterface = keyman.interface;
let formFactor = keyman.util.device.formFactor;
if(Lelem != null) {
// Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling)
// Note - virtual keys should be treated case-insensitive, so we force uppercasing here.
var layer=e['key'].spec.layer || '', keyName=e['keyId'].toUpperCase(), keyShiftState=this.getModifierState(this.layerId);
var nextLayer: string = e['key'].spec['nextlayer'];
keyman.domManager.initActiveElement(Lelem);
// Exclude menu and OSK hide keys from normal click processing
if(keyName == 'K_LOPT' || keyName == 'K_ROPT') {
this.optionKey(e, keyName, true);
return true;
}
// Turn off key highlighting (or preview)
this.highlightKey(e,false);
// The default OSK layout for desktop devices does not include nextlayer info, relying on modifier detection here.
if(formFactor == 'desktop') {
if(this.selectLayer(keyName, nextLayer)) {
return true;
}
}
// Prevent any output from 'ghost' (unmapped) keys
if(keyName != 'K_SPACE') {
var keyText=(<HTMLElement> e.childNodes[0]).innerHTML;
//// if(keyText == '' || keyText == '&nbsp;') return true; --> why?
}
Ls=Lelem._KeymanWebSelectionStart;
Le=Lelem._KeymanWebSelectionEnd;
keyman.uiManager.setActivatingUI(true);
com.keyman.DOMEventHandlers.states._IgnoreNextSelChange = 100;
keyman.domManager.focusLastActiveElement();
if(keyman.domManager._IsMozillaEditableIframe(<HTMLIFrameElement> Lelem,0)) {
Lelem = (<HTMLIFrameElement> Lelem).contentDocument.documentElement;
}
Lelem._KeymanWebSelectionStart=Ls;
Lelem._KeymanWebSelectionEnd=Le;
com.keyman.DOMEventHandlers.states._IgnoreNextSelChange = 0;
// ...end I3363 (Build 301)
(<any>keyman)._CachedSelectionStart = null; // I3319
// Deadkey matching continues to be troublesome.
// Deleting matched deadkeys here seems to correct some of the issues. (JD 6/6/14)
kbdInterface._DeadkeyDeleteMatched(); // Delete any matched deadkeys before continuing
//kbdInterface._DeadkeyResetMatched(); // I3318 (Not needed if deleted first?)
// First check the virtual key, and process shift, control, alt or function keys
Lkc = {
Ltarg:Lelem,
Lmodifiers:0,
Lstates:0,
Lcode:VisualKeyboard.keyCodes[keyName],
LisVirtualKey:true
};
// Set the flags for the state keys.
Lkc.Lstates |= this.stateKeys['K_CAPS'] ? VisualKeyboard.modifierCodes['CAPS'] : VisualKeyboard.modifierCodes['NO_CAPS'];
Lkc.Lstates |= this.stateKeys['K_NUMLOCK'] ? VisualKeyboard.modifierCodes['NUM_LOCK'] : VisualKeyboard.modifierCodes['NO_NUM_LOCK'];
Lkc.Lstates |= this.stateKeys['K_SCROLL'] ? VisualKeyboard.modifierCodes['SCROLL_LOCK'] : VisualKeyboard.modifierCodes['NO_SCROLL_LOCK'];
// Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys
if(keyName.substr(0,2) == 'U_') Lkc.LisVirtualKey=false;
// Get code for non-physical keys (T_KOKAI, U_05AB etc)
if(typeof Lkc.Lcode == 'undefined') {
Lkc.Lcode = this.getVKDictionaryCode(keyName);// Updated for Build 347
if(!Lkc.Lcode) {
// Special case for U_xxxx keys. This vk code will never be used
// in a keyboard, so we use this to ensure that keystroke processing
// occurs for the key.
Lkc.Lcode = 1;
}
}
// Override key shift state if specified for key in layout (corrected for popup keys KMEW-93)
keyShiftState = this.getModifierState(e['key'].spec['layer'] || layer);
// Define modifiers value for sending to keyboard mapping function
Lkc.Lmodifiers = keyShiftState;
// Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer.
if((Lkc.Lmodifiers & VisualKeyboard.modifierBitmasks['ALT_GR_SIM']) == VisualKeyboard.modifierBitmasks['ALT_GR_SIM'] && Layouts.emulatesAltGr()) {
Lkc.Lmodifiers &= ~VisualKeyboard.modifierBitmasks['ALT_GR_SIM'];
Lkc.Lmodifiers |= VisualKeyboard.modifierCodes['RALT'];
}
// Include *limited* support for mnemonic keyboards (Sept 2012)
// If a touch layout has been defined for a mnemonic keyout, do not perform mnemonic mapping for rules on touch devices.
if(activeKeyboard && activeKeyboard['KM'] && !(activeKeyboard['KVKL'] && formFactor != 'desktop')) {
if(Lkc.Lcode != VisualKeyboard.keyCodes['K_SPACE']) { // exception required, March 2013
Lkc.vkCode = Lkc.Lcode;
// So long as the key name isn't prefixed with 'U_', we'll get a default mapping based on the Lcode value.
// We need to determine the mnemonic base character - for example, SHIFT + K_PERIOD needs to map to '>'.
var mappedChar: string = this.defaultKeyOutput('K_xxxx', Lkc.Lcode, (layer.indexOf('shift') != -1 ? 0x10 : 0), false, null);
if(mappedChar) {
Lkc.Lcode = mappedChar.charCodeAt(0);
} // No 'else' - avoid remapping control + modifier keys!
if(this.stateKeys['K_CAPS']) {
if((Lkc.Lcode >= 65 && Lkc.Lcode <= 90) /* 'A' - 'Z' */ || (Lkc.Lcode >= 97 && Lkc.Lcode <= 122) /* 'a' - 'z' */) {
Lkc.Lmodifiers ^= 0x10; // Flip the 'shift' bit.
Lkc.Lcode ^= 0x20; // Flips the 'upper' vs 'lower' bit for the base 'a'-'z' ASCII alphabetics.
}
}
}
} else {
Lkc.vkCode=Lkc.Lcode;
}
// Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic
if(typeof activeKeyboard['KM'] == 'undefined') {
Lkc.Lcode=keyman.keyMapManager._USKeyCodeToCharCode(Lkc);
Lkc.LisVirtualKey=false;
}
// Pass this key code and state to the keyboard program
if(!activeKeyboard || (Lkc.Lcode != 0 && !kbdInterface.processKeystroke(keyman.util.device, Lelem, Lkc))) {
// Restore the virtual key code if a mnemonic keyboard is being used
Lkc.Lcode=Lkc.vkCode;
// Handle unmapped keys, including special keys
switch(keyName) {
case 'K_CAPS':
case 'K_NUMLOCK':
case 'K_SCROLL':
this.stateKeys[keyName] = ! this.stateKeys[keyName];
com.keyman.singleton.osk._Show();
break;
default:
// The following is physical layout dependent, so should be avoided if possible. All keys should be mapped.
var ch = this.defaultKeyOutput(keyName,Lkc.Lcode,keyShiftState,true,Lelem);
if(ch) {
kbdInterface.output(0, Lelem, ch);
}
}
}
// Swap layer as appropriate.
this.nextLayer = nextLayer;
this.selectLayer(keyName, nextLayer);
/* I732 END - 13/03/2007 MCD: End Positional Layout support in OSK */
Lelem._KeymanWebSelectionStart=null;
Lelem._KeymanWebSelectionEnd=null;
}
keyman.uiManager.setActivatingUI(false); // I2498 - KeymanWeb OSK does not accept clicks in FF when using automatic UI
return true;
}
// cancel = function(e) {} //cancel event is never generated by iOS
/**
* Select the next keyboard layer for layer switching keys
* The next layer will be determined from the key name unless otherwise specifed
*
* @param {string} keyName key identifier
* @param {number|string|undefined} nextLayerIn optional next layer identifier
* @return {boolean} return true if keyboard layer changed
*/
selectLayer(keyName: string, nextLayerIn: number | string): boolean {
var nextLayer = arguments.length < 2 ? null : nextLayerIn;
let keyman = com.keyman.singleton;
var isChiral = keyman.keyboardManager.isChiral();
// Layer must be identified by name, not number (27/08/2015)
if(typeof nextLayer == 'number') {
nextLayer = Layouts.getLayerId(nextLayer * 0x10);
}
// Identify next layer, if required by key
if(!nextLayer) {
switch(keyName) {
case 'K_LSHIFT':
case 'K_RSHIFT':
case 'K_SHIFT':
nextLayer = 'shift'; break;
case 'K_LCONTROL':
case 'K_LCTRL':
if(isChiral) {
nextLayer = 'leftctrl';
break;
}
case 'K_RCONTROL':
case 'K_RCTRL':
if(isChiral) {
nextLayer = 'rightctrl';
break;
}
case 'K_CTRL':
nextLayer = 'ctrl'; break;
case 'K_LMENU':
case 'K_LALT':
if(isChiral) {
nextLayer = 'leftalt';
break;
}
case 'K_RMENU':
case 'K_RALT':
if(isChiral) {
nextLayer = 'rightalt';
break;
}
case 'K_ALT':
nextLayer = 'alt'; break;
case 'K_ALTGR':
if(isChiral) {
nextLayer = 'leftctrl-rightalt';
} else {
nextLayer = 'ctrl-alt';
}
break;
case 'K_CURRENCIES':
case 'K_NUMERALS':
case 'K_SHIFTED':
case 'K_UPPER':
case 'K_LOWER':
case 'K_SYMBOLS':
nextLayer = 'default'; break;
}
}
if(!nextLayer) {
return false;
}
// Do not change layer unless needed (27/08/2015)
if(nextLayer == this.layerId && keyman.util.device.formFactor != 'desktop') {
return false;
}
// Change layer and refresh OSK
this.updateLayer(nextLayer);
com.keyman.singleton.osk._Show();
return true;
}
/**
* Sets the new layer id, allowing for toggling shift/ctrl/alt while preserving the remainder
* of the modifiers represented by the current layer id (where applicable)
*
* @param {string} id layer id (e.g. ctrlshift)
*/
updateLayer(id: string) {
var s=this.layerId, idx=id;
var i;
let keyman = com.keyman.singleton;
if(keyman.util.device.formFactor == 'desktop') {
// Need to test if target layer is a standard layer (based on the plain 'default')
var replacements= ['leftctrl', 'rightctrl', 'ctrl', 'leftalt', 'rightalt', 'alt', 'shift'];
for(i=0; i < replacements.length; i++) {
// Don't forget to remove the kebab-case hyphens!
idx=idx.replace(replacements[i] + '-', '');
idx=idx.replace(replacements[i],'');
}
// If we are presently on the default layer, drop the 'default' and go straight to the shifted mode.
// If on a common symbolic layer, drop out of symbolic mode and go straight to the shifted mode.
if(this.layerId == 'default' || this.layerId == 'numeric' || this.layerId == 'symbol' || this.layerId == 'currency' || idx != '') {
s = id;
}
// Otherwise, we are based upon the a layer that accepts modifier variations.
// Modify the layer according to the current state and key pressed.
//
// TODO: Consider: should this ever be allowed for a base layer other than 'default'? If not,
// if(idx == '') with accompanying if-else structural shift would be a far better test here.
else {
// Save our current modifier state.
var modifier=this.getModifierState(s);
// Strip down to the base modifiable layer.
for(i=0; i < replacements.length; i++) {
// Don't forget to remove the kebab-case hyphens!
s=s.replace(replacements[i] + '-', '');
s=s.replace(replacements[i],'');
}
// Toggle the modifier represented by our input argument.
switch(id) {
case 'shift':
modifier ^= VisualKeyboard.modifierCodes['SHIFT'];
break;
case 'leftctrl':
modifier ^= VisualKeyboard.modifierCodes['LCTRL'];
break;
case 'rightctrl':
modifier ^= VisualKeyboard.modifierCodes['RCTRL'];
break;
case 'ctrl':
modifier ^= VisualKeyboard.modifierCodes['CTRL'];
break;
case 'leftalt':
modifier ^= VisualKeyboard.modifierCodes['LALT'];
break;
case 'rightalt':
modifier ^= VisualKeyboard.modifierCodes['RALT'];
break;
case 'alt':
modifier ^= VisualKeyboard.modifierCodes['ALT'];
break;
default:
s = id;
}
// Combine our base modifiable layer and attach the new modifier variation info to obtain our destination layer.
if(s != 'default') {
if(s == '') {
s = Layouts.getLayerId(modifier);
} else {
s = Layouts.getLayerId(modifier) + '-' + s;
}
}
}
if(s == '') {
s = 'default';
}
} else {
// Mobile form-factor. Either the layout is specified by a keyboard developer with direct layer name references
// or all layers are accessed via subkey of a single layer-shifting key - no need for modifier-combining logic.
s = id;
}
// Actually set the new layer id.
this.layerId = s;
// Check that requested layer is defined (KMEA-1, but does not resolve issue)
for(i=0; i<this.layers.length; i++) {
if(this.layerId == this.layers[i].id) {
return;
}
}
// Show default layer if an undefined layer has been requested
this.layerId='default';
}
/**
* Get modifier key state from layer id
*
* @param {string} layerId layer id (e.g. ctrlshift)
* @return {number} modifier key state (desktop keyboards)
*/
getModifierState(layerId: string): number {
var modifier=0;
if(layerId.indexOf('shift') >= 0) {
modifier |= VisualKeyboard.modifierCodes['SHIFT'];
}
// The chiral checks must not be directly exclusive due each other to visual OSK feedback.
var ctrlMatched=false;
if(layerId.indexOf('leftctrl') >= 0) {
modifier |= VisualKeyboard.modifierCodes['LCTRL'];
ctrlMatched=true;
}
if(layerId.indexOf('rightctrl') >= 0) {
modifier |= VisualKeyboard.modifierCodes['RCTRL'];
ctrlMatched=true;
}
if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) {
modifier |= VisualKeyboard.modifierCodes['CTRL'];
}
var altMatched=false;
if(layerId.indexOf('leftalt') >= 0) {
modifier |= VisualKeyboard.modifierCodes['LALT'];
altMatched=true;
}
if(layerId.indexOf('rightalt') >= 0) {
modifier |= VisualKeyboard.modifierCodes['RALT'];
altMatched=true;
}
if(layerId.indexOf('alt') >= 0 && !altMatched) {
modifier |= VisualKeyboard.modifierCodes['ALT'];
}
return modifier;
}
/**
* Get the default key string. If keyName is U_xxxxxx, use that Unicode codepoint.
* Otherwise, lookup the virtual key code (physical keyboard mapping)
*
* @param {string} keyName Name of the key
* @param {number} n
* @param {number} keyShiftState
* @param {boolean} usingOSK
* @param {Object=} Lelem
* @return {string}
*/
defaultKeyOutput(keyName: string, n: number, keyShiftState: number, usingOSK: boolean, Lelem?: HTMLElement): string {
let keyman = com.keyman.singleton;
let domManager = keyman.domManager;
var ch = '', checkCodes = false;
var touchAlias = (Lelem && typeof(Lelem.base) != 'undefined');
// check if exact match to SHIFT's code. Only the 'default' and 'shift' layers should have default key outputs.
if(keyShiftState == 0) {
checkCodes = true;
} else if (keyShiftState == VisualKeyboard.modifierCodes['SHIFT']) {
checkCodes = true;
keyShiftState = 1; // It's used as an index.
} else {
console.warn("KMW only defines default key output for the 'default' and 'shift' layers!");
}
// If this was triggered by the OSK -or- if it was triggered within a touch-aliased DIV element.
if(touchAlias || usingOSK) {
var code = VisualKeyboard.keyCodes[keyName];
if(!code) {
code = n;
}
switch(code) {
case VisualKeyboard.keyCodes['K_BKSP']: //Only desktop UI, not touch devices. TODO: add repeat while mouse down for desktop UI
keyman.interface.defaultBackspace();
break;
case VisualKeyboard.keyCodes['K_TAB']:
domManager.moveToNext(keyShiftState);
break;
case VisualKeyboard.keyCodes['K_TABBACK']:
domManager.moveToNext(true);
break;
case VisualKeyboard.keyCodes['K_TABFWD']:
domManager.moveToNext(false);
break;
case VisualKeyboard.keyCodes['K_ENTER']:
// Insert new line in text area fields
if(Lelem.nodeName == 'TEXTAREA' || (typeof Lelem.base != 'undefined' && Lelem.base.nodeName == 'TEXTAREA')) {
return '\n';
// Or move to next field from TEXT fields
} else if(usingOSK) {
var inputEle: HTMLInputElement;
if(com.keyman.Util.instanceof(Lelem, "HTMLInputElement")) {
inputEle = <HTMLInputElement> Lelem;
} else if(typeof(Lelem.base) != 'undefined' && com.keyman.Util.instanceof(Lelem.base, "HTMLInputElement")) {
inputEle = <HTMLInputElement> Lelem.base;
}
if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) {
inputEle.disabled=false;
inputEle.form.submit();
} else {
domManager.moveToNext(false);
}
}
break;
case VisualKeyboard.keyCodes['K_SPACE']:
return ' ';
// break;
//
// // Problem: clusters, and doing them right.
// // The commented-out code below should be a decent starting point, but clusters make it complex.
//
// case VisualKeyboard.keyCodes['K_LEFT']:
// if(touchAlias) {
// var caretPos = keymanweb.getTextCaret(Lelem);
// keymanweb.setTextCaret(Lelem, caretPos - 1 >= 0 ? caretPos - 1 : 0);
// }
// break;
// case VisualKeyboard.keyCodes['K_RIGHT']:
// if(touchAlias) {
// var caretPos = keymanweb.getTextCaret(Lelem);
// keymanweb.setTextCaret(Lelem, caretPos + 1);
// }
// if(code == VisualKeyboard.keyCodes['K_RIGHT']) {
// break;
// }
// // Should we include this? It could be tricky to do correctly...
// case VisualKeyboard.keyCodes['K_DEL']:
// // Move caret right one unit, then backspace.
// if(touchAlias) {
// var caretPos = keymanweb.getTextCaret(Lelem);
// keymanweb.setTextCaret(Lelem, caretPos + 1);
// if(caretPos == keymanweb.getTextCaret(Lelem)) {
// // Failed to move right - there's nothing to delete.
// break;
// }
// kbdInterface.defaultBackspace();
// }
}
}
// TODO: Refactor the overloading of the 'n' parameter here into separate methods.
// Test for fall back to U_xxxxxx key id
// For this first test, we ignore the keyCode and use the keyName
if((keyName.substr(0,2) == 'U_')) {
var codePoint = parseInt(keyName.substr(2,6), 16);
if (((0x0 <= codePoint) && (codePoint <= 0x1F)) || ((0x80 <= codePoint) && (codePoint <= 0x9F))) {
// Code points [U_0000 - U_001F] and [U_0080 - U_009F] refer to Unicode C0 and C1 control codes.
// Check the codePoint number and do not allow output of these codes via U_xxxxxx shortcuts.
console.log("Suppressing Unicode control code: U_00" + codePoint.toString(16));
return ch;
} else {
// String.fromCharCode() is inadequate to handle the entire range of Unicode
// Someday after upgrading to ES2015, can use String.fromCodePoint()
ch=String.kmwFromCharCode(codePoint);
}
// Hereafter, we refer to keyCodes.
} else if(checkCodes) { // keyShiftState can only be '1' or '2'.
try {
if(n >= VisualKeyboard.keyCodes['K_0'] && n <= VisualKeyboard.keyCodes['K_9']) { // The number keys.
ch = VisualKeyboard.codesUS[keyShiftState][0][n-VisualKeyboard.keyCodes['K_0']];
} else if(n >=VisualKeyboard.keyCodes['K_A'] && n <= VisualKeyboard.keyCodes['K_Z']) { // The base letter keys
ch = String.fromCharCode(n+(keyShiftState?0:32)); // 32 is the offset from uppercase to lowercase.
} else if(n >= VisualKeyboard.keyCodes['K_COLON'] && n <= VisualKeyboard.keyCodes['K_BKQUOTE']) {
ch = VisualKeyboard.codesUS[keyShiftState][1][n-VisualKeyboard.keyCodes['K_COLON']];
} else if(n >= VisualKeyboard.keyCodes['K_LBRKT'] && n <= VisualKeyboard.keyCodes['K_QUOTE']) {
ch = VisualKeyboard.codesUS[keyShiftState][2][n-VisualKeyboard.keyCodes['K_LBRKT']];
}
} catch (e) {
console.error("Error detected with default mapping for key: code = " + n + ", shift state = " + (keyShiftState == 1 ? 'shift' : 'default'));
}
}
return ch;
}
/**
* Function _UpdateVKShift
* Scope Private
* @param {Object} e OSK event
* @param {number} v keyboard shift state
* @param {(boolean|number)} d set (1) or clear(0) shift state bits
* @return {boolean} Always true
* Description Update the current shift state within KMW
*/
_UpdateVKShift(e, v: number, d: boolean|number): boolean {
var keyShiftState=0, lockStates=0, i;
let keyman = com.keyman.singleton;
var lockNames = ['CAPS', 'NUM_LOCK', 'SCROLL_LOCK'];
var lockKeys = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'];
if(e) {
// read shift states from Pevent
keyShiftState = e.Lmodifiers;
lockStates = e.Lstates;
// Are we simulating AltGr? If it's a simulation and not real, time to un-simulate for the OSK.
if(keyman.keyboardManager.isChiral() && Layouts.emulatesAltGr() &&
(com.keyman.DOMEventHandlers.states.modStateFlags & VisualKeyboard.modifierBitmasks['ALT_GR_SIM']) == VisualKeyboard.modifierBitmasks['ALT_GR_SIM']) {
keyShiftState |= VisualKeyboard.modifierBitmasks['ALT_GR_SIM'];
keyShiftState &= ~VisualKeyboard.modifierCodes['RALT'];
}
for(i=0; i < lockNames.length; i++) {
if(lockStates & VisualKeyboard.stateBitmasks[lockNames[i]]) {
this.stateKeys[lockKeys[i]] = lockStates & VisualKeyboard.modifierCodes[lockNames[i]];
}
}
} else if(d) {
keyShiftState |= v;
for(i=0; i < lockNames.length; i++) {
if(v & VisualKeyboard.stateBitmasks[lockNames[i]]) {
this.stateKeys[lockKeys[i]] = true;
}
}
} else {
keyShiftState &= ~v;
for(i=0; i < lockNames.length; i++) {
if(v & VisualKeyboard.stateBitmasks[lockNames[i]]) {
this.stateKeys[lockKeys[i]] = false;
}
}
}
// Find and display the selected OSK layer
this.layerId=Layouts.getLayerId(keyShiftState);
// osk._UpdateVKShiftStyle will be called automatically upon the next _Show.
if(keyman.osk._Visible) {
keyman.osk._Show();
}
return true;
}
/**
* Function _UpdateVKShiftStyle
* Scope Private
* @param {string=} layerId
* Description Updates the OSK's visual style for any toggled state keys
*/
_UpdateVKShiftStyle(layerId?: string) {
var i, n, layer=null, layerElement=null;
if(layerId) {
for(n=0; n<this.layers.length; n++) {
if(this.layers[n]['id'] == this.layerId) {
break;
}
}
return; // Failed to find the requested layer.
} else {
n=this.layerIndex;
layerId=this.layers[n]['id'];
}
layer=this.layers[n];
// Set the on/off state of any visible state keys.
var states =['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'];
var keys =[layer.capsKey, layer.numKey, layer.scrollKey];
for(i=0; i < keys.length; i++) {
// Skip any keys not in the OSK!
if(keys[i] == null) {
continue;
}
keys[i]['sp'] = this.stateKeys[states[i]] ? Layouts.buttonClasses['SHIFT-ON'] : Layouts.buttonClasses['SHIFT'];
var btn = document.getElementById(layerId+'-'+states[i]);
this.setButtonClass(keys[i], btn, this.layout);
}
}
/**
* Attach appropriate class to each key button, according to the layout
*
* @param {Object} key key object
* @param {Object} btn button object
* @param {Object=} layout source layout description (optional, sometimes)
*/
setButtonClass(key, btn, layout?) {
let keyman = com.keyman.singleton;
var n=0, keyTypes=['default','shift','shift-on','special','special-on','','','','deadkey','blank','hidden'];
if(typeof key['dk'] == 'string' && key['dk'] == '1') {
n=8;
}
if(typeof key['sp'] == 'string') {
n=parseInt(key['sp'],10);
}
if(n < 0 || n > 10) {
n=0;
}
layout = layout || this.layout;
// Apply an overriding class for 5-row layouts
var nRows=layout['layer'][0]['row'].length;
if(nRows > 4 && keyman.util.device.formFactor == 'phone') {
btn.className='kmw-key kmw-5rows kmw-key-'+keyTypes[n];
} else {
btn.className='kmw-key kmw-key-'+keyTypes[n];
}
}
clearPopup() {
let oskManager = com.keyman.singleton.osk;
// Remove the displayed subkey array, if any, and cancel popup request
var sk=document.getElementById('kmw-popup-keys');
if(sk != null) {
if(sk.shim) {
oskManager._Box.removeChild(sk.shim);
}
sk.parentNode.removeChild(sk);
}
if(this.popupCallout) {
oskManager._Box.removeChild(this.popupCallout);
}
this.popupCallout = null;
if(this.subkeyDelayTimer) {
window.clearTimeout(this.subkeyDelayTimer);
this.subkeyDelayTimer = null;
}
this.popupBaseKey = null;
}
/**
* @summary Look up a custom virtual key code in the virtual key code dictionary KVKD. On first run, will build the dictionary.
*
* `VKDictionary` is constructed from the keyboard's `KVKD` member. This list is constructed
* at compile-time and is a list of 'additional' virtual key codes, starting at 256 (i.e.
* outside the range of standard virtual key codes). These additional codes are both
* `[T_xxx]` and `[U_xxxx]` custom key codes from the Keyman keyboard language. However,
* `[U_xxxx]` keys only generate an entry in `KVKD` if there is a corresponding rule that
* is associated with them in the keyboard rules. If the `[U_xxxx]` key code is only
* referenced as the id of a key in the touch layout, then it does not get an entry in
* the `KVKD` property.
*
* @private
* @param {string} keyName custom virtual key code to lookup in the dictionary
* @return {number} key code > 255 on success, or 0 if not found
*/
getVKDictionaryCode(keyName: string) {
let keyman = com.keyman.singleton;
var activeKeyboard = keyman.keyboardManager.activeKeyboard;
if(!activeKeyboard['VKDictionary']) {
var a=[];
if(typeof activeKeyboard['KVKD'] == 'string') {
// Build the VK dictionary
// TODO: Move the dictionary build into the compiler -- so compiler generates code such as following.
// Makes the VKDictionary member unnecessary.
// this.KVKD={"K_ABC":256,"K_DEF":257,...};
var s=activeKeyboard['KVKD'].split(' ');
for(var i=0; i<s.length; i++) {
a[s[i].toUpperCase()]=i+256; // We force upper-case since virtual keys should be case-insensitive.
}
}
activeKeyboard['VKDictionary']=a;
}
var res=activeKeyboard['VKDictionary'][keyName.toUpperCase()];
return res ? res : 0;
}
//#region 'native'-mode subkey handling
/**
* Display touch-hold array of 'sub-keys' above the currently touched key
* @param {Object} e primary key element
*/
showSubKeys(e: KeyElement) {
// Do not show subkeys if key already released
if(this.keyPending == null) {
return;
}
let keyman = com.keyman.singleton;
let util = keyman.util;
let device = util.device;
// A tag we directly set on a key element during its construction.
let subKeySpec: OSKKeySpec[] = e['subKeys'];
// Create holder DIV for subkey array, and set styles.
// A subkey array for Shift will only appear if extra layers exist
// The holder is position:fixed, but the keys do not need to be, as no scrolling
// is possible while the array is visible. So it is simplest to let the keys have
// position:static and display:inline-block
var subKeys=document.createElement('DIV'),i,
t,ts,t1,ts1,kDiv,ks,btn,bs;
var tKey = this.getDefaultKeyObject();
subKeys.id='kmw-popup-keys';
this.popupBaseKey = e;
// Does the popup array include the base key? *** condition for phone only ***
if(device.formFactor == 'phone') {
this.prependBaseKey(e);
}
// Must set position dynamically, not in CSS
var ss=subKeys.style;
ss.bottom=(parseInt(e.style.bottom,10)+parseInt(e.style.height,10)+4)+'px';
// Set key font according to layout, or defaulting to OSK font
// (copied, not inherited, since OSK is not a parent of popup keys)
ss.fontFamily=this.fontFamily;
// Copy the font size from the parent key, allowing for style inheritance
ss.fontSize=keyman.util.getStyleValue(e,'font-size');
ss.visibility='hidden';
var nKeys=subKeySpec.length,nRow,nRows,nCols;
nRows=Math.min(Math.ceil(nKeys/9),2);
nCols=Math.ceil(nKeys/nRows);
if(nRows > 1) {
ss.width=(nCols*e.offsetWidth+nCols*5)+'px';
}
// Add nested button elements for each sub-key
for(i=0; i<nKeys; i++) {
var needsTopMargin = false;
let nRow=Math.floor(i/nCols);
if(nRows > 1 && nRow > 0) {
needsTopMargin = true;
}
let keyGenerator = new com.keyman.osk.OSKSubKey(subKeySpec[i]);
let kDiv = keyGenerator.construct(this, <HTMLDivElement> e, needsTopMargin);
subKeys.appendChild(kDiv);
}
// Clear key preview if any
this.showKeyTip(null,false);
// Otherwise append the touch-hold (subkey) array to the OSK
keyman.osk._Box.appendChild(subKeys);
// And correct its position with respect to that element
ss=subKeys.style;
var x=util._GetAbsoluteX(e)+0.5*(e.offsetWidth-subKeys.offsetWidth), y,
xMax=(util.landscapeView()?screen.height:screen.width)-subKeys.offsetWidth;
if(x > xMax) {
x=xMax;
}
if(x < 0) {
x=0;
}
ss.left=x+'px';
// Add the callout
this.popupCallout = this.addCallout(e);
// Make the popup keys visible
ss.visibility='visible';
// And add a filter to fade main keyboard
subKeys.shim = document.createElement('DIV');
subKeys.shim.id = 'kmw-popup-shim';
keyman.osk._Box.appendChild(subKeys.shim);
// Highlight the duplicated base key (if a phone)
if(device.formFactor == 'phone') {
var bk = <KeyElement> subKeys.childNodes[0].firstChild;
this.keyPending = bk;
this.highlightKey(bk,true);//bk.className = bk.className+' kmw-key-touched';
}
}
/**
* Prepend the base key to the touch-hold key array (for phones)
*
* @param {Object} e base key object
*/
prependBaseKey(e: KeyElement) {
// This is a tag we set on the key element during its construction.
let subKeys: OSKKeySpec[] = e['subKeys'];
let keyman = com.keyman.singleton;
if(e && typeof(e.id) != 'undefined') {
//TODO: refactor this, it's pretty messy...
var i,
idx = e.id.split('-'),
baseId = e['keyId'],
layer = e['key'].spec['layer'],
sp = e['key'].spec['sp'],
nextlayer = e['key'].spec['nextlayer'];
if(typeof subKeys != 'undefined' && subKeys.length > 0 && (subKeys[0].id != baseId || subKeys[0].layer != layer)) {
var eCopy = new OSKKeySpec(baseId, '', undefined, sp, nextlayer); // {'id':baseId,'layer':'','key':undefined};
if(layer != '') {
eCopy['layer'] = layer;
}
for(i = 0; i < e.childNodes.length; i++) {
if(keyman.util.hasClass(<HTMLElement> e.childNodes[i], 'kmw-key-text')) {
break;
}
}
if(i < e.childNodes.length) {
eCopy['text'] = e.childNodes[i].textContent;
}
subKeys.splice(0, 0, eCopy);
}
}
}
//#endregion
/**
* Indicate the current language and keyboard on the space bar
**/
showLanguage() {
let keyman = com.keyman.singleton;
var lgName='',kbdName='';
var activeStub = keyman.keyboardManager.activeStub;
if(activeStub) {
lgName=activeStub['KL'];
kbdName=activeStub['KN'];
} else if(keyman.getActiveLanguage(true)) {
lgName=keyman.getActiveLanguage(true);
} else {
lgName='English';
}
try {
var t=<HTMLElement> this.spaceBar.firstChild.firstChild;
let tParent = <HTMLElement> t.parentNode;
if(typeof(tParent.className) == 'undefined' || tParent.className == '') {
tParent.className='kmw-spacebar';
} else {
tParent.className +=' kmw-spacebar';
}
t.className='kmw-spacebar-caption';
kbdName=kbdName.replace(/\s*keyboard\s*/i,'');
// We use a separate variable here to keep down on MutationObserver messages in keymanweb.js code.
var keyboardName = "";
if(kbdName == lgName) {
keyboardName=lgName;
} else {
keyboardName=lgName+' ('+kbdName+')';
}
// It sounds redundant, but this dramatically cuts down on browser DOM processing.
if(t.innerHTML != keyboardName) {
t.innerHTML = keyboardName;
}
}
catch(ex){}
}
/**
* Add or remove a class from a keyboard key (when touched or clicked)
* or add a key preview for phone devices
*
* @param {Object} key key affected
* @param {boolean} on add or remove highlighting
**/
highlightKey(key: KeyElement, on: boolean) {
// Do not change element class unless a key
if(!key || (key.className == '') || (key.className.indexOf('kmw-key-row') >= 0)) return;
var classes=key.className, cs = ' kmw-key-touched';
// For phones, use key preview rather than highlighting the key,
// except for space, bksp, enter, shift and popup keys
var usePreview = ((this.keytip != null)
&& (classes.indexOf('kmw-key-shift') < 0)
&& (classes.indexOf('kmw-spacebar') < 0)
&& (key.id.indexOf('popup') < 0 ));
if(usePreview) {
this.showKeyTip(key,on);
} else {
if(on && classes.indexOf(cs) < 0) {
key.className=classes+cs;
this.showKeyTip(null,false); // Moved here by Serkan
} else {
key.className=classes.replace(cs,'');
}
}
}
//#region Mouse-event handling
/**
* Mouse down/mouse over event handler (desktop only)
*
* @param {Event} e mouse over/mouse down event object
*/
mouseOverMouseDownHandler = function(this: VisualKeyboard, e: MouseEvent) {
let keyman = com.keyman.singleton;
let util = keyman.util;
var t = <HTMLElement> util.eventTarget(e);
if(t === null || util.device.formFactor != 'desktop') {
return;
}
if(t.nodeName == 'SPAN') {
t = <HTMLElement> t.parentNode;
}
let key = getKeyFrom(t);
if(util.eventType(e) == 'mousedown') {
this.currentKey=key.id;
util._CancelMouse(e);
this.highlightKey(key, true);
} else if(t.id == this.currentKey) {
this.highlightKey(key, true);
}
}.bind(this);
/**
* Mouse up/mouse out event handler (desktop only)
*
* @param {Event} e mouse up/out event object
*/
mouseUpMouseOutHandler = function(this: VisualKeyboard, e: MouseEvent) {
let keyman = com.keyman.singleton;
let util = keyman.util;
var t=<HTMLElement> util.eventTarget(e);
if(t === null || util.device.formFactor != 'desktop') {
return;
}
if(t.nodeName == 'SPAN') {
t = <HTMLElement> t.parentNode;
}
let key = getKeyFrom(t);
this.highlightKey(key, false);
// Process as click if mouse button released anywhere over key
if(util.eventType(e) == 'mouseup') {
if(key.id == this.currentKey) {
this.clickKey(getKeyFrom(key));
}
this.currentKey='';
}
}.bind(this);
//#endregion
getKeyEmFontSize() {
let keyman = com.keyman.singleton;
let util = keyman.util;
if(util.device.formFactor == 'desktop') {
let kbdFontSize = this.getFontSizeFromCookie();
let keySquareScale = 0.8; // Set in kmwosk.css, is relative.
return kbdFontSize * keySquareScale;
} else {
let emSizeStr = getComputedStyle(document.body).fontSize;
let emSize = util.getFontSizeStyle(emSizeStr).val;
let emScale = util.getFontSizeStyle(keyman.osk._Box).val;
return emSize * emScale;
}
}
getFontSizeFromCookie(): number {
let keyman = com.keyman.singleton;
let util = keyman.util;
var c = util.loadCookie('KeymanWeb_OnScreenKeyboard');
if(typeof(c) == 'undefined' || c == null) {
return 16;
}
var newHeight=util.toNumber(c['height'],0.15*screen.height);
if(newHeight > 0.5*screen.height) {
newHeight=0.5*screen.height;
}
return (newHeight/8);
}
/**
* Set the reference to a special function key for the
* currently visible OSK layer
*
* @param {number} nLayer Index of visible layer
* @param {string} keyId key identifier
* @return {Object} Reference to key
*/
getSpecialKey(nLayer: number, keyId: string): KeyElement {
let layers = this.kbdDiv.childNodes[0].childNodes;
if(nLayer >= 0 && nLayer < layers.length) {
// Special function keys will always be in bottom row (must modify code if not)
let rows = layers[nLayer].childNodes;
let keys = rows[rows.length-1].childNodes;
for(var k=0; k<keys.length; k++) {
let key = getKeyFrom(keys[k].firstChild);
if(key && key['keyId'] == keyId) {
return key;
}
}
}
return null;
}
show() {
let device = com.keyman.singleton.util.device;
var n,nLayer=-1, b = this.kbdDiv.childNodes[0].childNodes;
for(n=0; n < b.length; n++) {
let layerElement = <HTMLDivElement> b[n];
if(layerElement['layer'] == this.layerId) {
layerElement.style.display='block';
//b[n].style.visibility='visible';
this.nextLayer=this.layerId;
this.layerIndex=nLayer=n;
if(typeof this.layers[n]['nextlayer'] == 'string') {
this.nextLayer=this.layers[n]['nextlayer'];
}
// If osk._Show has been called, there's probably been a change in modifier or state key state. Keep it updated!
this._UpdateVKShiftStyle();
} else {
layerElement.style.display='none';
//layerElement.style.visibility='hidden';
}
}
if(device.touchable) {
// Identify and save references to the language key, hide keyboard key, and space bar
this.lgKey=this.getSpecialKey(nLayer,'K_LOPT'); //TODO: should be saved with layer
this.hkKey=this.getSpecialKey(nLayer,'K_ROPT');
// Always adjust screen height if iPhone or iPod, to take account of viewport changes
if(device.OS == 'iOS' && device.formFactor == 'phone') {
this.adjustHeights();
}
}
// Define for both desktop and touchable OSK
this.spaceBar=this.getSpecialKey(nLayer,'K_SPACE'); //TODO: should be saved with layer
}
/**
* Function showLayer
* Scope Private
* @param {string} id ID of the layer to show
* @return {boolean} true if the layer is shown, or false if it cannot be found
* Description Shows the layer identified by 'id' in the on screen keyboard
*/
showLayer(id: string): boolean {
let keyman = com.keyman.singleton;
if(keyman.keyboardManager.activeKeyboard) {
for(var i=0; i<this.layers.length; i++) {
if(this.layers[i].id == id) {
this.layerId=id;
keyman.osk._Show();
return true;
}
}
}
return false;
}
// /**
// * Function _VKeyGetTarget
// * Scope Private
// * @param {Object} e OSK event
// * @return {Object} Target element for key in OSK
// * Description Identify the OSK key clicked
// */
// _VKeyGetTarget(e: Event) {
// var Ltarg;
// e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
// Ltarg = util.eventTarget(e);
// if (Ltarg == null) {
// return null;
// }
// if (Ltarg.nodeType == 3) { // defeat Safari bug
// Ltarg = Ltarg.parentNode;
// }
// if (Ltarg.tagName == 'SPAN') {
// Ltarg = Ltarg.parentNode;
// }
// return Ltarg;
// }
/**
* Append a style sheet for the current keyboard if needed for specifying an embedded font
* or to re-apply the default element font
*
**/
appendStyleSheet() {
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
var activeKeyboard = keymanweb.keyboardManager.activeKeyboard;
var activeStub: com.keyman.KeyboardStub = keymanweb.keyboardManager.activeStub;
// Do not do anything if a null stub
if(activeStub == null) {
return;
}
// First remove any existing keyboard style sheet
if(this.styleSheet) {
util.removeStyleSheet(this.styleSheet);
}
var i, kfd=activeStub['KFont'], ofd=activeStub['KOskFont'];
// Add style sheets for embedded fonts if necessary (each font-face style will only be added once)
util.addFontFaceStyleSheet(kfd);
util.addFontFaceStyleSheet(ofd);
// Temporarily hide duplicated elements on non-desktop browsers
keymanweb.hideInputs();
// Build the style string and append (or replace) the font style sheet
// Note: Some browsers do not download the font-face font until it is applied,
// so must apply style before testing for font availability
// Extended to allow keyboard-specific custom styles for Build 360
var customStyle=this.addFontStyle(kfd,ofd);
if( activeKeyboard != null && typeof(activeKeyboard['KCSS']) == 'string') // KMEW-129
customStyle=customStyle+activeKeyboard['KCSS'];
this.styleSheet = util.addStyleSheet(customStyle); //Build 360
// Wait until font is loaded then align duplicated input elements with page elements
if(this.waitForFonts(kfd,ofd)) {
keymanweb.alignInputs();
}
}
/**
* Add or replace the style sheet used to set the font for input elements and OSK
*
* @param {Object} kfd KFont font descriptor
* @param {Object} ofd OSK font descriptor (if any)
* @return {string}
*
**/
addFontStyle(kfd, ofd): string {
let keymanweb = com.keyman.singleton;
// Get name of font to be applied
var fn=keymanweb.baseFont;
if(typeof(kfd) != 'undefined' && typeof(kfd['family']) != 'undefined') {
fn=kfd['family'];
}
// Unquote font name in base font (if quoted)
fn = fn.replace(/\u0022/g,'');
// Set font family chain for mapped elements and remove any double quotes
var rx=new RegExp('\\s?'+fn+',?'), ff=keymanweb.appliedFont.replace(/\u0022/g,'');
// Remove base font name from chain if present
ff = ff.replace(rx,'');
ff = ff.replace(/,$/,'');
// Then replace it at the head of the chain
if(ff == '') {
ff=fn;
} else {
ff=fn+','+ff;
}
// Re-insert quotes around individual font names
ff = '"' + ff.replace(/\,\s?/g,'","') + '"';
// Add to the stylesheet, quoted, and with !important to override any explicit style
var s='.keymanweb-font{\nfont-family:' + ff + ' !important;\n}\n';
// Set font family for OSK text
if(typeof(ofd) != 'undefined') {
s=s+'.kmw-key-text{\nfont-family:"'+ofd['family'].replace(/\u0022/g,'').replace(/,/g,'","')+'";\n}\n';
} else if(typeof(kfd) != 'undefined') {
s=s+'.kmw-key-text{\nfont-family:"'+kfd['family'].replace(/\u0022/g,'').replace(/,/g,'","')+'";\n}\n';
}
// Store the current font chain (with quote-delimited font names)
keymanweb.appliedFont = ff;
// Return the style string
return s;
}
/**
* Create copy of the OSK that can be used for embedding in documentation or help
* The currently active keyboard will be returned if PInternalName is null
*
* @param {string} PInternalName internal name of keyboard, with or without Keyboard_ prefix
* @param {number} Pstatic static keyboard flag (unselectable elements)
* @param {string=} argFormFactor layout form factor, defaulting to 'desktop'
* @param {(string|number)=} argLayerId name or index of layer to show, defaulting to 'default'
* @return {Object} DIV object with filled keyboard layer content
*/
buildDocumentationKeyboard(PInternalName,Pstatic,argFormFactor,argLayerId): HTMLElement { // I777
let keymanweb = com.keyman.singleton;
var PKbd=keymanweb.keyboardManager.activeKeyboard,Ln,
formFactor=(typeof(argFormFactor) == 'undefined' ? 'desktop' : argFormFactor),
layerId=(typeof(argLayerId) == 'undefined' ? 'default' : argLayerId);
var keyboardsList = keymanweb.keyboardManager.keyboards;
if(PInternalName != null) {
var p=PInternalName.toLowerCase().replace('keyboard_','');
for(Ln=0; Ln<keyboardsList.length; Ln++) {
if(p == keyboardsList[Ln]['KI'].toLowerCase().replace('keyboard_','')) {
PKbd=keyboardsList[Ln]; break;
}
}
}
if(!PKbd) {
return null;
}
var layouts=PKbd['KVKL'], layout=null, PVK=PKbd['KV'];
// Get the layout defined in the keyboard, or its nearest equivalent
if(typeof layouts == 'object') {
if(typeof(layouts[formFactor]) == 'object' && layouts[formFactor] != null) {
layout=layouts[formFactor];
} else if(formFactor == 'phone' && typeof(layouts['tablet']) == 'object' && layouts['tablet'] != null) {
layout=layouts['tablet'];
} else if(formFactor == 'tablet' && typeof(layouts['phone']) == 'object' && layouts['phone'] != null) {
layout=layouts['phone'];
} else if(typeof(layouts['desktop']) == 'object' && layouts['desktop'] != null) {
layout=layouts['desktop'];
}
}
// Else get a default layout for the device for this keyboard
if(layout == null && PVK != null) {
layout=Layouts.buildDefaultLayout(PVK,keymanweb.keyboardManager.getKeyboardModifierBitmask(PKbd),formFactor);
}
// Cannot create an OSK if no layout defined, just return empty DIV
if(layout != null) {
layout.keyLabels=((typeof(PKbd['KDU']) != 'undefined') && PKbd['KDU']);
}
// TODO: Fix this method's link!
let kbd=this.deviceDependentLayout(layout,formFactor);
kbd.className=formFactor+'-static kmw-osk-inner-frame';
// Select the layer to display, and adjust sizes
if(layout != null) {
var layer,row,key,Lr,Lk;
for(Ln=0; Ln<layout.layer.length; Ln++) {
layer=kbd.childNodes[Ln];
for(Lr=0; Lr<layer.childNodes.length; Lr++) {
row=layer.childNodes[Lr];
for(Lk=0; Lk<row.childNodes.length; Lk++) {
key=row.childNodes[Lk];
key.style.height='100%';
}
}
if(typeof(layerId) == 'number') {
layer.style.display=(Ln == layerId && layerId >= 0 ? 'block' : 'none');
} else if(typeof(layerId) == 'string') {
layer.style.display=(layout.layer[Ln].id == layerId ? 'block' : 'none');
} else {
layer.style.display=(Ln == 0 ? 'block' : 'none');
}
}
} else {
kbd.innerHTML="<p style='color:#c40; font-size:0.5em;margin:10px;'>No "+formFactor+" layout is defined for "+PKbd['KN']+".</p>";
}
// Add a faint border
kbd.style.border='1px solid #ccc';
return kbd;
}
onHide() {
// Remove highlighting from hide keyboard key, if applied
if(this.hkKey) {
this.highlightKey(this.hkKey,false);
}
}
}
}