/*** KeymanWeb 2.0 Copyright 2014 Tavultesoft Pty Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ***/ (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') 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) { // Clear the load failure timer, but only if this is the keyboard // that is currently pending (KMEW-101) if(keymanweb.loadTimer && (Pk['KI'] == keymanweb._LoadingInternalName)) { window.clearTimeout(keymanweb.loadTimer); keymanweb.loadTimer=null; } // 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; // The following should never be needed or even appropriate - the keyboard object does not // override stub information, but simply adds to it. The stub contains meta-data which either // overriddes or extends the keyboard object data. (Sept 2014) // 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']); } /** * 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} Pelem 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, n+1, Ptarg); if(cx === false) return true; else return (cx._kmwLength() < n+1); // I2243 - support nul in context; I3319 } /** * 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) if(e.vkCode > 255) keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts if(e.LisVirtualKey || keyCode > 255) // added keyCode test for same reason { if((Lruleshift & 0x4000) == 0x4000 || (keyCode > 255)) // added keyCode test to support extended keys { retVal = ((Lrulekey == keyCode) && ((Lruleshift&0x7F) == e.Lmodifiers)); //I3318, I3555 } } else if((Lruleshift & 0x4000) == 0) { retVal = (keyCode == Lrulekey); // I3318, I3555 } if(!retVal) this._DeadkeyResetMatched(); // I3318 return retVal; // I3318 }; /** * Function KKI * Scope Public * @param {number} n * @param {Object} Ptarg * @param {string} val * @param {number} ln * @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 1; // 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 {numebr} 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