/*** KeymanWeb 10.0 Copyright 2017 SIL International ***/ // If KMW is already initialized, the KMW script has been loaded more than once. We wish to prevent resetting the // KMW system, so we use the fact that 'initialized' is only 1 / true after all scripts are loaded for the initial // load of KMW. if(!window['tavultesoft']['keymanweb']['initialized']) { /*****************************************/ /* */ /* On-Screen (Visual) Keyboard Code */ /* */ /*****************************************/ (function() { // Declare KeymanWeb object var keymanweb=window['tavultesoft']['keymanweb'],osk=keymanweb['osk'],util=keymanweb['util'],device=util.device; var dbg=keymanweb.debug; // Force full initialization keymanweb.isEmbedded = false; /** * Set default device options * @param {Object} opt device options object */ keymanweb.setDefaultDeviceOptions=function(opt) { // Element attachment type if(opt['attachType'] == '') opt['attachType'] = (device.touchable ? 'manual' : 'auto'); } /** * Customized wait display * * @param {string|boolean} s displayed text (or false) */ util.wait = function(s) { // Keyboards loaded with page are initialized before the page is ready, // so cannot use the wait indicater (and don't need it, anyway) // Do not display if a blocking cloud server error has occurred (to prevent multiple errors) var bg=keymanweb.waiting; if(typeof(bg) == 'undefined' || bg == null || keymanweb.warned) return; var nn=bg.firstChild.childNodes; if(s) { bg.pending=true; window.setTimeout(function() { if(bg.pending) { window.scrollTo(0,0); nn[0].style.display='none'; nn[1].className='kmw-wait-text'; nn[1].innerHTML=s; nn[2].style.display='block'; bg.style.display='block'; } },1000); } else { if(bg.pending) { nn[1].innerHTML=''; bg.pending=false; bg.style.display='none'; } } } // Get default style sheet path keymanweb.getStyleSheetPath=function(ssName) { var ssPath = util['getOption']('resources')+'osk/'+ssName; return ssPath; } /** * Get keyboard path (relative or absolute) * KeymanWeb 2 revised keyboard location specification: * (a) absolute URL (includes ':') - load from specified URL * (b) relative URL (starts with /, ./, ../) - load with respect to current page * (c) filename only (anything else) - prepend keyboards option to URL * (e.g. default keyboards option will be set by Cloud) * * @param {string} Lfilename keyboard file name with optional prefix */ keymanweb.getKeyboardPath=function(Lfilename) { var rx=RegExp('^(([\.]/)|([\.][\.]/)|(/))|(:)'); return (rx.test(Lfilename) ? '' : keymanweb.options['keyboards']) + Lfilename; } /** * 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) == "XXXXabcdef" i.e. return as much as possible of the requested string, where X = \uFFFE */ keymanweb.KC_ = function(n, ln, Pelem) { var Ldv, tempContext = ''; if(Pelem.body) var Ldoc=Pelem; else var Ldoc=Pelem.ownerDocument; // I1481 - use Ldoc to get the ownerDocument when no selection is found if(device.touchable) { tempContext = keymanweb.getTextBeforeCaret(Pelem); } else if(keymanweb.legacy) { tempContext = Pelem.value; } else if(Ldoc && (Ldv=Ldoc.defaultView) && Ldv.getSelection && (Ldoc.designMode.toLowerCase() == 'on' || Pelem.contentEditable == 'true' || Pelem.contentEditable == 'plaintext-only' || Pelem.contentEditable === '')) { // I2457 - support contentEditable elements in mozilla, webkit /* Mozilla midas html editor and editable elements */ var Lsel = Ldv.getSelection(); if(Lsel.focusNode.nodeType == 3) { tempContext = Lsel.focusNode.substringData(0, Lsel.focusOffset); } } 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 } tempContext = Pelem.value._kmwSubstr(0, LselectionStart); } 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); // allows for supp chars //I3319 tempContext = Lrange.text; } if(tempContext._kmwLength() < n) { tempContext = Array(n-tempContext._kmwLength()+1).join("\uFFFE") + tempContext; } return tempContext._kmwSubstr(-n)._kmwSubstr(0,ln); } /** * Align all input fields with underlying elements after a rotation, resize, or change of element font * and/or set visibility * * @param {boolean} align align and make visible, else hide * **/ keymanweb.alignInputs = function(align) { if(device.touchable) { for(var i=0; i 0) keymanweb.inputList[i].base.style.visibility='hidden'; } else { keymanweb.inputList[i].style.visibility='hidden'; keymanweb.inputList[i].base.style.visibility='visible'; } } } } // Manage popup key highlighting osk.highlightSubKeys=function(k,x,y) { // Test for subkey array, return if none if(k == null || k.subKeys == null) return; // Highlight key at touch position (and clear other highlighting) var i,sk,x0,y0,x1,y1,onKey,skBox=document.getElementById('kmw-popup-keys'); // Show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353) if((osk.touchY-y > 5) && skBox == null) { if(osk.subkeyDelayTimer) window.clearTimeout(osk.subkeyDelayTimer); osk.showSubKeys(k); skBox=document.getElementById('kmw-popup-keys'); } for(i=0; i x0 && x < x1 && y > y0 && y < y1); osk.highlightKey(sk,onKey); if(onKey) osk.highlightKey(k,false); } catch(ex){} } } osk.optionKey=function(e,keyName,keyDown) { if(keyDown) { if(keyName.indexOf('K_LOPT') >= 0) osk.showLanguageMenu(); else if(keyName.indexOf('K_ROPT') >= 0) { keymanweb._IsActivatingKeymanWebUI=0; osk._Hide(true); keymanweb.hideCaret(); keymanweb._LastActiveElement = 0; } } } /** * Create a key preview element for phone devices */ osk.createKeyTip=function() { if(device.formFactor == 'phone') { if(osk.keytip == null) { osk.keytip=util._CreateElement('DIV'); osk.keytip.className='kmw-keytip'; osk.keytip.id = 'kmw-keytip'; // The following style is critical, so do not rely on external CSS osk.keytip.style.pointerEvents='none'; // Add CANVAS element for outline and SPAN for key label osk.keytip.appendChild(util._CreateElement('CANVAS')); osk.keytip.appendChild(util._CreateElement('SPAN')); osk.keytip.key = null; osk.keytip.state = false; } // Always append to _Box (since cleared during OSK Load) osk._Box.appendChild(osk.keytip); } } /** * Add (or remove) the keytip preview (if KeymanWeb on a phone device) * * @param {Object} key HTML key element * @param {boolean} on show or hide */ osk.showKeyTip=function(key,on) { var tip=osk.keytip; // Do not change the key preview unless key or state has changed if(tip == null || (key == tip.key && on == tip.state)) return; var sk=document.getElementById('kmw-popup-keys'), popup = (sk && sk.style.visibility == 'visible') // Create and display the preview if(on && !popup) { var y0 = util._GetAbsoluteY(osk._Box), h0 = osk._Box.offsetHeight, xLeft = util._GetAbsoluteX(key), xTop = util._GetAbsoluteY(key), xWidth = key.offsetWidth, xHeight = key.offsetHeight, kc = key.firstChild, kcs = kc.style, kts = tip.style, ktLabel = tip.childNodes[1], ktls = ktLabel.style, edge = 0, canvas = tip.firstChild, previewFontScale = 1.8; // Find key text element for(var i=0; i window.innerWidth - xWidth - xOverflow) { edge = 1; xLeft -= xOverflow } osk.drawPreview(canvas, xWidth, xHeight, edge); kts.left=(xLeft - xOverflow)+'px'; kts.display = 'block'; } // Hide the key preview else { tip.style.display = 'none'; } // Save the key preview state tip.key = key; tip.state = on; } /** * Draw key preview in element using CANVAS * @param {Object} canvas CANVAS element * @param {number} w width of touched key, px * @param {number} h height of touched key, px * @param {number} edge -1 left edge, 1 right edge, else 0 */ osk.drawPreview = function(canvas,w,h,edge) { var ctx = canvas.getContext('2d'), dx = (canvas.width - w)/2, hMax = canvas.height, w0 = 0, w1 = dx, w2 = w + dx, w3 = w + 2 * dx, h1 = 0.5 * hMax, h2 = 0.6 * hMax, h3 = hMax, r = 8; if(device.OS == 'Android') { r = 3; } // Adjust the preview shape at the edge of the keyboard switch(edge) { case -1: w1 -= dx; w2 -= dx; w3 -= dx; break; case 1: w0 += dx; w1 += dx; w2 += dx; break; } // Clear the canvas ctx.clearRect(0,0,canvas.width,canvas.height); // Define appearance of preview (cannot be done directly in CSS) if(device.OS == 'Android') { var wx=(w1+w2)/2; w1 = w2 = wx; ctx.fillStyle = '#999'; } else { ctx.fillStyle = '#ffffff'; } ctx.lineWidth = '1'; ctx.strokeStyle = '#cccccc'; // Draw outline ctx.save(); ctx.beginPath(); ctx.moveTo(w0+r,0); ctx.arcTo(w3,0,w3,r,r); if(device.OS == 'Android') { ctx.arcTo(w3,h1,w2,h2,r); ctx.arcTo(w2,h2,w1,h2,r); ctx.arcTo(w1,h2,w0,h1-r,r); } else { ctx.arcTo(w3,h1,w2,h2,r); ctx.arcTo(w2,h2,w2-r,h3,r); ctx.arcTo(w2,h3,w1,h3,r); ctx.arcTo(w1,h3,w1,h2-r,r); ctx.arcTo(w1,h2,w0,h1-r,r); } ctx.arcTo(w0,h1,w0,r,r); ctx.arcTo(w0,0,w0+r,0,r); ctx.fill(); ctx.stroke(); ctx.restore(); } /** * Add a callout for popup keys (if KeymanWeb on a phone device) * * @param {Object} key HTML key element * @return {Object} callout object */ osk.addCallout = function(key) { if(device.formFactor != 'phone' || device.OS != 'iOS') return null; var cc = util._CreateElement('DIV'),ccs = cc.style; cc.id = 'kmw-popup-callout'; osk._Box.appendChild(cc); // Create the callout var xLeft = key.offsetLeft, xTop = key.offsetTop, xWidth = key.offsetWidth, xHeight = key.offsetHeight; // Set position and style ccs.top = (xTop-6)+'px'; ccs.left = xLeft+'px'; ccs.width = xWidth+'px'; ccs.height = (xHeight+6)+'px'; // Return callout element, to allow removal later return cc; } /** * Touch hold key display management * * @param {Object} key base key object */ osk.touchHold = function(key) { // Clear and restart the popup timer if(osk.subkeyDelayTimer) { window.clearTimeout(osk.subkeyDelayTimer); osk.subkeyDelayTimer = null; } if(typeof key.subKeys != 'undefined' && key.subKeys != null) { osk.subkeyDelayTimer = window.setTimeout( function() { osk.clearPopup(); osk.showSubKeys(key); }, osk.popupDelay ); } } /** * Use rotation events to adjust OSK and input element positions and scaling as necessary */ keymanweb.handleRotationEvents=function() { if(device.OS == 'iOS') util.attachDOMEvent(window,'orientationchange',keymanweb.rotateDevice); // Also manage viewport rescaling after rotation on Android if(device.OS == 'Android') { osk.wasVisible=osk.isVisible; // Hide OSK at start of rotation if('onmozorientationchange' in screen) util.attachDOMEvent(screen,'mozorientationchange',osk.hideNow); else util.attachDOMEvent(window,'orientationchange',osk.hideNow); // Then align inputs and redisplay the OSK on resize event following rotation util.attachDOMEvent(window,'resize', function(){ keymanweb.alignInputs(true); osk.hideLanguageList(); osk._Load(); if(osk.wasVisible)osk._Show(); } ); } //TODO: may be able to recognize start of rotation using accelerometer call... //util.attachDOMEvent(window,'devicemotion',keymanweb.testRotation); } /** * Hide the URL bar and resize the OSK after rotation (device dependent) * * @param {Event} e event (not used directly) * */ keymanweb.rotateDevice = function(e) // Rewritten for I3363 (Build 301) { osk.hideLanguageList(); if(!osk._Visible) return; // Always re-adjust OSK rows for rounding to nearest pixel // if(osk.ready) // { //nLayer=osk.resetRowLengths(); // clear aligned flag for all layers //osk.adjustRowLengths(nLayer); // adjust lengths in visible layer // } osk.adjustHeights(); // Hide the URL bar on Android phones - offset is zero for iPhone, but should not be applied here if(device.formFactor == 'phone' && device.OS == 'Android') window.setTimeout(function(){window.scrollTo(0,1);},1000); } /** * Possible way to detect the start of a rotation and hide the OSK before it is adjusted in size * * @param {Object} e accelerometer rotation event * keymanweb.testRotation = function(e) { var r=e.rotationRate; if(typeof(r) != 'undefined') { dbg(r.alpha+' '+r.beta+' '+r.gamma); } } */ /** * Wait until font is loaded before applying stylesheet - test each 100 ms * @param {Object} kfd main font descriptor * @param {Object} ofd secondary font descriptor (OSK only) * @return {boolean} */ osk.waitForFonts=function(kfd,ofd) { if(typeof(kfd) == 'undefined' && typeof(ofd) == 'undefined') return true; if(typeof(kfd['files']) == 'undefined' && typeof(ofd['files']) == 'undefined') return true; var kReady=util.checkFontDescriptor(kfd),oReady=util.checkFontDescriptor(ofd); if(kReady && oReady) return true; keymanweb.fontCheckTimer=window.setInterval(function() { if(util.checkFontDescriptor(kfd) && util.checkFontDescriptor(ofd)) { window.clearInterval(keymanweb.fontCheckTimer); keymanweb.fontCheckTimer=null; keymanweb.alignInputs(true); } },100); // Align anyway as best as can if font appears to remain uninstalled after 5 seconds window.setTimeout(function() { if(keymanweb.fontCheckTimer) { window.clearInterval(keymanweb.fontCheckTimer); keymanweb.fontCheckTimer=null; keymanweb.alignInputs(true); // Don't notify - this is a management issue, not anything the user needs to deal with // TODO: Consider having an icon in the OSK with a bubble that indicates missing font //util.alert('Unable to download the font normally used with '+ks['KN']+'.'); } },5000); return false; } })(); }