/*** 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']) { (function() { // Declare KeymanWeb and util objects var keymanweb=window['tavultesoft']['keymanweb'], util=keymanweb['util'], osk=keymanweb['osk'],device=util.device,dbg=keymanweb.debug; //osk defined here, build 350 keymanweb.TSS_LAYER = 33; keymanweb.TSS_PLATFORM = 31; keymanweb._BeepObjects=[]; keymanweb._BeepTimeout=0; /** * Function KSF * Scope Public * Description Save keyboard focus */ keymanweb['KSF'] = keymanweb.KSF = function() // KeyboardSaveFocus { keymanweb._IgnoreNextSelChange = 1; } /** * Function KT * Scope Public * @param {string} Ptext Text to insert * @param {?number} PdeadKey Dead key number, if any (???) * @return {boolean} true if inserted * Description Insert text into active control */ keymanweb['KT'] = keymanweb.KT = function(Ptext,PdeadKey) // KeyboardInsertText { keymanweb.cachedContext.reset(); //_DebugEnter('InsertText'); var Lelem = keymanweb._LastActiveElement, Ls, Le, Lkc, Lsel, Lv=false; if(Lelem != null) { Ls=Lelem._KeymanWebSelectionStart; Le=Lelem._KeymanWebSelectionEnd; Lsel=keymanweb._Selection; //if(!Lsel || Lsel.parentElement() == Lelem) // I1506 //{ keymanweb._IsActivatingKeymanWebUI = 1; keymanweb._IgnoreNextSelChange = 100; keymanweb._FocusLastActiveElement(); if(keymanweb._IsMozillaEditableIframe(Lelem,0)) Lelem = Lelem.documentElement; // I3363 (Build 301) if(document.selection && Lsel != null) Lsel.select(); Lelem._KeymanWebSelectionStart=Ls; Lelem._KeymanWebSelectionEnd=Le; keymanweb._IgnoreNextSelChange = 0; if(Ptext!=null)keymanweb.KO(0, Lelem, Ptext); if((typeof(PdeadKey)!=='undefined') && (PdeadKey !== null)) keymanweb.KDO(0, Lelem, PdeadKey); Lelem._KeymanWebSelectionStart=null; Lelem._KeymanWebSelectionEnd=null; Lv=true; //} } //_DebugExit('InsertText'); return Lv; } /** * Function KR * Scope Public * @param {Object} Pk Keyboard object * Description Register and load the keyboard */ keymanweb['KR'] = keymanweb.KR = function(Pk) { // If initialization not yet complete, list the keyboard to be registered on completion of initialization if(!keymanweb['initialized']) { keymanweb.deferredKR.push(Pk); return; } var Li,Lstub; // Check if the active stub refers to this keyboard, else find applicable stub var Ps=keymanweb._ActiveStub; if(!Ps || !('KI' in Ps) || (Ps['KI'] != Pk['KI'])) { // Find the first stub for this keyboard for(Lstub=0;Lstub < keymanweb._KeyboardStubs.length; Lstub++) // I1511 - array prototype extended { Ps=keymanweb._KeyboardStubs[Lstub]; if(Pk['KI'] == Ps['KI'])break; Ps=null; } } // Build 369: ensure active stub defined when loading local keyboards if(keymanweb._ActiveStub == null && Ps != null) keymanweb._ActiveStub = Ps; // Register the stub for this language (unless it is already registered) // keymanweb.KRS(Ps?Ps:Pk); // Test if keyboard already loaded for(Li=0; Li 1) keymanweb._KeyboardStubs.sort(function(a,b){ var ax,bx; switch(a['KLC']) { case 'eng': ax=0; break; case 'fra': ax=1; break; case 'deu': ax=2; break; default: ax=10; break; } switch(b['KLC']) { case 'eng': bx=0; break; case 'fra': bx=1; break; case 'deu': bx=2; break; default: bx=10; break; } return ax-bx; }); */ // TODO: Need to distinguish between initial loading of a large number of stubs and any subsequent loading. // UI initialization should not be needed for each registration, only at end. // Reload this keyboard if it was the last active keyboard and // make any changes needed by UI for new keyboard stub // (Uncommented for Build 360) keymanweb.doKeyboardRegistered(Pstub['KI'],Pstub['KL'],Pstub['KN'],Pstub['KLC']); return null; } /** * Get *cached or 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'] = keymanweb.KC = function(n, ln, Pelem) { var v = keymanweb.cachedContext.get(n, ln); if(v !== null) return v; var r = keymanweb.KC_(n, ln, Pelem); keymanweb.cachedContext.set(n, ln, r); return r; } /** * Function KN * Scope Public * @param {number} n Length of context to check * @param {Object} Ptarg Element to work with (must be currently focused element) * @return {boolean} True if length of context is less than or equal to n * Description Test length of context, return true if the length of the context is less than or equal to n * * Example [abc|def] as INPUT, with the caret position marked by |: * KN(3,Pelem) == TRUE * KN(2,Pelem) == FALSE * KN(4,Pelem) == TRUE */ keymanweb['KN'] = keymanweb.KN = function(n, Ptarg) // KeyboardNul { var cx=this.KC(n+1, 1, Ptarg); if(cx === false) { // It appears that this can no longer be returned with KMW so probably should be removed // after testing return true; } // With #31, the result will be a replacement character if context is empty. return cx === "\uFFFE"; } /** * Function KCM * Scope Public * @param {number} n Number of characters to move back from caret * @param {Object} Ptarg Focused element * @param {string} val String to match * @param {number} ln Number of characters to return * @return {boolean} True if selected context matches val * Description Test keyboard context for match */ keymanweb['KCM'] = keymanweb.KCM = function(n, Ptarg, val, ln) // Keyboard_ContextMatch { //KeymanWeb._Debug('KeymanWeb.KCM(n='+n+', Ptarg, val='+val+', ln='+ln+'): return '+(this.KC(n,ln,Ptarg)==val)); var cx=this.KC(n, ln, Ptarg); if(cx !== false && cx === val) return true; // I3318 this._DeadkeyResetMatched(); // I3318 return false; } /** * Function KIK * Scope Public * @param {Object} e keystroke event * @return {boolean} true if keypress event * Description Test if event as a keypress event */ keymanweb['KIK'] = keymanweb.KIK = function(e) // Keyboard_IsKeypress { if(keymanweb._ActiveKeyboard['KM']) // I1380 - support KIK for positional layouts return !e.LisVirtualKey; // will now return true for U_xxxx keys, but not for T_xxxx keys else return keymanweb._USKeyCodeToCharCode(e) ? true : false; // I1380 - support KIK for positional layouts //if(e.charCode != 0) != null) // return e.charCode != 0; //return e.type == 'keypress'; } /** * Function KKM * Scope Public * @param {Object} e keystroke event * @param {number} Lruleshift * @param {number} Lrulekey * @return {boolean} True if key matches rule * Description Test keystroke with modifiers against rule */ keymanweb['KKM'] = keymanweb.KKM = function(e,Lruleshift,Lrulekey) // Keyboard_KeyMatch { var retVal = 0; // I3318 var keyCode = (e.Lcode == 173 ? 189 : e.Lcode); //I3555 (Firefox hyphen issue) var bitmask = keymanweb.getKeyboardModifierBitmask(); if(e.vkCode > 255) keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts if(e.LisVirtualKey || keyCode > 255) { if((Lruleshift & 0x4000) == 0x4000 || (keyCode > 255)) // added keyCode test to support extended keys { retVal = ((Lrulekey == keyCode) && ((Lruleshift & bitmask) == e.Lmodifiers)); //I3318, I3555 } } else if((Lruleshift & 0x4000) == 0) { retVal = (keyCode == Lrulekey); // I3318, I3555 } if(!retVal) this._DeadkeyResetMatched(); // I3318 return retVal != 0; // I3318 }; /** * Function KSM * Scope Public * @param {Object} e keystroke event * @param {number} Lstate * Description Test keystroke against state key rules */ keymanweb['KSM'] = keymanweb.KSM = function(e, Lstate) { // Keyboard_StateMatch return ((Lstate & e.Lstates) == Lstate); } /** * Function KKI * Scope Public * @param {Object} e * @return {Object} Object with event's virtual key flag, key code, and modifiers * Description Get object with extended key event information */ keymanweb['KKI'] = keymanweb.KKI = function(e) { var ei = {}; ei['vk'] = e.LisVirtualKey; ei['code'] = e.Lcode; ei['modifiers'] = e.Lmodifiers; return ei; }; /** * Function KDM * Scope Public * @param {number} n current cursor position * @param {Object} Ptarg target element * @param {number} d deadkey * @return {boolean} True if deadkey found selected context matches val * Description Match deadkey at current cursor position */ keymanweb['KDM'] = keymanweb.KDM = function(n, Ptarg, d) { if(keymanweb._DeadKeys.length == 0) return false; // I3318 var sp=keymanweb._SelPos(Ptarg); n = sp - n; for(var i = 0; i < keymanweb._DeadKeys.length; i++) if(keymanweb._DeadKeys[i].p == n && keymanweb._DeadKeys[i].d == d) { keymanweb._DeadKeys[i].matched = 1; return true; // I3318 } this._DeadkeyResetMatched(); // I3318 return false; } /** * Function KBR * Scope Public * Description Reset/terminate beep or flash (not currently used: Aug 2011) */ keymanweb['KBR'] = keymanweb.KBR = function() // KeyboardBeepReset { keymanweb.cachedContext.reset(); var Lbo; keymanweb._BeepTimeout = 0; for(Lbo=0;Lbo= 0; } /** * Function KO * Scope Public * @param {number} dn number of characters to overwrite * @param {Object} Pelem element to output to * @param {string} s string to output * Description Keyboard output */ keymanweb['KO'] = keymanweb.KO = function(dn, Pelem, s) // Keyboard_Output() { keymanweb.cachedContext.reset(); // KeymanTouch for Android uses direct insertion of the character string if('oninserttext' in keymanweb) { keymanweb['oninserttext'](dn,s); } if(Pelem.body) var Ldoc=Pelem; else var Ldoc=Pelem.ownerDocument; // I1481 - integration with rich editors not working 100% var Li, Ldv; if(Pelem.className.indexOf('keymanweb-input') >= 0) { var t=keymanweb.getTextBeforeCaret(Pelem); if(dn > 0) t=t._kmwSubstr(0,t._kmwLength()-dn)+s; else t=t+s; keymanweb.setTextBeforeCaret(Pelem,t); return; } if (keymanweb.legacy) { if(dn>0) Pelem.value=Pelem.value._kmwSubstr(0,Pelem.value._kmwLength()-dn)+s; //I3319 else Pelem.value=Pelem.value+s; } 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 { /* Editable iframe and contentEditable elements for mozilla */ var _IsEditableIframe = Ldoc.designMode.toLowerCase() == 'on'; if(_IsEditableIframe) var _CacheableCommands = keymanweb._CacheCommands(Ldoc); var Lsel = Ldv.getSelection(); var LselectionStart = Lsel.focusNode.nodeValue ? Lsel.focusNode.substringData(0,Lsel.focusOffset)._kmwLength() : 0; // I3319 if(!Lsel.isCollapsed) Lsel.deleteFromDocument(); // I2134, I2192 //KeymanWeb._Debug('KO: focusOffset='+Lsel.focusOffset+', dn='+dn+', s='+s+' focusNode.type='+Lsel.focusNode.nodeType+', focusNode.parentNode.tagName='+(Lsel.focusNode.parentNode?Lsel.focusNode.parentNode.tagName:'NULL') ); var Lrange = Lsel.getRangeAt(0); if(dn > 0) { Lrange.setStart(Lsel.focusNode, Lsel.focusOffset - Lsel.focusNode.nodeValue.substr(0,Lsel.focusOffset)._kmwSubstr(-dn).length); // I3319 Lrange.deleteContents(); } //KeymanWeb._Debug('KO: focusOffset='+Lsel.focusOffset+', dn='+dn+', s='+s+' focusNode.type='+Lsel.focusNode.nodeType+', focusNode.parentNode.tagName='+(Lsel.focusNode.parentNode?Lsel.focusNode.parentNode.tagName:'NULL') ); if(s._kmwLength() > 0) // I2132 - exception if s.length > 0, I3319 { if(Lsel.focusNode.nodeType == 3) { // I2134, I2192 // Already in a text node //KeymanWeb._Debug('KO: Already in a text node, adding "'+s+'": '+Lsel.focusOffset + '-> '+Lsel.toString()); var LfocusOffset = Lsel.focusOffset; //KeymanWeb._Debug('KO: node.text="'+Lsel.focusNode.data+'", node.length='+Lsel.focusNode.length); Lsel.focusNode.insertData(Lsel.focusOffset, s); try { Lsel.extend(Lsel.focusNode, LfocusOffset + s.length); } catch(e) { // Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection. scrollIntoView synchronises the content for selection Lsel.focusNode.parentNode.scrollIntoView(); Lsel.extend(Lsel.focusNode, LfocusOffset + s.length); } } else { // Create a new text node - empty control //KeymanWeb._Debug('KO: Creating a new text node for "'+s+'"'); var n = Ldoc.createTextNode(s); Lrange.insertNode(n); Lsel.extend(n,s.length); } } if(_IsEditableIframe) keymanweb._CacheCommandsReset(Ldoc, _CacheableCommands, null);// I2457 - support contentEditable elements in mozilla, webkit Lsel.collapseToEnd(); // Adjust deadkey positions if(dn >= 0) { keymanweb._DeadkeyDeleteMatched(); // I3318 keymanweb._DeadkeyAdjustPos(LselectionStart, -dn + s._kmwLength()); // I3318 } } // Internet Explorer (including IE9) else if(Ldoc && (Ldv=Ldoc.selection)) // build 77 - use elem.ownerDocument.selection { if(Ldoc.body.isContentEditable || Ldoc.designMode.toLowerCase()=='on') // I1295 - isContentEditable { var _CacheableCommands = keymanweb._CacheCommands(Ldoc); } var Lrange = Ldv.createRange(), Ls1; if(Lrange.text != '') { Ldv.clear(); dn = 0; } else Lrange.collapse(true); if(dn > 0) { Lrange.moveStart('character',-2*dn); // I3319 (next four lines var s0=Lrange.text,s1=s0._kmwSubstr(-dn); Lrange.collapse(false); //move start back to end Lrange.moveStart('character',-s1.length); } else dn = 0; Lrange.text = s; if(Ldoc.body.isContentEditable || Ldoc.designMode.toLowerCase()=='on') // I1295 - isContentEditable { Lrange.moveStart('character',-s.length); keymanweb._CacheCommandsReset(Ldoc, _CacheableCommands,Lrange.select); Lrange.moveStart('character',s.length); Lrange.select(); } // Adjust deadkey positions if(dn >= 0) { keymanweb._DeadkeyDeleteMatched(); // I3318 keymanweb._DeadkeyAdjustPos(LselectionStart, -dn + s._kmwLength()); // I3318 } keymanweb._Selection = Ldv.createRange(); keymanweb._Selection.select(); keymanweb._Selection.scrollIntoView(); } // Mozilla et al; IE9+ also recognizes setSelectionRange, but does not seem to work in exactly the same way as Mozilla else if (Pelem.setSelectionRange) { var LselectionStart, LselectionEnd; if(Pelem._KeymanWebSelectionStart != null) // changed to allow a value of 0 { LselectionStart = Pelem._KeymanWebSelectionStart; LselectionEnd = Pelem._KeymanWebSelectionEnd; } else { LselectionStart = Pelem.value._kmwCodeUnitToCodePoint(Pelem.selectionStart); // I3319 LselectionEnd = Pelem.value._kmwCodeUnitToCodePoint(Pelem.selectionEnd); // I3319 } var LscrollTop, LscrollLeft; if(Pelem.type.toLowerCase() == 'textarea' && typeof(Pelem.scrollTop) != 'undefined') { LscrollTop = Pelem.scrollTop; LscrollLeft = Pelem.scrollLeft; } if(dn < 0) // Don't delete, leave context alone (dn = -1) { Pelem.value = Pelem.value._kmwSubstring(0,LselectionStart) + s + Pelem.value._kmwSubstring(LselectionEnd); //I3319 dn = 0; } else if(LselectionStart < dn) Pelem.value = s + Pelem.value._kmwSubstring(LselectionEnd); //I3319 else Pelem.value = Pelem.value._kmwSubstring(0,LselectionStart-dn) + s + Pelem.value._kmwSubstring(LselectionEnd); //I3319 // Adjust deadkey positions if(dn >= 0) { keymanweb._DeadkeyDeleteMatched(); // I3318 keymanweb._DeadkeyAdjustPos(LselectionStart, -dn + s._kmwLength()); // I3318,I3319 } if (typeof(LscrollTop) != 'undefined') { Pelem.scrollTop = LscrollTop; Pelem.scrollLeft = LscrollLeft; } var caretPos=LselectionStart-dn+s._kmwLength(); // I3319 var caretPosUnits=Pelem.value._kmwCodePointToCodeUnit(caretPos); // I3319 Pelem.setSelectionRange(caretPosUnits,caretPosUnits); // I3319 Pelem._KeymanWebSelectionStart = null; Pelem._KeymanWebSelectionEnd = null; } // Refresh element content after change (if needed) if(typeof(keymanweb.refreshElementContent) == 'function') keymanweb.refreshElementContent(Pelem); } /** * Function KDO * Scope Public * @param {number} Pdn no of character to overwrite (delete) * @param {Object} Pelem element to output to * @param {number} Pd deadkey id * Description Record a deadkey at current cursor position, deleting Pdn characters first */ keymanweb['KDO'] = keymanweb.KDO = function(Pdn,Pelem,Pd) { keymanweb.cachedContext.reset(); var Lc = new Object(); if(Pdn >= 0) keymanweb.KO(Pdn,Pelem,""); //I3318 corrected to >= Lc.p=keymanweb._SelPos(Pelem); Lc.d=Pd; keymanweb._DeadKeys=keymanweb._push(keymanweb._DeadKeys,Lc); // _DebugDeadKeys(Pelem, 'KDeadKeyOutput: dn='+Pdn+'; deadKey='+Pd); } /** * Function KIO * Scope Public * @param {number} Pdn no of character to overwrite (delete) * @param {string} Ps string * @param {number} Pn index * @param {Object} Pelem element to output to * Description Output a character selected from the string according to the offset in the index array */ keymanweb['KIO'] = keymanweb.KIO = function(Pdn,Ps,Pn,Pelem) { keymanweb.cachedContext.reset(); if(keymanweb._AnyIndices[Pn-1] < Ps._kmwLength()) //I3319 keymanweb.KO(Pdn,Pelem,Ps._kmwCharAt(keymanweb._AnyIndices[Pn-1])); //I3319 } /** * Function _CacheCommands * Scope Private * @param {Object} _Document * @return {Array.} List of style commands that are cacheable * Description Build reate list of styles that can be applied in iframes */ keymanweb._CacheCommands = function(_Document) // I1204 - style application in IFRAMEs, I2192, I2134, I2192 { //var _CacheableBackColor=(_Document.selection?'hilitecolor':'backcolor'); var _CacheableCommands=[['backcolor',1],['fontname',1],['fontsize',1],['forecolor',1],['bold',0],['italic',0],['strikethrough',0],['subscript',0],['superscript',0],['underline',0]]; if(_Document.defaultView) keymanweb._push(_CacheableCommands,['hilitecolor',1]); for(var n=0;n < _CacheableCommands.length; n++) // I1511 - array prototype extended { //KeymanWeb._Debug('Command:'+_CacheableCommands[n][0]); keymanweb._push(_CacheableCommands[n],_CacheableCommands[n][1] ? _Document.queryCommandValue(_CacheableCommands[n][0]) : _Document.queryCommandState(_CacheableCommands[n][0])); } return _CacheableCommands; } /** * Function _CacheCommandReset * Scope Private * @param _Document * _CacheableCommands * _func * @return Nothing * Description Restore styles in IFRAMEs (??) */ keymanweb._CacheCommandsReset = function(_Document, _CacheableCommands, _func) { for(var n=0;n < _CacheableCommands.length; n++) // I1511 - array prototype extended { //KeymanWeb._Debug('ResetCacheCommand:'+_CacheableCommands[n][0]+'='+_CacheableCommands[n][2]); if(_CacheableCommands[n][1]) { if(_Document.queryCommandValue(_CacheableCommands[n][0]) != _CacheableCommands[n][2]) { if(_func)_func(); _Document.execCommand(_CacheableCommands[n][0], false, _CacheableCommands[n][2]); } } else if(_Document.queryCommandState(_CacheableCommands[n][0]) != _CacheableCommands[n][2]) {if(_func)_func(); //KeymanWeb._Debug('executing command '+_CacheableCommand[n][0]); _Document.execCommand(_CacheableCommands[n][0], false, null); } } } /** * KIFS compares the content of a system store with a string value * * @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported) * @param {string} strValue String value to compare to * @param {Object} Pelem Currently active element (may be needed by future tests) * @return {boolean} True if the test succeeds */ keymanweb['KIFS'] = keymanweb.KIFS = function(systemId,strValue,Pelem) { var result=true; if(systemId == keymanweb.TSS_LAYER) result = (osk.layerId === strValue); else if(systemId == keymanweb.TSS_PLATFORM) { var i,constraint,constraints=strValue.split(' '); for(i=0; i