spiegel-keyman/web/source/dom/domEventHandlers.ts
Marc Durdin 49eb062c65 feat(web): prediction casing follows current layer
Fixes #6455.

This involved adding an extra hint to the data passed into the lexical
model layer, being the CasingForm that the current layer wants to use.

Given there are several entry points to the lexical model layer, this
meant a few places where we needed to check the current layer.

I refactored the very long function `InputProcessor.processKeyEvent` to
extract out the `buildAlternates` function. There should be no
functional changes to the `buildAlternates` code. But it makes
`processKeyEvent` significantly easier to read.
2022-04-08 16:14:01 +10:00

920 lines
34 KiB
TypeScript

/// <reference path="touchAliasElement.ts" />
/// <reference path="preProcessor.ts" />
namespace com.keyman.dom {
/*
* 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 {
_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
_IgnoreBlurFocus: boolean = false; // Used to temporarily ignore focus changes
_Selection = null;
_SelectionControl: any = null; // Type behavior is as with activeElement and the like.
_activeElement: HTMLElement;
_lastActiveElement: HTMLElement;
focusing: boolean;
focusTimer: number;
changed: boolean; // Tracks if the element has been edited since gaining focus.
swallowKeypress: boolean; // Notes if a keypress should be swallowed; used when handing mnemonics.
/* ----------------------- Static event-related methods ------------------------ */
setFocusTimer(): void {
this.focusing=true;
this.focusTimer = window.setTimeout(function() {
this.focusing=false;
}.bind(this), 50)
}
}
/**
* 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);
/**
* 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;
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;
}
if(Ltarg['body']) {
Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes.
}
// 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(Ltarg.ownerDocument && Ltarg.ownerDocument.designMode == 'on') {
// continue; don't block this one!
} else if((device.touchable || !Ltarg.isContentEditable)
&& !(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLTextAreaElement)) {
return true;
}
// We condition on 'priorElement' below as a check to allow KMW to set a default active keyboard.
var priorElement = DOMEventHandlers.states._lastActiveElement;
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.body;
}
// Must set before _Blur / _Focus to avoid infinite recursion due to complications
// in setActiveKeyboard behavior with managed keyboard settings.
this.keyman.domManager.lastActiveElement = Ltarg;
this.keyman.domManager.activeElement = Ltarg; // I3363 (Build 301)
if(this.keyman.uiManager.justActivated) {
this._BlurKeyboardSettings(Ltarg);
} else {
this._FocusKeyboardSettings(Ltarg, priorElement ? false : true);
}
// Always do the common focus stuff, instantly returning if we're in an editable iframe.
if(this._CommonFocusHelper(Ltarg)) {
return true;
};
// 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, this.keyman.domManager.lastActiveElement);
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;
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;
}
if(Ltarg['body']) {
Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes.
}
// Makes sure we properly detect the TouchAliasElement root,
// rather than one of its constituent children.
if(this.keyman.util.device.touchable) {
Ltarg = findTouchAliasTarget(Ltarg);
if(!Ltarg) {
return true;
}
}
if(DOMEventHandlers.states._IgnoreNextSelChange) {
// If a keyboard calls saveFocus() (KSF), then ignore the
// next selection change
DOMEventHandlers.states._IgnoreNextSelChange--;
e.cancelBubble = true;
e.stopPropagation();
return true;
}
if(DOMEventHandlers.states._IgnoreBlurFocus) {
// Prevent triggering other blur-handling events (as possible)
e.cancelBubble = true;
e.stopPropagation();
return true;
}
// Hide the touch device input caret, if applicable I3363 (Build 301)
if(dom.Utils.instanceof(this.keyman.domManager.activeElement, "TouchAliasElement")) {
let lastAlias = <TouchAliasElement> this.keyman.domManager.activeElement;
lastAlias.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.frameElement as HTMLElement;
}
}
////keymanweb._SelectionControl = null;
if(this.keyman.domManager.lastActiveElement) {
this._BlurKeyboardSettings(this.keyman.domManager.lastActiveElement);
}
// Now that we've handled all prior-element maintenance, update the active and 'last-active element'.
this.keyman.domManager.activeElement = null; // I3363 (Build 301)
this.keyman.domManager.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;
let activeKeyboard = com.keyman.singleton.core.activeKeyboard;
if(!isActivating && activeKeyboard) {
activeKeyboard.notify(0, Utils.getOutputTarget(Ltarg as HTMLElement), 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);
this.doChangeEvent(Ltarg);
this.keyman['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, _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(lastElem: HTMLElement, PInternalName?: string, PLgCode?: string) {
var keyboardID = this.keyman.core.activeKeyboard ? this.keyman.core.activeKeyboard.id : '';
var langCode = this.keyman.keyboardManager.getActiveLanguage();
if(PInternalName !== undefined && PLgCode !== undefined) {
keyboardID = PInternalName;
langCode = PLgCode;
}
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(lastElem: HTMLElement, blockGlobalChange: boolean) {
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);
}
// Now that we've fully entered the new context, invalidate the context so we can generate initial predictions from it.
if(this.keyman.modelManager) {
let outputTarget = dom.Utils.getOutputTarget(lastElem);
this.keyman.core.languageProcessor.invalidateContext(outputTarget, this.keyman.core.keyboardProcessor?.layerId);
}
}
/**
* 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): boolean {
let keyman = com.keyman.singleton;
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;
const outputTarget = dom.Utils.getOutputTarget(target);
let activeKeyboard = keyman.core.activeKeyboard;
if(!uiManager.justActivated) {
if(target && outputTarget) {
outputTarget.deadkeys().clear();
}
if(activeKeyboard) {
activeKeyboard.notify(0, outputTarget, 1); // I2187
}
}
if(!uiManager.justActivated && DOMEventHandlers.states._SelectionControl != target) {
uiManager.isActivating = false;
}
uiManager.justActivated = false;
DOMEventHandlers.states._SelectionControl = target;
if(target && outputTarget) {
// Call the current keyboard's newContext handler;
// timeout is required in order to get the current
// selection, which is not ready at time of focus event,
// at least on Chrome
window.setTimeout(() => {
//console.log('processNewContextEvent called from focus');
com.keyman.singleton.core.processNewContextEvent(outputTarget);
});
}
if(keyman.core.languageProcessor.isActive) {
keyman.core.languageProcessor.predictFromTarget(outputTarget, keyman.core.keyboardProcessor?.layerId);
}
return false;
}
/**
* 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 activeKeyboard = this.keyman.core.activeKeyboard;
var util = this.keyman.util;
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;
}
return PreProcessor.keyDown(e);
}.bind(this);
doChangeEvent(_target: HTMLElement) {
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;
}
_Click: (e: MouseEvent) => boolean = function(this: DOMEventHandlers, e: MouseEvent): boolean {
let target = e.target as HTMLElement;
if(target && target['base']) {
target = target['base'];
}
//console.log('processNewContextEvent called from click');
com.keyman.singleton.core.processNewContextEvent(dom.Utils.getOutputTarget(target));
return true;
}.bind(this);
/**
* Function _KeyPress
* Scope Private
* Description Processes keypress event (does not pass data to keyboard)
*/
_KeyPress: (e: KeyboardEvent) => boolean = function(this: DOMEventHandlers, e: KeyboardEvent): boolean {
if(DOMEventHandlers.states._DisableInput || this.keyman.core.activeKeyboard == null) {
return true;
}
return PreProcessor.keyPress(e);
}.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 osk = this.keyman.osk;
var Levent = PreProcessor._GetKeyEventProperties(e, false);
if(Levent == null) {
return true;
}
let outputTarget = PreProcessor.getEventOutputTarget(e) as dom.targets.OutputTarget;
var inputEle = outputTarget.getElement();
// Since this part concerns DOM element + browser interaction management, we preprocess it for
// browser form commands before passing control to the Processor module.
if(Levent.Lcode == 13) {
var ignore = false;
if(outputTarget instanceof inputEle.ownerDocument.defaultView.HTMLTextAreaElement) {
ignore = true;
}
if(inputEle.base && inputEle.base instanceof inputEle.base.ownerDocument.defaultView.HTMLTextAreaElement) {
ignore = true;
}
if(!ignore) {
// For input fields, move to next input element
if(inputEle instanceof inputEle.ownerDocument.defaultView.HTMLInputElement) {
if(inputEle.type == 'search' || inputEle.type == 'submit') {
inputEle.form.submit();
} else {
this.keyman.domManager.moveToNext(false);
}
}
return true;
}
}
return PreProcessor.keyUp(e);
}.bind(this);
}
// -------------------------------------------------------------------------
/**
* Defines numerous functions for handling and modeling touch-based aliases.
*/
export class DOMTouchHandlers extends DOMEventHandlers {
firstTouch: {
x: number;
y: number;
};
constructor(keyman: KeymanBase) {
super(keyman);
}
private static selectTouch(e: TouchEvent): Touch {
/*
* During multi-touch events, it's possible for one or more touches of said multi-touch
* to be against irrelevant parts of the page. We only want to consider touches against
* valid OutputTargets - against elements of the page that KMW can attach to.
* With touch active... that's a TouchAliasElement.
*/
let isValidTouch = function(touch: Touch, target: EventTarget): boolean {
return e.target == target && !!(findTouchAliasTarget(touch.target as HTMLElement));
}
// The event at least tells us the event's target, which can be used to help check
// whether or not individual `Touch`es may be related to this specific event for
// an ongoing multitouch scenario.
let target = e.target;
// Find the first touch affected by this event that matches the current target.
for(let i=0; i < e.changedTouches.length; i++) {
if(isValidTouch(e.changedTouches[i], target)) {
return e.changedTouches[i];
}
}
// Shouldn't be possible. Just in case, we'd prefer a silent failure that allows
// callers to silently abort.
throw new Error("Could not select valid Touch for event.");
}
/**
* Handle receiving focus by simulated input field
*/
setFocus: (e?: TouchEvent|MSPointerEvent) => void = function(this: DOMTouchHandlers, e?: TouchEvent|MSPointerEvent): void {
DOMEventHandlers.states.setFocusTimer();
var tEvent: {
clientX: number;
clientY: number;
target?: EventTarget;
};
if(e && dom.Utils.instanceof(e, "TouchEvent")) {
try {
tEvent=DOMTouchHandlers.selectTouch(e as TouchEvent);
} catch(err) {
console.warn(err);
return;
}
} 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(this.keyman.domManager.lastActiveElement) {
tEvent.target = this.keyman.domManager.lastActiveElement;
// Shouldn't happen, but... just in case. Implemented late in 14.0 beta, so
// this detail was kept, though it's likely safe to eliminate.
if(tEvent.target['kmw_ip']) {
tEvent.target = tEvent.target['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'];
}
}
this.setFocusWithTouch(tEvent);
}.bind(this);
// Also handles initial touch responses.
setFocusWithTouch(tEvent: {clientX: number, clientY: number, target?: EventTarget}) {
var touchX=tEvent.clientX,touchY=tEvent.clientY;
// Some specifics rely upon which child of the TouchAliasElement received the actual event.
let tTarg=tEvent.target as HTMLElement;
// Determines the actual TouchAliasElement - the part tied to an OutputTarget.
let target = findTouchAliasTarget(tTarg);
if(!target) {
return;
}
// Some parts rely upon the scroller element.
let scroller = target.firstChild as HTMLElement;
// Move the caret and refocus if necessary
if(this.keyman.domManager.activeElement != target) {
// Hide the KMW caret
let prevTarget = <TouchAliasElement> this.keyman.domManager.activeElement;
// We're not 100% sure whether or not the next line can occur,
// but it's a decent failsafe regardless.
if(prevTarget && prevTarget['kmw_ip']) {
prevTarget = prevTarget['kmw_ip'] as TouchAliasElement;
}
// Make sure that we have the right type so that the expected method exists.
if(prevTarget && dom.Utils.instanceof(prevTarget, "TouchAliasElement")) {
prevTarget.hideCaret();
}
this.keyman.domManager.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);
// If clicked on DIV on the main element, rather than any part of the text representation,
// set caret to end of text
if(tTarg && tTarg == target) {
var x,cp;
x=dom.Utils.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);
}
target.setTextCaret(cp);
target.scrollInput();
// nextSibling - the scrollbar element.
} else if(tTarg != scroller.nextSibling) { // 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=target.getText()._kmwLength();
cp=target.getTextCaret();
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=dom.Utils.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;
target.setTextCaret(cp);
}
while(dom.Utils.getAbsoluteY(caret)-dy > touchY && cp > cpMin) {
target.setTextCaret(--cp);
}
while(dom.Utils.getAbsoluteY(caret)-dy < touchY-yRow && cp < cpMax) {
target.setTextCaret(++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=dom.Utils.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;
}
target.setTextCaret(cp);
}
while(snapOrder(dom.Utils.getAbsoluteX(caret), touchX) && cp > cpMin) {
target.setTextCaret(--cp);
}
while(!snapOrder(dom.Utils.getAbsoluteX(caret), touchX) && cp < cpMax) {
target.setTextCaret(++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.
*/
if(this.keyman.domManager.lastActiveElement) {
this._BlurKeyboardSettings(this.keyman.domManager.lastActiveElement);
}
// With the attachment API update, we now directly track the old legacy control behavior.
this.keyman.domManager.lastActiveElement = target;
target.showCaret();
/*
* 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(target, 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;
}
}
/**
* Close OSK and remove simulated caret on losing focus
*/
cancelInput(): void {
this.keyman.domManager.activeElement = null;
this.keyman.domManager.lastActiveElement = null;
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
var elem: HTMLElement;
if(('relatedTarget' in e) && e.relatedTarget) {
elem = e.relatedTarget as HTMLElement;
}
this.executeBlur(elem);
}.bind(this);
executeBlur(elem: HTMLElement) {
this.keyman['resetContext']();
if(elem) {
this.doChangeEvent(elem);
if(elem.nodeName != 'DIV' || elem.className.indexOf('keymanweb-input') == -1) {
this.cancelInput();
return;
}
}
//Hide the OSK
if(!DOMEventHandlers.states.focusing && !this.keyman.uiManager.justActivated) {
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 end event for an input element
*/
dragEnd: (e: TouchEvent|MouseEvent) => void = function(this: DOMTouchHandlers, e: TouchEvent|MouseEvent) {
e.stopPropagation();
this.firstTouch = null;
}.bind(this);
/**
* Handle the touch move event for an input element
*/
dragInput: (e: TouchEvent|MouseEvent) => void = function(this: DOMTouchHandlers, e: TouchEvent|MouseEvent) {
// Prevent dragging window
if(e.cancelable) {
// If a touch-alias element is scrolling, this may be false.
// Tends to result in a spam of console errors when e.cancelable == false.
e.preventDefault();
}
e.stopPropagation();
// Identify the target from the touch list or the event argument (IE 10 only)
var target: HTMLElement;
let touch: Touch;
if(dom.Utils.instanceof(e, "TouchEvent")) {
try {
touch=DOMTouchHandlers.selectTouch(e as TouchEvent);
} catch(err) {
console.warn(err);
return;
}
target = touch.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)
target = findTouchAliasTarget(target);
if(!target) {
return;
}
var x, y;
if(dom.Utils.instanceof(e, "TouchEvent")) {
x = touch.screenX;
y = touch.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= dom.Utils.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;
}
}
}
// Should refactor to use TouchAliasElement's version; target is an instance of the class.
this.setScrollBar(target);
}.bind(this);
/**
* 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;
if(!e || e.className == null || e.className.indexOf('keymanweb-input') < 0 || !osk) {
return;
}
// Get the absolute position of the caret
var s2=<HTMLElement>e.firstChild.childNodes[1], y=dom.Utils.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);
}
}
}
}