spiegel-keyman/web/source/keymanweb.js

4298 lines
153 KiB
JavaScript

/***
KeymanWeb 2.0
Copyright 2017 SIL International
***/
/******************************************************************
* Main Keyman Web Module
*
* Code enclosed as an anonymous function to protect name space
*
******************************************************************/
(function()
{
// Declare KeymanWeb, OnScreen Keyboard and Util objects
var keymanweb=window['tavultesoft']['keymanweb'],osk=keymanweb['osk'],util=keymanweb['util'],device=util.device;
/**
* Function debug
* Scope Private
* @param {(string|Object)} s string (or object) to print
* Description Simple debug display (upper right of screen)
* Extended to support multiple arguments May 2015
*/
keymanweb['debug']=keymanweb.debug=function(s){
var p;
if(keymanweb.debugElement == null)
{
var d=document.createElement('DIV'),ds=d.style;
ds.position='absolute';ds.width='30%';ds.maxHeight='50%';ds.top='0';ds.right='0';
ds.minHeight='50px'; ds.border='1px solid blue'; ds.whiteSpace='pre-line';ds.overflowY='scroll';
p=document.createElement('P'); p.id='debug_output';p.style.margin='2px';
d.appendChild(p);
document.body.appendChild(d);
keymanweb.debugElement=p;
}
if((p=document.getElementById('debug_output')) == null) return;
if(arguments.length == 0)
if(typeof p.textContent != 'undefined') p.textContent=''; else p.innerHTML='';
else
{
var ts=new Date().toTimeString().substr(3,5),t=ts+' ',t1,k,m,sx;
for(k=0; k<arguments.length; k++)
{
if(k > 0) t = t + '; ';
sx = arguments[k];
if(typeof sx == 'object')
{
if(sx == null)
{
t = t + 'null';
}
else
{
t1 = '';
for(m in sx)
{
if(t1.length > 0) t1 = t1 + ', ';
t1 = t1 + m + ':';
switch(typeof sx[m])
{
case 'string':
case 'number':
case 'boolean':
t1 = t1 + sx[m]; break;
default:
t1 = t1 + typeof sx[m]; break;
}
if(t1.length > 1024)
{
t1 = t1.substr(0,1000)+'...'; break;
}
}
if(t1.length > 0) t = t + '{' + t1 + '}';
}
}
else
{
t = t + sx;
}
}
// Truncate if necessary to avoid memory problems
if(t.length > 1500) t = t.substr(0,1500) + ' (more)';
if(typeof p.textContent != 'undefined')
p.textContent=t+'\n'+p.textContent;
else
p.innerHTML=t+'<br />'+p.innerHTML;
}
}
keymanweb.debugElement=null;
var dbg=keymanweb.debug;
/**
* Function addEventListener
* Scope Public
* @param {string} event event to handle
* @param {function(Event)} func event handler function
* @return {boolean} value returned by util.addEventListener
* Description Wrapper function to add and identify KeymanWeb-specific event handlers
*/
keymanweb['addEventListener'] = keymanweb.addEventListener = function(event, func)
{
return util.addEventListener('kmw.'+event, func);
}
/**
* Function setUpTouchDevice
* Scope Private
* Description Initialize event handling and duplicate input fields for touch-input devices
*/
keymanweb.setupTouchDevice = function()
{
/**
* Ideally, OSK behaviour should emulate internal keyboard, but
* it is not possible to do that while allowing native scrolling.
* The compromise adopted is that a touchstart or touchmove event
* on any part of the page other than an input element or the OSK
* itself will temporarily hide the OSK until the touchend or
* window.scroll event is fired. Hiding the OSK in this way seems
* less disturbing than having it move as the page is scrolled.
*
* All of this may be better if we can reliably get the y-offset
* from the CSS transform and apply that to repositioning the OSK
* using a timed event loop.
*/
/**
* Close OSK and remove simulated caret on losing focus
*/
keymanweb.cancelInput = function()
{
keymanweb._ActiveElement=null;
keymanweb.hideCaret();
osk.hideNow();
}
/**
* Handle losing focus from simulated input field
*
* @param {Event} e event
*/
keymanweb.setBlur = function(e)
{
// This works OK for iOS, but may need something else for other platforms
if(('relatedTarget' in e) && e.relatedTarget)
{
if(e.relatedTarget.nodeName != 'DIV' || e.relatedTarget.className.indexOf('keymanweb-input') == -1)
{
keymanweb.cancelInput(); return;
}
}
//Hide the OSK
if(!keymanweb.focusing) keymanweb.cancelInput();
}
/**
* Handle receiving focus by simulated input field
*
* @param {Event=} e event
*/
keymanweb.setFocus=function(e)
{
//e.stopPropagation(); // not doing anything useful, no child elements
//e.preventDefault(); // prevents user selection or scrolling, may be better if they are allowed?
keymanweb.focusing=true;
keymanweb.focusTimer=window.setTimeout(function(){keymanweb.focusing=false;},1000);
var tEvent=e;
if(e)
{
if(typeof e.touches == 'object') tEvent=e.touches[0];
}
// Allow external code to set focus and thus display the OSK on touch devices if required (KMEW-123)
else
{
tEvent={clientX:0,clientY:0}
// Will usually be called from setActiveElement, which should define _LastActiveElement
if(keymanweb._LastActiveElement)
tEvent.target = keymanweb._LastActiveElement['kmw_ip'];
// but will default to first input or text area on page if _LastActiveElement is null
else
tEvent.target = keymanweb.sortedInputs[0]['kmw_ip'];
}
var touchX=tEvent.clientX,touchY=tEvent.clientY,tTarg=tEvent.target,scroller;
// Identify the scroller element
if(tTarg.nodeName == 'SPAN')
scroller=tTarg.parentNode;
else if(tTarg.className != null && tTarg.className.indexOf('keymanweb-input') >= 0)
scroller=tTarg.firstChild;
else
scroller=tTarg;
// And the actual target element
var target=scroller.parentNode;
// Move the caret and refocus if necessary
if(keymanweb._ActiveElement != target)
{
// Hide the KMW caret
keymanweb.hideCaret();
keymanweb._ActiveElement=target;
// The issue here is that touching a DIV does not actually set the focus for iOS, even when enabled to accept focus (by setting tabIndex=0)
// We must explicitly set the focus in order to remove focus from any non-KMW input
target.focus(); //Android native browsers may not like this, but it is needed for Chrome, Safari
}
// Correct element directionality if required
keymanweb._SetTargDir(target);
// What we really want to do is to blur any active element that is not a KMW input,
// but the following line does not work as might be expected, even though the correct element is referenced.
// It is as though blur is ignored if focus is supposed to have been moved, even if it has not in fact been moved?
//if(document.activeElement.nodeName != 'DIV' && document.activeElement.nodeName != 'BODY') document.activeElement.blur();
// Refresh the internal keyboard flag state
keymanweb.useInternalKeyboard=false;
// And display the OSK if not already visible
if(osk.ready && !osk._Visible) osk._Show();
// If clicked on DIV, set caret to end of text
if(tTarg.nodeName == 'DIV' )
{
var x,cp;
x=util._GetAbsoluteX(scroller.firstChild);
if(target.dir == 'rtl')
{
x += scroller.firstChild.offsetWidth;
cp=(touchX > x ? 0 : 100000);
}
else
cp=(touchX<x ? 0 : 100000);
keymanweb.setTextCaret(target,cp);
keymanweb.scrollInput(target);
}
// Otherwise, if clicked on text in SPAN, set at touch position
else
{
var caret,cp,cpMin,cpMax,x,y,dy,yRow,iLoop;
caret=scroller.childNodes[1]; //caret span
cpMin=0;
cpMax=keymanweb.getText(target)._kmwLength();
cp=keymanweb.getTextCaret(target);
dy=document.body.scrollTop;
// Vertical scrolling
if(target.base.nodeName == 'TEXTAREA')
{
yRow=Math.round(target.base.offsetHeight/target.base.rows);
for(iLoop=0; iLoop<16; iLoop++)
{
y=util._GetAbsoluteY(caret)-dy; //top of caret
if(y > touchY && cp > cpMin && cp != cpMax) {cpMax=cp; cp=Math.round((cp+cpMin)/2);}
else if(y < touchY-yRow && cp < cpMax && cp != cpMin) {cpMin=cp; cp=Math.round((cp+cpMax)/2);}
else break;
keymanweb.setTextCaret(target,cp);
}
while(util._GetAbsoluteY(caret)-dy > touchY && cp > cpMin)keymanweb.setTextCaret(target,--cp);
while(util._GetAbsoluteY(caret)-dy < touchY-yRow && cp < cpMax) keymanweb.setTextCaret(target,++cp);
}
// Caret repositioning for horizontal scrolling of RTL text
if(target.dir == 'rtl')
{
for(iLoop=0; iLoop<16; iLoop++)
{
x=util._GetAbsoluteX(caret); //left of caret
if(x < touchX && cp > cpMin && cp != cpMax) {cpMax=cp; cp=Math.round((cp+cpMin)/2);}
else if(x > touchX && cp < cpMax && cp != cpMin) {cpMin=cp; cp=Math.round((cp+cpMax)/2);}
else break;
keymanweb.setTextCaret(target,cp);
}
while(util._GetAbsoluteX(caret) < touchX && cp > cpMin) keymanweb.setTextCaret(target,--cp);
while(util._GetAbsoluteX(caret) > touchX && cp < cpMax) keymanweb.setTextCaret(target,++cp);
}
// Caret repositioning for horizontal scrolling of standard (LTR) text
else
{
for(iLoop=0; iLoop<16; iLoop++) // assumes fields shorter than 2**16 characters
{
x=util._GetAbsoluteX(caret); //left of caret
if(x > touchX && cp > cpMin && cp != cpMax)
{
cpMax=cp; cp=Math.round((cp+cpMin)/2);
}
else if(x < touchX && cp < cpMax && cp != cpMin)
{
cpMin=cp; cp=Math.round((cp+cpMax)/2);
}
else break;
keymanweb.setTextCaret(target,cp);
}
while(util._GetAbsoluteX(caret) > touchX && cp > cpMin) keymanweb.setTextCaret(target,--cp);
while(util._GetAbsoluteX(caret) < touchX && cp < cpMax) keymanweb.setTextCaret(target,++cp);
}
}
// TODO: The following is copied from old control focus... not sure how much is needed
keymanweb._ActiveControl = null;
keymanweb._LastActiveElement = target;
for(var Ln=0; Ln < keymanweb._Controls.length; Ln++) // I1511 - array prototype extended
if(keymanweb._Controls[Ln].LControl == target)
{
keymanweb._ActiveControl = keymanweb._Controls[Ln];
break;
}
if(keymanweb._ActiveControl != null && keymanweb._ActiveControl.LDefaultInternalName != null)
{
if(!keymanweb._JustActivatedKeymanWebUI)
{
keymanweb._SetActiveKeyboard(keymanweb._ActiveControl.LDefaultInternalName,'',true);
}
else
keymanweb._ActiveControl.LDefaultInternalName = keymanweb._ActiveKeyboard == null ? '' : keymanweb._ActiveKeyboard['KI'];
}
//TODO: the logic of the following line doesn't look right!! Both variables are true, but that doesn't make sense!
//_Debug(keymanweb._IsIEEditableIframe(Ltarg,1) + '...' +keymanweb._IsMozillaEditableIframe(Ltarg,1));
if(!keymanweb._IsIEEditableIframe(target,1) || !keymanweb._IsMozillaEditableIframe(target,1))
{
keymanweb._DisableInput = 1; return;
}
keymanweb._DisableInput = 0;
if(!keymanweb._JustActivatedKeymanWebUI)
{
keymanweb._DeadKeys = [];
keymanweb._NotifyKeyboard(0,target,1); // I2187
}
if(!keymanweb._JustActivatedKeymanWebUI && keymanweb._SelectionControl != target)
keymanweb._IsActivatingKeymanWebUI = 0;
keymanweb._JustActivatedKeymanWebUI = 0;
keymanweb._SelectionControl = target;
}
/**
* Create a caret to be appended to the scroller of the focussed input field.
* The caret is appended to the scroller so that it will automatically be clipped
* when the user manually scrolls it outside the element boundaries.
* It is positioned exactly over the hidden span that is inserted between the
* text spans before and after the insertion point.
*/
keymanweb.caret=document.createElement('DIV');
var cs=keymanweb.caret.style;
cs.position='absolute';
cs.height='16px'; // default height, actual height set from element properties
cs.width='2px';
cs.backgroundColor='blue';
cs.border='none';
cs.left=cs.top='0px'; // actual position set relative to parent when displayed
cs.display='block';
cs.visibility='hidden';
cs.zIndex='9998'; // immediately below the OSK
/**
* Position the caret at the start of the second span within the scroller
*
* @param {Object} e input DIV element (copy of INPUT or TEXTAREA)
*/
keymanweb.showCaret=function(e)
{
if(!e || !e.firstChild || (e.firstChild.childNodes.length<3)) return;
var scroller=e.firstChild,cs=keymanweb.caret.style,sp2=scroller.childNodes[1];
// Attach the caret to this scroller and position it over the caret span
if(keymanweb.caret.parentNode != scroller) scroller.appendChild(keymanweb.caret);
cs.left=sp2.offsetLeft+'px';
cs.top=sp2.offsetTop+'px';
cs.height=(sp2.offsetHeight-1)+'px';
cs.visibility='hidden'; // best to wait for timer to display caret
// Scroll into view if required
keymanweb.scrollBody(e);
// Display and position the scrollbar if necessary
keymanweb.setScrollBar(e);
}
/**
* Display and position a scrollbar in the input field if needed
*
* @param {Object} e input DIV element (copy of INPUT or TEXTAREA)
*/
keymanweb.setScrollBar=function(e)
{
// Display the scrollbar if necessary. Added TEXTAREA condition to correct rotation issue KMW-5. Fixed for 310 beta.
var scroller=e.childNodes[0],sbs=e.childNodes[1].style;
if((scroller.offsetWidth > e.offsetWidth || scroller.offsetLeft < 0) && (e.base.nodeName != 'TEXTAREA'))
{
sbs.height='4px';
sbs.width=100*(e.offsetWidth/scroller.offsetWidth)+'%';
sbs.left=100*(-scroller.offsetLeft/scroller.offsetWidth)+'%';
sbs.top='0';
sbs.visibility='visible';
}
else if(scroller.offsetHeight > e.offsetHeight || scroller.offsetTop < 0)
{
sbs.width='4px';
sbs.height=100*(e.offsetHeight/scroller.offsetHeight)+'%';
sbs.top=100*(-scroller.offsetTop/scroller.offsetHeight)+'%';
sbs.left='0';
sbs.visibility='visible';
}
else
{
sbs.visibility='hidden';
}
}
/**
* Description Toggle state of caret in simulated input field
*/
keymanweb.flashCaret = function()
{
if(device.touchable && keymanweb._ActiveElement != null)
{
var cs=keymanweb.caret.style;
if(cs.visibility != 'visible') cs.visibility='visible'; else cs.visibility='hidden';
}
}
/**
* Hide caret in simulated input field, update underlying INPUT or TEXTAREA
*/
keymanweb.hideCaret = function()
{
var e=keymanweb._LastActiveElement,s=null;
if(e && e.className != null && e.className.indexOf('keymanweb-input') >= 0)
{
// Always copy text back to underlying field on blur
e.base.value = keymanweb.getText(e);
// And set the scroller caret to the end of the element content
keymanweb.setText(e,null,100000);
// Set the element scroll to zero (or max for RTL INPUT)
var ss=e.firstChild.style;
if(e.base.nodeName == 'TEXTAREA')
ss.top='0';
else
{
if(e.base.dir == 'rtl') ss.left=(e.offsetWidth-e.firstChild.offsetWidth-8)+'px';
else ss.left='0';
}
// And hide the caret and scrollbar
if(keymanweb.caret.parentNode) keymanweb.caret.parentNode.removeChild(keymanweb.caret);
keymanweb.caret.style.visibility='hidden';
if(e.childNodes.length > 1 ) e.childNodes[1].style.visibility='hidden';
}
}
// Start the caret flash timer
keymanweb.timerId = window.setInterval(keymanweb.flashCaret,500);
/**
* Insert text into simulated input field at indicated character position
*
* @param {Object} e simulated input field DIV
* @param {?string} t text to insert in element
* @param {?number} cp caret position (characters)
*/
keymanweb.setText = function(e,t,cp)
{
if(e && e.childNodes.length > 0)
{
var d=e.firstChild,tLen=0;
if(d.childNodes.length >= 3)
{
var s1=d.childNodes[0],s2=d.childNodes[2],t1,t2;
// Read current text if null passed (for caret positioning)
if(t === null)
{
t1=(typeof(s1.textContent) == 'string' ? s1.textContent : s1.innerText);
t2=(typeof(s2.textContent) == 'string' ? s2.textContent : s2.innerText);
t=t1+t2;
}
if(cp < 0) cp = 0; //if(typeof t._kmwLength == 'undefined') return;
tLen=t._kmwLength();
if(cp === null || cp > tLen) cp=tLen;
t1=t._kmwSubstr(0,cp); t2=t._kmwSubstr(cp);
if(typeof(s1.textContent) == 'string') s1.textContent=t1; else s1.innerText=t1;
if(typeof(s2.textContent) == 'string') s2.textContent=t2; else s2.innerText=t2;
}
}
keymanweb.updateBaseElement(e,tLen); // KMW-3, KMW-29
}
/**
* Set content, visibility, background and borders of input and base elements (KMW-3,KMW-29)
*
* @param {Object} e input element
* @param {number} n length of text in field
*/
keymanweb.updateBaseElement=function(e,n)
{
e.base.value=keymanweb.getText(e); //KMW-29
e.style.backgroundColor=(n==0?'transparent':window.getComputedStyle(e.base,null).backgroundColor);
if(device.OS == 'iOS')
{
e.base.style.visibility=(n==0?'visible':'hidden');
}
}
/**
* Get simulated input field content
*
* @param {Object} e simulated input field DIV
* @return {string} entire text in simulated input field
*/
keymanweb.getText=function(e)
{
if(e == null) return '';
return (typeof(e.textContent) == 'string' ? e.textContent : e.innerText);
}
/**
* Get text up to the caret from simulated input field
*
* @param {Object} e simulated input field DIV
* @return {string} Context for simulated input field
*/
keymanweb.getTextBeforeCaret=function(e)
{
if(e && e.childNodes.length > 1)
{
var d=e.firstChild;
if(d.childNodes.length > 0)
{
var s1=d.childNodes[0];
if('textContent' in s1)
return s1.textContent;
if('innerText' in s1)
return s1.innerText;
}
}
return '';
}
/**
* Replace text up to the caret in the simulated input field
*
* @param {Object} e simulated input field DIV
* @param {string} t string to insert
*/
keymanweb.setTextBeforeCaret=function(e,t)
{
if(e && e.childNodes.length > 0)
{
var d=e.firstChild,tLen=0;
if(d.childNodes.length > 1)
{
var s1=d.childNodes[0],s2=d.childNodes[2];
// Collapse (trailing) whitespace to a single space for INPUT fields (also prevents wrapping)
if(e.base.nodeName != 'TEXTAREA') t=t.replace(/\s+$/,' ');
if('textContent' in s1) s1.textContent=t;
else if('innerText' in s1) s1.innerText=t;
// Test total length in order to control base element visibility
tLen=t.length;
if('textContent' in s2) tLen=tLen+s2.textContent.length;
else if('innerText' in s2) tLen=tLen+s2.innerText.length;
}
}
// Update the base element then scroll into view if necessary
keymanweb.updateBaseElement(e,tLen); //KMW-3, KMW-29
keymanweb.scrollInput(e);
}
/**
* Description Get current position of caret in simulated input field
*
* @param {Object} e simulated input field DIV
* @return {number} caret character position in simulated input field
*/
keymanweb.getTextCaret=function(e)
{
return keymanweb.getTextBeforeCaret(e)._kmwLength();
}
/**
* Set current position of caret in simulated input field then display the caret
*
* @param {Object} e element (object) or element id (string) of simulated input field
* @param {number} cp caret position (character index)
*/
keymanweb.setTextCaret=function(e,cp)
{
keymanweb.setText(e,null,cp);
keymanweb.showCaret(e);
}
/**
* Handle the touch move event for an input element
*
* @param {Event} e touchmove event
*/
keymanweb.dragInput=function(e)
{
// Prevent dragging window
e.preventDefault(); e.stopPropagation();
// Identify the target from the touch list or the event argument (IE 10 only)
var target=(typeof e.targetTouches == 'object' ? e.targetTouches[0].target : e.target);
if(target == null) return;
// Identify the input element from the touch event target (touched element may be contained by input)
if(target.className == null || target.className.indexOf('keymanweb-input') < 0) target=target.parentNode;
if(target.className == null || target.className.indexOf('keymanweb-input') < 0) target=target.parentNode;
if(target.className == null || target.className.indexOf('keymanweb-input') < 0) return;
var x=(typeof e.touches == 'object' ? e.touches[0].screenX : e.screenX),
y=(typeof e.touches == 'object' ? e.touches[0].screenY : e.screenY);
// Allow content of input elements to be dragged horizontally or vertically
if(typeof keymanweb.firstTouch == 'undefined' || keymanweb.firstTouch == null)
keymanweb.firstTouch={x:x,y:y};
else
{
var x0=keymanweb.firstTouch.x,y0=keymanweb.firstTouch.y,
scroller=target.firstChild,dx,dy,x1;
if(target.base.nodeName == 'TEXTAREA')
{
var yOffset=parseInt(scroller.style.top,10);
if(isNaN(yOffset)) yOffset=0;
dy=y0-y;
if(dy < -4 || dy > 4)
{
scroller.style.top=(yOffset<dy?yOffset-dy:0)+'px';
keymanweb.firstTouch.y=y;
}
}
else
{
var xOffset=parseInt(scroller.style.left,10);
if(isNaN(xOffset)) xOffset=0;
dx=x0-x;
if(dx < -4 || dx > 4)
{
// Limit dragging beyond the defined text (to avoid dragging the text completely out of view)
var xMin=0,xMax=util._GetAbsoluteX(target)+target.offsetWidth-scroller.offsetWidth-32;
if(target.base.dir == 'rtl')xMin=16; else xMax=xMax-24;
x1=xOffset-dx;
if(x1 > xMin) x1=xMin;
if(x1 < xMax) x1=xMax;
scroller.style.left=x1+'px';
keymanweb.firstTouch.x=x;
}
}
}
keymanweb.setScrollBar(target);
}
/**
* Scroll the input field horizontally (INPUT base element) or
* vertically (TEXTAREA base element) to bring the caret into view
* as text is entered or deleted form an element
*
* @param {Object} e simulated input field object with focus
*/
keymanweb.scrollInput=function(e)
{
if(!e || !e.firstChild || e.className == null || e.className.indexOf('keymanweb-input') < 0 ) return;
var scroller=e.firstChild;
if(scroller.childNodes.length < 3) return;
// Get the actual absolute position of the caret and the element
var s2=scroller.childNodes[1],
cx=util._GetAbsoluteX(s2),cy=util._GetAbsoluteY(s2),
ex=util._GetAbsoluteX(e),ey=util._GetAbsoluteY(e),
x=parseInt(scroller.style.left,10),
y=parseInt(scroller.style.top,10),
dx=0,dy=0;
// Scroller offsets must default to zero
if(isNaN(x)) x=0; if(isNaN(y)) y=0;
// Scroll input field vertically if necessary
if(e.base.nodeName == 'TEXTAREA')
{
var rowHeight=Math.round(e.offsetHeight/e.base.rows);
if(cy < ey) dy=cy-ey;
if(cy > ey+e.offsetHeight-rowHeight) dy=cy-ey-e.offsetHeight+rowHeight;
if(dy != 0)scroller.style.top=(y<dy?y-dy:0)+'px';
}
// or scroll horizontally if needed
else
{
if(cx < ex+8) dx=cx-ex-12;
if(cx > ex+e.offsetWidth-12) dx=cx-ex-e.offsetWidth+12;
if(dx != 0)scroller.style.left=(x<dx?x-dx:0)+'px';
}
// Display the caret (and scroll into view if necessary)
keymanweb.showCaret(e);
}
/**
* Scroll the document body vertically to bring the active input into view
*
* @param {Object} e simulated input field object being focussed
*/
keymanweb.scrollBody=function(e)
{
if(!e || e.className == null || e.className.indexOf('keymanweb-input') < 0 || !osk.ready) return;
// Get the absolute position of the caret
var s2=e.firstChild.childNodes[1],y=util._GetAbsoluteY(s2),t=window.pageYOffset,dy=0;
if(y < t)
{
dy=y-t;
}
else
{
dy=y-t-(window.innerHeight-osk._Box.offsetHeight-s2.offsetHeight-2);
if(dy < 0) dy=0;
}
// Hide OSK, then scroll, then re-anchor OSK with absolute position (on end of scroll event)
if(dy != 0)
window.scrollTo(0,dy+window.pageYOffset);
}
/**
* Correct the position and size of a duplicated input element
* @param {Element} x simulated input element
**/
keymanweb.updateInput = function(x)
{
var xs=x.style,b=x.base,
s=window.getComputedStyle(b,null),
mLeft=parseInt(s.marginLeft,10),
mTop=parseInt(s.marginTop,10),
x1=util._GetAbsoluteX(b),y1=util._GetAbsoluteY(b);
var p=x.offsetParent;
if(p)
{
x1=x1-util._GetAbsoluteX(p); y1=y1-util._GetAbsoluteY(p);
}
if(isNaN(mLeft)) mLeft=0; if(isNaN(mTop)) mTop=0;
xs.left=(x1-mLeft)+'px'; xs.top=(y1-mTop)+'px';
// FireFox does not want the offset!
if(typeof(s.MozBoxSizing) != 'undefined') {xs.left=x1+'px'; xs.top=y1+'px';}
var w=b.offsetWidth,h=b.offsetHeight,
pLeft=parseInt(s.paddingLeft,10),pRight=parseInt(s.paddingRight,10),
pTop=parseInt(s.paddingTop,10),pBottom=parseInt(s.paddingBottom,10),
bLeft=parseInt(s.borderLeft,10),bRight=parseInt(s.borderRight,10),
bTop=parseInt(s.borderTop,10),bBottom=parseInt(s.borderBottom,10);
// If using content-box model, must subtract the padding and border,
// but *not* for border-box (as for WordPress PlugIn)
var boxSizing='undefined';
if(typeof(s.boxSizing) != 'undefined') boxSizing=s.boxSizing;
else if(typeof(s.MozBoxSizing) != 'undefined') boxSizing=s.MozBoxSizing;
if(boxSizing == 'content-box')
{
if(!isNaN(pLeft)) w -= pLeft; if(!isNaN(pRight)) w -= pRight;
if(!isNaN(bLeft)) w -= bLeft; if(!isNaN(bRight)) w -= bRight;
if(!isNaN(pTop)) h -= pTop; if(!isNaN(pBottom)) h -= pBottom;
if(!isNaN(bTop)) h -= bTop; if(!isNaN(bBottom)) h -= bBottom;
}
if(device.OS == 'Android')
{
// FireFox - adjust padding to match input and text area defaults
if(typeof(s.MozBoxSizing) != 'undefined')
{
xs.paddingTop=(pTop+1)+'px';
xs.paddingLeft=pLeft+'px';
if(x.base.nodeName == 'TEXTAREA')
xs.marginTop='1px';
else
xs.marginLeft='1px';
w--;h--;
}
// Chrome, Opera, native browser (?)
else
{
w++;h++;
}
}
xs.width=w+'px'; xs.height=h+'px';
}
/**
* Function isKMWInput
* Scope Private
* @param {Element} x An element from the page.
* @return {boolean} true if the element accepts touch input.
* Description Examines potential input elements to determine their classification re: touch handling.
*/
keymanweb.isKMWInput = function(x) {
var c, lcTagName = x.tagName.toLowerCase();
// In case this function is called more than once on the same element, we need a quick out.
if(x.kmwInput) return x.kmwInput;
if(lcTagName == 'textarea') {
c = x.className;
x.kmwInput=false;
if((!c || c.indexOf('kmw-disabled') < 0) && !x.readOnly) {
x.disabled=true; x.kmwInput=true;
}
} else if(lcTagName == 'input') {
c=x.className;
x.kmwInput=false;
if((!c || c.indexOf('kmw-disabled') < 0) && !x.readOnly) {
if(x.type == 'text' || x.type == 'search') {
x.disabled=true; x.kmwInput=true;
}
}
}
return x.kmwInput;
}
/**
* Function setupTouchPage
* Scope Private
* Description Saves the list of static inputs for touch devices (touch-based browsers, mobile) and
* and completes their setup.
*/
keymanweb.setupTouchPage = function() {
// Initialize and protect input elements for touch-screen devices (but never for apps)
// NB: now set disabled=true rather than readonly, since readonly does not always
// prevent element from getting focus, e.g. within a LABEL element.
// c.f. http://kreotekdev.wordpress.com/2007/11/08/disabled-vs-readonly-form-fields/
var ipList = [].concat(
util.arrayFromNodeList(document.getElementsByTagName('input')),
util.arrayFromNodeList(document.getElementsByTagName('textarea'))
);
for(var n=0;n<ipList.length;n++) {
keymanweb.setupTouchElement(ipList[n]);
}
}
/**
* Function setupTouchElement
* Scope Private
* @param {Element} Pelem An input or textarea element from the page.
* @return {boolean} Returns true if it creates a simulated input element for Pelem; false if not.
* Description Creates a simulated input element for the specified INPUT or TEXTAREA, comprising:
* an outer DIV, matching the position, size and style of the base element
* a scrollable DIV within that outer element
* two SPAN elements within the scrollable DIV, to hold the text before and after the caret
*
* The left border of the second SPAN is flashed on and off as a visible caret
*
* Also ensures the element is registered on keymanweb's internal input list.
*/
keymanweb.setupTouchElement = function(Pelem)
{
// Touch doesn't worry about iframes.
if(Pelem.tagName.toLowerCase() == 'iframe') {
return false;
}
if(!keymanweb.isKMWInput(Pelem)) {
keymanweb.setupNonKMWTouchElement(Pelem);
return false;
}
/*
* Does this element already have a simulated touch element established? If so,
* just reuse it - if it isn't still in the input list!
*/
if(Pelem['kmw_ip']) {
if(keymanweb.inputList.indexOf(Pelem['kmw_ip']) != -1) {
return false;
}
keymanweb.inputList.push(Pelem['kmw_ip']);
console.log("Unexpected state - this element's simulated input DIV should have been removed from the page!");
return true; // May need setup elsewhere since it's just been re-added!
}
// The simulated touch element doesn't already exist? Time to initialize it.
var x=document.createElement('DIV');
x['base']=x.base=Pelem;
// Set font for base element
x.base.className = x.base.className ? x.base.className + ' keymanweb-font' : 'keymanweb-font';
// Add the exposed member 'kmw_ip' to allow page to refer to duplicated element
Pelem['kmw_ip']=x;
// Make sure the element's on the list of registered inputs. It can't NOT be, so we're fine.
keymanweb.inputList.push(x);
// Superimpose custom input fields for each input or textarea, unless readonly or disabled
// Copy essential styles from each base element to the new DIV
var d,s1,s2,s3,bs,xs,ds,ss1,ss2,ss3,x1,y1;
x.className='keymanweb-input';
x.dir=x.base.dir;
// Add a scrollable interior div
d=document.createElement('DIV');
bs=window.getComputedStyle(x.base,null);
xs=x.style;
xs.overflow='hidden';
xs.position='absolute';
//xs.border='1px solid gray';
xs.border='hidden'; // hide when element empty - KMW-3
xs.border='none';
xs.borderRadius='5px';
// Add a scroll bar (horizontal for INPUT elements, vertical for TEXTAREA elements)
var sb=document.createElement('DIV'), sbs=sb.style;
sbs.position='absolute';
sbs.height=sbs.width='4px';
sbs.left=sbs.top='0';
sbs.display='block';
sbs.visibility='hidden';
sbs.backgroundColor='#808080';
sbs.borderRadius='2px';
// And add two spans for the text content before and after the caret, and a caret span
s1=document.createElement('SPAN');
s2=document.createElement('SPAN');
s3=document.createElement('SPAN');
s1.innerHTML=s2.innerHTML=s3.innerHTML='';
s1.className=s2.className=s3.className='keymanweb-font';
d.appendChild(s1);d.appendChild(s3);d.appendChild(s2);
x.appendChild(d); x.appendChild(sb);
// Adjust input element properties so that it matches the base element as closely as possible
ds=d.style; ds.position='absolute';
ss1=s1.style;ss2=s2.style;ss3=s3.style;ss1.border=ss2.border='none';
//ss1.backgroundColor='rgb(220,220,255)';ss2.backgroundColor='rgb(220,255,220)'; //only for testing
ss1.height=ss2.height='100%';
ss1.fontFamily=ss2.fontFamily=ds.fontFamily=bs.fontFamily;
// Set vertical centering for input elements
if(x.base.nodeName.toLowerCase() == 'input') {
if(!isNaN(parseInt(bs.height,10))) {
ss1.lineHeight=ss2.lineHeight=bs.height;
}
}
// The invisible caret-positioning span must have a border to ensure that
// it remains in the layout, but colour doesn't matter, as it is never visible.
// Span margins are adjusted to compensate for the border and maintain text positioning.
ss3.border='1px solid red';
ss3.visibility='hidden';
ss3.marginLeft=ss3.marginRight='-1px';
// Set the outer element padding *after* appending the element,
// otherwise Firefox misaligns the two elements
xs.padding='8px';
// Set internal padding to match the TEXTAREA and INPUT elements
ds.padding='0px 2px'; // OK for iPad, possibly device-dependent
if(device.OS == 'Android' && bs.backgroundColor == 'transparent') {
ds.backgroundColor='#fff';
} else {
ds.backgroundColor=bs.backgroundColor;
}
// Set the tabindex to 0 to allow a DIV to accept focus and keyboard input
// c.f. http://www.w3.org/WAI/GL/WCAG20/WD-WCAG20-TECHS/SCR29.html
x.tabIndex='0';
// Disable (internal) pan and zoom on KMW input elements for IE10
x.style.msTouchAction='none';
// On touch event, reposition the text caret and prepare for OSK input
// Removed 'onfocus=' as that resulted in handling the event twice (on iOS, anyway)
x.addEventListener('touchstart', keymanweb.setFocus);
x.onmspointerdown=function(e) {
e.preventDefault();
e.stopPropagation();
return keymanweb.setFocus(e);
};
x.addEventListener('touchend', function(e) {
e.stopPropagation();
});
x.onmspointerup=function(e) {
e.stopPropagation();
};
// Disable internal scroll when input element in focus
x.addEventListener('touchmove', keymanweb.dragInput, false);
x.onmspointermove=keymanweb.dragInput;
// Hide keyboard and caret when losing focus from simulated input field
x.onblur=keymanweb.setBlur;
// Note that touchend event propagates and is processed by body touchend handler
// re-setting the first touch point for a drag
if(x.base.nodeName.toLowerCase() == 'textarea') {
s1.style.whiteSpace=s2.style.whiteSpace='pre-wrap'; //scroll vertically
} else {
s1.style.whiteSpace=s2.style.whiteSpace='pre'; //scroll horizontally
}
x.base.parentNode.appendChild(x);
// Refresh style pointers, and match the field sizes
keymanweb.updateInput(x);
xs=x.style;
xs.color=bs.color; //xs.backgroundColor=bs.backgroundColor;
xs.fontFamily=bs.fontFamily; xs.fontSize=bs.fontSize;
xs.fontWeight=bs.fontWeight; xs.textDecoration=bs.textDecoration;
xs.padding=bs.padding; xs.margin=bs.margin;
xs.border=bs.border; xs.borderRadius=bs.borderRadius;
//xs.color='red'; //use only for checking alignment
// Prevent highlighting of underlying element (Android)
if('webkitTapHighlightColor' in xs) {
xs.webkitTapHighlightColor='rgba(0,0,0,0)';
}
if(x.base.nodeName.toLowerCase() == 'textarea') {
// Correct rows value if defaulted and box height set by CSS
// The rows value is used when setting the caret vertically
if(x.base.rows == 2) { // 2 is default value
var h=parseInt(bs.height,10)-parseInt(bs.paddingTop,10)-parseInt(bs.paddingBottom,10),
dh=parseInt(bs.fontSize,10),calcRows=Math.round(h/dh);
if(calcRows > x.base.rows+1) {
x.base.rows=calcRows;
}
}
ds.width=xs.width; ds.minHeight=xs.height;
} else {
ds.minWidth=xs.width; ds.height=xs.height;
}
x.base.style.visibility='hidden'; // hide by default: KMW-3
// Add an explicit event listener to allow the duplicated input element
// to be adjusted for any changes in base element location or size
// This will be called for each element after any rotation, as well as after user-initiated changes
// It has to be wrapped in an anonymous function to preserve scope and be applied to each element.
(function(xx){
xx.base.addEventListener('resize',function(e){
/* A timeout is needed to let the base element complete its resizing before our
* simulated element can properly resize itself.
*
* Not doing this causes errors if the input elements are resized for whatever reason, such as
* changing languages to a text with greater height.
*/
window.setTimeout(function (){
keymanweb.updateInput(xx);
}, 1);
},false);
})(x);
// And copy the text content
keymanweb.setText(x,x.base.value,null);
return true;
}
/**
* Function setupNonKMWTouchElement
* Scope Private
* @param {Object} x A child element of document.
* Description Performs handling for the specified disabled input element on touch-based systems.
*/
keymanweb.setupNonKMWTouchElement = function(x) {
x.addEventListener('touchstart',function() {
keymanweb.focusing=false;
clearTimeout(keymanweb.focusTimer);
osk.hideNow();
},false);
}
keymanweb.setupTouchPage();
}
/*********************************************************
*
* End of main touch-device initialization.
*
*********************************************************/
/**
* Function setupDesktopPage
* Scope Private
* Description Save list of inputs for non-touch devices (desktop browsers)
*
* @suppress {checkTypes}
*/
keymanweb.setupDesktopPage = function() {
var ipList = [].concat(
util.arrayFromNodeList(document.getElementsByTagName('input')),
util.arrayFromNodeList(document.getElementsByTagName('textarea'))
);
ipList.forEach(keymanweb.setupDesktopElement);
};
/**
* Function setupDesktopElement
* Scope Private
* @param {Element} Pelem An element from the document to be touch-enabled.
* Description Setup one element for non-touch devices and add it to the inputList if it is an input element (desktop browsers).
* Only returns true if the element is a valid input for keymanweb and it is not presently tracked as an input element.
* @return {boolean}
*/
keymanweb.setupDesktopElement = function(Pelem) {
var lcTagName = Pelem.tagName.toLowerCase();
// If it's not one of these, we don't need to hook the OSK into it.
if(!(lcTagName == "input" || lcTagName == "textarea")) {
return false;
}
if(Pelem.className.indexOf('kmw-disabled') < 0 && !Pelem.readOnly)
{
var index = keymanweb.inputList.indexOf(Pelem);
if(index != -1) {
return false;
}
Pelem.className += (Pelem.className ? ' ' : '') + 'keymanweb-font';
keymanweb.inputList.push(Pelem);
return true;
}
return false;
};
/**
* Get the user-specified (or default) font for the first mapped input or textarea element
* before applying any keymanweb styles or classes
*
* @return {string}
**/
keymanweb.getBaseFont = function()
{
var ipInput=document.getElementsByTagName('INPUT'),
ipTextArea=document.getElementsByTagName('TEXTAREA'),
n=0,fs,fsDefault='Arial,sans-serif';
if(ipInput.length == 0 && ipTextArea.length == 0) n=0;
else if(ipInput.length > 0 && ipTextArea.length == 0) n=1;
else if(ipInput.length == 0 && ipTextArea.length > 0) n=2;
else if(ipInput[0].offsetTop < ipTextArea[0].offsetTop) n=1;
else if(ipInput[0].offsetTop > ipTextArea[0].offsetTop) n=2;
else if(ipInput[0].offsetLeft < ipTextArea[0].offsetLeft) n=1;
else if(ipInput[0].offsetLeft > ipTextArea[0].offsetLeft) n=2;
switch(n)
{
case 0:
fs=fsDefault;
case 1:
fs=util.getStyleValue(ipInput[0],'font-family');
case 2:
fs=util.getStyleValue(ipTextArea[0],'font-family');
}
if(typeof(fs) == 'undefined' || fs == 'monospace') fs=fsDefault;
return fs;
}
/**
* Input element stub functions, redefined when a touch device is initialized
*/
/**
* Function stub, for desktop browsers
*
* @param {Event=} e event
*/
keymanweb.setFocus = function(e){};
keymanweb.timerID = null;
/**
* Function stub, for desktop browsers
*
* @param {Object} e element (object) or element id (string) of simulated input field
* @return {string} entire text in simulated input field
*/
keymanweb.getText = function(e){return '';};
/**
* Function stub, for desktop browsers
*
* @param {Object} e element (object) or element id (string) of simulated input field
* @param {?string} t text to insert in element
* @param {?number} cp caret position (characters)
*/
keymanweb.setText = function(e,t,cp){};
/**
* Function stub, for desktop browsers
*
* @return {string}
*/
keymanweb.getTextBeforeCaret = function(){return '';};
/**
* Function stub, for desktop browsers
*
* @param {Object} e element (object) or element id (string) of simulated input field
* @return {string} Context for simulated input field
*/
keymanweb.setTextBeforeCaret = function(e){};
/**
* Function stub, for desktop browsers
*
* @param {Object} e element (object) or element id (string) of simulated input field
* @return {number} caret character position in simulated input field
*/
keymanweb.getTextCaret = function(e){return 0;};
/**
* Function stub, for desktop browsers
*
* @param {Object} e element (object) or element id (string) of simulated input field
* @param {number} cp caret character position in simulated input field
*/
keymanweb.setTextCaret = function(e,cp){};
/**
* Function stub, for desktop browser
*/
keymanweb.hideCaret = function(){};
/**
* Function stub, for desktop browsers
*/
keymanweb.flashCaret = function(){};
/**
* Function stub, for desktop browsers
*
* @param {Object} x element
*/
keymanweb.updateInput=function(x){};
// End of I3363 (Build 301) additions
/**
* Exposed function to load keyboards by name. One or more arguments may be used
*
* @param {string|Object} x keyboard name string or keyboard metadata JSON object
*
*/
keymanweb['addKeyboards'] = function(x)
{
if(arguments.length == 0)
keymanweb.keymanCloudRequest('',false);
else
keymanweb.addKeyboardArray(arguments);
}
/**
* Build 362: addKeyboardArray() link to Cloud. One or more arguments may be used
*
* @param {string|Object} x keyboard name string or keyboard metadata JSON object
*
*/
keymanweb.addKeyboardArray = function(x)
{
// Store all keyboard meta-data for registering later if called before initialization
if(!keymanweb['initialized'])
{
for(var k=0; k<x.length; k++)
keymanweb.deferredStubs.push(x[k]);
return;
}
// Ignore empty array passed as argument
if(x.length == 0) return;
// Create a temporary array of metadata objects from the arguments used
var i,j,kp,kbid,lgid,kvid,cmd='',comma='';
keymanweb.cloudList=[];
for(i=0; i<x.length; i++)
{
if(typeof(x[i]) == 'string' && x[i].length > 0)
{
var pList=x[i].split('@'),lList=[''],tEntry;
if(pList[0].toLowerCase() == 'english') pList[0] = 'us';
if(pList.length > 1)
{
lList=pList[1].split(',');
}
for(j=0; j<lList.length; j++)
{
tEntry={'id':pList[0]};
if(lList[j] != '') tEntry['language']=lList[j];
if(pList.length > 2) tEntry['version']=pList[2];
keymanweb.cloudList.push(tEntry);
}
}
if(typeof(x[i]) == 'object' && x[i] != null)
{
// Register any local keyboards immediately:
// - must specify filename, keyboard name, language codes, region codes
// - no request will be sent to cloud
if(typeof(x[i]['filename']) == 'string')
{
if(!keymanweb.addStub(x[i]))
alert('To use a custom keyboard, you must specify file name, keyboard name, language, language code and region code.');
}
else
{
lList=x[i]['language'];
//Array or single entry?
if(typeof(lList.length) == 'number') {
for(j=0; j<lList.length; j++)
keymanweb.cloudList.push(
{'id':x[i]['id'],'language':x[i]['language'][j]['id']}
);
}
// Single language element
else
{
keymanweb.cloudList.push(
{'id':x[i]['id'],'language':x[i]['language']['id']}
);
}
}
}
}
// Return if all keyboards being registered are local and fully specified
if(keymanweb.cloudList.length == 0) return;
// Update the keyboard metadata list from keyman.com - build the command
cmd='&keyboardid=';
for(i=0; i<keymanweb.cloudList.length; i++)
{
kp=keymanweb.cloudList[i];
kbid=kp['id']; lgid=''; kvid='';
if(typeof(kp['language']) == 'string' && kp['language'] != '')
lgid=kp['language'];
if(typeof(kp['version']) == 'string' && kp['version'] != '')
kvid=kp['version'];
if(lgid != '')
{
kbid=kbid+'@'+lgid;
if(kvid != '') kbid=kbid+'@'+kvid;
}
else
{
if(kvid != '') kbid=kbid+'@@'+kvid;
}
//TODO: add specifier validation...
cmd=cmd+comma+kbid;
comma=',';
}
// Request keyboard metadata from the Keyman Cloud keyboard metadata server
keymanweb.keymanCloudRequest(cmd,false);
}
/**
* Request keyboard metadata from the Keyman Cloud keyboard metadata server
*
* @param {string} cmd command string
* @param {boolean?} byLanguage if true, context=languages, else context=keyboards
**/
keymanweb.keymanCloudRequest = function(cmd,byLanguage)
{
var URL='https://r.keymanweb.com/api/4.0/',
tFlag,Lscript = util._CreateElement('SCRIPT');
URL=URL+((arguments.length > 1) && byLanguage ? 'languages' : 'keyboards')
+'?jsonp=tavultesoft.keymanweb.register';
// Set callback timer
tFlag='&timerid='+window.setTimeout(
function(){keymanweb.serverUnavailable(cmd);}
,10000);
Lscript.charset="UTF-8";
Lscript.src = URL+cmd+tFlag;
Lscript.type = 'text/javascript';
try {
document.body.appendChild(Lscript);
}
catch(ex) {
document.getElementsByTagName('head')[0].appendChild(Lscript);
}
}
/**
* Add default or all keyboards for a given language
*
* @param {string} arg Language name (multiple arguments allowed)
**/
keymanweb['addKeyboardsForLanguage'] = function(arg)
{
keymanweb.addLanguageKeyboards(arguments);
}
/**
* Add default or all keyboards for a given language
*
* @param {Object} languages Array of language names
**/
keymanweb.addLanguageKeyboards = function(languages)
{
var i,j,lgName,ll=keymanweb.languageList,cmd,first,addAll;
// Defer registering keyboards by language until the language list has been loaded
if(ll == null)
{
first = (keymanweb.languagesPending.length == 0);
for(i=0; i<languages.length; i++)
keymanweb.languagesPending.push(languages[i]);
if(first) keymanweb.keymanCloudRequest('',true);
}
// Identify and register each keyboard by language name
else
{
cmd = '';
for(i=0; i<languages.length; i++)
{
lgName = languages[i].toLowerCase();
addAll = (lgName.substr(-1,1) == '$');
if(addAll) lgName = lgName.substr(0,lgName.length-1);
for(j=0; j<ll.length; j++)
{
if(lgName == ll[j]['name'].toLowerCase())
{
if(cmd != '') cmd = cmd + ',';
cmd = cmd+'@'+ll[j]['id'];
if(addAll) cmd = cmd + '$';
break;
}
}
}
if(cmd == '')
util.alert('No keyboards are available for '+languages[0]+'. '
+'Does it have another language name?');
else
keymanweb.keymanCloudRequest('&keyboardid='+cmd,false);
}
}
/**
* Register a fully specified keyboard (add meta-data for each language) immediately
*
* @param {Object} arg
**/
keymanweb.addStub = function(arg)
{
if(typeof(arg['id']) != 'string') return false;
if(typeof(arg['language']) == 'undefined') return false;
// Default the keyboard name to its id, capitalized
if(typeof(arg['name']) != 'string')
{
arg['name'] = arg['id'].replace('_',' ');
arg['name'] = arg['name'].substr(0,1).toUpperCase()+arg['name'].substr(1);
}
var lgArg=arg['language'],lgList=[],i,lg;
if(typeof(lgArg.length) == 'undefined') lgList[0] = lgArg; else lgList = lgArg;
var localOptions={
'keyboardBaseUri':keymanweb.options['keyboards'],
'fontBaseUri':keymanweb.options['fonts']
};
// Add a stub for each correctly specified language
for(i=0; i<lgList.length; i++)
keymanweb.mergeStub(arg,lgList[i],localOptions);
return true;
}
/**
* Find a keyboard stub by id in the registered keyboards list
*
* @param {string} kid internal keyboard id (without 'Keyboard_' prefix)
* @param {string} lgid language code
*
**/
keymanweb.findStub = function(kid,lgid)
{
var i,ss=keymanweb._KeyboardStubs;
for(i=0; i<ss.length; i++)
{
if((ss[i]['KI'] == 'Keyboard_'+kid) && (ss[i]['KLC'] == lgid))
return ss[i];
}
return null;
}
// Language regions as defined by cloud server
keymanweb.regions = ['World','Africa','Asia','Europe','South America','North America','Oceania','Central America','Middle East'];
keymanweb.regionCodes = ['un','af','as','eu','sa','na','oc','ca','me'];
/**
* Create or update a keyboard meta-data 'stub' during keyboard registration
*
* @param {Object} kp (partial) keyboard meta-data object
* @param {Object} lp language object
* @param {Object} options KeymanCloud callback options
*
**/
keymanweb.mergeStub = function(kp,lp,options)
{
var sp=keymanweb.findStub(kp['id'],lp['id']);
if(sp == null)
{
sp={'KI':'Keyboard_'+kp['id'],'KLC':lp['id']};
keymanweb._KeyboardStubs.push(sp);
}
// Accept region as number (from Cloud server), code, or name
var region=lp['region'],rIndex=0;
if(typeof(region) == 'number')
{
if(region < 1 || region > 9) rIndex = 0; else rIndex = region-1;
}
else if(typeof(region) == 'string')
{
var list = (region.length == 2 ? keymanweb.regionCodes : keymanweb.regions);
for(var i=0; i<list.length; i++)
{
if(region.toLowerCase() == list[i].toLowerCase())
{
rIndex=i; break;
}
}
}
var rx=null;
if(typeof(sp['KL']) == 'undefined') sp['KL'] = lp['name'];
if(typeof(sp['KR']) == 'undefined') sp['KR'] = keymanweb.regions[rIndex];
if(typeof(sp['KRC']) == 'undefined') sp['KRC'] = keymanweb.regionCodes[rIndex];
if(typeof(sp['KN']) == 'undefined') sp['KN'] = kp['name'];
if(typeof(sp['KF']) == 'undefined')
{
rx=RegExp('^(([\.]/)|([\.][\.]/)|(/))|(:)');
sp['KF'] = kp['filename'];
if(!rx.test(sp['KF'])) sp['KF'] = options['keyboardBaseUri']+sp['KF'];
}
// Font path defined by cloud entry
var fp,fontPath=options['fontBaseUri'];
// or overridden locally, in page source
if(keymanweb.options['fonts'] != '')
{
fontPath=keymanweb.options['fonts'];
rx=new RegExp('^https?\:');
if(!rx.test(fontPath))
{
if(fontPath.substr(0,2) == '//')
fontPath = keymanweb.protocol + fontPath;
else if(fontPath.substr(0,1) == '/')
fontPath = keymanweb.rootPath + fontPath.substr(1);
else
fontPath = keymanweb.rootPath + fontPath;
}
}
else
{
keymanweb.options.fonts=fontPath;
}
// Add font specifiers where necessary and not overridden by user
if(typeof(sp['KFont']) == 'undefined' && typeof(lp['font']) != 'undefined')
{
fp=sp['KFont']=lp['font'];
fp['files']=fp['source'];
fp['path']=fontPath;
}
// Fixed OSK font issue Github #7 (9/1/2015)
if(typeof(sp['KOskFont']) == 'undefined' && typeof(lp['oskFont']) != 'undefined')
{
fp=sp['KOskFont']=lp['oskFont'];
fp['files']=fp['source'];
fp['path']=fontPath;
}
// Update the UI
keymanweb.doKeyboardRegistered(sp['KI'],sp['KL'],sp['KN'],sp['KLC']);
// If the first stub, must load (and optionally display) the keyboard
if(keymanweb._KeyboardStubs.length == 1 && document.readyState=='complete') keymanweb._SetActiveKeyboard(sp['KI'], sp['KLC']);
}
/**
* Register a keyboard for each associated language
*
* @param {Object} kp Keyboard Object or Object array
* @param {Object} options keymanCloud callback options
* @param {number} nArg keyboard index in argument array
*
**/
keymanweb.registerLanguagesForKeyboard = function(kp,options,nArg)
{
var i,j,id,nDflt=0,kbId='';
// Do not attempt to process badly formatted requests
if(typeof(kp) == 'undefined') return;
if(typeof(options['keyboardid']) == 'string')
kbId=options['keyboardid'].split(',')[nArg];
// When keyboards requested by language code, several keyboards may be returned as an array
if(typeof(kp.length) == 'number')
{
// If language code is suffixed by $, register all keyboards for this language
if(kp.length == 1 || kbId.substr(-1,1) == '$' || kbId == '')
{
for(i=0; i<kp.length; i++)
keymanweb.registerLanguagesForKeyboard(kp[i],options,nArg);
}
// Register the default keyboard for the language code
// Until a default is defined, the default will be the Windows keyboard,
// that is, the keyboard named for the languge (exception: English:US), or the
// first keyboard found.
else
{
for(i=0; i<kp.length; i++)
{
id=kp[i].id.toLowerCase(); if(id == 'us') id='english';
for(j=0; j<kp[i]['languages'].length; j++)
{
if(id == kp[i]['languages'][j]['name'].toLowerCase())
{
nDflt = i; break;
}
}
}
keymanweb.registerLanguagesForKeyboard(kp[nDflt],options,nArg);
}
}
// Otherwise, process a single keyboard for the specified languages
else
{
// May need to filter returned stubs by language
var lgCode=kbId.split('@')[1];
if(typeof(lgCode) == 'string') lgCode=lgCode.replace(/\$$/,'');
// Can only add keyboard stubs for defined languages
var ll=kp['languages'];
if(typeof(ll) != 'undefined')
{
if(typeof(ll.length) == 'number')
{
for(i=0; i<ll.length; i++)
{
if(typeof(lgCode) == 'undefined' || ll[i]['id'] == lgCode)
keymanweb.mergeStub(kp,ll[i],options);
}
}
else
{
keymanweb.mergeStub(kp,ll,options);
}
}
}
}
/**
* Call back from cloud for adding keyboard metadata
*
* @param {Object} x metadata object
**/
keymanweb['register'] = function(x)
{
var options=x['options'];
// Always clear the timer associated with this callback
if(x['timerid']) window.clearTimeout(x['timerid']);
// Indicate if unable to register keyboard
if(typeof(x['error']) == 'string')
{
var badName='';
if(typeof(x['keyboardid']) == 'string')
badName = x['keyboardid'].substr(0,1).toUpperCase()+x['keyboardid'].substr(1);
keymanweb.serverUnavailable(badName+' keyboard not found.');
return;
}
// Ignore callback unless the context is defined
if(typeof(options) == 'undefined' || typeof(options['context']) == 'undefined') return;
// Register each keyboard for the specified language codes
if(options['context'] == 'keyboard')
{
var i,kp=x['keyboard'];
// Process array of keyboard definitions
if(typeof(kp.length) == 'number')
{
for(i=0; i<kp.length; i++)
{
keymanweb.registerLanguagesForKeyboard(kp[i],options,i);
}
}
// Process a single keyboard definition
else
{
keymanweb.registerLanguagesForKeyboard(kp,options,0);
}
}
// Download the full list of supported keyboard languages
else if(options['context'] == 'language')
{
keymanweb.languageList = x['languages'];
if(keymanweb.languagesPending)
keymanweb.addLanguageKeyboards(keymanweb.languagesPending);
keymanweb.languagesPending = [];
}
}
/**
* Display warning if Keyman Cloud server fails to respond
*
* @param {string} cmd command string sent to Cloud
*
**/
keymanweb.serverUnavailable = function(cmd)
{
util.alert(cmd == '' ? 'Unable to connect to Keyman Cloud server!' : cmd);
keymanweb.warned=true;
}
/**
* Build 362: removeKeyboards() remove keyboard from list of available keyboards
*
* @param {string} x keyboard name string
*
*/
keymanweb["removeKeyboards"] = function(x)
{
if(arguments.length == 0) return;
var i,j,ss=keymanweb._KeyboardStubs;
for(i=0; i<arguments.length; i++)
{
for(j=ss.length-1; j>=0; j--)
{
if('Keyboard_'+arguments[i] == ss[j]['KI'] && ss.length > 1)
{
ss.splice(j,1); break;
}
}
}
// Always reset to the first remaining keyboard
keymanweb._SetActiveKeyboard(ss[0]['KI'],ss[0]['KLC'],true);
// then update the UI keyboard menu
keymanweb.doKeyboardUnregistered();
}
/**
* Browser dependent initialization
*/
if(document.selection) // only defined for IE
{
var appVer=navigator.appVersion;
// Legacy support variables
if(typeof(document.createEventObject)=='undefined' && (appVer.indexOf('MSIE 5.0') >= 0
|| appVer.indexOf('MSIE 4.0') >= 0 || appVer.indexOf('MSIE 3.0') >= 0)) keymanweb.legacy=1;
else if(appVer.indexOf('MSIE 6.0') >= 0) keymanweb._IE = 6;
else if(appVer.indexOf('MSIE 7.0') >= 0) keymanweb._IE = 7;
else if(appVer.indexOf('MSIE 8.0') >= 0) keymanweb._IE = 8;
if(keymanweb._IE && document.compatMode=='BackCompat') keymanweb._IE = 6;
}
// I732 START - Support for European underlying keyboards #1
if(typeof(window['KeymanWeb_BaseLayout']) !== 'undefined')
osk._BaseLayout = window['KeymanWeb_BaseLayout'];
else
osk._BaseLayout = 'us';
keymanweb._BrowserIsSafari = (navigator.userAgent.indexOf('AppleWebKit') >= 0); // I732 END - Support for European underlying keyboards #1
/**
* Function _GetEventObject
* Scope Private
* @param {Event=} e Event object if passed by browser
* @return Event object
* Description Gets the event object from the window when using Internet Explorer
* and handles getting the event correctly in frames
*/
keymanweb._GetEventObject=function(e) // I2404 - Attach to controls in IFRAMEs
{
if (!e) {
e = window.event;
if(!e) {
var elem = keymanweb._GetLastActiveElement();
if(elem) {
elem = elem.ownerDocument;
if(elem) {
elem = elem.parentWindow;
}
if(!elem) {
return null;
}
e = elem.event;
}
}
}
return e;
}
/**
* Function _push
* Scope Private
* @param {Array} Parray Array
* @param {*} Pval Value to be pushed or appended to array
* @return {Array} Returns extended array
* Description Push (if possible) or append a value to an array
*/
keymanweb._push = function(Parray, Pval)
{
if(Parray.push) Parray.push(Pval);
else Parray=Parray.concat(Pval);
return Parray;
}
/**
* Function attachToControl
* Scope Public
* @param {Object} Pelem Element to which KMW will be attached
* Description Attaches KMW to control (or IFrame)
*/
keymanweb['attachToControl'] = keymanweb.attachToControl = function(Pelem)
{
// Check that the element is neither readonly nor disabled for KeymanWeb
var ro=Pelem.attributes['readonly'],cn=Pelem.className;
if(typeof ro == 'object' && ro.value != 'false' ) return;
if(typeof cn == 'string' && cn.indexOf('kmw-disabled') >= 0) return;
if(Pelem.tagName.toLowerCase() == 'iframe')
keymanweb._AttachToIframe(Pelem);
else
{
util.attachDOMEvent(Pelem,'focus', keymanweb._ControlFocus);
util.attachDOMEvent(Pelem,'blur', keymanweb._ControlBlur);
Pelem.onkeypress = keymanweb._KeyPress;
Pelem.onkeydown = keymanweb._KeyDown;
Pelem.onkeyup = keymanweb._KeyUp;
}
}
/**
* Function _AttachToIframe
* Scope Private
* @param {Object} Pelem IFrame to which KMW will be attached
* Description Attaches KeymanWeb to IFrame
*/
keymanweb._AttachToIframe = function(Pelem)
{
try
{
var Lelem=Pelem.contentWindow.document;
/* editable Iframe */
if(Lelem)
{
if(Lelem.parentWindow)
{
// Internet Explorer
if(Lelem.designMode.toLowerCase() == 'on' || Lelem.body.isContentEditable) // I1295 - fix non-attachment for some forms of IFRAMEs
{
// I1480 - Attach to IFRAME instead of document
util.attachDOMEvent(Pelem,'focus', keymanweb._ControlFocus);
util.attachDOMEvent(Pelem,'blur', keymanweb._ControlBlur);
util.attachDOMEvent(Lelem,'keydown', keymanweb._KeyDown); // I2404 - Update for attaching to elements within IFRAMEs, don't attach to read-only IFRAMEs
util.attachDOMEvent(Lelem,'keypress', keymanweb._KeyPress);
util.attachDOMEvent(Lelem,'keyup', keymanweb._KeyUp);
if(!keymanweb.legacy)
{ // I1481 - Attach to the selectionchange in the iframe (and do a selchange to get the new selection)
/* IE: call _SelectionChange when the user changes the selection */
util.attachDOMEvent(Lelem, 'selectionchange', keymanweb._SelectionChange);
keymanweb._SelectionChange();
}
}
else {
keymanweb._AttachToControls(Lelem);
}
}
else
{
if(Lelem.designMode.toLowerCase() == 'on')
{
// Mozilla // I2404 - Attach to IFRAMEs child objects, only editable IFRAMEs here
util.attachDOMEvent(Lelem,'focus', keymanweb._ControlFocus);
util.attachDOMEvent(Lelem,'blur', keymanweb._ControlBlur);
util.attachDOMEvent(Lelem,'keydown', keymanweb._KeyDown);
util.attachDOMEvent(Lelem,'keypress', keymanweb._KeyPress);
util.attachDOMEvent(Lelem,'keyup', keymanweb._KeyUp);
}
else
{
keymanweb._AttachToControls(Lelem); // I2404 - Manage IE events in IFRAMEs
}
}
}
}
catch(err)
{
// do not attempt to attach to the iframe as it is from another domain - XSS denied!
}
}
/**
* Function GetEnabled
* Scope Private
* @return {boolean} True if KMW enabled
* Description Test if KMW enabled
*/
keymanweb.GetEnabled = function()
{
return keymanweb._Enabled;
}
/**
* Function SetEnabled
* Scope Private
* @param {(boolean|number)} Pvalue True to enable KMW
* Description Enable or disable KMW
*/
keymanweb.SetEnabled = function(Pvalue)
{
if(Pvalue) Pvalue=1; else Pvalue=0;
if(keymanweb._Enabled != Pvalue)
{
keymanweb._Enabled = Pvalue;
if((!Pvalue) && keymanweb['HideInterface']) keymanweb['HideInterface'](); //JMD 3/9/10
}
}
/**
* Function DisableControl
* Scope Private
* @param {Object} Pelem Element to be disabled
* Description Disable KMW control element
*/
keymanweb.DisableControl = function(Pelem)
{
var Ln, Lc;
for(Ln=0; Ln<keymanweb._Controls.length; Ln++) // I1511 - array prototype extended
if(keymanweb._Controls[Ln].LControl == Pelem)
{
keymanweb._Controls[Ln].LEnabled = 0;
return;
}
Lc = {LControl:Pelem, LEnabled:0, LDefaultInternalName:'-'};
keymanweb._Controls=keymanweb._push(keymanweb._Controls,Lc);
}
/**
* Function EnableControl
* Scope Private
* @param {Object} Pelem Element to be enabled
* Description Enable KMW control element
*/
keymanweb.EnableControl = function(Pelem)
{
var Ln, Lc;
for(Ln=0; Ln<keymanweb._Controls.length; Ln++) // I1511 - array prototype extended
if(keymanweb._Controls[Ln].LControl == Pelem)
{
keymanweb._Controls[Ln].LEnabled = 1;
return;
}
Lc = {LControl:Pelem, LEnabled:1, LDefaultInternalName:'-'};
keymanweb._Controls=keymanweb._push(keymanweb._Controls,Lc);
}
/**
* Function SetDefaultKeyboardForControl
* Scope Private
* @param {Object} Pelem Control element
* @param {string} Pkbd Keyboard
* Description Set default keyboard for control
*/
keymanweb.SetDefaultKeyboardForControl = function(Pelem, Pkbd)
{
var Ln, Lc;
/* pass null for kbd to specify no default, or '' to specify English */
for(Ln=0; Ln< keymanweb._Controls.length; Ln++) // I1511 - array prototype extended
if(keymanweb._Controls[Ln].LControl == Pelem)
{
keymanweb._Controls[Ln].LDefaultInternalName = Pkbd;
return;
}
Lc = {LControl:Pelem, LEnabled:1, LDefaultInternalName:Pkbd};
keymanweb._Controls=keymanweb._push(keymanweb._Controls,Lc);
}
/**
* Set focus to last active target element (browser-dependent)
*/
keymanweb['focusLastActiveElement'] = keymanweb._FocusLastActiveElement = function()
{
if(!keymanweb._LastActiveElement) return;
keymanweb._JustActivatedKeymanWebUI = 1;
if(keymanweb._IsMozillaEditableIframe(keymanweb._LastActiveElement,0))
keymanweb._LastActiveElement.defaultView.focus(); // I3363 (Build 301)
else if(keymanweb._LastActiveElement.focus) keymanweb._LastActiveElement.focus();
}
/**
* Get the last active target element *before* KMW activated (I1297)
*
* @return {Object}
*/
keymanweb['getLastActiveElement'] = keymanweb._GetLastActiveElement = function()
{
return keymanweb._LastActiveElement;
}
/**
* Set the active input element directly optionally setting focus
*
* @param {Object|string} e element id or element
* @param {boolean=} setFocus optionally set focus (KMEW-123)
**/
keymanweb['setActiveElement']=keymanweb.setActiveElement=function(e,setFocus)
{
if(typeof(e) == 'string') e=document.getElementById(e);
keymanweb._ActiveElement=keymanweb._LastActiveElement=e;
// Allow external focusing KMEW-123
if(arguments.length > 1 && setFocus)
{
if(device.touchable) keymanweb.setFocus();
else keymanweb['focusLastActiveElement']();
}
}
/**
* Function getUIState
* Scope Public
* @return {Object.<string,(boolean|number)>}
* Description Return object with activation state of UI:
* activationPending (bool): KMW being activated
* activated (bool): KMW active
*/
keymanweb['getUIState'] = keymanweb.getUIState = function()
{
var p={};
p['activationPending'] = p.activationPending = keymanweb._IsActivatingKeymanWebUI;
p['activated'] = p.activated = keymanweb._JustActivatedKeymanWebUI;
return p;
}
/**
* Set or clear the IsActivatingKeymanWebUI flag (exposed function)
*
* @param {(boolean|number)} state Activate (true,false)
*/
keymanweb['activatingUI'] = function(state)
{
keymanweb._IsActivatingKeymanWebUI = (state ? 1 : 0);
}
//TODO: add more complete description of what ControlFocus really does
/**
* Respond to KeymanWeb-aware input element receiving focus
*
* @param {Event} e Event object
* @return {boolean} always true (?)
*/
keymanweb._ControlFocus = function(e)
{
var Ltarg, Ln;
if(!keymanweb._Enabled) return true;
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) return true;
if (e.target) Ltarg = e.target;
else if (e.srcElement) Ltarg = e.srcElement;
else return true;
// Prevent any action if a protected input field
if(device.touchable && (Ltarg.className == null || Ltarg.className.indexOf('keymanweb-input') < 0)) return true;
// Or if not a remappable input field
var en=Ltarg.nodeName.toLowerCase();
if(en == 'input')
{
var et=Ltarg.type.toLowerCase();
if(!(et == 'text' || et == 'search')) return true;
}
else if(en != 'textarea') return true;
keymanweb._ActiveElement=Ltarg; // I3363 (Build 301)
if (Ltarg.nodeType == 3) // defeat Safari bug
Ltarg = Ltarg.parentNode;
var LfocusTarg = Ltarg;
// Ensure that focussed element is visible above the keyboard
if(device.touchable && (Ltarg.className == null || Ltarg.className.indexOf('keymanweb-input') < 0)) keymanweb.scrollBody(Ltarg);
if(Ltarg.tagName=='IFRAME') //**TODO: check case reference
{
keymanweb._AttachToIframe(Ltarg);
Ltarg=Ltarg.contentWindow.document;
}
//??keymanweb._Selection = null;
keymanweb._ActiveControl = null;
keymanweb._LastActiveElement = Ltarg;
for(Ln=0; Ln < keymanweb._Controls.length; Ln++) // I1511 - array prototype extended
if(keymanweb._Controls[Ln].LControl == Ltarg)
{
keymanweb._ActiveControl = keymanweb._Controls[Ln];
break;
}
if(keymanweb._ActiveControl != null && keymanweb._ActiveControl.LDefaultInternalName != null)
{
if(!keymanweb._JustActivatedKeymanWebUI)
{
keymanweb._SetActiveKeyboard(keymanweb._ActiveControl.LDefaultInternalName,'',true);
}
else
keymanweb._ActiveControl.LDefaultInternalName = keymanweb._ActiveKeyboard == null ? '' : keymanweb._ActiveKeyboard['KI'];
}
//TODO: the logic of the following line doesn't look right!! Both variables are true, but that doesn't make sense!
//_Debug(keymanweb._IsIEEditableIframe(Ltarg,1) + '...' +keymanweb._IsMozillaEditableIframe(Ltarg,1));
if(!keymanweb._IsIEEditableIframe(Ltarg,1) || !keymanweb._IsMozillaEditableIframe(Ltarg,1))
{
keymanweb._DisableInput = 1;
return true;
}
keymanweb._DisableInput = 0;
if(!keymanweb._JustActivatedKeymanWebUI)
{
keymanweb._DeadKeys = [];
keymanweb._NotifyKeyboard(0,Ltarg,1); // I2187
}
if(!keymanweb._JustActivatedKeymanWebUI && keymanweb._SelectionControl != Ltarg)
keymanweb._IsActivatingKeymanWebUI = 0;
keymanweb._JustActivatedKeymanWebUI = 0;
keymanweb._SelectionControl = Ltarg;
Ltarg._KeymanWebSelectionStart = Ltarg._KeymanWebSelectionEnd = null; // I3363 (Build 301)
// Set element directionality (but only if element is empty)
keymanweb._SetTargDir(Ltarg);
//Execute external (UI) code needed on focus if required
keymanweb.doControlFocused(LfocusTarg,keymanweb._ActiveControl);
// Force display of OSK for touch input device, or if a CJK keyboard, to ensure visibility of pick list
if(device.touchable)
{
osk._Enabled=1;
}
else
{
// Conditionally show the OSK when control receives the focus
if(osk.ready)
{
if(keymanweb.isCJK()) osk._Enabled=1;
if(osk._Enabled) osk._Show(); else osk._Hide(false);
}
}
// TODO: This is possibly the main sequencing issue, as ControlFocus may get called before other elements
// are ready, or keyboards downloaded. Need some way to ensure that the OSK is displayed when everything is ready.
// We have slightly different focus management on IE to other browsers. This is because
// IE has problems with loss of focus when clicking on another element. This can probably
// be resolved in the future by using MouseDown and MouseUp events instead of Click events
// on the VK elements but for now we just use the smoother interaction on Firefox and Chrome,
// and let IE do the focus dance. This means some careful management of the
// IsActivatingKeymanWebUI and LastActiveElement variables, so be careful with any changes
// to these.
// if(keymanweb._IE) keymanweb._LastActiveElement = null; // I2498 - KeymanWeb OSK does not accept clicks in FF when using automatic UI
return true;
}
/**
* Function _IsIEEditableIframe
* Scope Private
* @param {Object} Pelem Iframe element
* {boolean|number} PtestOn 1 to test if frame content is editable (TODO: unclear exactly what this is doing!)
* @return {boolean}
* Description Test if element is an IE editable IFrame
*/
keymanweb._IsIEEditableIframe = function(Pelem,PtestOn)
{
var Ldv, Lvalid = Pelem && (Ldv=Pelem.tagName) && Ldv.toLowerCase() == 'body' && (Ldv=Pelem.ownerDocument) && Ldv.parentWindow;
return (!PtestOn && Lvalid) || (PtestOn && (!Lvalid || Pelem.isContentEditable));
}
/**
* Function _IsMozillaEditableIframe
* Scope Private
* @param {Object} Pelem Iframe element
* @param {boolean|number} PtestOn 1 to test if 'designMode' is 'ON'
* @return {boolean}
* Description Test if element is a Mozilla editable IFrame
*/
keymanweb._IsMozillaEditableIframe = function(Pelem,PtestOn)
{
var Ldv, Lvalid = Pelem && (Ldv=Pelem.defaultView) && Ldv.frameElement;
return (!PtestOn && Lvalid) || (PtestOn && (!Lvalid || Ldv.document.designMode.toLowerCase()=='on'));
}
/**
* Respond to KMW losing focus on event
*
* @param {Event} e Event object
* @return {boolean} Always true (?)
*/
keymanweb._ControlBlur = function(e)
{
var Ltarg;
if(!keymanweb._Enabled) return true;
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) return true;
if (e.target) Ltarg = e.target;
else if (e.srcElement) Ltarg = e.srcElement;
else return true;
keymanweb._ActiveElement = null; // I3363 (Build 301)
// Hide the touch device input caret, if applicable I3363 (Build 301)
if(device.touchable) keymanweb.hideCaret();
if (Ltarg.nodeType == 3) // defeat Safari bug
Ltarg = Ltarg.parentNode;
if(Ltarg.tagName=='IFRAME')
Ltarg=Ltarg.contentWindow.document;
if (Ltarg.setSelectionRange)
{
//Ltarg._KeymanWebSelectionStart = Ltarg.selectionStart;
//Ltarg._KeymanWebSelectionEnd = Ltarg.selectionEnd;
Ltarg._KeymanWebSelectionStart = Ltarg.value._kmwCodeUnitToCodePoint(Ltarg.selectionStart); //I3319
Ltarg._KeymanWebSelectionEnd = Ltarg.value._kmwCodeUnitToCodePoint(Ltarg.selectionEnd); //I3319
}
////keymanweb._SelectionControl = null;
keymanweb._LastActiveElement = Ltarg;
if(keymanweb._ActiveControl != null && keymanweb._ActiveControl.LDefaultInternalName != null)
if(keymanweb._ActiveKeyboard == null)
keymanweb._ActiveControl.LDefaultInternalName = '';
else
keymanweb._ActiveControl.LDefaultInternalName = keymanweb._ActiveKeyboard['KI'];
/* If the KeymanWeb UI is active as a user changes controls, all UI-based effects should be restrained to this control in case
* the user is manually specifying languages on a per-control basis.
*/
keymanweb._JustActivatedKeymanWebUI = 0;
keymanweb._ActiveControl = null;
if(!keymanweb._IsActivatingKeymanWebUI) keymanweb._NotifyKeyboard(0,Ltarg,0); // I2187
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs //TODO: is this really needed again????
keymanweb.doControlBlurred(Ltarg,e,keymanweb._IsActivatingKeymanWebUI);
// Hide the OSK when the control is blurred, unless the UI is being temporarily selected
if(osk.ready && !keymanweb._IsActivatingKeymanWebUI) osk._Hide(false);
return true;
}
/****************************************************************
*
* Provide for external processing on events
*
***************************************************************/
/**
* Function doControlBlurred
* Scope Private
* @param {Object} _target element losing focus
* @param {Event} _event event object
* @param {(boolean|number)} _isActivating activation state
* @return {boolean}
* Description Execute external (UI) code needed on blur
*/
keymanweb.doControlBlurred = function(_target,_event,_isActivating)
{
var p={}; p['target']=_target; p['event']=_event; p['isActivating']=_isActivating;
return util.callEvent('kmw.controlblurred',p);
}
/**
* Function doControlFocused
* Scope Private
* @param {Object} _target element gaining focus
* @param {Object} _activeControl currently active control
* @return {boolean}
* Description Execute external (UI) code needed on focus
*/
keymanweb.doControlFocused = function(_target,_activeControl)
{
var p={}; p['target']=_target; p['activeControl']=_activeControl;
return util.callEvent('kmw.controlfocused',p);
}
/**
* Execute external (UI) code needed on registering keyboard, used
* to update each UIs language menu
*
* Note that the argument object is not at present used by any UI,
* since the menu is always fully recreated when needed, but the arguments
* remain defined to allow for possible use in future (Aug 2014)
*
* @param {string} _internalName
* @param {string} _language
* @param {string} _keyboardName
* @param {string} _languageCode
* @return {boolean}
*/
keymanweb.doKeyboardRegistered = function(_internalName,_language,_keyboardName,_languageCode)
{
var p={'internalName':_internalName,'language':_language,'keyboardName':_keyboardName,'languageCode':_languageCode};
return util.callEvent('kmw.keyboardregistered',p);
}
/**
* Execute external (UI) code to rebuild menu when deregistering keyboard
*
* @return {boolean}
*/
keymanweb.doKeyboardUnregistered = function()
{
var p={};
return util.callEvent('kmw.keyboardregistered',p);
}
/**
* Execute external (UI) code needed on laoding keyboard
*
* @param {string} _internalName
* @return {boolean}
*/
keymanweb.doKeyboardLoaded = function(_internalName)
{
var p={}; p['keyboardName']=_internalName;
return util.callEvent('kmw.keyboardloaded',p);
}
/**
* Function doBeforeKeyboardChange
* Scope Private
* @param {string} _internalName
* @param {string} _languageCode
* @return {boolean}
* Description Execute external (UI) code needed before changing keyboard
*/
keymanweb.doBeforeKeyboardChange = function(_internalName,_languageCode)
{
var p={}; p['internalName']=_internalName; p['languageCode']=_languageCode;
return util.callEvent('kmw.beforekeyboardchange',p);
}
/**
* Execute external (UI) code needed *after* changing keyboard
*
* @param {string} _internalName
* @param {string} _languageCode
* @param {boolean=} _indirect
* @return {boolean}
*/
keymanweb.doKeyboardChange = function(_internalName,_languageCode,_indirect)
{
var p={}; p['internalName']=_internalName; p['languageCode']=_languageCode;
p['indirect']=(arguments.length > 2 ? _indirect : false);
return util.callEvent('kmw.keyboardchange',p);
}
/**
* Function doUnloadOSK
* Scope Private
* @return {boolean}
* Description Execute external (UI) code if any needed after unloading OSK (probably not required)
*/
keymanweb.doUnloadOSK = function()
{
var p={};
return util.callEvent('kmw.unloadosk',p);
}
/**
* Function doLoadUI
* Scope Private
* @return {boolean}
* Description Execute UI initialization code after loading the UI
*/
keymanweb.doLoadUI = function()
{
var p={};
return util.callEvent('kmw.loaduserinterface',p);
}
/**
* Function doUnloadUI
* Scope Private
* @return {boolean}
* Description Execute UI cleanup code before unloading the UI (may not be required?)
*/
keymanweb.doUnloadUI = function()
{
var p={};
return util.callEvent('kmw.unloaduserinterface',p);
}
/*****************************************************************************
*
* Provide for handling the initial focus event differently
* The first focus event can happen before we get the WindowLoad,
* e.g. if the page activates a control on WindowLoad itself,
* so trap that and run it through to the page
*
*****************************************************************************/
/**
* Function _BubbledFocus
* Scope Private
* @param {Event} e Event object
* Description Respond to KMW receiving bubbled focus on event (TODO: may not be needed, not currently doing anything)
*/
keymanweb._BubbledFocus = function(e) { /*KeymanWeb._WindowLoad(e);*/ }
if (window.addEventListener)
window.addEventListener('focus', keymanweb._BubbledFocus, true);
/**
* Function _GetKeyEventProperties
* Scope Private
* @param {Event} e Event object
* @return {Object.<string,*>} KMW keyboard event object:
* Description Get object with target element, key code, shift state, virtual key state
* Ltarg=target element
* Lcode=keyCode
* Lmodifiers=shiftState
* LisVirtualKeyCode e.g. ctrl/alt key
* LisVirtualKey e.g. Virtual key or non-keypress event
*/
keymanweb._GetKeyEventProperties = function(e)
{
var s = new Object();
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(!e) return null;
if(e.cancelBubble === true) return null; // I2457 - Facebook meta-event generation mess -- two events generated for a keydown in Facebook contentEditable divs
if (e.target) s.Ltarg = e.target;
else if (e.srcElement) s.Ltarg = e.srcElement;
else return null;
if (s.Ltarg.nodeType == 3) // defeat Safari bug
s.Ltarg = s.Ltarg.parentNode;
if (e.keyCode) s.Lcode = e.keyCode;
else if (e.which) s.Lcode = e.which;
else return null;
s.Lmodifiers =
(e.shiftKey ? 0x10 : 0) |
(e.ctrlKey ? (e.ctrlLeft ? 0x20 : 0x20) : 0) |
(e.altKey ? (e.altLeft ? 0x40 : 0x40) : 0); // I3363 (Build 301)
//s.LisVirtualKey = (e.charCode != null && (e.charCode == 0 || (s.Lmodifiers & 0x60) != 0)) || e.type != 'keypress';
//s.LisVirtualKeyCode = false;
s.LisVirtualKeyCode = (typeof e.charCode != 'undefined' && e.charCode != null && (e.charCode == 0 || (s.Lmodifiers & 0x60) != 0));
s.LisVirtualKey = s.LisVirtualKeyCode || e.type != 'keypress';
return s;
}
/**
* Function _SelectionChange
* Scope Private
* @return {boolean}
* Description Respond to selection change event
*/
keymanweb._SelectionChange = function()
{
if(keymanweb._IgnoreNextSelChange)
{
keymanweb._IgnoreNextSelChange--;
}
else
{
var Ls=document.selection;
if(Ls.type.toLowerCase()!='control') // && document.selection.createRange().parentElement() == keymanweb._SelectionControl) // && window.event.srcElement == keymanweb._SelectionControl)
{
var Lrange=Ls.createRange();
if(!keymanweb._Selection || !keymanweb._Selection.isEqual(Lrange))
{
keymanweb._Selection = Lrange;
/* Delete deadkeys for IE when certain keys pressed */
keymanweb._DeadKeys = [];
}
}
}
return true;
}
// TODO: find out if _FindCaret is still needed for current FireFox, delete if not
/**
* Function _FindCaret
* Scope Private
* @param {Object} Pelem element
* Description Work around a problem with scrolling text boxes and input boxes in Firefox, not needed for other browsers
*/
keymanweb._FindCaret = function(Pelem) // I779
{
if(!Pelem.createTextRange && Pelem.selectionStart)
{
var Levent=document.createEvent('KeyboardEvent');
if(Levent.initKeyEvent)
{
Levent.initKeyEvent('keypress',true,true,null,false,false,false,false,0,32);
var LkeyPress = keymanweb._KeyPress;
/**
* Function _KeyPress
* Scope Private
* @param {Event} e event
* Description Temporarily disable keypress event handling TODO: does this really work??? objects are passed by reference so should be OK
*/
keymanweb._KeyPress = function(e) {};
Pelem.dispatchEvent(Levent);
Levent=document.createEvent('KeyboardEvent');
Levent.initKeyEvent('keypress',true,true,null,false,false,false,false,8,0);
Pelem.dispatchEvent(Levent);
keymanweb._KeyPress = LkeyPress;
}
}
}
/**
* Function _NotifyKeyboard
* Scope Private
* @param {number} _PCommand event code (16,17,18) or 0
* @param {Object} _PTarget target element
* @param {number} _PData 1 or 0
* Description Notifies keyboard of keystroke or other event
*/
keymanweb._NotifyKeyboard = function(_PCommand,_PTarget,_PData) // I2187
{
if(keymanweb._ActiveKeyboard != null && typeof(keymanweb._ActiveKeyboard['KNS']) == 'function') keymanweb._ActiveKeyboard['KNS'](_PCommand,_PTarget,_PData);
}
/**
* Function _KeyDown
* Scope Private
* @param {Event} e event
* @return {boolean}
* Description Processes keydown event and passes data to keyboard
*/
keymanweb._KeyDown = function(e)
{
var Ldv,eClass='';
keymanweb._KeyPressToSwallow = 0;
if(!keymanweb._Enabled || keymanweb._DisableInput || keymanweb._ActiveKeyboard == null ||
(keymanweb._ActiveControl != null && !keymanweb._ActiveControl.LEnabled)) return true;
// Prevent mapping element is readonly or tagged as kmw-disabled
var el=util.eventTarget(e);
if(device.touchable)
{
if(el && typeof el.kmwInput != 'undefined' && el.kmwInput == false) return true;
}
else if(el && el.className.indexOf('kmw-disabled') >= 0) return true;
// Or if OSK not yet ready (for any reason)
if(!osk.ready) return true;
// Get event properties
var Levent = keymanweb._GetKeyEventProperties(e);
if(Levent == null) return true;
switch(Levent.Lcode)
{
case 8:
keymanweb._DeadKeys = [];
break; // I3318 (always clear deadkeys after backspace)
case 16:
case 17:
case 18:
keymanweb._NotifyKeyboard(Levent.Lcode,Levent.Ltarg,1);
return osk._UpdateVKShift(Levent, Levent.Lcode-15, 1); // I2187
}
// I1207
if((Ldv=Levent.Ltarg.ownerDocument) && (Ldv=Ldv.selection) && (Levent.Lcode<33 || Levent.Lcode>40))
{
Ldv.createRange().select();
}
if(!window.event)
{
// I1466 - Convert the - keycode on mnemonic as well as positional layouts
// FireFox, Mozilla Suite
if(keymanweb._VKMap_FF_IE['k'+Levent.Lcode]) Levent.Lcode=keymanweb._VKMap_FF_IE['k'+Levent.Lcode];
}
//else
//{
// Safari, IE, Opera?
//}
if(!keymanweb._ActiveKeyboard['KM'])
{
// Positional Layout
var LeventMatched=0;
/* 13/03/2007 MCD: Swedish: Start mapping of keystroke to US keyboard */
var Lbase=keymanweb._VKMap[osk._BaseLayout];
if(Lbase && Lbase['k'+Levent.Lcode]) Levent.Lcode=Lbase['k'+Levent.Lcode];
/* 13/03/2007 MCD: Swedish: End mapping of keystroke to US keyboard */
if(typeof(keymanweb._ActiveKeyboard['KM'])=='undefined' && !(Levent.Lmodifiers & 0x60))
{
// Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic
var Levent2={Lcode:keymanweb._USKeyCodeToCharCode(Levent),Ltarg:Levent.Ltarg,Lmodifiers:0,LisVirtualKey:0};
if(keymanweb.callKeyboardStartGroup(Levent2.Ltarg,Levent2)) LeventMatched=1;
}
LeventMatched = LeventMatched || keymanweb.callKeyboardStartGroup(Levent.Ltarg,Levent);
// Support backspace in simulated input DIV from physical keyboard where not matched in rule I3363 (Build 301)
if(Levent.Lcode == 8 && !LeventMatched && Levent.Ltarg.className != null && Levent.Ltarg.className.indexOf('keymanweb-input') >= 0)
keymanweb.KO(1,keymanweb._LastActiveElement,"");
}
else
{
// Mnemonic layout
if(Levent.Lcode == 8) // I1595 - Backspace for mnemonic
{
keymanweb._KeyPressToSwallow = 1;
if(!keymanweb.callKeyboardStartGroup(Levent.Ltarg,Levent))
keymanweb.KO(1,keymanweb._LastActiveElement,""); // I3363 (Build 301)
return false; //added 16/3/13 to fix double backspace on mnemonic layouts on desktop
}
else
keymanweb._KeyPressToSwallow = 0;
}
if(!LeventMatched && Levent.Lcode >= 96 && Levent.Lcode <= 111)
{
// Number pad, numlock on
// _Debug('KeyPress NumPad code='+Levent.Lcode+'; Ltarg='+Levent.Ltarg.tagName+'; LisVirtualKey='+Levent.LisVirtualKey+'; _KeyPressToSwallow='+keymanweb._KeyPressToSwallow+'; keyCode='+(e?e.keyCode:'nothing'));
if(Levent.Lcode < 106) var Lch = Levent.Lcode-48;
else Lch = Levent.Lcode-64;
keymanweb.KO(0, Levent.Ltarg, String._kmwFromCharCode(Lch)); //I3319
LeventMatched = 1;
}
if(LeventMatched)
{
keymanweb._FindCaret(Levent.Ltarg); //I779
if(e && e.preventDefault) e.preventDefault();
keymanweb._KeyPressToSwallow = (e?e.keyCode:0);
return false;
}
else keymanweb._KeyPressToSwallow = 0;
if(Levent.Lcode == 8)
{
/* Backspace - delete deadkeys, also special rule if desired? */
// This is needed to prevent jumping to previous page, but why??? // I3363 (Build 301)
if(Levent.Ltarg.className != null && Levent.Ltarg.className.indexOf('keymanweb-input') >= 0) return false;
}
return true;
}
keymanweb.callKeyboardStartGroup = function(Ltarg, Levent) {
keymanweb._CachedSelectionStart = null; // I3319
keymanweb._DeadkeyResetMatched(); // I3318
keymanweb.cachedContext.reset();
return keymanweb._ActiveKeyboard['gs'](Ltarg, Levent);
}
/**
* Function _KeyPress
* Scope Private
* @param {Event} e event
* @return {boolean}
* Description Processes keypress event (does not pass data to keyboard)
*/
keymanweb._KeyPress = function(e)
{
var Levent;
if(!keymanweb._Enabled || keymanweb._DisableInput || keymanweb._ActiveKeyboard == null ||
(keymanweb._ActiveControl != null && !keymanweb._ActiveControl.LEnabled)) return true;
Levent = keymanweb._GetKeyEventProperties(e);
if(Levent == null || Levent.LisVirtualKey) return true;
// _Debug('KeyPress code='+Levent.Lcode+'; Ltarg='+Levent.Ltarg.tagName+'; LisVirtualKey='+Levent.LisVirtualKey+'; _KeyPressToSwallow='+keymanweb._KeyPressToSwallow+'; keyCode='+(e?e.keyCode:'nothing'));
/* I732 START - 13/03/2007 MCD: Swedish: Start positional keyboard layout code: prevent keystroke */
if(!keymanweb._ActiveKeyboard['KM'])
{
if(!keymanweb._KeyPressToSwallow) return true;
if(Levent.Lcode < 0x20 || (keymanweb._BrowserIsSafari && (Levent.Lcode > 0xF700 && Levent.Lcode < 0xF900))) return true;
e = keymanweb._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
if(e) e.returnValue = false;
return false;
}
/* I732 END - 13/03/2007 MCD: Swedish: End positional keyboard layout code */
if(keymanweb._KeyPressToSwallow || keymanweb.callKeyboardStartGroup(Levent.Ltarg,Levent))
{
keymanweb._KeyPressToSwallow=0;
if(e && e.preventDefault) e.preventDefault();
keymanweb._FindCaret(Levent.Ltarg); // I779
return false;
}
keymanweb._KeyPressToSwallow=0;
return true;
}
/**
* Function _KeyUp
* Scope Private
* @param {Event} e event
* @return {boolean}
* Description Processes keyup event and passes event data to keyboard
*/
keymanweb._KeyUp = function(e)
{
var Levent = keymanweb._GetKeyEventProperties(e);
if(Levent == null || !osk.ready) return true;
switch(Levent.Lcode)
{
case 13:
if(Levent.Ltarg.nodeName == 'TEXTAREA') break;
// For input fields, move to next input element
if(Levent.Ltarg.type == 'search' || Levent.Ltarg.type == 'submit')
Levent.Ltarg.form.submit();
else
keymanweb.moveToNext(false);
return true;
case 16:
case 17:
case 18: keymanweb._NotifyKeyboard(Levent.Lcode,Levent.Ltarg,0); return osk._UpdateVKShift(Levent, Levent.Lcode-15, 1); // I2187
}
// I736 start
var Ldv;
if((Ldv=Levent.Ltarg.ownerDocument) && (Ldv=Ldv.selection) && Ldv.type != 'control') // I1479 - avoid createRange on controls
{
Ldv=Ldv.createRange();
//if(Ldv.parentElement()==Levent.Ltarg) //I1505
keymanweb._Selection = Ldv;
}
// I736 end
//if(document.selection && document.selection.type=='Text') keymanweb._Selection=document.selection.createRange();
//if(!KeymanWeb._Enabled) return true;
//if (!e) e = window.event;
return false;
}
//TODO: check return of _KeyUp - what happens if returning true or false ?? what if null returned?
/**
* Move focus to next (or previous) input or text area element on TAB
* Uses list of actual input elements
*
* Note that _ActiveElement on touch devices returns the DIV that overlays
* the input element, not the element itself.
*
* @param {number|boolean} bBack Direction to move (0 or 1)
*/
keymanweb.moveToNext=function(bBack)
{
var i,t=keymanweb.sortedInputs,
activeBase=keymanweb._ActiveElement;
if(t.length == 0) return;
// For touchable devices, get the base element of the DIV
if(device.touchable) activeBase=activeBase.base;
// Identify the active element in the list of inputs ordered by position
for(i=0; i<t.length; i++)
{
if(t[i] == activeBase) break;
}
// Find the next (or previous) element in the list
if(bBack) i=i-1; else i=i+1;
if(i >= t.length) i=i-t.length;
if(i < 0) i=i+t.length;
// Move to the selected element
if(device.touchable)
{
// Set focusing flag to prevent OSK disappearing
keymanweb.focusing=true;
var target=t[i]['kmw_ip'];
// Focus if next element is non-mapped
if(typeof(target) == 'undefined')
{
t[i].focus();
}
// Or reposition the caret on the input DIV if mapped
else
{
keymanweb._ActiveElement=keymanweb._LastActiveElement=target;
keymanweb.setTextCaret(target,10000);
keymanweb.scrollInput(target);
}
}
// Behaviour for desktop browsers
else
{
t[i].focus();
}
}
/**
* Move focus to user-specified element
*
* @param {string|Object} e element or element id
*
**/
keymanweb['moveToElement'] = function(e)
{
var i;
if(typeof(e) == 'string') e=document.getElementById(e);
if(device.touchable && e['kmw_ip'])
e['kmw_ip'].focus();
else
e.focus();
}
/**
* Change active keyboard to keyboard selected by (internal) name and language code
*
* Test if selected keyboard already loaded, and simply update active stub if so.
* Otherwise, insert a script to download and insert the keyboard from the repository
* or user-indicated file location.
*
* @param {string} PInternalName
* @param {string=} PLgCode
* @param {boolean=} saveCookie
*/
keymanweb._SetActiveKeyboard = function(PInternalName,PLgCode,saveCookie)
{
var n,Ln;
// Set default language code
if(arguments.length < 2 || (!PLgCode)) {
PLgCode='---';
}
// Check that the saved keyboard is currently registered
for(n=0; n<keymanweb._KeyboardStubs.length; n++) {
if(PInternalName == keymanweb._KeyboardStubs[n]['KI']) {
if(PLgCode == keymanweb._KeyboardStubs[n]['KLC'] || PLgCode == '---') break;
}
}
// Mobile device addition: force selection of the first keyboard if none set
if(device.touchable && (PInternalName == '' || PInternalName == null || n >= keymanweb._KeyboardStubs.length))
{
PInternalName=keymanweb._KeyboardStubs[0]['KI']; PLgCode=keymanweb._KeyboardStubs[0]['KLC'];
}
// Save name of keyboard (with language code) as a cookie
if(arguments.length > 2 && saveCookie) keymanweb.saveCurrentKeyboard(PInternalName,PLgCode);
// Check if requested keyboard and stub are currently active
if(keymanweb._ActiveStub && keymanweb._ActiveKeyboard
&& keymanweb._ActiveKeyboard['KI'] == PInternalName
&& keymanweb._ActiveStub['KI'] == PInternalName //this part of test should not be necessary, but keep anyway
&& keymanweb._ActiveStub['KLC'] == PLgCode
&& !keymanweb.mustReloadKeyboard
) return;
// Check if current keyboard matches requested keyboard, but not stub
if(keymanweb._ActiveKeyboard && (keymanweb._ActiveKeyboard['KI'] == PInternalName))
{
// If so, simply update the active stub
for(Ln=0; Ln<keymanweb._KeyboardStubs.length; Ln++)
{
if((keymanweb._KeyboardStubs[Ln]['KI'] == PInternalName) && (keymanweb._KeyboardStubs[Ln]['KLC'] == PLgCode))
{
keymanweb._ActiveStub = keymanweb._KeyboardStubs[Ln];
// Append a stylesheet for this keyboard for keyboard specific styles
// or if needed to specify an embedded font
osk.appendStyleSheet();
// Re-initializate OSK before returning if required
if(keymanweb.mustReloadKeyboard) osk._Load();
return;
}
}
}
keymanweb._ActiveKeyboard = null; keymanweb._ActiveStub = null;
// Hide OSK and do not update keyboard list if using internal keyboard (desktops)
if(PInternalName == '') {
osk._Hide(false);
if(typeof(util.wait) == 'function') {
util.wait(false);
}
return;
}
// Determine if the keyboard was previously loaded but is not active and use the prior load if so.
for(Ln=0; Ln<keymanweb._Keyboards.length; Ln++) // I1511 - array prototype extended
{
if(keymanweb._Keyboards[Ln]['KI'] == PInternalName)
{
keymanweb._ActiveKeyboard = keymanweb._Keyboards[Ln];
keymanweb._SetTargDir(keymanweb._LastActiveElement); // I2077 - LTR/RTL timing
// and update the active stub
for(var Ls=0; Ls<keymanweb._KeyboardStubs.length; Ls++)
{
if((keymanweb._KeyboardStubs[Ls]['KI'] == PInternalName) && (keymanweb._KeyboardStubs[Ls]['KLC'] == PLgCode))
{
keymanweb._ActiveStub = keymanweb._KeyboardStubs[Ls]; break;
}
}
break;
}
}
if(keymanweb._ActiveKeyboard == null)
{
for(Ln=0; Ln<keymanweb._KeyboardStubs.length; Ln++) // I1511 - array prototype extended
{
if((keymanweb._KeyboardStubs[Ln]['KI'] == PInternalName)
&& ((keymanweb._KeyboardStubs[Ln]['KLC'] == PLgCode) || (PLgCode == '---')))
{
// Force OSK display for CJK keyboards (keyboards using a pick list)
if(keymanweb.isCJK(keymanweb._KeyboardStubs[Ln]) || device.touchable) osk._Enabled = 1;
// Create a script to load from the server - when it finishes loading, it will register itself,
// detect that it is active, and focus as appropriate. The second test is needed to allow recovery from a failed script load
// Ensure we're not already loading the keyboard.
if(!keymanweb._KeyboardStubs[Ln].asyncLoader) {
// Always (temporarily) hide the OSK when loading a new keyboard, to ensure that a failure to load doesn't leave the current OSK displayed
if(osk.ready) osk._Hide(false);
var loadingStub = keymanweb._KeyboardStubs[Ln];
// Tag the stub so that we don't double-load the keyboard!
loadingStub.asyncLoader = {};
var kbdName = loadingStub['KN'];
var lngName = loadingStub['KL'];
kbdName = kbdName.replace(/\s*keyboard\s*/i, '');
// Setup our default error-messaging callback if it should be implemented.
if(typeof(util.wait) == 'function') { // No error messaging if this function doesn't exist.
loadingStub.asyncLoader.callback = function(altString) {
// Thanks, Closure errors.
if(typeof(util.wait) == 'function') {
util.wait(false);
}
if(!altString) {
altString = 'Sorry, the '+kbdName+' keyboard for '+lngName+' is not currently available.';
}
util.alert(altString, function() {
keymanweb['setActiveKeyboard']('');
});
if(Ln > 0) {
var Ps = keymanweb._KeyboardStubs[0];
keymanweb._SetActiveKeyboard(Ps['KI'], Ps['KLC'], true);
}
}
loadingStub.asyncLoader.timer = window.setTimeout(loadingStub.asyncLoader.callback, 10000);
}
//Display the loading delay bar (Note: only append 'keyboard' if not included in name.)
var wText='Installing keyboard<br/>' + kbdName;
if(typeof(util.wait) == 'function') {
util.wait(wText);
}
// Installing the script immediately does not work reliably if two keyboards are
// loaded in succession if there is any delay in downloading the script.
// It works much more reliably if deferred (KMEW-101, build 356)
// The effect of a delay can also be tested, for example, by setting the timeout to 5000
window.setTimeout(function(){keymanweb.installKeyboard(loadingStub);},0);
}
keymanweb._ActiveStub=keymanweb._KeyboardStubs[Ln];
return;
}
}
keymanweb._SetTargDir(keymanweb._LastActiveElement); // I2077 - LTR/RTL timing
}
var Pk=keymanweb._ActiveKeyboard; // I3319
if(Pk !== null) // I3363 (Build 301)
String.kmwEnableSupplementaryPlane(Pk && ((Pk['KS'] && (Pk['KS'] == 1)) || (Pk['KN'] == 'Hieroglyphic'))); // I3319
// Initialize the OSK (provided that the base code has been loaded)
osk._Load();
}
/**
* Install a keyboard script that has been downloaded from a keyboard server
*
* @param {Object} kbdStub keyboard stub to be loaded.
*
**/
keymanweb.installKeyboard = function(kbdStub)
{
var Lscript = util._CreateElement('SCRIPT');
Lscript.charset="UTF-8"; // KMEW-89
Lscript.type = 'text/javascript';
var kbdFile = kbdStub['KF'];
var kbdLang = kbdStub['KL'];
var kbdName = kbdStub['KN'];
// Add a handler for cases where the new <script> block fails to load.
Lscript.addEventListener('error', function() {
if(kbdStub.asyncLoader.timer !== null) {
// Clear the timeout timer.
window.clearTimeout(kbdStub.asyncLoader.timer);
kbdStub.asyncLoader.timer = null;
}
// Do not output error messages when embedded. (KMEA/KMEI)
if(!keymanweb.embedded) {
// We already know the load has failed... why wait?
kbdStub.asyncLoader.callback('Cannot find the ' + kbdName + ' keyboard for ' + kbdLang + '.');
console.log('Error: cannot find the', kbdName, 'keyboard for', kbdLang, 'at', kbdFile + '.');
}
kbdStub.asyncLoader = null;
}, true);
// The load event will activate a newly-loaded keyboard if successful and report an error if it is not.
Lscript.addEventListener('load', function() {
if(kbdStub.asyncLoader.timer !== null) {
// Clear the timeout timer.
window.clearTimeout(kbdStub.asyncLoader.timer);
kbdStub.asyncLoader.timer = null;
}
// To determine if the load was successful, we'll need to check the keyboard array for our desired keyboard.
// Test if keyboard already loaded
var kbd = keymanweb._getKeyboardInternal(kbdStub['KI']), Li;
if(kbd != undefined) { // Is cleared upon a successful load.
//Activate keyboard, if it's still the active stub.
if(kbdStub == keymanweb._ActiveStub) {
keymanweb.doBeforeKeyboardChange(kbd['KI'],kbdStub['KLC']);
keymanweb._ActiveKeyboard=kbd;
if(keymanweb._LastActiveElement != null)
{
keymanweb._JustActivatedKeymanWebUI = 1;
keymanweb._SetTargDir(keymanweb._LastActiveElement);
}
String.kmwEnableSupplementaryPlane(kbdStub && ((kbdStub['KS'] && (kbdStub['KS'] == 1)) || (kbd['KN'] == 'Hieroglyphic'))); // I3319 - SMP extension, I3363 (Build 301)
keymanweb.saveCurrentKeyboard(kbd['KI'],kbdStub['KLC']);
// Prepare and show the OSK for this keyboard
osk._Load();
// Remove the wait message, if defined
if(typeof (util.wait) == 'function') {
util.wait(false);
}
} // A handler portion for cases where the new <script> block loads, but fails to process.
} else if(!keymanweb.embedded) { // Do not output error messages when embedded. (KMEA/KMEI)
kbdStub.asyncLoader.callback('Error registering the ' + kbdName + ' keyboard for ' + kbdLang + '.');
console.log('Error registering the', kbdName, 'keyboard for', kbdLang + '.');
}
kbdStub.asyncLoader = null;
}, true);
// IE likes to instantly start loading the file when assigned to an element, so we do this after the rest
// of our setup.
Lscript.src = keymanweb.getKeyboardPath(kbdFile);
try {
document.body.appendChild(Lscript);
}
catch(ex) {
document.getElementsByTagName('head')[0].appendChild(Lscript);
}
}
/**
* Function _getKeyboardInternal
* Scope Private
* @param {string} keyboardID
* @return {Object|null}
* Description Returns the internal, registered keyboard object - not the stub, but the keyboard itself.
*/
keymanweb._getKeyboardInternal = function(keyboardID) {
var Li;
for(Li=0; Li<keymanweb._Keyboards.length; Li++) {
if(keyboardID == keymanweb._Keyboards[Li]['KI']) {
return keymanweb._Keyboards[Li];
}
}
return null;
}
/**
* Function isCJK
* Scope Public
* @param {Object=} k0
* @return {boolean}
* Description Tests if active keyboard (or optional argument) uses a pick list (Chinese, Japanese, Korean, etc.)
* (This function accepts either keyboard structure.)
*/
keymanweb['isCJK'] = function(k0)
{
var k=keymanweb._ActiveKeyboard, lg='';
if(arguments.length > 0) k = k0;
if(k)
{
if(typeof(k['KLC']) != 'undefined') lg = k['KLC'];
else if(typeof(k['LanguageCode']) != 'undefined') lg = k['LanguageCode'];
}
return ((lg == 'cmn') || (lg == 'jpn') || (lg == 'kor'));
}
keymanweb.isCJK = keymanweb['isCJK']; // I3363 (Build 301)
/**
* Allow to change active keyboard by (internal) keyboard name
*
* @param {string} PInternalName Internal name
* @param {string} PLgCode Language code
*/
keymanweb['setActiveKeyboard'] = function(PInternalName,PLgCode)
{
//TODO: This does not make sense: the callbacks should be in _SetActiveKeyboard, not here,
// since this is always called FROM the UI, which should not need notification.
// If UI callbacks are needed at all, they should be within _SetActiveKeyboard
keymanweb.doBeforeKeyboardChange(PInternalName,PLgCode);
keymanweb._SetActiveKeyboard(PInternalName,PLgCode,true);
if(keymanweb._LastActiveElement != null) keymanweb._FocusLastActiveElement();
keymanweb.doKeyboardChange(PInternalName,PLgCode);
}
/**
* Function getActiveKeyboard
* Scope Public
* @return {string} Name of active keyboard
* Description Return internal name of currently active keyboard
*/
keymanweb['getActiveKeyboard'] = function()
{
if(keymanweb._ActiveKeyboard == null) return '';
return keymanweb._ActiveKeyboard['KI'];
}
/**
* Function getActiveLanguage
* Scope Public
* @return {string} language code
* Description Return language code for currently selected language
*/
keymanweb['getActiveLanguage'] = function()
{
if(keymanweb._ActiveStub == null) return '';
return keymanweb._ActiveStub['KLC'];
}
//TODO: find all references to next three routines and disambiguate!!
/**
* Get keyboard meta data for the selected keyboard and language
*
* @param {string} PInternalName Internal name of keyboard
* @param {string=} PlgCode language code
* @return {Object} Details of named keyboard
*
**/
keymanweb['getKeyboard'] = function(PInternalName,PlgCode)
{
var Ln, Lrn;
for(Ln=0; Ln<keymanweb._KeyboardStubs.length; Ln++)
{
Lrn = keymanweb._GetKeyboardDetail(keymanweb._KeyboardStubs[Ln]);
if(Lrn['InternalName'] == PInternalName)
{
if(arguments.length < 2) return Lrn;
if(Lrn['LanguageCode'] == PlgCode) return Lrn;
}
}
return null;
}
/**
* Function GetKeyboardDetail
* Scope Private
* @param {string} PInternalName Internal name of keyboard
* @return {Object} Details of named keyboard
* TODO: should it be Array or Object???
* Description Return keyboard details object
*/
keymanweb.GetKeyboardDetail = function(PInternalName) // I2079 - GetKeyboardDetail function
{
var Lr=keymanweb['getKeyboards']();
for(var Ln=0; Ln<Lr.length; Ln++)
if(Lr[Ln]['InternalName'] == PInternalName) return Lr[Ln];
return null;
}
/**
* Get an associative array of keyboard identification strings
* This was defined as an array, so is kept that way, but
* Javascript treats it as an object anyway
*
* @param {Object} Lkbd Keyboard object
* @return {Object} Copy of keyboard identification strings
*
*/
keymanweb._GetKeyboardDetail = function(Lkbd) // I2078 - Full keyboard detail
{
var Lr={};
Lr['Name'] = Lkbd['KN'];
Lr['InternalName'] = Lkbd['KI'];
Lr['LanguageName'] = Lkbd['KL']; // I1300 - Add support for language names
Lr['LanguageCode'] = Lkbd['KLC']; // I1702 - Add support for language codes, region names, region codes, country names and country codes
Lr['RegionName'] = Lkbd['KR'];
Lr['RegionCode'] = Lkbd['KRC'];
Lr['CountryName'] = Lkbd['KC'];
Lr['CountryCode'] = Lkbd['KCC'];
Lr['KeyboardID'] = Lkbd['KD'];
Lr['Font'] = Lkbd['KFont'];
Lr['OskFont'] = Lkbd['KOskFont'];
return Lr;
}
/**
* Get array of available keyboard stubs
*
* @return {Array} Array of available keyboards
*
*/
keymanweb['getKeyboards'] = function()
{
var Lr = [], Ln, Lstub, Lrn;
for(Ln=0; Ln<keymanweb._KeyboardStubs.length; Ln++) // I1511 - array prototype extended
{
Lstub = keymanweb._KeyboardStubs[Ln];
Lrn = keymanweb._GetKeyboardDetail(Lstub); // I2078 - Full keyboard detail
Lr=keymanweb._push(Lr,Lrn);
}
return Lr;
}
/**
* Function _WindowUnload
* Scope Private
* Description Remove handlers before detaching KMW window
*/
keymanweb._WindowUnload = function()
{
// Allow the UI to release its own resources
keymanweb.doUnloadUI();
// Allow the OSK to release its own resources
if(osk.ready) osk._Unload(); // I3363 (Build 301)
keymanweb._LastActiveElement = 0;
}
// Complete page initialization only after the page is fully loaded, including any embedded fonts
// This avoids the need to use a timer to test for the fonts
util.attachDOMEvent(window, 'load',function(e){
//keymanweb.completeInitialization();
// Always return to top of page after a page reload
document.body.scrollTop=0;
if(typeof document.documentElement != 'undefined') document.documentElement.scrollTop=0;
},false);
// Attach this handler to window unload event
util.attachDOMEvent(window, 'unload', keymanweb._WindowUnload,false); // added fourth argument (default value)
/**
* Function _AttachToControls
* Scope Private
* Parameters {Object} Pelem element
* Description Attach KMW to editable controls
*/
keymanweb._AttachToControls = function(Pelem) // I1961
{
/**
* Function LiTmp
* Scope Private
* @param {string} _colon type of element
* @return {Array} array of elements of specified type
* Description Local function to get list of editable controls
*/
var LiTmp = function(_colon){return Pelem.getElementsByTagName(_colon);};
// If the element Pelem has any child elements, we wish to analyze those.
var Linputs = LiTmp('INPUT'),
Ltextareas = LiTmp('TEXTAREA'),
Lframes = LiTmp('IFRAME'),
Lce = document.evaluate ? document.evaluate('//*[@contenteditable and @contenteditable != "false"]', document, null, XPathResult.ANY_TYPE, null) : null; // I2457 - support contentEditable elements in mozilla, webkit
for(var Li = 0; Li < Linputs.length; Li++)
if(Linputs[Li].type.toLowerCase() == 'text') keymanweb.attachToControl(Linputs[Li]);
for(Li = 0; Li < Ltextareas.length; Li++)
keymanweb.attachToControl(Ltextareas[Li]);
for(Li = 0; Li < Lframes.length; Li++)
try {
if(Lframes[Li].contentWindow.document)
keymanweb._AttachToIframe(Lframes[Li]);
}
catch(err) { /* Do not attempt to access iframes outside this site */ }
if(Lce) // I2457 - support contentEditable elements in mozilla, webkit
{
for (var Lc = Lce.iterateNext(); Lc; Lc = Lce.iterateNext())
{
keymanweb.attachToControl(Lc);
}
}
// Attach keymanweb-input DIV elements I3363 (Build 301)
for(Li = 0; Li<keymanweb.inputList.length; Li++)
{
keymanweb.attachToControl(keymanweb.inputList[Li]);
}
}
/**
* Return a path that has is always terminated by a slash
*
* @param {string} p folder path
* @return {string}
**/
keymanweb.fixPath = function(p)
{
if(p.length == 0) return p;
var q=p.substr(p.length-1,1);
if(q == '/' || q == '\\') return p;
return p+'/';
}
/**
* Function Initialization
* Scope Public
* @param {Object} arg object array of user-defined properties
* Description KMW window initialization
*/
keymanweb['init'] = keymanweb.init = function(arg)
{
var i,j,c,e,p,eTextArea,eInput,opt,dTrailer,ds;
// Local function to convert relative to absolute URLs
// with respect to the source path, server root and protocol
var fixPath = function(p)
{
if(p.length == 0) return p;
// Add delimiter if missing
if(p.substr(p.length-1,1) != '/') p = p+'/';
// Absolute
if((p.replace(/^(http)s?:.*/,'$1') == 'http')
|| (p.replace(/^(file):.*/,'$1') == 'file'))
return p;
// Absolute (except for protocol)
if(p.substr(0,2) == '//')
return keymanweb.protocol+p;
// Relative to server root
if(p.substr(0,1) == '/')
return keymanweb.rootPath+p.substr(1);
// Otherwise, assume relative to source path
return keymanweb.srcPath+p;
}
// Explicit (user-defined) parameter initialization
opt=keymanweb.options;
if(typeof(arg) == 'object' && arg !== null)
{
for(p in opt)
{
if(arg.hasOwnProperty(p)) opt[p] = arg[p];
}
}
// Get default paths and device options
if(opt['root'] != '') keymanweb.rootPath = fixPath(opt['root']);
// Keyboards and fonts are located with respect to the server root by default
//if(opt['keyboards'] == '') opt['keyboards'] = keymanweb.rootPath+'keyboard/';
//if(opt['fonts'] == '') opt['fonts'] = keymanweb.rootPath+'font/';
// Resources are located with respect to the engine by default
if(opt['resources'] == '') opt['resources'] = keymanweb.srcPath;
// Convert resource, keyboard and font paths to absolute URLs
opt['resources'] = fixPath(opt['resources']);
opt['keyboards'] = fixPath(opt['keyboards']);
opt['fonts'] = fixPath(opt['fonts']);
// Set element attachment type
if(opt['attachType'] == '') opt['attachType'] = (device.touchable ? 'manual' : 'auto');
/*
// Only initialize options the first time init is called
if(typeof(keymanweb.options['resources']) == 'undefined')
{
var root='',fontSource='',resources='',keyboards='';
// Get values of global variables if defined
if(typeof(window['KeymanWeb_Root']) == 'string') root=window['KeymanWeb_Root'];
if(typeof(window['KeymanWeb_Resources']) == 'string') resources=window['KeymanWeb_Resources'];
if(typeof(window['KeymanWeb_Keyboards']) == 'string') keyboards=window['KeymanWeb_Keyboards'];
if(typeof(window['KeymanWeb_FontUriBasePath']) == 'string') fontSource=window['KeymanWeb_FontUriBasePath'];
var opt={};
opt['root']=root;
opt['resources']=resources;
opt['keyboards']=keyboards;
opt['fonts'] = fontSource;
if(typeof(window['KeymanWeb_AttachType']) == 'string')
opt['attachType']=window['KeymanWeb_AttachType'];
else
opt['attachType'] = (device.touchable ? 'manual' : 'auto');
if(typeof(window['KeymanWeb_FloatUI']) == 'object')
opt['ui'] = window['KeymanWeb_FloatUI'];
else
opt['ui'] = '';
if(device.touchable) opt['ui'] = 'none';
keymanweb.options = opt;
}
else
{
var opt=keymanweb.options;
}
// Update the option if required by a subsequent call
if(arguments.length > 0 && typeof(arg)=='object' && arg != null)
{
for(var p in opt)
{
if(arg.hasOwnProperty(p)) opt[p] = arg[p];
}
}
// Get default root path and device options
if(opt['root'] == '') opt['root'] = keymanweb.rootPath;
// Use root-relative paths for resources and keyboards unless set by page source
if(opt['resources'] == '') opt['resources']=opt['root'];
if(opt['keyboards'] == '') opt['keyboards']=opt['root']+'kbd/';
if(opt['fonts'] == '') opt['fonts']=opt['root']+'font/';
*/
// Set default device options
keymanweb.setDefaultDeviceOptions(opt);
// Only do remainder of initialization once!
if(keymanweb['initialized']) return;
// Do not initialize until the document has been fully loaded
if(document.readyState !== 'complete')
{
window.setTimeout(function(){keymanweb.init(arg);},50);
return;
}
keymanweb._MasterDocument = window.document;
/**
* Initialization of touch devices and browser interfaces must be done
* after all resources are loaded, during final stage of initialization
*
*/
// Treat Android devices as phones if either (reported) screen dimension is less than 4"
if(device.OS == 'Android')
{
// Determine actual device characteristics I3363 (Build 301)
// TODO: device.dpi may no longer be needed - if so, get rid of it.
device.dpi = util.getDPI(); //TODO: this will not work when called from HEAD!!
device.formFactor=((screen.height < 4.0*device.dpi) || (screen.width < 4.0*device.dpi)) ? 'phone' : 'tablet';
}
if (window.removeEventListener)
window.removeEventListener('focus', keymanweb._BubbledFocus, true);
if(keymanweb.options['attachType'] != 'manual')
keymanweb._AttachToControls(document); // I1961
// Set exposed initialization flag member for UI (and other) code to use
keymanweb['initialized'] = 1;
// Finish keymanweb and OSK initialization once all necessary resources are available
osk.prepare();
// Create and save the remote keyboard loading delay indicator
util.prepareWait();
// Register deferred keyboard stubs (addKeyboards() format)
keymanweb.addKeyboardArray(keymanweb.deferredStubs);
// KRS stubs (legacy format registration)
for(j=0; j<keymanweb.deferredKRS.length; j++)
keymanweb.KRS(keymanweb.deferredKRS[j]);
// Initialize the desktop UI
keymanweb.initializeUI()
// Register deferred keyboards
for(j=0; j<keymanweb.deferredKR.length; j++)
keymanweb.KR(keymanweb.deferredKR[j]);
// Exit initialization here if target already fully initialized (if external routine exists)
if(!keymanweb.fullInitialization) return;
// Determine the default font for mapped elements
keymanweb.appliedFont=keymanweb.baseFont=keymanweb.getBaseFont();
// Create an ordered list of all input and textarea fields
keymanweb.listInputs();
// Add orientationchange event handler to manage orientation changes on mobile devices
// Initialize touch-screen device interface I3363 (Build 301)
if(device.touchable) {
keymanweb.handleRotationEvents();
keymanweb.setupTouchDevice();
}
// Initialize desktop browser interface
else
keymanweb.setupDesktopPage();
// Initialize the OSK and set default OSK styles
// Note that this should *never* be called before the OSK has been initialized.
// However, it possibly may be called before the OSK has been fully defined with the current keyboard, need to check.
//osk._Load();
// I1476 - Handle SELECT overlapping BEGIN TODO: Move this to float UI code ??
if(util._GetIEVersion() == 6) osk.shim = util['createShim'](); // I3363 (Build 301)
// I1476 - Handle SELECT overlapping END
//document.body.appendChild(osk._Box);
//osk._Load(false);
// I3363 (Build 301)
if(device.touchable)
{
// Handle OSK touchend events (prevent propagation)
osk._Box.addEventListener('touchend',function(e){e.stopPropagation();},false);
// Add a blank DIV to the bottom of the page to allow the bottom of the page to be shown
dTrailer=document.createElement('DIV'); ds=dTrailer.style;
ds.width='100%';ds.height=(screen.width/2)+'px';
document.body.appendChild(dTrailer);
// On Chrome, scrolling up or down causes the URL bar to be shown or hidden
// according to whether or not the document is at the top of the screen.
// But when doing that, each OSK row top and height gets modified by Chrome
// looking very ugly. Itwould be best to hide the OSK then show it again
// when the user scroll finishes, but Chrome has no way to reliably report
// the touch end event after a move. c.f. http://code.google.com/p/chromium/issues/detail?id=152913
// The best compromise behaviour is simply to hide the OSK whenever any
// non-input and non-OSK element is touched.
if(device.OS == 'Android' && navigator.userAgent.indexOf('Chrome') > 0)
{
keymanweb.hideOskWhileScrolling=function(e)
{
if(typeof(osk._Box) == 'undefined') return;
if(typeof(osk._Box.style) == 'undefined') return;
// The following tests are needed to prevent the OSK from being hidden during normal input!
p=e.target.parentNode;
if(typeof(p) != 'undefined' && p != null)
{
if(p.className.indexOf('keymanweb-input') >= 0) return;
if(p.className.indexOf('kmw-key-') >= 0) return;
if(typeof(p.parentNode) != 'undefined')
{
p=p.parentNode;
if(p.className.indexOf('keymanweb-input') >= 0) return;
if(p.className.indexOf('kmw-key-') >= 0) return;
}
}
osk.hideNow();
}
document.body.addEventListener('touchstart',keymanweb.hideOskWhileScrolling,false);
}
else
{
keymanweb.conditionallyHideOsk = function()
{
// Should not hide OSK if simply closing the language menu (30/4/15)
if(keymanweb.hideOnRelease && !osk.lgList) osk.hideNow();
keymanweb.hideOnRelease=false;
}
keymanweb.hideOskIfOnBody = function(e)
{
keymanweb.touchY=e.touches[0].screenY;
keymanweb.hideOnRelease=true;
}
keymanweb.cancelHideIfScrolling = function(e)
{
var y=e.touches[0].screenY,y0=keymanweb.touchY;
if(y-y0 > 5 || y0-y < 5) keymanweb.hideOnRelease = false;
}
document.body.addEventListener('touchstart',keymanweb.hideOskIfOnBody,false);
document.body.addEventListener('touchmove',keymanweb.cancelHideIfScrolling,false);
document.body.addEventListener('touchend',keymanweb.conditionallyHideOsk,false);
}
}
if(osk.shim)
document.body.appendChild(osk.shim); // I1476 - Handle SELECT overlapping
//document.body.appendChild(keymanweb._StyleBlock);
// IE: call _SelectionChange when the user changes the selection
if(document.selection && !keymanweb.legacy)
util.attachDOMEvent(document, 'selectionchange', keymanweb._SelectionChange);
// Restore and reload the currently selected keyboard
keymanweb.restoreCurrentKeyboard();
/* Setup of handlers for dynamically-added and (eventually) dynamically-removed elements.
* Reference: https://developer.mozilla.org/en/docs/Web/API/MutationObserver
*
* We place it here so that it loads after most of the other UI loads, reducing the MutationObserver's overhead.
* Of course, we only want to dynamically add elements if the user hasn't enabled the manual attachment option.
*/
if(keymanweb.options['attachType'] != 'manual') { //I1961
var observationTarget = document.querySelector('body');
keymanweb.mutationObserver = new MutationObserver(function(mutations) {
var inputElementAdditions = [];
var inputElementRemovals = [];
for(var i=0; i < mutations.length; i++) {
var mutation = mutations[i];
for(var j=0; j < mutation.addedNodes.length; j++) {
var addedNode = mutation.addedNodes[j];
var lcTagName = addedNode.tagName ? addedNode.tagName.toLowerCase() : "";
// Will need to handle this in case of child elements in a newly-added element with child elements.
if(addedNode.getElementsByTagName) {
inputElementAdditions = inputElementAdditions.concat(
util.arrayFromNodeList(addedNode.getElementsByTagName('input')),
util.arrayFromNodeList(addedNode.getElementsByTagName('textarea')),
util.arrayFromNodeList(addedNode.getElementsByTagName('iframe'))
);
}
if(lcTagName == 'input' || lcTagName == 'textarea' || lcTagName == 'iframe') {
inputElementAdditions.push(addedNode);
}
}
for(j = 0; j < mutation.removedNodes.length; j++) {
var removedNode = mutation.removedNodes[j];
var lcTagName = removedNode.tagName ? removedNode.tagName.toLowerCase() : "";
// Will need to handle this in case of child elements in a newly-added element with child elements.
if(removedNode.getElementsByTagName) {
inputElementRemovals = inputElementRemovals.concat(
util.arrayFromNodeList(removedNode.getElementsByTagName('input')),
util.arrayFromNodeList(removedNode.getElementsByTagName('textarea')),
util.arrayFromNodeList(removedNode.getElementsByTagName('iframe'))
);
}
// After all mutations have been handled, we need to recompile our .sortedInputs array.
}
}
for(var k = 0; k < inputElementAdditions.length; k++) {
keymanweb._MutationAdditionObserved(inputElementAdditions[k]);
}
for(k = 0; k < inputElementRemovals.length; k++) {
keymanweb._MutationRemovalObserved(inputElementRemovals[k]);
}
/* After all mutations have been handled, we need to recompile our .sortedInputs array, but only.
* if any have actually occurred.
*/
if(inputElementAdditions.length || inputElementRemovals.length) {
if(!device.touchable) {
keymanweb.listInputs();
} else if(device.touchable) { // If something was added or removed, chances are it's gonna mess up our touch-based layout scheme, so let's update the touch elements.
window.setTimeout(function() {
keymanweb.listInputs();
for(var k = 0; k < keymanweb.sortedInputs.length; k++) {
if(keymanweb.sortedInputs[k]['kmw_ip']) {
keymanweb.updateInput(keymanweb.sortedInputs[k]['kmw_ip']);
}
}
}, 1);
}
}
});
var observationConfig = { childList: true, subtree: true };
keymanweb.mutationObserver.observe(observationTarget, observationConfig);
}
// Set exposed initialization flag to 2 to indicate deferred initialization also complete
keymanweb['initialized']=2;
}
/**
* Function _MutationAdditionObserved
* Scope Private
* @param {Element} Pelem A page input, textarea, or iframe element.
* Description Used by the MutationObserver event handler to properly setup any elements dynamically added to the document post-initialization.
*
*/
keymanweb._MutationAdditionObserved = function(Pelem) {
if(!device.touchable) {
// keymanweb.attachToControl is written to handle iframes, but setupDesktopElement is not.
if(keymanweb.setupDesktopElement(Pelem)) {
keymanweb.attachToControl(Pelem);
} else if(Pelem.tagName.toLowerCase() == 'iframe') {
//Problem: the iframe is loaded asynchronously, and we must wait for it to load fully before hooking in.
var attachFunctor = function() { // Triggers at the same time as iframe's onload property, after its internal document loads.
keymanweb.attachToControl(Pelem);
};
Pelem.addEventListener('load', attachFunctor);
/* If the iframe has somehow already loaded, we can't expect the onload event to be raised. We ought just
* go ahead and perform our callback's contents.
*
* keymanweb.attachToControl() is now idempotent, so even if our call 'whiffs', it won't cause long-lasting
* problems.
*/
if(Pelem.contentDocument.readyState == 'complete') {
attachFunctor();
}
}
} else {
keymanweb.setupTouchElement(Pelem);
}
}
// Used by the mutation event handler to properly decouple any elements dynamically removed from the document.
keymanweb._MutationRemovalObserved = function(Pelem) {
var element = Pelem;
if(device.touchable) {
element = Pelem['kmw_ip'];
// We get weird repositioning errors if we don't remove our simulated input element - and permanently.
Pelem.parentNode.removeChild(element);
delete Pelem['kmw_ip'];
}
var index = keymanweb.inputList.indexOf(Pelem);
if(index != -1) {
keymanweb.inputList.splice(index, 1);
}
}
// Create an ordered list of all text and search input elements and textarea elements
// except any tagged with class 'kmw-disabled'
// TODO: email and url types should perhaps use default keyboard only
keymanweb.listInputs = function()
{
var i,eList=[],
t1=document.getElementsByTagName('INPUT'),
t2=document.getElementsByTagName('TEXTAREA');
for(i=0; i<t1.length; i++)
{
switch(t1[i].type)
{
case 'text':
case 'search':
case 'email':
case 'url':
if(t1[i].className.indexOf('kmw-disabled') < 0)
eList.push({ip:t1[i],x:util._GetAbsoluteX(t1[i]),y:util._GetAbsoluteY(t1[i])});
break;
}
}
for(i=0; i<t2.length; i++)
{
if(t2[i].className.indexOf('kmw-disabled') < 0)
eList.push({ip:t2[i],x:util._GetAbsoluteX(t2[i]),y:util._GetAbsoluteY(t2[i])});
}
/**
* Local function to sort by screen position
*
* @param {Object} e1 first object
* @param {Object} e2 second object
* @return {number} y-difference between object positions, or x-difference if y values the same
*/
var xySort=function(e1,e2)
{
if(e1.y != e2.y) return e1.y-e2.y;
return e1.x-e2.x;
}
// Sort elements by Y then X
eList.sort(xySort);
// Create a new list of sorted elements
var tList=[];
for(i=0;i<eList.length;i++)
tList.push(eList[i].ip);
// Return the sorted element list
keymanweb.sortedInputs=tList;
}
/**
* Initialize the desktop user interface as soon as it is ready
**/
keymanweb.initializeUI = function()
{
if(typeof(keymanweb['ui']['initialize'])=='function')
{
keymanweb['ui']['initialize']();
// Display the OSK (again) if enabled, in order to set its position correctly after
// adding the UI to the page
osk._Show();
}
else
window.setTimeout(keymanweb.initializeUI,1000);
}
/**
* Test if caret position is determined from the active element, or
* from the synthesized overlay element (touch devices)
*
* @return {boolean}
**/
keymanweb.isPositionSynthesized = function()
{
return device.touchable;
}
/**
* Function _SelPos
* Scope Private
* @param {Object} Pelem Element
* @return {number} Selection start
* Description Get start of selection (with supplementary plane modifications)
*/
keymanweb._SelPos = function(Pelem)
{
var Ldoc, Ldv, isMSIE=(util._GetIEVersion()<999); // I3363 (Build 301)
if(keymanweb.isPositionSynthesized())
return keymanweb.getTextCaret(Pelem);
if(Pelem._KeymanWebSelectionStart)
return Pelem._KeymanWebSelectionStart;
// Mozilla, IE9
else if (Pelem.setSelectionRange)
return Pelem.value.substr(0,Pelem.selectionStart)._kmwLength();
// contentEditable elements, Mozilla midas
else if((Ldv=Pelem.ownerDocument) && (Ldv=Ldv.defaultView) && Ldv.getSelection
&& Pelem.ownerDocument.designMode.toLowerCase() == 'on') {
var Lsel = Ldv.getSelection();
if(Lsel.focusNode.nodeType == 3)
return Lsel.focusNode.substringData(0,Lsel.focusOffset)._kmwLength();
}
// IE8 and earlier
else if(isMSIE)
{
// Get position within input or textarea element
if(typeof(Pelem.value) == 'string') {
var ss=keymanweb.getInputSelection(Pelem);
return Pelem.value.substr(0,ss.start)._kmwLength();
}
// Get position within content-editable region
if(Pelem.body) Ldoc=Pelem; else Ldoc=Pelem.ownerDocument; // I1481 - integration with rich editors not working 100%
if(Ldoc) Ldv=Ldoc.selection; else return 0;
var Lrange = Ldv.createRange();
Lrange.moveStart('textedit',-1);
return Lrange.text._kmwLength();
}
return 0;
}
/* Old code without SMP mods
keymanweb._SelPos = function(Pelem)
{
var Ldv;
if(Pelem._KeymanWebSelectionStart) return Pelem._KeymanWebSelectionStart;
else if (Pelem.setSelectionRange)
return Pelem.selectionStart;
else if((Ldv=Pelem.ownerDocument) && (Ldv=Ldv.defaultView) && Ldv.getSelection && Pelem.ownerDocument.designMode.toLowerCase() == 'on') // && Pelem.tagName == 'HTML')
{
var Lsel = Ldv.getSelection();
if(Lsel.focusNode.nodeType == 3) return Lsel.focusOffset;
}
return 0;
}*/
/**
* Function getInputSelection
* Scope Private
* @param {Object} el element
* @return {Object.<string,number>} selection start
* Description Get input selection for all(?) browsers, per Tim Down
* http://stackoverflow.com/questions/3053542/how-to-get-the-start-and-end-points-of-selection-in-text-area/3053640#3053640
* But only works for input fields, not for content editable fields!!!
**/
keymanweb.getInputSelection = function(el)
{
var start = 0, end = 0, normalizedValue = '', range, textInputRange, len = 0, endRange;
if(typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart; end = el.selectionEnd;
} else {
range = document.selection.createRange();
if(range && range.parentElement() == el) {
len = el.value.length;
normalizedValue = el.value.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = el.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end of the input,
// since moveStart/moveEnd doesn't return what we want in those cases
endRange = el.createTextRange();
endRange.collapse(false);
if(textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if(textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
return {start: start, end: end};
}
// *** I3319 Supplementary Plane modifications - end new code
// I3318 - deadkey changes START
/**
* Function _DeadkeyResetMatched
* Scope Private
* Description Clear all matched deadkey flags
*/
keymanweb._DeadkeyResetMatched = function()
{
var Li, _Dk = keymanweb._DeadKeys;
for(Li = 0; Li < _Dk.length; Li++) _Dk[Li].matched = 0;
}
/**
* Function _DeadkeyDeleteMatched
* Scope Private
* Description Delete matched deadkeys from context
*/
keymanweb._DeadkeyDeleteMatched = function()
{
var Li, _Dk = keymanweb._DeadKeys;
for(Li = 0; Li < _Dk.length; Li++) if(_Dk[Li].matched) _Dk.splice(Li,1);
}
/**
* Function _DeadkeyAdjustPos
* Scope Private
* @param {number} Lstart start position in context
* @param {number} Ldelta characters to adjust by
* Description Adjust saved positions of deadkeys in context
*/
keymanweb._DeadkeyAdjustPos = function(Lstart, Ldelta)
{
var Li, _Dk = keymanweb._DeadKeys;
for(Li = 0; Li < _Dk.length; Li++) if(_Dk[Li].p > Lstart) _Dk[Li].p += Ldelta;
}
// I3318 - deadkey changes END
/**
* Set target element text direction (LTR or RTL), but only if the element is empty
*
* If the element base directionality is changed after it contains content, unless all the text
* has the same directionality, text runs will be re-ordered which is confusing and causes
* incorrect caret positioning
*
* @param {Object} Ptarg Target element
*/
keymanweb._SetTargDir = function(Ptarg)
{
var elDir=((keymanweb._ActiveKeyboard != null) && (keymanweb._ActiveKeyboard['KRTL'])) ? 'rtl' : 'ltr';
if(Ptarg)
{
if(device.touchable)
{
if(Ptarg.textContent.length == 0)
{
Ptarg.base.dir=Ptarg.dir=elDir;
keymanweb.setTextCaret(Ptarg,10000);
}
}
else
{
if(Ptarg.value.length == 0) Ptarg.dir=elDir;
}
}
}
/**
* Reset OSK shift states when entering or exiting the active element
**/
keymanweb._ResetVKShift = function()
{
if(!keymanweb._IsActivatingKeymanWebUI)
{
if(osk._UpdateVKShift) osk._UpdateVKShift(null,15,0); //this should be enabled !!!!! TODO
}
}
/**
* Function addHotKey
* Scope Public
* @param {number} keyCode
* @param {number} shiftState
* @param {function(Object)} handler
* Description Add hot key handler to array of document-level hotkeys triggered by key up event
*/
keymanweb['addHotKey'] = keymanweb.addHotKey = function(keyCode,shiftState,handler)
{
// Test if existing handler for this code and replace it if so
for(var i=0; i<keymanweb._HotKeys.length; i++)
{
if(keymanweb._HotKeys[i].Code == keyCode && keymanweb._HotKeys[i].Shift == shiftState)
{
keymanweb._HotKeys[i].Handler = handler; return;
}
}
// Otherwise add it to the array
keymanweb._HotKeys.push({Code:keyCode,Shift:shiftState,Handler:handler});
}
/**
* Function removeHotKey
* Scope Public
* @param {number} keyCode
* @param {number} shiftState
* Description Remove a hot key handler from array of document-level hotkeys triggered by key up event
*/
keymanweb['removeHotKey'] = keymanweb.removeHotKey = function(keyCode,shiftState)
{
for(var i=0; i<keymanweb._HotKeys.length; i++)
{
if(keymanweb._HotKeys[i].Code == keyCode && keymanweb._HotKeys[i].Shift == shiftState)
{
keymanweb._HotKeys.splice(i,1); return;
}
}
}
/**
* Function _ProcessHotKeys
* Scope Private
* @param {Event} e event
* Description Passes control to handlers according to the hotkey pressed
*/
keymanweb._ProcessHotKeys = function(e)
{
if(!e) e = window.event;
if (e.keyCode) var _Lcode = e.keyCode;
else if (e.which) var _Lcode = e.which;
else return 0;
// Removed testing of e.shiftKey==null I3363 (Build 301)
var _Lmodifiers =
(e.shiftKey ? 0x10 : 0) |
(e.ctrlKey ? (e.ctrlLeft ? 0x20 : 0x20) : 0) |
(e.altKey ? (e.altLeft ? 0x40 : 0x40) : 0);
for(var i=0; i<keymanweb._HotKeys.length; i++)
{
if(_Lcode == keymanweb._HotKeys[i].Code)
{
if(_Lmodifiers == keymanweb._HotKeys[i].Shift)
{
keymanweb._HotKeys[i].Handler();
e.returnValue = 0;
if(e && e.preventDefault) e.preventDefault();
e.cancelBubble = true;
return false;
}
}
}
return true;
}
util.attachDOMEvent(document, 'keyup', keymanweb._ProcessHotKeys,false);
/* TODO: why not use util.loadCookie and saveCookie?? */
/**
* Function saveCurrentKeyboard
* Scope Private
* @param {string} PInternalName name of keyboard
* @param {string} PLgCode language code
* Description Saves current keyboard as a cookie
*/
keymanweb.saveCurrentKeyboard = function(PInternalName,PLgCode)
{
var s = "current="+PInternalName+":"+PLgCode;
util.saveCookie('KeymanWeb_Keyboard',{'current':PInternalName+':'+PLgCode});
}
/**
* Restore the most recently used keyboard, if still available
*/
keymanweb.restoreCurrentKeyboard = function()
{
var stubs=keymanweb._KeyboardStubs,i,n=stubs.length;
// Do nothing if no stubs loaded
if(stubs.length < 1) return;
// If no saved keyboard, default to US English, else first loaded stub
var d=keymanweb.getSavedKeyboard();
var t=d.split(':');
// Identify the stub with the saved keyboard
t=d.split(':');
if(t.length < 2) t[1]='';
// This loop is needed to select the correct stub when several apply to a given keyboard
// TODO: There should be a better way!
for(i=0; i<n; i++)
{
if(stubs[i]['KI'] == t[0] && (stubs[i]['KLC'] == t[1] || t[1] == '')) break;
}
// Restore the stub with the saved keyboard, or else the first keyboard that matches
// if((i < n) || (device.touchable && (keymanweb._ActiveKeyboard == null)))
if((i < n) || (keymanweb._ActiveKeyboard == null))
{
keymanweb._SetActiveKeyboard(t[0],t[1],false);
keymanweb.doKeyboardChange(t[0],t[1]); // And update the UI if necessary
}
}
/**
* Gets the cookie for the name and language code of the most recently active keyboard
*
* Defaults to US English, but this needs to be user-set in later revision (TODO)
*
* @return {string} InternalName:LanguageCode
**/
keymanweb['getSavedKeyboard'] = keymanweb.getSavedKeyboard = function()
{
var v=util.loadCookie('KeymanWeb_Keyboard');
if(typeof(v['current']) != 'string') return 'Keyboard_us:eng';
// Check that the requested keyboard is included in the available keyboard stubs
var n,stubs=keymanweb._KeyboardStubs,kd;
for(n=0; n<stubs.length; n++)
{
kd=stubs[n]['KI']+':'+stubs[n]['KLC'];
if(kd == v['current']) return kd;
}
// Default to US English if available (but don't assume it is first)
for(n=0; n<stubs.length; n++)
{
kd=stubs[n]['KI']+':'+stubs[n]['KLC'];
if(kd == 'Keyboard_us:eng') return kd;
}
// Otherwise use the first keyboard stub
if(stubs.length > 0) return stubs[0]['KI']+':'+stubs[0]['KLC'];
// Or US English if no stubs loaded (should never happen)
return 'Keyboard_us:eng';
}
util.attachDOMEvent(window, 'focus', keymanweb._ResetVKShift,false); // I775
util.attachDOMEvent(window, 'blur', keymanweb._ResetVKShift,false); // I775
// Initialize supplementary plane string extensions
String.kmwEnableSupplementaryPlane(false);
})();