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 = 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)) { (e)[id] = (data)[id]; } } return e; } export function isKey(elem: Node): boolean { return elem && ('key' in elem) && (( elem['key']) instanceof OSKKey); } export function getKeyFrom(elem: Node): KeyElement { if(isKey(elem)) { return 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 = ( 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((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 = (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 = (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 = (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 = (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; bsnwindow['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 = (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; n0; 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 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 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 void = function(this: VisualKeyboard, e: TouchEvent) { // Identify the key touched var t = 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, 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 = 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 = 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 = target; try { if(t) { if(util.hasClass(t,'kmw-key')) { return getKeyFrom(t); } if(t.parentNode && util.hasClass( t.parentNode,'kmw-key')) { return getKeyFrom(t.parentNode); } if(t.firstChild && util.hasClass( 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 = 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(( t.childNodes[k].firstChild).className.indexOf('key-hidden') >= 0) { continue; } x1 = ( t.childNodes[k]).offsetLeft; x2 = x1 + ( 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 = 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 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=( e.childNodes[0]).innerHTML; //// if(keyText == '' || keyText == ' ') 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( Lelem,0)) { Lelem = ( Lelem).contentDocument.documentElement; } Lelem._KeymanWebSelectionStart=Ls; Lelem._KeymanWebSelectionEnd=Le; com.keyman.DOMEventHandlers.states._IgnoreNextSelChange = 0; // ...end I3363 (Build 301) (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= 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 = Lelem; } else if(typeof(Lelem.base) != 'undefined' && com.keyman.Util.instanceof(Lelem.base, "HTMLInputElement")) { inputEle = 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 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 1) { ss.width=(nCols*e.offsetWidth+nCols*5)+'px'; } // Add nested button elements for each sub-key for(i=0; i 1 && nRow > 0) { needsTopMargin = true; } let keyGenerator = new com.keyman.osk.OSKSubKey(subKeySpec[i]); let kDiv = keyGenerator.construct(this, 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 = 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( 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= this.spaceBar.firstChild.firstChild; let tParent = 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 = util.eventTarget(e); if(t === null || util.device.formFactor != 'desktop') { return; } if(t.nodeName == 'SPAN') { t = 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= util.eventTarget(e); if(t === null || util.device.formFactor != 'desktop') { return; } if(t.nodeName == 'SPAN') { t = 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 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= 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="

No "+formFactor+" layout is defined for "+PKbd['KN']+".

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