spiegel-keyman/web/source/kmwdomevents.ts
Joshua Horton cd35335a06
Merge pull request #651 from keymanapp/web-testing-robustness
Web automated testing robustness
2018-03-09 10:49:48 +07:00

1578 lines
No EOL
58 KiB
TypeScript

namespace com.keyman {
/*
* 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.
*
*/
export class CommonDOMStates {
_KeyPressToSwallow: number;
_DisableInput: boolean = false; // Should input be disabled?
_IgnoreNextSelChange: number = 0; // when a visual keyboard key is mouse-down, ignore the next sel change because this stuffs up our history
_Selection = null;
_SelectionControl: any = null; // Type behavior is as with activeElement and the like.
modStateFlags: number = 0; // Tracks the present state of the physical keyboard's active modifier flags. Needed for AltGr simulation.
activeElement: any; // TODO: Add type and fix resulting bugs!
lastActiveElement: any; // TODO: Add type and fix resulting bugs!
focusing: boolean;
focusTimer: number;
changed: boolean; // Tracks if the element has been edited since gaining focus.
/* ----------------------- Static event-related methods ------------------------ */
setFocusTimer(): void {
this.focusing=true;
this.focusTimer = window.setTimeout(function() {
this.focusing=false;
}.bind(this), 1000)
}
}
/**
* Declares a base, non-touch oriented implementation of all relevant DOM-related event handlers and state functions.
*/
export class DOMEventHandlers {
// TODO: resolve/refactor out!
protected keyman: KeymanBase;
// This is only static within a given initialization of KeymanWeb. Perhaps it would be best as an initialization
// parameter and member field?
static states: CommonDOMStates = new CommonDOMStates();
constructor(keyman: KeymanBase) {
this.keyman = keyman;
}
/**
* Handle receiving focus by simulated input field
*/
setFocus: (e?: TouchEvent|MSPointerEvent) => void = function(e?: TouchEvent|MSPointerEvent): void {
// Touch-only handler.
}.bind(this);
/**
* 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 {
// Touch-only method.
return '';
}
/**
*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 {
// Touch-only method.
}
/**
* Get text up to the caret from simulated input field
*
* @return {string}
*/
getTextBeforeCaret(e: HTMLElement): string {
// Touch-only method.
return '';
}
/**
* 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 {
// Touch-only method.
}
/**
* 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 {
// Touch-only method.
return 0;
}
/**
* 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 {
// Touch-only method.
}
/**
* Hides the simulated caret for touch-aliased elements.
*/
hideCaret(): void {
// Touch-only method.
}
/**
* Toggle state of caret in simulated input field
*/
flashCaret: () => void = function(): void {
// Touch-only handler.
}.bind(this);
/**
* Correct the position and size of a duplicated input element
*
* @param {Object} x element
*/
updateInput(x: HTMLElement) {
// Touch-only method.
}
/**
* Handles touch-based loss of focus events.
*/
setBlur: (e: FocusEvent) => void = function(e: FocusEvent) {
// Touch-only handler.
}.bind(this);
// End of I3363 (Build 301) additions
// Universal DOM event handlers (both desktop + touch)
//TODO: add more complete description of what ControlFocus really does
/**
* Respond to KeymanWeb-aware input element receiving focus
*/
_ControlFocus: (e: FocusEvent) => boolean = function(this: DOMEventHandlers, e: FocusEvent): boolean {
var Ltarg: HTMLElement | Document, Ln;
var device = this.keyman.util.device;
var osk = this.keyman.osk;
e = this.keyman._GetEventObject<FocusEvent>(e); // I2404 - Manage IE events in IFRAMEs
Ltarg = this.keyman.util.eventTarget(e) as HTMLElement;
if (Ltarg == null) {
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(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLInputElement) {
var et=Ltarg.type.toLowerCase();
if(!(et == 'text' || et == 'search')) {
return true;
}
} else if((device.touchable || !Ltarg.isContentEditable)
&& !(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLTextAreaElement)) {
return true;
}
DOMTouchHandlers.states.activeElement = Ltarg; // I3363 (Build 301)
if (Ltarg.nodeType == 3) { // defeat Safari bug
Ltarg = Ltarg.parentNode as HTMLElement;
}
var LfocusTarg = Ltarg;
// Ensure that focussed element is visible above the keyboard
if(Ltarg.className == null || Ltarg.className.indexOf('keymanweb-input') < 0) {
if(this instanceof DOMTouchHandlers) {
(this as DOMTouchHandlers).scrollBody(Ltarg);
}
}
if(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLIFrameElement) { //**TODO: check case reference
this.keyman.domManager._AttachToIframe(Ltarg as HTMLIFrameElement);
Ltarg=Ltarg.contentWindow.document;
}
//??keymanweb._Selection = null;
// We condition on 'priorElement' below as a check to allow KMW to set a default active keyboard.
var priorElement = DOMEventHandlers.states.lastActiveElement;
DOMEventHandlers.states.lastActiveElement = Ltarg;
if(this.keyman.uiManager.justActivated) {
this._BlurKeyboardSettings();
} else {
this._FocusKeyboardSettings(priorElement ? false : true);
}
// Always do the common focus stuff, instantly returning if we're in an editable iframe.
if(this._CommonFocusHelper(Ltarg)) {
return true;
};
Ltarg._KeymanWebSelectionStart = Ltarg._KeymanWebSelectionEnd = null; // I3363 (Build 301)
// Set element directionality (but only if element is empty)
if(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLElement) {
this.keyman.domManager._SetTargDir(Ltarg);
}
//Execute external (UI) code needed on focus if required
this.doControlFocused(LfocusTarg, DOMEventHandlers.states.lastActiveElement);
// 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(this.keyman.keyboardManager.isCJK()) {
osk._Enabled=1;
}
if(osk._Enabled) {
osk._Show();
} else {
osk._Hide(false);
}
}
}
return true;
}.bind(this);
/**
* 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
*/
doControlFocused(_target: HTMLElement, _activeControl: HTMLElement): boolean {
var p={};
p['target']=_target;
p['activeControl']=_activeControl;
return this.keyman.util.callEvent('kmw.controlfocused',p);
}
/**
* Respond to KMW losing focus on event
*/
_ControlBlur: (e: FocusEvent) => boolean = function(this: DOMEventHandlers, e: FocusEvent): boolean {
var Ltarg: HTMLElement | Document;
e = this.keyman._GetEventObject<FocusEvent>(e); // I2404 - Manage IE events in IFRAMEs
Ltarg = this.keyman.util.eventTarget(e) as HTMLElement;
if (Ltarg == null) {
return true;
}
DOMEventHandlers.states.activeElement = null; // I3363 (Build 301)
// Hide the touch device input caret, if applicable I3363 (Build 301)
if(this instanceof DOMTouchHandlers) {
this.hideCaret();
}
if (Ltarg.nodeType == 3) { // defeat Safari bug
Ltarg = Ltarg.parentNode as HTMLElement;
}
if(Ltarg.ownerDocument) {
if(Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLIFrameElement) {
Ltarg=Ltarg.contentWindow.document;
}
if (Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLInputElement
|| Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLTextAreaElement) {
//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;
this._BlurKeyboardSettings();
// Now that we've handled all prior-element maintenance, update the 'last active element'.
DOMEventHandlers.states.lastActiveElement = Ltarg;
/* 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.
*/
this.keyman.uiManager.justActivated = false;
var isActivating = this.keyman.uiManager.isActivating;
if(!isActivating) {
this.keyman.keyboardManager.notifyKeyboard(0, Ltarg, 0); // I2187
}
//e = this.keyman._GetEventObject<FocusEvent>(e); // I2404 - Manage IE events in IFRAMEs //TODO: is this really needed again????
this.doControlBlurred(Ltarg, e, isActivating);
// Hide the OSK when the control is blurred, unless the UI is being temporarily selected
if(this.keyman.osk.ready && !isActivating) {
this.keyman.osk._Hide(false);
}
this.doChangeEvent(Ltarg);
this.keyman.interface.resetContext();
return true;
}.bind(this);
/**
* 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
*/
doControlBlurred(_target: HTMLElement|Document, _event: Event, _isActivating: boolean|number): boolean {
var p={};
p['target']=_target;
p['event']=_event;
p['isActivating']=_isActivating;
return this.keyman.util.callEvent('kmw.controlblurred',p);
}
/**
* Function _BlurKeyboardSettings
* Description Stores the last active element's keyboard settings. Should be called
* whenever a KMW-enabled page element loses control.
*/
_BlurKeyboardSettings(PInternalName?: string, PLgCode?: string) {
var keyboardID = this.keyman.keyboardManager.activeKeyboard ? this.keyman.keyboardManager.activeKeyboard['KI'] : '';
var langCode = this.keyman.keyboardManager.getActiveLanguage();
if(PInternalName !== undefined && PLgCode !== undefined) {
keyboardID = PInternalName;
langCode = PLgCode;
}
var lastElem = DOMEventHandlers.states.lastActiveElement;
if(lastElem && lastElem._kmwAttachment.keyboard != null) {
lastElem._kmwAttachment.keyboard = keyboardID;
lastElem._kmwAttachment.languageCode = langCode;
} else {
this.keyman.globalKeyboard = keyboardID;
this.keyman.globalLanguageCode = langCode;
}
}
/**
* Function _FocusKeyboardSettings
* @param {boolean} blockGlobalChange A flag indicating if the global keyboard setting should be ignored for this call.
* Description Restores the newly active element's keyboard settings. Should be called
* whenever a KMW-enabled page element gains control, but only once the prior
* element's loss of control is guaranteed.
*/
_FocusKeyboardSettings(blockGlobalChange: boolean) {
var lastElem = DOMEventHandlers.states.lastActiveElement;
if(lastElem && lastElem._kmwAttachment.keyboard != null) {
this.keyman.keyboardManager.setActiveKeyboard(lastElem._kmwAttachment.keyboard,
lastElem._kmwAttachment.languageCode);
} else if(!blockGlobalChange) {
this.keyman.keyboardManager.setActiveKeyboard(this.keyman.globalKeyboard, this.keyman.globalLanguageCode);
}
}
/**
* Function _CommonFocusHelper
* @param {Element} target
* @returns {boolean}
* Description Performs common state management for the various focus events of KeymanWeb.
* The return value indicates whether (true) or not (false) the calling event handler
* should be terminated immediately after the call.
*/
_CommonFocusHelper(target: HTMLElement|Document): boolean {
var uiManager = this.keyman.uiManager;
//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(target.ownerDocument && target instanceof target.ownerDocument.defaultView.HTMLIFrameElement) {
if(!this.keyman.domManager._IsIEEditableIframe(target, 1) ||
!this.keyman.domManager._IsMozillaEditableIframe(target, 1)) {
DOMEventHandlers.states._DisableInput = true;
return true;
}
}
DOMEventHandlers.states._DisableInput = false;
if(!uiManager.justActivated) {
// Needs refactor when the Callbacks interface PR goes through!
this.keyman['interface']._DeadKeys = [];
this.keyman.keyboardManager.notifyKeyboard(0,target,1); // I2187
}
if(!uiManager.justActivated && DOMEventHandlers.states._SelectionControl != target) {
uiManager.isActivating = false;
}
uiManager.justActivated = false;
DOMEventHandlers.states._SelectionControl = target;
return false;
}
/**
* Function _SelectionChange
* Scope Private
* Description Respond to selection change event
*/
_SelectionChange: () => boolean = function(this: DOMEventHandlers): boolean {
if(DOMEventHandlers.states._IgnoreNextSelChange) {
DOMEventHandlers.states._IgnoreNextSelChange--;
}
return true;
}.bind(this);
/**
* Function _GetEventKeyCode
* Scope Private
* @param {Event} e Event object
* Description Finds the key code represented by the event.
*/
_GetEventKeyCode(e: KeyboardEvent) {
if (e.keyCode) {
return e.keyCode;
} else if (e.which) {
return e.which;
} else {
return null;
}
}
/**
* Function _GetKeyEventProperties
* Scope Private
* @param {Event} e Event object
* @param {boolean=} keyState true if call results from a keyDown event, false if keyUp, undefined if keyPress
* @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
*/
_GetKeyEventProperties(e: KeyboardEvent, keyState?: boolean) {
var s = new com.keyman.KeyEvent();
e = this.keyman._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs
s.Ltarg = this.keyman.util.eventTarget(e) as HTMLElement;
if (s.Ltarg == null) {
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 (s.Ltarg.nodeType == 3) {// defeat Safari bug
s.Ltarg = s.Ltarg.parentNode as HTMLElement;
}
s.Lcode = this._GetEventKeyCode(e);
if (s.Lcode == null) {
return null;
}
// Stage 1 - track the true state of the keyboard's modifiers.
var osk = this.keyman.osk, prevModState = DOMEventHandlers.states.modStateFlags, curModState = 0x0000;
var ctrlEvent = false, altEvent = false;
switch(s.Lcode) {
case osk.keyCodes['K_CTRL']: // The 3 shorter "K_*CTRL" entries exist in some legacy keyboards.
case osk.keyCodes['K_LCTRL']:
case osk.keyCodes['K_RCTRL']:
case osk.keyCodes['K_CONTROL']:
case osk.keyCodes['K_LCONTROL']:
case osk.keyCodes['K_RCONTROL']:
ctrlEvent = true;
break;
case osk.keyCodes['K_LMENU']: // The 2 "K_*MENU" entries exist in some legacy keyboards.
case osk.keyCodes['K_RMENU']:
case osk.keyCodes['K_ALT']:
case osk.keyCodes['K_LALT']:
case osk.keyCodes['K_RALT']:
altEvent = true;
break;
}
/**
* Two separate conditions exist that should trigger chiral modifier detection. Examples below use CTRL but also work for ALT.
*
* 1. The user literally just pressed CTRL, so the event has a valid `location` property we can utilize.
* Problem: its layer isn't presently activated within the OSK.
*
* 2. CTRL has been held a while, so the OSK layer is valid, but the key event doesn't tell us the chirality of the active CTRL press.
* Bonus issue: RAlt simulation may cause erasure of this location property, but it should ONLY be empty if pressed in this case.
* We default to the 'left' variants since they're more likely to exist and cause less issues with RAlt simulation handling.
*
* In either case, `e.getModifierState("Control")` is set to true, but as a result does nothing to tell us which case is active.
*
* `e.location != 0` if true matches condition 1 and matches condition 2 if false.
*/
curModState |= (e.getModifierState("Shift") ? 0x10 : 0);
if(e.getModifierState("Control")) {
curModState |= ((e.location != 0 && ctrlEvent) ?
(e.location == 1 ? osk.modifierCodes['LCTRL'] : osk.modifierCodes['RCTRL']) : // Condition 1
prevModState & 0x0003); // Condition 2
}
if(e.getModifierState("Alt")) {
curModState |= ((e.location != 0 && altEvent) ?
(e.location == 1 ? osk.modifierCodes['LALT'] : osk.modifierCodes['RALT']) : // Condition 1
prevModState & 0x000C); // Condition 2
}
// Stage 2 - detect state key information. It can be looked up per keypress with no issue.
s.Lstates = 0;
if(e.getModifierState("CapsLock")) {
s.Lstates = osk.modifierCodes['CAPS'];
} else {
s.Lstates = osk.modifierCodes['NO_CAPS'];
}
if(e.getModifierState("NumLock")) {
s.Lstates |= osk.modifierCodes['NUM_LOCK'];
} else {
s.Lstates |= osk.modifierCodes['NO_NUM_LOCK'];
}
if(e.getModifierState("ScrollLock") || e.getModifierState("Scroll")) { // "Scroll" for IE9.
s.Lstates |= osk.modifierCodes['SCROLL_LOCK'];
} else {
s.Lstates |= osk.modifierCodes['NO_SCROLL_LOCK'];
}
// We need these states to be tracked as well for proper OSK updates.
curModState |= s.Lstates;
// Stage 3 - Set our modifier state tracking variable and perform basic AltGr-related management.
s.LmodifierChange = DOMEventHandlers.states.modStateFlags != curModState;
DOMEventHandlers.states.modStateFlags = curModState;
// For European keyboards, not all browsers properly send both key-up events for the AltGr combo.
var altGrMask = osk.modifierCodes['RALT'] | osk.modifierCodes['LCTRL'];
if((prevModState & altGrMask) == altGrMask && (curModState & altGrMask) != altGrMask) {
// We just released AltGr - make sure it's all released.
curModState &= ~ altGrMask;
}
// Perform basic filtering for Windows-based ALT_GR emulation on European keyboards.
if(curModState & osk.modifierCodes['RALT']) {
curModState &= ~osk.modifierCodes['LCTRL'];
}
// Stage 4 - map the modifier set to the appropriate keystroke's modifiers.
if(this.keyman.keyboardManager.isChiral()) {
s.Lmodifiers = curModState & osk.modifierBitmasks.CHIRAL;
// Note for future - embedding a kill switch here or in keymanweb.osk.emulatesAltGr would facilitate disabling
// AltGr / Right-alt simulation.
if(osk.emulatesAltGr() && (s.Lmodifiers & osk.modifierBitmasks['ALT_GR_SIM']) == osk.modifierBitmasks['ALT_GR_SIM']) {
s.Lmodifiers ^= osk.modifierBitmasks['ALT_GR_SIM'];
s.Lmodifiers |= osk.modifierCodes['RALT'];
}
} else {
// No need to sim AltGr here; we don't need chiral ALTs.
s.Lmodifiers =
(e.getModifierState("Shift") ? 0x10 : 0) |
((curModState & (osk.modifierCodes['LCTRL'] | osk.modifierCodes['RCTRL'])) ? 0x20 : 0) |
((curModState & (osk.modifierCodes['LALT'] | osk.modifierCodes['RALT'])) ? 0x40 : 0);
}
// The 0x6F used to be 0x60 - this adjustment now includes the chiral alt and ctrl modifiers in that check.
s.LisVirtualKeyCode = (typeof e.charCode != 'undefined' && e.charCode != null && (e.charCode == 0 || (s.Lmodifiers & 0x6F) != 0));
s.LisVirtualKey = s.LisVirtualKeyCode || e.type != 'keypress';
return s;
}
/**
* Function _KeyDown
* Scope Private
* Description Processes keydown event and passes data to keyboard.
*
* Note that the test-case oriented 'recorder' stubs this method to facilitate keystroke
* recording for use in test cases. If changing this function, please ensure the recorder is
* not affected.
*/
_KeyDown: (e: KeyboardEvent) => boolean = function(this: DOMEventHandlers, e: KeyboardEvent): boolean {
var Ldv: Document, eClass='';
var activeKeyboard = this.keyman.keyboardManager.activeKeyboard;
var osk = this.keyman.osk;
var util = this.keyman.util;
var kbdInterface = this.keyman['interface'];
DOMEventHandlers.states._KeyPressToSwallow = 0;
if(DOMEventHandlers.states._DisableInput || activeKeyboard == null) {
return true;
}
// Prevent mapping element is readonly or tagged as kmw-disabled
var el=util.eventTarget(e) as HTMLElement;
if(util.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 = this._GetKeyEventProperties(e, true);
if(Levent == null) {
return true;
}
switch(Levent.Lcode) {
case 8:
kbdInterface.clearDeadkeys();
break; // I3318 (always clear deadkeys after backspace)
case 16: //"K_SHIFT":16,"K_CONTROL":17,"K_ALT":18
case 17:
case 18:
case 20: //"K_CAPS":20, "K_NUMLOCK":144,"K_SCROLL":145
case 144:
case 145:
// For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode.
this.keyman.keyboardManager.notifyKeyboard(Levent.Lcode,Levent.Ltarg,1);
if(!util.device.touchable) {
return osk._UpdateVKShift(Levent, Levent.Lcode-15, 1); // I2187
} else {
return true;
}
}
if(Levent.LmodifierChange) {
this.keyman.keyboardManager.notifyKeyboard(0,Levent.Ltarg,1);
osk._UpdateVKShift(Levent, 0, 1);
}
if(!window.event) {
// I1466 - Convert the - keycode on mnemonic as well as positional layouts
// FireFox, Mozilla Suite
if(this.keyman.keyMapManager.browserMap.FF['k'+Levent.Lcode]) {
Levent.Lcode=this.keyman.keyMapManager.browserMap.FF['k'+Levent.Lcode];
}
}
//else
//{
// Safari, IE, Opera?
//}
if(!activeKeyboard['KM']) {
// Positional Layout
var LeventMatched=0;
/* 13/03/2007 MCD: Swedish: Start mapping of keystroke to US keyboard */
var Lbase=this.keyman.keyMapManager.languageMap[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(activeKeyboard['KM'])=='undefined' && !(Levent.Lmodifiers & 0x60)) {
// Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic
var Levent2: com.keyman.LegacyKeyEvent = {
Lcode:this.keyman.keyMapManager._USKeyCodeToCharCode(Levent),
Ltarg:Levent.Ltarg,
Lmodifiers:0,
LisVirtualKey:0
};
if(kbdInterface.processKeystroke(util.physicalDevice, Levent2.Ltarg, Levent2)) {
LeventMatched=1;
}
}
LeventMatched = LeventMatched || kbdInterface.processKeystroke(util.physicalDevice,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) {
this.keyman.interface.defaultBackspace();
}
} else {
// Mnemonic layout
if(Levent.Lcode == 8) { // I1595 - Backspace for mnemonic
DOMEventHandlers.states._KeyPressToSwallow = 1;
if(!kbdInterface.processKeystroke(util.physicalDevice,Levent.Ltarg,Levent)) {
this.keyman.interface.defaultBackspace(); // I3363 (Build 301)
}
return false; //added 16/3/13 to fix double backspace on mnemonic layouts on desktop
}
else {
DOMEventHandlers.states._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;
}
kbdInterface.output(0, Levent.Ltarg, String._kmwFromCharCode(Lch)); //I3319
LeventMatched = 1;
}
if(LeventMatched) {
if(e && e.preventDefault) {
e.preventDefault();
e.stopPropagation();
}
DOMEventHandlers.states._KeyPressToSwallow = (e ? this._GetEventKeyCode(e) : 0);
return false;
} else {
DOMEventHandlers.states._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;
}
}
if(typeof((Levent.Ltarg as HTMLElement).base) != 'undefined') {
// Simulated touch elements have no default text-processing - we need to rely on a strategy similar to
// that of the OSK here.
var ch = osk.defaultKeyOutput('',Levent.Lcode,Levent.Lmodifiers,false,Levent.Ltarg);
if(ch) {
kbdInterface.output(0, Levent.Ltarg, ch);
return false;
}
}
return true;
}.bind(this);
doChangeEvent(_target: HTMLElement|Document) {
if(DOMEventHandlers.states.changed) {
var event: Event;
if(typeof Event == 'function') {
event = new Event('change', {"bubbles": true, "cancelable": false});
} else { // IE path
event = document.createEvent("HTMLEvents");
event.initEvent('change', true, false);
}
// Ensure that touch-aliased elements fire as if from the aliased element.
if(_target['base'] && _target['base']['kmw_ip']) {
_target = _target['base'];
}
_target.dispatchEvent(event);
}
DOMEventHandlers.states.changed = false;
}
/**
* Function _KeyPress
* Scope Private
* Description Processes keypress event (does not pass data to keyboard)
*/
_KeyPress: (e: KeyboardEvent) => boolean = function(this: DOMEventHandlers, e: KeyboardEvent): boolean {
var Levent;
if(DOMEventHandlers.states._DisableInput || this.keyman.keyboardManager.activeKeyboard == null) {
return true;
}
Levent = this._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(!this.keyman.keyboardManager.activeKeyboard['KM']) {
if(!DOMEventHandlers.states._KeyPressToSwallow) {
return true;
}
if(Levent.Lcode < 0x20 || ((<any>this.keyman)._BrowserIsSafari && (Levent.Lcode > 0xF700 && Levent.Lcode < 0xF900))) {
return true;
}
e = this.keyman._GetEventObject<KeyboardEvent>(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(DOMEventHandlers.states._KeyPressToSwallow || this.keyman['interface'].processKeystroke(this.keyman.util.physicalDevice,Levent.Ltarg,Levent)) {
DOMEventHandlers.states._KeyPressToSwallow=0;
if(e && e.preventDefault) {
e.preventDefault();
e.stopPropagation();
}
return false;
}
DOMEventHandlers.states._KeyPressToSwallow=0;
return true;
}.bind(this);
/**
* Function _KeyUp
* Scope Private
* Description Processes keyup event and passes event data to keyboard
*/
_KeyUp: (e: KeyboardEvent) => boolean = function(this: DOMEventHandlers, e: KeyboardEvent): boolean {
var keyboardManager = this.keyman.keyboardManager;
var osk = this.keyman.osk;
var Levent = this._GetKeyEventProperties(e, false);
if(Levent == null || !osk.ready) {
return true;
}
switch(Levent.Lcode) {
case 13:
if(Levent.Ltarg instanceof Levent.Ltarg.ownerDocument.defaultView.HTMLTextAreaElement) {
break;
}
if(Levent.Ltarg.base && Levent.Ltarg.base instanceof Levent.Ltarg.base.ownerDocument.defaultView.HTMLTextAreaElement) {
break;
}
// For input fields, move to next input element
if(Levent.Ltarg instanceof Levent.Ltarg.ownerDocument.defaultView.HTMLInputElement) {
var inputEle = Levent.Ltarg;
if(inputEle.type == 'search' || inputEle.type == 'submit') {
inputEle.form.submit();
} else {
this.keyman.domManager.moveToNext(false);
}
}
return true;
case 16: //"K_SHIFT":16,"K_CONTROL":17,"K_ALT":18
case 17:
case 18:
case 20: //"K_CAPS":20, "K_NUMLOCK":144,"K_SCROLL":145
case 144:
case 145:
keyboardManager.notifyKeyboard(Levent.Lcode,Levent.Ltarg,0);
if(!this.keyman.util.device.touchable) {
return osk._UpdateVKShift(Levent, Levent.Lcode-15, 1); // I2187
} else {
return true;
}
}
if(Levent.LmodifierChange){
keyboardManager.notifyKeyboard(0,Levent.Ltarg,0);
osk._UpdateVKShift(Levent, 0, 1); // I2187
}
return false;
}.bind(this);
}
// -------------------------------------------------------------------------
/**
* Defines numerous functions for handling and modeling touch-based aliases.
*/
export class DOMTouchHandlers extends DOMEventHandlers {
// Stores the simulated caret element.
caret: HTMLDivElement;
caretTimerId: number;
firstTouch: {
x: number;
y: number;
};
constructor(keyman: KeymanBase) {
super(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
*
*/
setFocus: (e?: TouchEvent|MSPointerEvent) => void = function(this: DOMTouchHandlers, e?: TouchEvent|MSPointerEvent): void {
var kmw = this.keyman;
var osk = this.keyman.osk;
var util = this.keyman.util;
DOMEventHandlers.states.setFocusTimer();
var tEvent: {
clientX: number;
clientY: number;
target?: EventTarget;
};
if(Util.instanceof(e, "TouchEvent")) {
tEvent=(e as TouchEvent).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 DOMEventHandlers.states.lastActiveElement
if(DOMEventHandlers.states.lastActiveElement) {
tEvent.target = DOMEventHandlers.states.lastActiveElement['kmw_ip'];
// but will default to first input or text area on page if DOMEventHandlers.states.lastActiveElement is null
} else {
tEvent.target = this.keyman.domManager.sortedInputs[0]['kmw_ip'];
}
}
var touchX=tEvent.clientX,touchY=tEvent.clientY;
var tTarg=tEvent.target as HTMLElement;
var scroller: HTMLElement;
// Identify the scroller element
if(Util.instanceof(tTarg, "HTMLSpanElement")) {
scroller=tTarg.parentNode as HTMLElement;
} else if(tTarg.className != null && tTarg.className.indexOf('keymanweb-input') >= 0) {
scroller=tTarg.firstChild as HTMLElement;
} else {
scroller=tTarg;
}
// And the actual target element
var target=scroller.parentNode as HTMLElement;
// Move the caret and refocus if necessary
if(DOMEventHandlers.states.activeElement != target) {
// Hide the KMW caret
this.hideCaret();
DOMEventHandlers.states.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.keyman.domManager._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();
// 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(Util.instanceof(tTarg, "HTMLDivElement")) {
var x,cp;
x=util._GetAbsoluteX(scroller.firstChild as HTMLElement);
if(target.dir == 'rtl') {
x += (scroller.firstChild as HTMLElement).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 instanceof target.base.ownerDocument.defaultView.HTMLTextAreaElement) {
yRow=Math.round(target.base.offsetHeight/(target.base as HTMLTextAreaElement).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._BlurKeyboardSettings();
// With the attachment API update, we now directly track the old legacy control behavior.
DOMEventHandlers.states.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._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._CommonFocusHelper(target)) {
return;
}
}.bind(this);
getText(e: HTMLElement): string {
if(e == null) {
return '';
}
return e.textContent;
}
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=s1.textContent;
t2=s2.textContent;
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);
s1.textContent=t1;
s2.textContent=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];
return s1.textContent;
}
}
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+$/,' ');
}
s1.textContent=t;
// Test total length in order to control base element visibility
tLen=t.length;
tLen=tLen+s2.textContent.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=DOMEventHandlers.states.lastActiveElement, s=null;
if(e && e.className != null && e.className.indexOf('keymanweb-input') >= 0) {
// Always copy text back to underlying field on blur
if(e.base instanceof e.base.ownerDocument.defaultView.HTMLTextAreaElement
||e.base instanceof e.base.ownerDocument.defaultView.HTMLInputElement) {
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 as HTMLElement).style;
if(e.base.nodeName == 'TEXTAREA') {
ss.top='0';
} else {
if(e.base.dir == 'rtl') {
ss.left=(e.offsetWidth-(e.firstChild as HTMLElement).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] as HTMLElement).style.visibility='hidden';
}
}
}
flashCaret: () => void = function(this: DOMTouchHandlers): void {
if(this.keyman.util.device.touchable && DOMEventHandlers.states.activeElement != null) {
var cs=this.caret.style;
cs.visibility = cs.visibility != 'visible' ? 'visible' : 'hidden';
}
}.bind(this);
/**
* 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.keyman.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 as HTMLElement;
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) {
var Ldv = e.base.ownerDocument.defaultView;
if(e.base instanceof Ldv.HTMLInputElement ||e.base instanceof Ldv.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.keyman.util.device.OS == 'iOS') {
e.base.style.visibility=(n==0?'visible':'hidden');
}
}
/**
* Close OSK and remove simulated caret on losing focus
*/
cancelInput(): void {
DOMEventHandlers.states.activeElement=null;
this.hideCaret();
this.keyman.osk.hideNow();
};
/**
* Handle losing focus from simulated input field
*/
setBlur: (e: FocusEvent) => void = function(this: DOMTouchHandlers, e: FocusEvent) {
// This works OK for iOS, but may need something else for other platforms
this.keyman.interface.resetContext();
if(('relatedTarget' in e) && e.relatedTarget) {
var elem: HTMLElement = e.relatedTarget as HTMLElement;
this.doChangeEvent(elem);
if(elem.nodeName != 'DIV' || elem.className.indexOf('keymanweb-input') == -1) {
this.cancelInput();
return;
}
}
//Hide the OSK
if(!DOMEventHandlers.states.focusing) {
this.cancelInput();
}
}.bind(this);
/**
* 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
*/
dragInput: (e: TouchEvent|MouseEvent) => void = function(this: DOMTouchHandlers, 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(Util.instanceof(e, "TouchEvent")) {
target = (e as TouchEvent).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(Util.instanceof(e, "TouchEvent")) {
x = (e as TouchEvent).touches[0].screenX;
y = (e as TouchEvent).touches[0].screenY;
} else {
x = (e as MouseEvent).screenX;
y = (e as MouseEvent).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.keyman.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);
}.bind(this);
/**
* 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.keyman.util;
// Get the actual absolute position of the caret and the element
var s2=scroller.childNodes[1] as HTMLElement,
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 instanceof e.base.ownerDocument.defaultView.HTMLTextAreaElement) {
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';
} 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.keyman.osk;
var util = this.keyman.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);
}
}
}
}