// KeymanWeb 10.0 // Copyright 2017 SIL International /*****************************************/ /* */ /* Embedded application-specific code */ /* */ /*****************************************/ (function() { // Declare KeymanWeb and related objects var keymanweb=window['keyman'],osk=keymanweb['osk'],util=keymanweb['util'],device=util.device; var kbdInterface=keymanweb['interface']; // Allow definition of application name keymanweb.options['app']=''; // Flag to control refreshing of a keyboard that is already loaded keymanweb.mustReloadKeyboard = true; // Skip full page initialization - skips native-mode only code keymanweb.isEmbedded = true; osk.popupDelay = 400; // Delay must be less than native touch-hold delay // Set default device options keymanweb.setDefaultDeviceOptions = function(opt) { opt['attachType'] = 'manual'; device.app=opt['app']; device.touchable=true; device.formFactor='phone'; if(navigator && navigator.userAgent && navigator.userAgent.indexOf('iPad') >= 0) device.formFactor='tablet'; if(device.app.indexOf('Mobile') >= 0) device.formFactor='phone'; if(device.app.indexOf('Tablet') >= 0) device.formFactor='tablet'; device.browser='native'; }; // Get default style sheet path keymanweb.getStyleSheetPath = function(ssName) { return keymanweb.rootPath+ssName; }; // Get KMEI, KMEA keyboard path (overrides default function, allows direct app control of paths) keymanweb.getKeyboardPath = function(Lfilename, packageID) { return Lfilename + "?v=" + (new Date()).getTime(); /*cache buster*/ }; /** * Force reload of resource * * @param {string} s unmodified URL * @return modified URL */ util.unCached = function(s) { var t=(new Date().getTime()); s = s + '?v=' + t; return s; }; util.wait = function() { // Empty stub - this function should not be implemented or used within embedded code routes. console.warn("util.wait() call attempted in embedded mode!"); // Sends log message to embedding app. }; util.alert = function() { // Empty stub - this function should not be implemented or used within embedded code routes. console.warn("util.alert() call attempted in embedded mode!"); // Sends log message to embedding app. }; // TODO: This needs to be discussed with Serkan - can possibly get context without any reference to Pelem?? /** * Get (uncached) keyboard context for a specified range, relative to caret * * @param {number} n Number of characters to move back from caret * @param {number} ln Number of characters to return * @param {Object} Pelem Element to work with (must be currently focused element) * @return {string} Context string * * Example [abcdef|ghi] as INPUT, with the caret position marked by |: * KC(2,1,Pelem) == "e" * KC(3,3,Pelem) == "def" * KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string */ keymanweb.KC_ = function(n, ln, Pelem) { var Ldv, Ldoc; if(Pelem.body) Ldoc=Pelem; else Ldoc=Pelem.ownerDocument; // I1481 - use Ldoc to get the ownerDocument when no selection is found if(keymanweb.legacy) { return Pelem.value._kmwSubstr(Pelem.length-n, ln); //I3319 } else if(Ldoc && (Ldv=Ldoc.defaultView) && Ldv.getSelection && (Ldoc.designMode.toLowerCase() == 'on' || Pelem.contentEditable == 'true' || Pelem.contentEditable == 'plaintext-only' || Pelem.contentEditable === '')) // && Pelem.tagName == 'HTML') && Pelem.tagName == 'HTML') // I2457 - support contentEditable elements in mozilla, webkit { /* Mozilla midas html editor and editable elements */ var Lsel = Ldv.getSelection(); if(Lsel.focusNode.nodeType == 3) { if(Lsel.focusOffset > 2*n) // I3319 SMP extension return Lsel.focusNode.substringData(Lsel.focusOffset - 2*n, 2*n)._kmwSubstr(-n)._kmwSubstr(0,ln); // I3319 else return Lsel.focusNode.substringData(0, Lsel.focusOffset)._kmwSubstr(-n)._kmwSubstr(0,ln); // I3319 } else return ""; } else if (Pelem.setSelectionRange) { /* Mozilla other controls */ var LselectionStart, LselectionEnd; if(Pelem._KeymanWebSelectionStart) { LselectionStart = Pelem._KeymanWebSelectionStart; LselectionEnd = Pelem._KeymanWebSelectionEnd; //KeymanWeb._Debug('KeymanWeb.KC: _KeymanWebSelectionStart=TRUE LselectionStart='+LselectionStart+'; LselectionEnd='+LselectionEnd); } else { if(keymanweb._CachedSelectionStart === null || Pelem.selectionStart !== keymanweb._LastCachedSelection) // I3319, KMW-1 { keymanweb._LastCachedSelection = Pelem.selectionStart; // KMW-1 keymanweb._CachedSelectionStart = Pelem.value._kmwCodeUnitToCodePoint(Pelem.selectionStart); // I3319 keymanweb._CachedSelectionEnd = Pelem.value._kmwCodeUnitToCodePoint(Pelem.selectionEnd); // I3319 } LselectionStart = keymanweb._CachedSelectionStart; // I3319 LselectionEnd = keymanweb._CachedSelectionEnd; // I3319 } if(LselectionStart < n) { // Looking for context before start of text buffer so return non-characters to pad result var tempContext = Array(n-LselectionStart+1).join("\uFFFE") + Pelem.value._kmwSubstr(0,LselectionStart); return tempContext._kmwSubstr(0,ln); } //dbg(n+' '+ln+' '+Pelem.value._kmwSubstring(LselectionStart-n,LselectionStart-n+ln)); return Pelem.value._kmwSubstring(LselectionStart-n,LselectionStart-n+ln); //I3319, KMW-1 } else if(Ldoc && (Ldv=Ldoc.selection)) // build 77 - use elem.ownerDocument instead of document // I1481 - use Ldoc to get the ownerDocument when no selection is found { /* IE */ var Lrange = Ldv.createRange(); //if (Lrange.parentElement() == Pelem) { // build 77 - ignore parent of selection Lrange.moveStart('character',-2*n); //I3319 return Lrange.text._kmwSubstr(-n)._kmwSubstring(0,ln); //I3319 //} } return ""; }; /** * Refresh element content after change of text (if required) * * @param {Object} Pelem input element */ keymanweb.refreshElementContent = function(Pelem) { if('ontextchange' in keymanweb) keymanweb['ontextchange'](Pelem); }; /** * Set target element text direction (LTR or RTL): not functional for KMEI, KMEA * * @param {Object} Ptarg Target element */ keymanweb._SetTargDir = function(Ptarg){}; /** * Align input fields (should not be needed with KMEI, KMEA) * * @param {boolean} align align and make visible, else hide * **/ keymanweb.alignInputs = function(align) {}; /** * Use rotation events to adjust OSK element positions and scaling if necessary */ keymanweb.handleRotationEvents = function() {}; /** * Adjust the absolute height of each keyboard element after a rotation - modified for KMEI, 13/11/14 * **/ osk.adjustHeights=function() { if(!osk._Box || !osk._Box.firstChild || !osk._Box.firstChild.firstChild || !osk._Box.firstChild.firstChild.childNodes) return false; var layers=osk._Box.firstChild.firstChild.childNodes, nRows=layers[0].childNodes.length, oskHeight=osk.getHeight(), rowHeight=Math.floor(oskHeight/nRows), nLayer,nRow,rs,keys,nKeys,nKey,key,ks,j,pad=4,fs=1.0; if(device.OS == 'Android' && 'devicePixelRatio' in window) rowHeight = rowHeight/window.devicePixelRatio; oskHeight=nRows*rowHeight; var b=osk._Box,bs=b.style; bs.height=bs.maxHeight=(oskHeight+3)+'px'; b=b.firstChild.firstChild; bs=b.style; bs.height=bs.maxHeight=(oskHeight+3)+'px'; if(device.formFactor == 'phone') fs = 0.65; pad = Math.round(0.15*rowHeight); bs.fontSize=fs+'em'; var resizeLabels=(device.OS == 'iOS' && device.formFactor == 'phone' && util.landscapeView()); for(nLayer=0;nLayer 0)) key.childNodes[0].style.fontSize='6px'; } } } return true; }; /** * Caret position always determined from the active (but hidden) element * * @return {boolean} **/ keymanweb.isPositionSynthesized = function() { return false; }; /** * correctOSKTextSize handles rotation event -- currently rebuilds keyboard and adjusts font sizes */ keymanweb['correctOSKTextSize']=function() { if(osk.adjustHeights()) { osk._Load(); } }; // Send the subkey array to iOS, with centre,top of base key position /** * Create a popup key array natively * * @param {Object} key base key element */ osk.touchHold = function(key) { if(key.subKeys && (typeof(window['oskCreatePopup']) == 'function')) { var xBase=util._GetAbsoluteX(key)-util._GetAbsoluteX(osk._DivVKbd)+key.offsetWidth/2, yBase=util._GetAbsoluteY(key)-util._GetAbsoluteY(osk._DivVKbd); if(device.formFactor == 'phone') osk.prependBaseKey(key); osk.popupBaseKey = key; osk.popupPending=true; window['oskCreatePopup'](key.subKeys,xBase,yBase,key.offsetWidth,key.offsetHeight); } }; /** * Function called by iOS when a device-implemented keyboard popup is displayed or hidden * * @param {boolean} isVisible * **/ keymanweb['popupVisible'] = function(isVisible) { osk.popupVisible = isVisible; }; // Popup key highlighting (managed by device, dummy call) osk.highlightSubKeys = function(k,x,y){}; // Create a keytip (dummy call - actual keytip handled by native code) osk.createKeyTip = function() { if(device.formFactor == 'phone') osk.keytip = {key:null,state:false}; }; // Send the key details to KMEI or KMEA for showing or hiding the native-code keytip osk.showKeyTip = function(key,on) { var tip = osk.keytip, showPreview = window['oskCreateKeyPreview'], clearPreview = window['oskClearKeyPreview']; if(tip == null || (key == tip.key && on == tip.state)) return; if(on && (typeof showPreview == 'function')) { var xBase=util._GetAbsoluteX(key)-util._GetAbsoluteX(osk._DivVKbd)+key.offsetWidth/2, yBase=util._GetAbsoluteY(key)-util._GetAbsoluteY(osk._DivVKbd), kc; // Find key text element for(var i=0; i= 0 && key.id.indexOf('K_SPACE') < 0) showPreview(xBase,yBase,key.offsetWidth,key.offsetHeight,kc.innerHTML); } else if(!on && (typeof clearPreview == 'function')) { if(osk.touchCount == 0 || key == null) clearPreview(); } tip.key = key; tip.state = on; }; osk.menuEvent = null; osk.optionKey = function(e,keyName,keyDown) { if(keyName.indexOf('K_LOPT') >= 0) { if(keyDown) { osk.menuEvent = e; if('showKeyboardList' in keymanweb) keymanweb['showKeyboardList'](); } else { if(osk.menuEvent) osk.highlightKey(osk.menuEvent,false); if(typeof(window['menuKeyUp']) == 'function') window['menuKeyUp'](); osk.menuEvent = null; } } else if(keyName.indexOf('K_ROPT') >= 0) { if(keyDown) { osk.highlightKey(e,false); if('hideKeyboard' in keymanweb) keymanweb['hideKeyboard'](); } } }; // For KMEI and KMEA will always assume fonts are already installed osk.waitForFonts = function(kfd,ofd){return true;}; /** * Return position of language menu key to KeymanTouch * * @return {string} comma-separated x,y,w,h of language menu key * **/ keymanweb['touchMenuPos'] = function() { if(osk.lgKey == null) return ''; var key=osk.lgKey; // A CSS change of kmd-key-square from position:fixed to position:static was needed // for Android 4.3 to display the OSK correctly, but resulted in the position of // the menu key not being returned correctly. The following line gets the // key element, instead of the key-square element, fixes this. It should be // removed again when the key-square elements are all removed as planned. if(typeof key.firstChild != 'undefined' && key.firstChild != null && osk.hasClass(key.firstChild,'kmw-key')) key = key.firstChild; var w=key.offsetWidth, h=key.offsetHeight, x=util._GetAbsoluteX(key) - util._GetAbsoluteX(osk._DivVKbd) + w/2, y=util._GetAbsoluteY(key) - util._GetAbsoluteY(osk._DivVKbd); return x+','+y+','+w+','+h; }; /** * Accept an external key ID (from KeymanTouch) and pass to the keyboard mapping * * @param {string} keyName key identifier **/ keymanweb['executePopupKey'] = function(keyName) { if(!keymanweb._ActiveKeyboard) return false; /* Clear any pending (non-popup) key */ osk.keyPending = null; // Changes for Build 353 to resolve KMEI popup key issues keyName=keyName.replace('popup-',''); //remove popup prefix if present (unlikely) var t=keyName.split('-'),layer=(t.length>1?t[0]:osk.layerId); keyName=t[t.length-1]; if(layer == 'undefined') layer=osk.layerId; var Lelem=keymanweb._LastActiveElement,Lkc,keyShiftState=osk.getModifierState(layer); if(keymanweb._ActiveElement == null) keymanweb._ActiveElement=Lelem; // Process modifier key action if(osk.selectLayer(keyName,null)) return true; // Check the virtual key Lkc = {Ltarg:Lelem,Lmodifiers:0,Lstates:0,Lcode:osk.keyCodes[keyName],LisVirtualKey:true}; // Set the flags for the state keys. Lkc.Lstates |= osk._stateKeys['K_CAPS'] ? osk.modifierCodes['CAPS'] : osk.modifierCodes['NO_CAPS']; Lkc.Lstates |= osk._stateKeys['K_NUMLOCK'] ? osk.modifierCodes['NUM_LOCK'] : osk.modifierCodes['NO_NUM_LOCK']; Lkc.Lstates |= osk._stateKeys['K_SCROLL'] ? osk.modifierCodes['SCROLL_LOCK'] : osk.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.isVirtualKey=false; // Get code for non-physical keys if(typeof Lkc.Lcode == 'undefined') { Lkc.Lcode = osk.getVKDictionaryCode(keyName); if (!Lkc.Lcode) { // Special case for U_xxxx keys Lkc.Lcode = 1; } } //if(!Lkc.Lcode) return false; // Value is now zero if not known (Build 347) //Build 353: revert to prior test to try to fix lack of KMEI output, May 1, 2014 if(isNaN(Lkc.Lcode) || !Lkc.Lcode) return false; // 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 & osk.modifierBitmasks['ALT_GR_SIM']) == osk.modifierBitmasks['ALT_GR_SIM'] && osk.emulatesAltGr()) { Lkc.Lmodifiers &= ~osk.modifierBitmasks['ALT_GR_SIM']; Lkc.Lmodifiers |= osk.modifierCodes['RALT']; } Lkc.vkCode=Lkc.Lcode; // Pass this key code and state to the keyboard program if(!keymanweb._ActiveKeyboard || Lkc.Lcode == 0) return false; // If key is mapped, return true if(kbdInterface.processKeystroke(util.device, Lelem, Lkc)) return true; keymanweb.processDefaultMapping(Lkc.Lcode, keyShiftState, Lelem, keyName); return true; }; /** * API endpoint for hardware keystroke events from Android external keyboards * * @param {number} code key identifier * @param {number} shift shift state (0x01=left ctrl 0x02=right ctrl 0x04=left alt 0x08=right alt * 0x10=shift 0x20=ctrl 0x40=alt) * @param {number} lstates lock state (0x0200=no caps 0x0400=num 0x0800=no num 0x1000=scroll 0x2000=no scroll locks) **/ keymanweb['executeHardwareKeystroke'] = function(code, shift, lstates = 0) { if(!keymanweb._ActiveKeyboard || code == 0) { return false; } // Clear any pending (non-popup) key osk.keyPending = null; var Lelem = keymanweb._LastActiveElement; if(keymanweb._ActiveElement == null) { keymanweb._ActiveElement = Lelem; } // Check the virtual key var Lkc = { Ltarg: keymanweb._ActiveElement, Lmodifiers: shift, vkCode: code, Lcode: code, Lstates: lstates, LisVirtualKey: true, LisVirtualKeyCode: false }; try { // Pass this key code and state to the keyboard program // If key is mapped, return true if(kbdInterface.processKeystroke(util.physicalDevice, Lelem, Lkc)) { return true; } return keymanweb.processDefaultMapping(Lkc.Lcode, shift, Lelem, ''); } catch (err) { console.error(err.message, err); return false; } }; /** * Process default mapping only if necessary (last resort) * @param {number} code key identifier * @param {number} shift shift state (0x01=left ctrl 0x02=right ctrl 0x04=left alt 0x08=right alt * 0x10=shift 0x20=ctrl 0x40=alt) * @param {Object} Lelem element to output to * @param {string} keyName * @return {boolean} true if key code successfully processed */ keymanweb.processDefaultMapping = function(code, shift, Lelem, keyName) { if (code == osk.keyCodes.K_SPACE) { kbdInterface.output(0, Lelem, ' '); return true; } else if (code == osk.keyCodes.K_ENTER) { kbdInterface.output(0, Lelem, '\n'); return true; } var ch = osk.defaultKeyOutput(keyName, code, shift, false); if(ch) { kbdInterface.output(0, Lelem, ch); return true; } return false; } })();