Refactored the DOM events into their own separate source file. Further refactoring will be needed down the line.

This commit is contained in:
Joshua A. Horton 2018-01-11 11:12:19 +07:00
parent 95068cec55
commit fbf863ca16
2 changed files with 864 additions and 858 deletions

View file

@ -2,867 +2,11 @@
/// <reference path="kmwexthtml.ts" />
// References the base KMW object.
/// <reference path="kmwbase.ts" />
// References DOM event handling interfaces and classes.
/// <reference path="kmwdomevents.ts" />
// Includes KMW string extension declarations.
/// <reference path="kmwstring.ts" />
/**
* Declares an interface for implementation of touch-based alias event handlers and state functions.
*/
interface AliasElementHandlers {
/**
* Handle receiving focus by simulated input field
*
* @param {Event=} e event
*/
setFocus(e?: TouchEvent|MSPointerEvent): void;
/**
* Get simulated input field content
*
* @param {Object} e element (object) of simulated input field
* @return {string} entire text in simulated input field
*/
getText(e: HTMLElement): string;
/**
*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)
*/
setText(e: HTMLElement, t?: string, cp?: number): void;
/**
* Get text up to the caret from simulated input field
*
* @return {string}
*/
getTextBeforeCaret(e: HTMLElement): string;
/**
* Replace text up to the caret in the simulated input field
*
* @param {Object} e element (object) of simulated input field
* @param {string} t Context for simulated input field
*/
setTextBeforeCaret(e: HTMLElement, t: string): void;
/**
* Description Get current position of caret in simulated input field
*
* @param {Object} e element (object) of simulated input field
* @return {number} caret character position in simulated input field
*/
getTextCaret(e: HTMLElement): number;
/**
* Set current position of caret in simulated input field then display the caret
*
* @param {Object} e element (object) of simulated input field
* @param {number} cp caret character position in simulated input field
*/
setTextCaret(e: HTMLElement, cp: number): void;
/**
* Hides the simulated caret for touch-aliased elements.
*/
hideCaret(): void;
/**
* Toggle state of caret in simulated input field
*/
flashCaret(): void;
/**
* Correct the position and size of a duplicated input element
*
* @param {Object} x element
*/
updateInput(x: HTMLElement);
/**
* Handles touch-based loss of focus events.
*/
setBlur(e: FocusEvent);
// End of I3363 (Build 301) additions
}
// -------------------------------------------------------------------------
/**
* Defines numerous functions for handling and modeling touch-based aliases.
*/
class TouchHandlers implements AliasElementHandlers {
// TODO: resolve/refactor out!
keymanweb: any;
// Stores the simulated caret element.
caret: HTMLDivElement;
caretTimerId: number;
firstTouch: {
x: number;
y: number;
};
constructor(keyman: any) {
this.keymanweb = keyman;
this.initCaret();
}
initCaret(): void {
/**
* 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.
*/
this.caret=<HTMLDivElement> document.createElement('DIV');
var cs=this.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
// Start the caret flash timer
this.caretTimerId = window.setInterval(this.flashCaret,500);
}
/**
* Handle receiving focus by simulated input field
*
* @param {Event=} e event
*/
setFocus(e?: TouchEvent|MSPointerEvent): void {
//e.stopPropagation(); // not doing anything useful, no child elements
//e.preventDefault(); // prevents user selection or scrolling, may be better if they are allowed?
// Warning: Event handlers can change the reference of 'this', so we need to re-resolve it.
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._setFocus(e);
}
/**
* Handles the actual _setFocus details, now with 'this' as our touch-handling interface.
*/
_setFocus(e?: TouchEvent|MSPointerEvent): void {
var kmw = this.keymanweb;
var osk = this.keymanweb.osk;
var util = this.keymanweb.util;
this.keymanweb.focusing=true;
this.keymanweb.focusTimer=window.setTimeout(function(){
kmw.focusing=false;
}, 1000);
var tEvent: {
clientX: number;
clientY: number;
target?: any;
};
if(e instanceof TouchEvent) {
tEvent=e.touches[0];
} else { // Allow external code to set focus and thus display the OSK on touch devices if required (KMEW-123)
tEvent={clientX:0, clientY:0}
// Will usually be called from setActiveElement, which should define _LastActiveElement
if(this.keymanweb._LastActiveElement) {
tEvent.target = this.keymanweb._LastActiveElement['kmw_ip'];
// but will default to first input or text area on page if _LastActiveElement is null
} else {
tEvent.target = this.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(this.keymanweb._ActiveElement != target) {
// Hide the KMW caret
this.hideCaret();
this.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
this.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
this.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);
}
this.setTextCaret(target,cp);
this.scrollInput(target);
} else { // Otherwise, if clicked on text in SPAN, set at touch position
var caret,cp,cpMin,cpMax,x,y,dy,yRow,iLoop;
caret=scroller.childNodes[1]; //caret span
cpMin=0;
cpMax=this.getText(target)._kmwLength();
cp=this.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;
this.setTextCaret(target,cp);
}
while(util._GetAbsoluteY(caret)-dy > touchY && cp > cpMin) {
this.setTextCaret(target,--cp);
}
while(util._GetAbsoluteY(caret)-dy < touchY-yRow && cp < cpMax) {
this.setTextCaret(target,++cp);
}
}
// Caret repositioning for horizontal scrolling of RTL text
// snapOrder - 'snaps' the touch location in a manner corresponding to the 'ltr' vs 'rtl' orientation.
// Think of it as performing a floor() function, but the floor depends on the origin's direction.
var snapOrder;
if(target.dir == 'rtl') { // I would use arrow functions, but IE doesn't like 'em.
snapOrder = function(a, b) {
return a < b;
};
} else {
snapOrder = function(a, b) {
return a > b;
};
}
for(iLoop=0; iLoop<16; iLoop++) {
x=util._GetAbsoluteX(caret); //left of caret
if(snapOrder(x, touchX) && cp > cpMin && cp != cpMax) {
cpMax=cp;
cp=Math.round((cp+cpMin)/2);
} else if(!snapOrder(x, touchX) && cp < cpMax && cp != cpMin) {
cpMin=cp;
cp=Math.round((cp+cpMax)/2);
} else {
break;
}
this.setTextCaret(target,cp);
}
while(snapOrder(util._GetAbsoluteX(caret), touchX) && cp > cpMin) {
this.setTextCaret(target,--cp);
}
while(!snapOrder(util._GetAbsoluteX(caret), touchX) && cp < cpMax) {
this.setTextCaret(target,++cp);
}
}
/**
* This event will trigger before keymanweb.setBlur is triggered. Now that we're allowing independent keyboard settings
* for controls, we have to act here to preserve the outgoing control's keyboard settings.
*
* If we 'just activated' the KeymanWeb UI, we need to save the new keyboard change as appropriate.
*/
this.keymanweb._BlurKeyboardSettings();
// With the attachment API update, we now directly track the old legacy control behavior.
this.keymanweb._LastActiveElement = target;
/**
* If we 'just activated' the KeymanWeb UI, we need to save the new keyboard change as appropriate.
* If not, we need to activate the control's preferred keyboard.
*/
this.keymanweb._FocusKeyboardSettings(false);
// Always do the common focus stuff, instantly returning if we're in an editable iframe.
// This parallels the if-statement in _ControlFocus - it may be needed as this if-statement in the future,
// despite its present redundancy.
if(this.keymanweb._CommonFocusHelper(target)) {
return;
}
}
getText(e: HTMLElement): string {
if(e == null) {
return '';
}
return (typeof(e.textContent) == 'string' ? e.textContent : e.innerText);
}
setText(e: HTMLElement, t?: string, cp?: number): void {
if(e && e.childNodes.length > 0) {
var d=e.firstChild,tLen=0;
if(d.childNodes.length >= 3) {
var s1=<HTMLElement> d.childNodes[0], s2=<HTMLElement> 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;
}
}
}
this.updateBaseElement(e,tLen); // KMW-3, KMW-29
}
getTextBeforeCaret(e: HTMLElement) {
if(e && e.childNodes.length > 1) {
var d=e.firstChild;
if(d.childNodes.length > 0) {
var s1=<HTMLElement> d.childNodes[0];
if('textContent' in s1) {
return s1.textContent;
}
if('innerText' in s1) {
return s1.innerText;
}
}
}
return '';
}
setTextBeforeCaret(e: HTMLElement, t: string): void {
if(e && e.childNodes.length > 0) {
var d=e.firstChild,tLen=0;
if(d.childNodes.length > 1) {
var s1=<HTMLElement> d.childNodes[0], s2=<HTMLElement> 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
this.updateBaseElement(e,tLen); //KMW-3, KMW-29
this.scrollInput(e);
}
getTextCaret(e: HTMLElement): number {
return this.getTextBeforeCaret(e)._kmwLength();
}
setTextCaret(e: HTMLElement, cp: number): void {
this.setText(e,null,cp);
this.showCaret(e);
}
hideCaret() {
var e=this.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 = this.getText(e);
// And set the scroller caret to the end of the element content
this.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(this.caret.parentNode) {
this.caret.parentNode.removeChild(this.caret);
}
this.caret.style.visibility='hidden';
if(e.childNodes.length > 1 ) {
e.childNodes[1].style.visibility='hidden';
}
}
}
flashCaret(): void {
// Is typically accessed via timer, so 'this' is not valid.
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._flashCaret();
}
/**
* Handles the actual _flashCaret details, now with 'this' as our touch-handling interface.
*/
_flashCaret(): void {
if(this.keymanweb.util.device.touchable && this.keymanweb._ActiveElement != null) {
var cs=this.caret.style;
cs.visibility = cs.visibility != 'visible' ? 'visible' : 'hidden';
}
}
/**
* Position the caret at the start of the second span within the scroller
*
* @param {Object} e input DIV element (copy of INPUT or TEXTAREA)
*/
showCaret(e: HTMLElement) {
if(!e || !e.firstChild || (e.firstChild.childNodes.length<3)) {
return;
}
var scroller=e.firstChild, cs=this.caret.style, sp2=<HTMLElement>scroller.childNodes[1];
// Attach the caret to this scroller and position it over the caret span
if(this.caret.parentNode != scroller) {
scroller.appendChild(this.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
this.scrollBody(e);
// Display and position the scrollbar if necessary
this.setScrollBar(e);
}
updateInput(x: HTMLDivElement) {
var util = this.keymanweb.util;
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(util.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--;
} else { // Chrome, Opera, native browser (?)
w++;
h++;
}
}
xs.width=w+'px';
xs.height=h+'px';
}
/**
* 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
*/
updateBaseElement(e: HTMLElement, n: number) {
if(e.base instanceof HTMLInputElement || e.base instanceof HTMLTextAreaElement) {
e.base.value = this.getText(e); //KMW-29
} else {
e.base.textContent = this.getText(e);
}
e.style.backgroundColor=(n==0?'transparent':window.getComputedStyle(e.base,null).backgroundColor);
if(this.keymanweb.util.device.OS == 'iOS') {
e.base.style.visibility=(n==0?'visible':'hidden');
}
}
/**
* Close OSK and remove simulated caret on losing focus
*/
cancelInput(): void {
this.keymanweb._ActiveElement=null;
this.hideCaret();
this.keymanweb.osk.hideNow();
};
/**
* Handle losing focus from simulated input field
*
* @param {Event} e event
*/
setBlur(e: FocusEvent) {
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._setBlur(e);
}
_setBlur(e: FocusEvent) {
// This works OK for iOS, but may need something else for other platforms
if(('relatedTarget' in e) && e.relatedTarget) {
var elem: HTMLElement = e.relatedTarget as HTMLElement;
if(elem.nodeName != 'DIV' || elem.className.indexOf('keymanweb-input') == -1) {
this.cancelInput(); return;
}
}
//Hide the OSK
if(!this.keymanweb.focusing) {
this.cancelInput();
}
}
/**
* Display and position a scrollbar in the input field if needed
*
* @param {Object} e input DIV element (copy of INPUT or TEXTAREA)
*/
setScrollBar(e: HTMLElement) {
// Display the scrollbar if necessary. Added TEXTAREA condition to correct rotation issue KMW-5. Fixed for 310 beta.
var scroller=<HTMLElement>e.childNodes[0], sbs=(<HTMLElement>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';
}
}
/**
* Handle the touch move event for an input element
*
* @param {Event} e touchmove event
*/
dragInput(e: TouchEvent|MouseEvent) {
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._dragInput(e);
}
_dragInput(e: TouchEvent|MouseEvent) {
// Prevent dragging window
e.preventDefault();
e.stopPropagation();
// Identify the target from the touch list or the event argument (IE 10 only)
var target: HTMLElement;
if(e instanceof TouchEvent) {
target = e.targetTouches[0].target as HTMLElement;
} else {
target = e.target as HTMLElement;
}
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=<HTMLElement> target.parentNode;
if(target.className == null || target.className.indexOf('keymanweb-input') < 0) target=<HTMLElement> target.parentNode;
if(target.className == null || target.className.indexOf('keymanweb-input') < 0) return;
var x, y;
if(e instanceof TouchEvent) {
x = e.touches[0].screenX;
y = e.touches[0].screenY;
} else {
x = e.screenX;
y = e.screenY;
}
// Allow content of input elements to be dragged horizontally or vertically
if(typeof this.firstTouch == 'undefined' || this.firstTouch == null) {
this.firstTouch={x:x,y:y};
} else {
var x0=this.firstTouch.x,y0=this.firstTouch.y,
scroller=target.firstChild as HTMLElement,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';
this.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=this.keymanweb.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';
this.firstTouch.x=x;
}
}
}
this.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
*/
scrollInput(e: HTMLElement) {
if(!e || !e.firstChild || e.className == null || e.className.indexOf('keymanweb-input') < 0 ) {
return;
}
var scroller=e.firstChild as HTMLElement;
if(scroller.childNodes.length < 3) {
return;
}
var util = this.keymanweb.util;
// 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/(<HTMLTextAreaElement>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';
} else { // or scroll horizontally if needed
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)
this.showCaret(e);
}
/**
* Scroll the document body vertically to bring the active input into view
*
* @param {Object} e simulated input field object being focussed
*/
scrollBody(e: HTMLElement): void {
var osk = this.keymanweb.osk;
var util = this.keymanweb.util;
if(!e || e.className == null || e.className.indexOf('keymanweb-input') < 0 || !osk.ready) {
return;
}
// Get the absolute position of the caret
var s2=<HTMLElement>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);
}
}
}
// -------------------------------------------------------------------------
/**
* Stub function equivalents to alias element handling, for use with desktop mode. Dummies out
* the aliasing behaviors for internal system logic consistency.
*/
class AliasStubs implements AliasElementHandlers {
/**
* Input element stub functions, serving as empty alternates for non-aliased elements.
*/
setFocus(e?: Event): void {
};
getText(e: HTMLElement): string {
return '';
}
setText(e: HTMLElement, t?: string, cp?: number): void {
}
getTextBeforeCaret(): string {
return '';
}
setTextBeforeCaret(e: HTMLElement, t: string): void {
}
getTextCaret(e: HTMLElement): number {
return 0;
}
setTextCaret(e: HTMLElement, cp: number) {
}
hideCaret(): void {
}
flashCaret(): void {
}
updateInput(x: HTMLElement) {
}
setBlur(e: FocusEvent) {
}
}
// -------------------------------------------------------------------------
/**
* This class serves as the intermediary between KeymanWeb and any given web page's elements.
*/

862
web/source/kmwdomevents.ts Normal file
View file

@ -0,0 +1,862 @@
/*
* Note that for many of the actual events represented by methods in this file, `this` is replaced
* automatically by JavaScript's event handling system. As such, many 'wrapper' variants of the events
* exist to restore the object-oriented hierarchy below.
*
*/
/**
* Declares an interface for implementation of touch-based alias event handlers and state functions.
*/
interface AliasElementHandlers {
/**
* Handle receiving focus by simulated input field
*
* @param {Event=} e event
*/
setFocus(e?: TouchEvent|MSPointerEvent): void;
/**
* Get simulated input field content
*
* @param {Object} e element (object) of simulated input field
* @return {string} entire text in simulated input field
*/
getText(e: HTMLElement): string;
/**
*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)
*/
setText(e: HTMLElement, t?: string, cp?: number): void;
/**
* Get text up to the caret from simulated input field
*
* @return {string}
*/
getTextBeforeCaret(e: HTMLElement): string;
/**
* Replace text up to the caret in the simulated input field
*
* @param {Object} e element (object) of simulated input field
* @param {string} t Context for simulated input field
*/
setTextBeforeCaret(e: HTMLElement, t: string): void;
/**
* Description Get current position of caret in simulated input field
*
* @param {Object} e element (object) of simulated input field
* @return {number} caret character position in simulated input field
*/
getTextCaret(e: HTMLElement): number;
/**
* Set current position of caret in simulated input field then display the caret
*
* @param {Object} e element (object) of simulated input field
* @param {number} cp caret character position in simulated input field
*/
setTextCaret(e: HTMLElement, cp: number): void;
/**
* Hides the simulated caret for touch-aliased elements.
*/
hideCaret(): void;
/**
* Toggle state of caret in simulated input field
*/
flashCaret(): void;
/**
* Correct the position and size of a duplicated input element
*
* @param {Object} x element
*/
updateInput(x: HTMLElement);
/**
* Handles touch-based loss of focus events.
*/
setBlur(e: FocusEvent);
// End of I3363 (Build 301) additions
}
// -------------------------------------------------------------------------
/**
* Defines numerous functions for handling and modeling touch-based aliases.
*/
class TouchHandlers implements AliasElementHandlers {
// TODO: resolve/refactor out!
keymanweb: any;
// Stores the simulated caret element.
caret: HTMLDivElement;
caretTimerId: number;
firstTouch: {
x: number;
y: number;
};
constructor(keyman: any) {
this.keymanweb = keyman;
this.initCaret();
}
initCaret(): void {
/**
* 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.
*/
this.caret=<HTMLDivElement> document.createElement('DIV');
var cs=this.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
// Start the caret flash timer
this.caretTimerId = window.setInterval(this.flashCaret,500);
}
/**
* Handle receiving focus by simulated input field
*
* @param {Event=} e event
*/
setFocus(e?: TouchEvent|MSPointerEvent): void {
//e.stopPropagation(); // not doing anything useful, no child elements
//e.preventDefault(); // prevents user selection or scrolling, may be better if they are allowed?
// Warning: Event handlers can change the reference of 'this', so we need to re-resolve it.
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._setFocus(e);
}
/**
* Handles the actual _setFocus details, now with 'this' as our touch-handling interface.
*/
_setFocus(e?: TouchEvent|MSPointerEvent): void {
var kmw = this.keymanweb;
var osk = this.keymanweb.osk;
var util = this.keymanweb.util;
this.keymanweb.focusing=true;
this.keymanweb.focusTimer=window.setTimeout(function(){
kmw.focusing=false;
}, 1000);
var tEvent: {
clientX: number;
clientY: number;
target?: any;
};
if(e instanceof TouchEvent) {
tEvent=e.touches[0];
} else { // Allow external code to set focus and thus display the OSK on touch devices if required (KMEW-123)
tEvent={clientX:0, clientY:0}
// Will usually be called from setActiveElement, which should define _LastActiveElement
if(this.keymanweb._LastActiveElement) {
tEvent.target = this.keymanweb._LastActiveElement['kmw_ip'];
// but will default to first input or text area on page if _LastActiveElement is null
} else {
tEvent.target = this.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(this.keymanweb._ActiveElement != target) {
// Hide the KMW caret
this.hideCaret();
this.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
this.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
this.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);
}
this.setTextCaret(target,cp);
this.scrollInput(target);
} else { // Otherwise, if clicked on text in SPAN, set at touch position
var caret,cp,cpMin,cpMax,x,y,dy,yRow,iLoop;
caret=scroller.childNodes[1]; //caret span
cpMin=0;
cpMax=this.getText(target)._kmwLength();
cp=this.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;
this.setTextCaret(target,cp);
}
while(util._GetAbsoluteY(caret)-dy > touchY && cp > cpMin) {
this.setTextCaret(target,--cp);
}
while(util._GetAbsoluteY(caret)-dy < touchY-yRow && cp < cpMax) {
this.setTextCaret(target,++cp);
}
}
// Caret repositioning for horizontal scrolling of RTL text
// snapOrder - 'snaps' the touch location in a manner corresponding to the 'ltr' vs 'rtl' orientation.
// Think of it as performing a floor() function, but the floor depends on the origin's direction.
var snapOrder;
if(target.dir == 'rtl') { // I would use arrow functions, but IE doesn't like 'em.
snapOrder = function(a, b) {
return a < b;
};
} else {
snapOrder = function(a, b) {
return a > b;
};
}
for(iLoop=0; iLoop<16; iLoop++) {
x=util._GetAbsoluteX(caret); //left of caret
if(snapOrder(x, touchX) && cp > cpMin && cp != cpMax) {
cpMax=cp;
cp=Math.round((cp+cpMin)/2);
} else if(!snapOrder(x, touchX) && cp < cpMax && cp != cpMin) {
cpMin=cp;
cp=Math.round((cp+cpMax)/2);
} else {
break;
}
this.setTextCaret(target,cp);
}
while(snapOrder(util._GetAbsoluteX(caret), touchX) && cp > cpMin) {
this.setTextCaret(target,--cp);
}
while(!snapOrder(util._GetAbsoluteX(caret), touchX) && cp < cpMax) {
this.setTextCaret(target,++cp);
}
}
/**
* This event will trigger before keymanweb.setBlur is triggered. Now that we're allowing independent keyboard settings
* for controls, we have to act here to preserve the outgoing control's keyboard settings.
*
* If we 'just activated' the KeymanWeb UI, we need to save the new keyboard change as appropriate.
*/
this.keymanweb._BlurKeyboardSettings();
// With the attachment API update, we now directly track the old legacy control behavior.
this.keymanweb._LastActiveElement = target;
/**
* If we 'just activated' the KeymanWeb UI, we need to save the new keyboard change as appropriate.
* If not, we need to activate the control's preferred keyboard.
*/
this.keymanweb._FocusKeyboardSettings(false);
// Always do the common focus stuff, instantly returning if we're in an editable iframe.
// This parallels the if-statement in _ControlFocus - it may be needed as this if-statement in the future,
// despite its present redundancy.
if(this.keymanweb._CommonFocusHelper(target)) {
return;
}
}
getText(e: HTMLElement): string {
if(e == null) {
return '';
}
return (typeof(e.textContent) == 'string' ? e.textContent : e.innerText);
}
setText(e: HTMLElement, t?: string, cp?: number): void {
if(e && e.childNodes.length > 0) {
var d=e.firstChild,tLen=0;
if(d.childNodes.length >= 3) {
var s1=<HTMLElement> d.childNodes[0], s2=<HTMLElement> 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;
}
}
}
this.updateBaseElement(e,tLen); // KMW-3, KMW-29
}
getTextBeforeCaret(e: HTMLElement) {
if(e && e.childNodes.length > 1) {
var d=e.firstChild;
if(d.childNodes.length > 0) {
var s1=<HTMLElement> d.childNodes[0];
if('textContent' in s1) {
return s1.textContent;
}
if('innerText' in s1) {
return s1.innerText;
}
}
}
return '';
}
setTextBeforeCaret(e: HTMLElement, t: string): void {
if(e && e.childNodes.length > 0) {
var d=e.firstChild,tLen=0;
if(d.childNodes.length > 1) {
var s1=<HTMLElement> d.childNodes[0], s2=<HTMLElement> 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
this.updateBaseElement(e,tLen); //KMW-3, KMW-29
this.scrollInput(e);
}
getTextCaret(e: HTMLElement): number {
return this.getTextBeforeCaret(e)._kmwLength();
}
setTextCaret(e: HTMLElement, cp: number): void {
this.setText(e,null,cp);
this.showCaret(e);
}
hideCaret() {
var e=this.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 = this.getText(e);
// And set the scroller caret to the end of the element content
this.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(this.caret.parentNode) {
this.caret.parentNode.removeChild(this.caret);
}
this.caret.style.visibility='hidden';
if(e.childNodes.length > 1 ) {
e.childNodes[1].style.visibility='hidden';
}
}
}
flashCaret(): void {
// Is typically accessed via timer, so 'this' is not valid.
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._flashCaret();
}
/**
* Handles the actual _flashCaret details, now with 'this' as our touch-handling interface.
*/
_flashCaret(): void {
if(this.keymanweb.util.device.touchable && this.keymanweb._ActiveElement != null) {
var cs=this.caret.style;
cs.visibility = cs.visibility != 'visible' ? 'visible' : 'hidden';
}
}
/**
* Position the caret at the start of the second span within the scroller
*
* @param {Object} e input DIV element (copy of INPUT or TEXTAREA)
*/
showCaret(e: HTMLElement) {
if(!e || !e.firstChild || (e.firstChild.childNodes.length<3)) {
return;
}
var scroller=e.firstChild, cs=this.caret.style, sp2=<HTMLElement>scroller.childNodes[1];
// Attach the caret to this scroller and position it over the caret span
if(this.caret.parentNode != scroller) {
scroller.appendChild(this.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
this.scrollBody(e);
// Display and position the scrollbar if necessary
this.setScrollBar(e);
}
updateInput(x: HTMLDivElement) {
var util = this.keymanweb.util;
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(util.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--;
} else { // Chrome, Opera, native browser (?)
w++;
h++;
}
}
xs.width=w+'px';
xs.height=h+'px';
}
/**
* 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
*/
updateBaseElement(e: HTMLElement, n: number) {
if(e.base instanceof HTMLInputElement || e.base instanceof HTMLTextAreaElement) {
e.base.value = this.getText(e); //KMW-29
} else {
e.base.textContent = this.getText(e);
}
e.style.backgroundColor=(n==0?'transparent':window.getComputedStyle(e.base,null).backgroundColor);
if(this.keymanweb.util.device.OS == 'iOS') {
e.base.style.visibility=(n==0?'visible':'hidden');
}
}
/**
* Close OSK and remove simulated caret on losing focus
*/
cancelInput(): void {
this.keymanweb._ActiveElement=null;
this.hideCaret();
this.keymanweb.osk.hideNow();
};
/**
* Handle losing focus from simulated input field
*
* @param {Event} e event
*/
setBlur(e: FocusEvent) {
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._setBlur(e);
}
_setBlur(e: FocusEvent) {
// This works OK for iOS, but may need something else for other platforms
if(('relatedTarget' in e) && e.relatedTarget) {
var elem: HTMLElement = e.relatedTarget as HTMLElement;
if(elem.nodeName != 'DIV' || elem.className.indexOf('keymanweb-input') == -1) {
this.cancelInput(); return;
}
}
//Hide the OSK
if(!this.keymanweb.focusing) {
this.cancelInput();
}
}
/**
* Display and position a scrollbar in the input field if needed
*
* @param {Object} e input DIV element (copy of INPUT or TEXTAREA)
*/
setScrollBar(e: HTMLElement) {
// Display the scrollbar if necessary. Added TEXTAREA condition to correct rotation issue KMW-5. Fixed for 310 beta.
var scroller=<HTMLElement>e.childNodes[0], sbs=(<HTMLElement>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';
}
}
/**
* Handle the touch move event for an input element
*
* @param {Event} e touchmove event
*/
dragInput(e: TouchEvent|MouseEvent) {
var keyman: KeymanBase = window['keyman'];
keyman.domManager.touchHandlers._dragInput(e);
}
_dragInput(e: TouchEvent|MouseEvent) {
// Prevent dragging window
e.preventDefault();
e.stopPropagation();
// Identify the target from the touch list or the event argument (IE 10 only)
var target: HTMLElement;
if(e instanceof TouchEvent) {
target = e.targetTouches[0].target as HTMLElement;
} else {
target = e.target as HTMLElement;
}
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=<HTMLElement> target.parentNode;
if(target.className == null || target.className.indexOf('keymanweb-input') < 0) target=<HTMLElement> target.parentNode;
if(target.className == null || target.className.indexOf('keymanweb-input') < 0) return;
var x, y;
if(e instanceof TouchEvent) {
x = e.touches[0].screenX;
y = e.touches[0].screenY;
} else {
x = e.screenX;
y = e.screenY;
}
// Allow content of input elements to be dragged horizontally or vertically
if(typeof this.firstTouch == 'undefined' || this.firstTouch == null) {
this.firstTouch={x:x,y:y};
} else {
var x0=this.firstTouch.x,y0=this.firstTouch.y,
scroller=target.firstChild as HTMLElement,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';
this.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=this.keymanweb.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';
this.firstTouch.x=x;
}
}
}
this.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
*/
scrollInput(e: HTMLElement) {
if(!e || !e.firstChild || e.className == null || e.className.indexOf('keymanweb-input') < 0 ) {
return;
}
var scroller=e.firstChild as HTMLElement;
if(scroller.childNodes.length < 3) {
return;
}
var util = this.keymanweb.util;
// 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/(<HTMLTextAreaElement>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';
} else { // or scroll horizontally if needed
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)
this.showCaret(e);
}
/**
* Scroll the document body vertically to bring the active input into view
*
* @param {Object} e simulated input field object being focussed
*/
scrollBody(e: HTMLElement): void {
var osk = this.keymanweb.osk;
var util = this.keymanweb.util;
if(!e || e.className == null || e.className.indexOf('keymanweb-input') < 0 || !osk.ready) {
return;
}
// Get the absolute position of the caret
var s2=<HTMLElement>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);
}
}
}
// -------------------------------------------------------------------------
/**
* Stub function equivalents to alias element handling, for use with desktop mode. Dummies out
* the aliasing behaviors for internal system logic consistency.
*/
class AliasStubs implements AliasElementHandlers {
/**
* Input element stub functions, serving as empty alternates for non-aliased elements.
*/
setFocus(e?: Event): void {
};
getText(e: HTMLElement): string {
return '';
}
setText(e: HTMLElement, t?: string, cp?: number): void {
}
getTextBeforeCaret(): string {
return '';
}
setTextBeforeCaret(e: HTMLElement, t: string): void {
}
getTextCaret(e: HTMLElement): number {
return 0;
}
setTextCaret(e: HTMLElement, cp: number) {
}
hideCaret(): void {
}
flashCaret(): void {
}
updateInput(x: HTMLElement) {
}
setBlur(e: FocusEvent) {
}
}