mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-06 08:55:34 +00:00
1837 lines
No EOL
70 KiB
TypeScript
1837 lines
No EOL
70 KiB
TypeScript
// Includes KMW-added property declaration extensions for HTML elements.
|
|
/// <reference path="../kmwexthtml.ts" />
|
|
// References the base KMW object.
|
|
/// <reference path="../kmwbase.ts" />
|
|
// References DOM event handling interfaces and classes.
|
|
/// <reference path="domEventHandlers.ts" />
|
|
// References DOM-specific output handling.
|
|
/// <reference path="domDefaultOutput.ts" />
|
|
// References other DOM-specific web-core overrides.
|
|
/// <reference path="domOverrides.ts" />
|
|
// Defines the touch-alias element structure used for mobile devices.
|
|
/// <reference path="touchAliasElement.ts" />
|
|
// Defines per-element-type OutputTarget element wrapping.
|
|
/// <reference path="targets/wrapElement.ts" />
|
|
// Defines cookie-based variable store serialization
|
|
/// <reference path="variableStoreCookieSerializer.ts" />
|
|
|
|
namespace com.keyman.dom {
|
|
// Utility object used to handle beep (keyboard error response) operations.
|
|
class BeepData {
|
|
e: HTMLElement;
|
|
c: string;
|
|
|
|
constructor(e: HTMLElement) {
|
|
this.e = e;
|
|
this.c = e.style.backgroundColor;
|
|
}
|
|
|
|
reset(): void {
|
|
this.e.style.backgroundColor = this.c;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class serves as the intermediary between KeymanWeb and any given web page's elements.
|
|
*/
|
|
export class DOMManager {
|
|
private keyman: KeymanBase;
|
|
|
|
/**
|
|
* Implements the AliasElementHandlers interface for touch interaction.
|
|
*/
|
|
touchHandlers?: DOMTouchHandlers;
|
|
|
|
/**
|
|
* Implements stubs for the AliasElementHandlers interface for non-touch interaction.
|
|
*/
|
|
nonTouchHandlers: DOMEventHandlers;
|
|
|
|
/**
|
|
* Tracks the attachment MutationObserver.
|
|
*/
|
|
attachmentObserver: MutationObserver;
|
|
|
|
/**
|
|
* Tracks the enablement MutationObserver.
|
|
*/
|
|
enablementObserver: MutationObserver;
|
|
|
|
/**
|
|
* Tracks a list of event-listening elements.
|
|
*
|
|
* In touch mode, this should contain touch-aliasing DIVs, but will contain other elements in non-touch mode.
|
|
*/
|
|
inputList: HTMLElement[] = []; // List of simulated input divisions for touch-devices I3363 (Build 301)
|
|
|
|
/**
|
|
* Tracks a visually-sorted list of elements that are KMW-enabled.
|
|
*/
|
|
sortedInputs: HTMLElement[] = []; // List of all INPUT and TEXTAREA elements ordered top to bottom, left to right
|
|
|
|
_BeepObjects: BeepData[] = []; // BeepObjects - maintains a list of active 'beep' visual feedback elements
|
|
_BeepTimeout: number = 0; // BeepTimeout - a flag indicating if there is an active 'beep'.
|
|
// Set to 1 if there is an active 'beep', otherwise leave as '0'.
|
|
|
|
// Used for special touch-based page interactions re: element activation on touch devices.
|
|
deactivateOnScroll: boolean = false;
|
|
deactivateOnRelease: boolean = false;
|
|
touchY: number; // For scroll-related aspects on iOS.
|
|
|
|
touchStartActivationHandler: (e: TouchEvent) => boolean;
|
|
touchMoveActivationHandler: (e: TouchEvent) => boolean;
|
|
touchEndActivationHandler: (e: TouchEvent) => boolean;
|
|
|
|
constructor(keyman: KeymanBase) {
|
|
this.keyman = keyman;
|
|
|
|
if(keyman.util.device.touchable) {
|
|
this.touchHandlers = new DOMTouchHandlers(keyman);
|
|
}
|
|
|
|
this.nonTouchHandlers = new DOMEventHandlers(keyman);
|
|
}
|
|
|
|
shutdown() {
|
|
// Catch and notify of any shutdown errors, but don't let errors fail unit tests.
|
|
try {
|
|
if(this.enablementObserver) {
|
|
this.enablementObserver.disconnect();
|
|
}
|
|
if(this.attachmentObserver) {
|
|
this.attachmentObserver.disconnect();
|
|
}
|
|
|
|
for(let input of this.inputList) {
|
|
this.disableInputElement(input);
|
|
}
|
|
|
|
// On shutdown, we remove our general focus-suppression handlers as well.
|
|
this.keyman.util.detachDOMEvent(document.body, 'focus', DOMManager.suppressFocusCheck, true);
|
|
this.keyman.util.detachDOMEvent(document.body, 'blur', DOMManager.suppressFocusCheck, true);
|
|
|
|
// Also, the base-page touch handlers for activation management.
|
|
if(this.touchStartActivationHandler) {
|
|
this.keyman.util.detachDOMEvent(document.body, 'touchstart', this.touchStartActivationHandler, false);
|
|
this.keyman.util.detachDOMEvent(document.body, 'touchmove', this.touchMoveActivationHandler, false);
|
|
this.keyman.util.detachDOMEvent(document.body, 'touchend', this.touchEndActivationHandler, false);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error occurred during shutdown");
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function beep KB (DOM-side implementation)
|
|
* Scope Public
|
|
* @param {Object} Pelem element to flash
|
|
* Description Flash body as substitute for audible beep; notify embedded device to vibrate
|
|
*/
|
|
doBeep(outputTarget: targets.OutputTarget) {
|
|
// Handles embedded-mode beeps.
|
|
let keyman = com.keyman.singleton;
|
|
if ('beepKeyboard' in keyman) {
|
|
keyman['beepKeyboard']();
|
|
return;
|
|
}
|
|
|
|
if(!(outputTarget instanceof targets.OutputTarget)) {
|
|
return;
|
|
}
|
|
|
|
// All code after this point is DOM-based, triggered by the beep.
|
|
var Pelem: HTMLElement = outputTarget.getElement();
|
|
if(outputTarget instanceof dom.targets.DesignIFrame) {
|
|
Pelem = outputTarget.docRoot; // I1446 - beep sometimes fails to flash when using OSK and rich control
|
|
}
|
|
|
|
if(!Pelem) {
|
|
return; // There's no way to signal a 'beep' to null, so just cut everything short.
|
|
}
|
|
|
|
if(!Pelem.style || typeof(Pelem.style.backgroundColor)=='undefined') {
|
|
return;
|
|
}
|
|
|
|
for(var Lbo=0; Lbo<this._BeepObjects.length; Lbo++) { // I1446 - beep sometimes fails to return background color to normal
|
|
// I1511 - array prototype extended
|
|
if(this._BeepObjects[Lbo].e == Pelem) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this._BeepObjects = com.keyman.singleton._push(this._BeepObjects, new BeepData(Pelem));
|
|
// TODO: This is probably a bad color choice if "dark mode" is enabled. A proper implementation
|
|
// would probably require some 'fun' CSS work, though.
|
|
Pelem.style.backgroundColor = '#000000';
|
|
if(this._BeepTimeout == 0) {
|
|
this._BeepTimeout = 1;
|
|
window.setTimeout(this.beepReset.bind(this), 50);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function beepReset
|
|
* Scope Public
|
|
* Description Reset/terminate beep or flash (not currently used: Aug 2011)
|
|
*/
|
|
beepReset(): void {
|
|
com.keyman.singleton.core.keyboardInterface.resetContextCache();
|
|
|
|
var Lbo;
|
|
this._BeepTimeout = 0;
|
|
for(Lbo=0;Lbo<this._BeepObjects.length;Lbo++) { // I1511 - array prototype extended
|
|
this._BeepObjects[Lbo].reset();
|
|
}
|
|
this._BeepObjects = [];
|
|
}
|
|
|
|
/**
|
|
* Function getHandlers
|
|
* Scope Private
|
|
* @param {Element} Pelem An input, textarea, or touch-alias element from the page.
|
|
* @returns {Object}
|
|
*/
|
|
getHandlers(Pelem: HTMLElement): DOMEventHandlers {
|
|
var _attachObj = Pelem.base ? Pelem.base._kmwAttachment : Pelem._kmwAttachment;
|
|
|
|
if(_attachObj) {
|
|
return _attachObj.touchEnabled ? this.touchHandlers : this.nonTouchHandlers;
|
|
} else {
|
|
// Best guess solution.
|
|
return this.keyman.touchAliasing;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function enableTouchElement
|
|
* Scope Private
|
|
* @param {Element} Pelem An input or textarea element from the page.
|
|
* @return {boolean} Returns true if it creates a simulated input element for Pelem; false if not.
|
|
* Description Creates a simulated input element for the specified INPUT or TEXTAREA, comprising:
|
|
* an outer DIV, matching the position, size and style of the base element
|
|
* a scrollable DIV within that outer element
|
|
* two SPAN elements within the scrollable DIV, to hold the text before and after the caret
|
|
*
|
|
* The left border of the second SPAN is flashed on and off as a visible caret
|
|
*
|
|
* Also ensures the element is registered on keymanweb's internal input list.
|
|
*/
|
|
enableTouchElement(Pelem: HTMLElement) {
|
|
// Touch doesn't worry about iframes.
|
|
if(Pelem.tagName.toLowerCase() == 'iframe') {
|
|
return false;
|
|
}
|
|
|
|
if(this.isKMWDisabled(Pelem)) {
|
|
this.setupNonKMWTouchElement(Pelem);
|
|
return false;
|
|
} else {
|
|
// Initialize and protect input elements for touch-screen devices (but never for apps)
|
|
// NB: now set disabled=true rather than readonly, since readonly does not always
|
|
// prevent element from getting focus, e.g. within a LABEL element.
|
|
// c.f. http://kreotekdev.wordpress.com/2007/11/08/disabled-vs-readonly-form-fields/
|
|
Pelem.kmwInput = true;
|
|
}
|
|
|
|
// Remove any handlers for "NonKMWTouch" elements, since we're enabling it here.
|
|
Pelem.removeEventListener('touchstart', this.nonKMWTouchHandler);
|
|
|
|
/*
|
|
* Does this element already have a simulated touch element established? If so,
|
|
* just reuse it - if it isn't still in the input list!
|
|
*/
|
|
if(Pelem['kmw_ip']) {
|
|
|
|
if(this.inputList.indexOf(Pelem['kmw_ip']) != -1) {
|
|
return false;
|
|
}
|
|
|
|
this.inputList.push(Pelem['kmw_ip']);
|
|
|
|
console.log("Unexpected state - this element's simulated input DIV should have been removed from the page!");
|
|
|
|
return true; // May need setup elsewhere since it's just been re-added!
|
|
}
|
|
|
|
// The simulated touch element doesn't already exist? Time to initialize it.
|
|
let x=dom.constructTouchAlias(Pelem);
|
|
if(this.isAttached(x)) {
|
|
x._kmwAttachment.interface = dom.targets.wrapElement(x);
|
|
} else {
|
|
this.setupElementAttachment(x); // The touch-alias should have its own wrapper.
|
|
}
|
|
Pelem._kmwAttachment = x._kmwAttachment; // It's an object reference we need to alias.
|
|
|
|
// Set font for base element
|
|
this.enableInputElement(x, true);
|
|
|
|
// Superimpose custom input fields for each input or textarea, unless readonly or disabled
|
|
|
|
// On touch event, reposition the text caret and prepare for OSK input
|
|
// Removed 'onfocus=' as that resulted in handling the event twice (on iOS, anyway)
|
|
|
|
// We know this to be the correct set of handlers because we're setting up a touch element.
|
|
var touchHandlers = this.touchHandlers;
|
|
|
|
x.addEventListener('touchstart', touchHandlers.setFocus);
|
|
x.onmspointerdown=function(e: MSPointerEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return touchHandlers.setFocus(e);
|
|
};
|
|
|
|
x.addEventListener('touchend', touchHandlers.dragEnd, false);
|
|
|
|
x.onmspointerup=function(e) {
|
|
e.stopPropagation();
|
|
};
|
|
|
|
// Disable internal scroll when input element in focus
|
|
x.addEventListener('touchmove', touchHandlers.dragInput, false);
|
|
x.onmspointermove=touchHandlers.dragInput;
|
|
|
|
// Hide keyboard and caret when losing focus from simulated input field
|
|
x.onblur=touchHandlers.setBlur;
|
|
|
|
// Note that touchend event propagates and is processed by body touchend handler
|
|
// re-setting the first touch point for a drag
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Function disableTouchElement
|
|
* Scope Private
|
|
* @param {Element} Pelem An input or textarea element from the page.
|
|
* Description Destroys the simulated input element for the specified INPUT or TEXTAREA and reverts
|
|
* back to desktop-style 'enablement' for the base control.
|
|
*/
|
|
disableTouchElement(Pelem: HTMLElement) {
|
|
// Do not check for the element being officially disabled - it's also used for detachment.
|
|
|
|
// Touch doesn't worry about iframes.
|
|
if(Pelem.tagName.toLowerCase() == 'iframe') {
|
|
return; // If/when we do support this, we'll need an iframe-level manager for it.
|
|
}
|
|
|
|
if(Pelem['kmw_ip']) {
|
|
var index = this.inputList.indexOf(Pelem['kmw_ip']);
|
|
if(index != -1) {
|
|
this.inputList.splice(index, 1);
|
|
}
|
|
|
|
Pelem.style.visibility='visible'; // hide by default: KMW-3
|
|
Pelem.disabled = false;
|
|
Pelem.removeEventListener('resize', Pelem['kmw_ip']._kmwResizeHandler);
|
|
|
|
// Disable touch-related handling code.
|
|
this.disableInputElement(Pelem['kmw_ip']);
|
|
Pelem._kmwAttachment.interface = dom.targets.wrapElement(Pelem);
|
|
|
|
// We get weird repositioning errors if we don't remove our simulated input element - and permanently.
|
|
if(Pelem.parentNode) {
|
|
Pelem.parentNode.removeChild(Pelem['kmw_ip']);
|
|
}
|
|
delete Pelem['kmw_ip'];
|
|
}
|
|
|
|
this.setupNonKMWTouchElement(Pelem);
|
|
}
|
|
|
|
/**
|
|
* Function nonKMWTouchHandler
|
|
* Scope Private
|
|
* Description A handler for KMW-touch-disabled elements when operating on touch devices.
|
|
*/
|
|
nonKMWTouchHandler = function(x) {
|
|
DOMEventHandlers.states.focusing=false;
|
|
clearTimeout(DOMEventHandlers.states.focusTimer);
|
|
this.keyman.osk.hideNow();
|
|
}.bind(this);
|
|
|
|
/**
|
|
* Function setupNonKMWTouchElement
|
|
* Scope Private
|
|
* @param {Element} x A child element of document.
|
|
* Description Performs handling for the specified disabled input element on touch-based systems.
|
|
*/
|
|
setupNonKMWTouchElement(x: HTMLElement) {
|
|
this.keyman.util.attachDOMEvent(x, 'touchstart', this.nonKMWTouchHandler, false);
|
|
|
|
// Signify that touch isn't enabled on the control.
|
|
if(this.isAttached(x)) {
|
|
x._kmwAttachment.touchEnabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function enableInputElement
|
|
* Scope Private
|
|
* @param {Element} Pelem An element from the document to be enabled with full KMW handling.
|
|
* @param {boolean=} isAlias A flag that indicates if the element is a simulated input element for touch.
|
|
* Description Performs the basic enabling setup for one element and adds it to the inputList if it is an input element.
|
|
* Note that this method is called for both desktop and touch control routes; the touch route calls it from within
|
|
* enableTouchElement as it must first establish the simulated touch element to serve as the alias "input element" here.
|
|
* Note that the 'kmw-disabled' property is managed by the MutationObserver and by the surface API calls.
|
|
*/
|
|
enableInputElement(Pelem: HTMLElement, isAlias?: boolean) {
|
|
var baseElement = isAlias ? Pelem['base'] : Pelem;
|
|
|
|
if(!this.isKMWDisabled(baseElement)) {
|
|
if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) {
|
|
this._AttachToIframe(Pelem);
|
|
} else {
|
|
if(!isAlias) {
|
|
this.setupElementAttachment(Pelem);
|
|
}
|
|
|
|
baseElement.className = baseElement.className ? baseElement.className + ' keymanweb-font' : 'keymanweb-font';
|
|
this.inputList.push(Pelem);
|
|
|
|
this.keyman.util.attachDOMEvent(baseElement,'focus', this.getHandlers(Pelem)._ControlFocus);
|
|
this.keyman.util.attachDOMEvent(baseElement,'blur', this.getHandlers(Pelem)._ControlBlur);
|
|
this.keyman.util.attachDOMEvent(baseElement,'click', this.getHandlers(Pelem)._Click);
|
|
|
|
// These need to be on the actual input element, as otherwise the keyboard will disappear on touch.
|
|
Pelem.onkeypress = this.getHandlers(Pelem)._KeyPress;
|
|
Pelem.onkeydown = this.getHandlers(Pelem)._KeyDown;
|
|
Pelem.onkeyup = this.getHandlers(Pelem)._KeyUp;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function disableInputElement
|
|
* Scope Private
|
|
* @param {Element} Pelem An element from the document to be enabled with full KMW handling.
|
|
* @param {boolean=} isAlias A flag that indicates if the element is a simulated input element for touch.
|
|
* Description Inverts the process of enableInputElement, removing all event-handling from the element.
|
|
* Note that the 'kmw-disabled' property is managed by the MutationObserver and by the surface API calls.
|
|
*/
|
|
disableInputElement(Pelem: HTMLElement, isAlias?: boolean) {
|
|
if(!Pelem) {
|
|
return;
|
|
}
|
|
|
|
var baseElement = isAlias ? Pelem['base'] : Pelem;
|
|
// Do NOT test for pre-disabledness - we also use this to fully detach without officially 'disabling' via kmw-disabled.
|
|
if((Pelem.ownerDocument.defaultView && Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) ||
|
|
Pelem instanceof HTMLIFrameElement) {
|
|
this._DetachFromIframe(Pelem);
|
|
} else {
|
|
var cnIndex = baseElement.className.indexOf('keymanweb-font');
|
|
if(cnIndex > 0 && !isAlias) { // See note about the alias below.
|
|
baseElement.className = baseElement.className.replace('keymanweb-font', '').trim();
|
|
}
|
|
|
|
// Remove the element from our internal input tracking.
|
|
var index = this.inputList.indexOf(Pelem);
|
|
if(index > -1) {
|
|
this.inputList.splice(index, 1);
|
|
}
|
|
|
|
if(!isAlias) { // See note about the alias below.
|
|
this.keyman.util.detachDOMEvent(baseElement,'focus', this.getHandlers(Pelem)._ControlFocus);
|
|
this.keyman.util.detachDOMEvent(baseElement,'blur', this.getHandlers(Pelem)._ControlBlur);
|
|
this.keyman.util.detachDOMEvent(baseElement,'click', this.getHandlers(Pelem)._Click);
|
|
}
|
|
// These need to be on the actual input element, as otherwise the keyboard will disappear on touch.
|
|
Pelem.onkeypress = null;
|
|
Pelem.onkeydown = null;
|
|
Pelem.onkeyup = null;
|
|
}
|
|
|
|
// If we're disabling an alias, we should fully enable the base version. (Thinking ahead to toggleable-touch mode.)
|
|
if(isAlias) {
|
|
this.inputList.push(baseElement);
|
|
|
|
baseElement.onkeypress = this.getHandlers(Pelem)._KeyPress;
|
|
baseElement.onkeydown = this.getHandlers(Pelem)._KeyDown;
|
|
baseElement.onkeyup = this.getHandlers(Pelem)._KeyUp;
|
|
}
|
|
|
|
var lastElem = this.lastActiveElement;
|
|
if(lastElem == Pelem || lastElem == Pelem['kmw_ip']) {
|
|
if(this.activeElement == lastElem) {
|
|
this.activeElement = null;
|
|
}
|
|
this.lastActiveElement = null;
|
|
this.keyman.osk.startHide(false);
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Function isKMWDisabled
|
|
* Scope Private
|
|
* @param {Element} x An element from the page.
|
|
* @return {boolean} true if the element's properties indicate a 'disabled' state.
|
|
* Description Examines attachable elements to determine their default enablement state.
|
|
*/
|
|
isKMWDisabled(x: HTMLElement): boolean {
|
|
var c = x.className;
|
|
|
|
// Exists for some HTMLElements.
|
|
if(x['readOnly']) {
|
|
return true;
|
|
} else if(c && c.indexOf('kmw-disabled') >= 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Function attachToControl
|
|
* Scope Public
|
|
* @param {Element} Pelem Element to which KMW will be attached
|
|
* Description Attaches KMW to control (or IFrame)
|
|
*/
|
|
attachToControl(Pelem: HTMLElement) {
|
|
var touchable = this.keyman.util.device.touchable;
|
|
|
|
// Exception for IFrame elements, in case of async loading issues. (Fixes fun iframe loading bug with Chrome.)
|
|
if(this.isAttached(Pelem) && !(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement)) {
|
|
return; // We're already attached.
|
|
}
|
|
|
|
if(this.isKMWInput(Pelem)) {
|
|
if(!this.isKMWDisabled(Pelem)) {
|
|
if(touchable && !this.keyman.isEmbedded) {
|
|
this.enableTouchElement(Pelem);
|
|
} else {
|
|
this.enableInputElement(Pelem);
|
|
}
|
|
} else {
|
|
if(touchable) {
|
|
this.setupNonKMWTouchElement(Pelem);
|
|
}
|
|
}
|
|
} else if(touchable) {
|
|
this.setupNonKMWTouchElement(Pelem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function detachFromControl
|
|
* Scope Public
|
|
* @param {Element} Pelem Element from which KMW will detach
|
|
* Description Detaches KMW from a control (or IFrame)
|
|
*/
|
|
detachFromControl(Pelem: HTMLElement) {
|
|
if(!(this.isAttached(Pelem) || Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement)) {
|
|
return; // We never were attached.
|
|
}
|
|
|
|
// #1 - if element is enabled, disable it. But don't manipulate the 'kmw-disabled' tag.
|
|
if(this.isKMWInput(Pelem)) {
|
|
// Is it already disabled?
|
|
if(!this.isKMWDisabled(Pelem)) {
|
|
this._DisableControl(Pelem);
|
|
}
|
|
}
|
|
|
|
// #2 - clear attachment data.
|
|
this.clearElementAttachment(Pelem);
|
|
}
|
|
|
|
/**
|
|
* Function isAttached
|
|
* Scope Private
|
|
* @param {Element} x An element from the page.
|
|
* @return {boolean} true if KMW is attached to the element, otherwise false.
|
|
*/
|
|
isAttached(x: HTMLElement) {
|
|
return x._kmwAttachment ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Function isKMWInput
|
|
* Scope Private
|
|
* @param {Element} x An element from the page.
|
|
* @return {boolean} true if the element is viable for KMW attachment.
|
|
* Description Examines potential input elements to determine whether or not they are viable for KMW attachment.
|
|
* Also filters elements not supported for touch devices when device.touchable == true.
|
|
*/
|
|
isKMWInput(x: HTMLElement): boolean {
|
|
var touchable = this.keyman.util.device.touchable;
|
|
|
|
if(x instanceof x.ownerDocument.defaultView.HTMLTextAreaElement) {
|
|
return true;
|
|
} else if(x instanceof x.ownerDocument.defaultView.HTMLInputElement) {
|
|
if (x.type == 'text' || x.type == 'search') {
|
|
return true;
|
|
}
|
|
} else if(x instanceof x.ownerDocument.defaultView.HTMLIFrameElement && !touchable) { // Do not allow iframe attachment if in 'touch' mode.
|
|
try {
|
|
if(x.contentWindow) {
|
|
if(x.contentWindow.document) { // Only allow attachment if the iframe's internal document is valid.
|
|
return true;
|
|
}
|
|
} // else nothing?
|
|
}
|
|
catch(err) {
|
|
/* Do not attempt to access iframes outside this site */
|
|
console.warn("Error during attachment to / detachment from iframe: ");
|
|
console.warn(err);
|
|
}
|
|
} else if(x.isContentEditable && !touchable) { // Only allow contentEditable attachment outside of 'touch' mode.
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Function setupElementAttachment
|
|
* Scope Private
|
|
* @param {Element} x An element from the page valid for KMW attachment
|
|
* Description Establishes the base KeymanWeb data for newly-attached elements.
|
|
* Does not establish input hooks, which are instead handled during enablement.
|
|
*/
|
|
setupElementAttachment(x: HTMLElement) {
|
|
// The `_kmwAttachment` property tag maintains all relevant KMW-maintained data regarding the element.
|
|
// It is disgarded upon de-attachment.
|
|
if(x._kmwAttachment) {
|
|
return;
|
|
} else {
|
|
// Problem: tries to wrap IFrames that aren't design-mode.
|
|
// The elements in the contained document get separately wrapped, so this doesn't need a proper wrapper.
|
|
//
|
|
// Its attachment process might need some work.
|
|
let eleInterface = dom.targets.wrapElement(x);
|
|
// May should filter better for IFrames.
|
|
if(!(eleInterface || dom.Utils.instanceof(x, "HTMLIFrameElement"))) {
|
|
console.warn("Could not create processing interface for newly-attached element!");
|
|
}
|
|
|
|
x._kmwAttachment = new AttachmentInfo(eleInterface, null, this.keyman.util.device.touchable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function clearElementAttachment
|
|
* Scope Private
|
|
* @param {Element} x An element from the page valid for KMW attachment
|
|
* Description Establishes the base KeymanWeb data for newly-attached elements.
|
|
* Does not establish input hooks, which are instead handled during enablement.
|
|
*/
|
|
clearElementAttachment(x: HTMLElement) {
|
|
// We need to clear the object when de-attaching; helps prevent memory leaks.
|
|
x._kmwAttachment = null;
|
|
}
|
|
|
|
/**
|
|
* Function _AttachToIframe
|
|
* Scope Private
|
|
* @param {Element} Pelem IFrame to which KMW will be attached
|
|
* Description Attaches KeymanWeb to IFrame
|
|
*/
|
|
_AttachToIframe(Pelem: HTMLIFrameElement) {
|
|
var util = this.keyman.util;
|
|
|
|
try {
|
|
var Lelem=Pelem.contentWindow.document;
|
|
/* editable Iframe */
|
|
if(Lelem) {
|
|
if(Lelem.designMode.toLowerCase() == 'on') {
|
|
// I2404 - Attach to IFRAMEs child objects, only editable IFRAMEs here
|
|
if(util.device.browser == 'firefox') {
|
|
util.attachDOMEvent(Lelem,'focus', this.getHandlers(Pelem)._ControlFocus);
|
|
util.attachDOMEvent(Lelem,'blur', this.getHandlers(Pelem)._ControlBlur);
|
|
} else { // Chrome, Safari
|
|
util.attachDOMEvent(Lelem.body,'focus', this.getHandlers(Pelem)._ControlFocus);
|
|
util.attachDOMEvent(Lelem.body,'blur', this.getHandlers(Pelem)._ControlBlur);
|
|
}
|
|
util.attachDOMEvent(Lelem.body,'keydown', this.getHandlers(Pelem)._KeyDown);
|
|
util.attachDOMEvent(Lelem.body,'keypress', this.getHandlers(Pelem)._KeyPress);
|
|
util.attachDOMEvent(Lelem.body,'keyup', this.getHandlers(Pelem)._KeyUp);
|
|
|
|
// Set up a reference alias; the internal document will need the same attachment info!
|
|
this.setupElementAttachment(Pelem);
|
|
Lelem.body._kmwAttachment = Pelem._kmwAttachment;
|
|
} else {
|
|
// Lelem is the IFrame's internal document; set 'er up!
|
|
this._SetupDocument(Lelem.body); // I2404 - Manage IE events in IFRAMEs
|
|
}
|
|
}
|
|
}
|
|
catch(err)
|
|
{
|
|
// do not attempt to attach to the iframe as it is from another domain - XSS denied!
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function _DetachFromIframe
|
|
* Scope Private
|
|
* @param {Element} Pelem IFrame to which KMW will be attached
|
|
* Description Detaches KeymanWeb from an IFrame
|
|
*/
|
|
_DetachFromIframe(Pelem: HTMLIFrameElement) {
|
|
var util = this.keyman.util;
|
|
|
|
try {
|
|
var Lelem=Pelem.contentWindow.document;
|
|
/* editable Iframe */
|
|
if(Lelem) {
|
|
if(Lelem.designMode.toLowerCase() == 'on') {
|
|
// Mozilla // I2404 - Attach to IFRAMEs child objects, only editable IFRAMEs here
|
|
if(util.device.browser == 'firefox') {
|
|
// Firefox won't handle these events on Lelem.body - only directly on Lelem (the doc) instead.
|
|
util.detachDOMEvent(Lelem,'focus', this.getHandlers(Pelem)._ControlFocus);
|
|
util.detachDOMEvent(Lelem,'blur', this.getHandlers(Pelem)._ControlBlur);
|
|
} else { // Chrome, Safari
|
|
util.detachDOMEvent(Lelem.body,'focus', this.getHandlers(Pelem)._ControlFocus);
|
|
util.detachDOMEvent(Lelem.body,'blur', this.getHandlers(Pelem)._ControlBlur);
|
|
}
|
|
util.detachDOMEvent(Lelem.body,'keydown', this.getHandlers(Pelem)._KeyDown);
|
|
util.detachDOMEvent(Lelem.body,'keypress', this.getHandlers(Pelem)._KeyPress);
|
|
util.detachDOMEvent(Lelem.body,'keyup', this.getHandlers(Pelem)._KeyUp);
|
|
|
|
// Remove the reference to our prior attachment data!
|
|
Lelem.body._kmwAttachment = null;
|
|
} else {
|
|
// Lelem is the IFrame's internal document; set 'er up!
|
|
this._ClearDocument(Lelem.body); // I2404 - Manage IE events in IFRAMEs
|
|
}
|
|
}
|
|
}
|
|
catch(err)
|
|
{
|
|
// do not attempt to attach to the iframe as it is from another domain - XSS denied!
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function _GetDocumentEditables
|
|
* Scope Private
|
|
* @param {Element} Pelem HTML element
|
|
* @return {Array<Element>} A list of potentially-editable controls. Further filtering [as with isKMWInput() and
|
|
* isKMWDisabled()] is required.
|
|
*/
|
|
_GetDocumentEditables(Pelem: HTMLElement): (HTMLElement)[] {
|
|
var util = this.keyman.util;
|
|
|
|
var possibleInputs: (HTMLElement)[] = [];
|
|
|
|
// Document.ownerDocument === null, so we better check that it's not null before proceeding.
|
|
if(Pelem.ownerDocument && Pelem instanceof Pelem.ownerDocument.defaultView.HTMLElement) {
|
|
var dv = Pelem.ownerDocument.defaultView;
|
|
|
|
if(Pelem instanceof dv.HTMLInputElement || Pelem instanceof dv.HTMLTextAreaElement) {
|
|
possibleInputs.push(Pelem);
|
|
} else if(Pelem instanceof dv.HTMLIFrameElement) {
|
|
possibleInputs.push(Pelem);
|
|
}
|
|
}
|
|
|
|
// Constructing it like this also allows for individual element filtering for the auto-attach MutationObserver without errors.
|
|
if(Pelem.getElementsByTagName) {
|
|
/**
|
|
* Function LiTmp
|
|
* Scope Private
|
|
* @param {string} _colon type of element
|
|
* @return {Array<Element>} array of elements of specified type
|
|
* Description Local function to get list of editable controls
|
|
*/
|
|
var LiTmp = function(_colon: string): HTMLElement[] {
|
|
return util.arrayFromNodeList(Pelem.getElementsByTagName(_colon));
|
|
};
|
|
|
|
// Note that isKMWInput() will block IFRAME elements as necessary for touch-based devices.
|
|
possibleInputs = possibleInputs.concat(LiTmp('INPUT'), LiTmp('TEXTAREA'), LiTmp('IFRAME'));
|
|
}
|
|
|
|
// Not all active browsers may support the method, but only those that do would work with contenteditables anyway.
|
|
if(Pelem.querySelectorAll) {
|
|
possibleInputs = possibleInputs.concat(util.arrayFromNodeList(Pelem.querySelectorAll('[contenteditable]')));
|
|
}
|
|
|
|
if(Pelem.ownerDocument && Pelem instanceof Pelem.ownerDocument.defaultView.HTMLElement && Pelem.isContentEditable) {
|
|
possibleInputs.push(Pelem);
|
|
}
|
|
|
|
return possibleInputs;
|
|
}
|
|
|
|
/**
|
|
* Function _SetupDocument
|
|
* Scope Private
|
|
* @param {Element} Pelem - the root element of a document, including IFrame documents.
|
|
* Description Used to automatically attach KMW to editable controls, regardless of control path.
|
|
*/
|
|
_SetupDocument(Pelem: HTMLElement) { // I1961
|
|
var possibleInputs = this._GetDocumentEditables(Pelem);
|
|
|
|
for(var Li = 0; Li < possibleInputs.length; Li++) {
|
|
var input = possibleInputs[Li];
|
|
|
|
// It knows how to handle pre-loaded iframes appropriately.
|
|
this.attachToControl(possibleInputs[Li] as HTMLElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function _ClearDocument
|
|
* Scope Private
|
|
* @param {Element} Pelem - the root element of a document, including IFrame documents.
|
|
* Description Used to automatically detach KMW from editable controls, regardless of control path.
|
|
* Mostly used to clear out all controls of a detached IFrame.
|
|
*/
|
|
_ClearDocument(Pelem: HTMLElement) { // I1961
|
|
var possibleInputs = this._GetDocumentEditables(Pelem);
|
|
|
|
for(var Li = 0; Li < possibleInputs.length; Li++) {
|
|
var input = possibleInputs[Li];
|
|
|
|
// It knows how to handle pre-loaded iframes appropriately.
|
|
this.detachFromControl(possibleInputs[Li] as HTMLElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set target element text direction (LTR or RTL), but only if the element is empty
|
|
*
|
|
* If the element base directionality is changed after it contains content, unless all the text
|
|
* has the same directionality, text runs will be re-ordered which is confusing and causes
|
|
* incorrect caret positioning
|
|
*
|
|
* @param {Object} Ptarg Target element
|
|
*/
|
|
_SetTargDir(Ptarg: HTMLElement) {
|
|
let activeKeyboard = com.keyman.singleton.core.activeKeyboard;
|
|
var elDir=(activeKeyboard && activeKeyboard.isRTL) ? 'rtl' : 'ltr';
|
|
|
|
if(Ptarg) {
|
|
if(this.keyman.util.device.touchable) {
|
|
let alias = <dom.TouchAliasElement> Ptarg;
|
|
if(Ptarg.textContent.length == 0) {
|
|
alias.base.dir=alias.dir=elDir;
|
|
alias.setTextCaret(10000);
|
|
}
|
|
} else {
|
|
if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement
|
|
|| Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLTextAreaElement) {
|
|
if((Ptarg as HTMLInputElement|HTMLTextAreaElement).value.length == 0) {
|
|
Ptarg.dir=elDir;
|
|
}
|
|
} else if(typeof Ptarg.textContent == "string" && Ptarg.textContent.length == 0) { // As with contenteditable DIVs, for example.
|
|
Ptarg.dir=elDir;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function _DisableControl
|
|
* Scope Private
|
|
* @param {Element} Pelem Element to be disabled
|
|
* Description Disable KMW control element
|
|
*/
|
|
_DisableControl(Pelem: HTMLElement) {
|
|
// Only operate on attached elements! Non-design-mode IFrames don't get attachment markers, so we check them specifically instead.
|
|
if(this.isAttached(Pelem) || Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) {
|
|
if(this.keyman.util.device.touchable) {
|
|
this.disableTouchElement(Pelem);
|
|
this.setupNonKMWTouchElement(Pelem);
|
|
|
|
var keyman = this.keyman;
|
|
|
|
// If a touch alias was removed, chances are it's gonna mess up our touch-based layout scheme, so let's update the touch elements.
|
|
window.setTimeout(function() {
|
|
this.listInputs();
|
|
|
|
for(var k = 0; k < this.sortedInputs.length; k++) {
|
|
if(this.sortedInputs[k]['kmw_ip']) {
|
|
this.sortedInputs[k]['kmw_ip'].updateInput(this.sortedInputs[k]['kmw_ip']);
|
|
}
|
|
}
|
|
}.bind(this), 1);
|
|
} else {
|
|
this.listInputs(); // Fix up our internal input ordering scheme.
|
|
}
|
|
|
|
this.disableInputElement(Pelem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function _EnableControl
|
|
* Scope Private
|
|
* @param {Element} Pelem Element to be enabled
|
|
* Description Enable KMW control element
|
|
*/
|
|
_EnableControl(Pelem: HTMLElement) {
|
|
if(this.isAttached(Pelem)) { // Only operate on attached elements!
|
|
if(this.keyman.util.device.touchable) {
|
|
this.enableTouchElement(Pelem);
|
|
|
|
var keyman = this.keyman;
|
|
|
|
// If we just added a new input alias, some languages will mess up our touch-based layout scheme
|
|
// if we don't update the touch elements.
|
|
window.setTimeout(function() {
|
|
keyman.domManager.listInputs();
|
|
|
|
for(var k = 0; k < this.sortedInputs.length; k++) {
|
|
if(this.sortedInputs[k]['kmw_ip']) {
|
|
this.sortedInputs[k]['kmw_ip'].updateInput(this.sortedInputs[k]['kmw_ip']);
|
|
}
|
|
}
|
|
}.bind(this), 1);
|
|
} else {
|
|
this.enableInputElement(Pelem);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create an ordered list of all text and search input elements and textarea elements
|
|
// except any tagged with class 'kmw-disabled'
|
|
// TODO: email and url types should perhaps use default keyboard only
|
|
listInputs() {
|
|
var i,eList=[],
|
|
t1=document.getElementsByTagName<'input'>('input'),
|
|
t2=document.getElementsByTagName<'textarea'>('textarea');
|
|
|
|
var util = this.keyman.util;
|
|
|
|
for(i=0; i<t1.length; i++) {
|
|
switch(t1[i].type) {
|
|
case 'text':
|
|
case 'search':
|
|
case 'email':
|
|
case 'url':
|
|
if(t1[i].className.indexOf('kmw-disabled') < 0) {
|
|
eList.push({ip:t1[i], x: dom.Utils.getAbsoluteX(t1[i]), y: dom.Utils.getAbsoluteY(t1[i])});
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
for(i=0; i<t2.length; i++) {
|
|
if(t2[i].className.indexOf('kmw-disabled') < 0)
|
|
eList.push({ip:t2[i], x: dom.Utils.getAbsoluteX(t2[i]), y: dom.Utils.getAbsoluteY(t2[i])});
|
|
}
|
|
|
|
/**
|
|
* Local function to sort by screen position
|
|
*
|
|
* @param {Object} e1 first object
|
|
* @param {Object} e2 second object
|
|
* @return {number} y-difference between object positions, or x-difference if y values the same
|
|
*/
|
|
var xySort=function(e1,e2)
|
|
{
|
|
if(e1.y != e2.y) return e1.y-e2.y;
|
|
return e1.x-e2.x;
|
|
}
|
|
|
|
// Sort elements by Y then X
|
|
eList.sort(xySort);
|
|
|
|
// Create a new list of sorted elements
|
|
var tList=[];
|
|
for(i=0;i<eList.length;i++)
|
|
tList.push(eList[i].ip);
|
|
|
|
// Return the sorted element list
|
|
this.sortedInputs=tList;
|
|
}
|
|
|
|
_EnablementMutationObserverCore = function(mutations: MutationRecord[]) {
|
|
for(var i=0; i < mutations.length; i++) {
|
|
var mutation = mutations[i];
|
|
|
|
// ( ? : ) needed as a null check.
|
|
var disabledBefore = mutation.oldValue ? mutation.oldValue.indexOf('kmw-disabled') >= 0 : false;
|
|
var disabledAfter = (mutation.target as HTMLElement).className.indexOf('kmw-disabled') >= 0;
|
|
|
|
if(disabledBefore && !disabledAfter) {
|
|
this._EnableControl(mutation.target);
|
|
} else if(!disabledBefore && disabledAfter) {
|
|
this._DisableControl(mutation.target);
|
|
}
|
|
|
|
// 'readonly' triggers on whether or not the attribute exists, not its value.
|
|
if(!disabledAfter && mutation.attributeName == "readonly") {
|
|
var readonlyBefore = mutation.oldValue ? mutation.oldValue != null : false;
|
|
var elem = mutation.target;
|
|
|
|
if(elem instanceof elem.ownerDocument.defaultView.HTMLInputElement
|
|
|| elem instanceof elem.ownerDocument.defaultView.HTMLTextAreaElement) {
|
|
var readonlyAfter = elem.readOnly;
|
|
|
|
if(readonlyBefore && !readonlyAfter) {
|
|
this._EnableControl(mutation.target);
|
|
} else if(!readonlyBefore && readonlyAfter) {
|
|
this._DisableControl(mutation.target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}.bind(this);
|
|
|
|
_AutoAttachObserverCore = function(mutations: MutationRecord[]) {
|
|
var inputElementAdditions = [];
|
|
var inputElementRemovals = [];
|
|
|
|
for(var i=0; i < mutations.length; i++) {
|
|
var mutation = mutations[i];
|
|
|
|
for(var j=0; j < mutation.addedNodes.length; j++) {
|
|
inputElementAdditions = inputElementAdditions.concat(this._GetDocumentEditables(mutation.addedNodes[j]));
|
|
}
|
|
|
|
for(j = 0; j < mutation.removedNodes.length; j++) {
|
|
inputElementRemovals = inputElementRemovals.concat(this._GetDocumentEditables(mutation.removedNodes[j]));
|
|
}
|
|
}
|
|
|
|
for(var k = 0; k < inputElementAdditions.length; k++) {
|
|
if(this.isKMWInput(inputElementAdditions[k])) { // Apply standard element filtering!
|
|
this._MutationAdditionObserved(inputElementAdditions[k]);
|
|
}
|
|
}
|
|
|
|
for(k = 0; k < inputElementRemovals.length; k++) {
|
|
if(this.isKMWInput(inputElementRemovals[k])) { // Apply standard element filtering!
|
|
this._MutationRemovalObserved(inputElementRemovals[k]);
|
|
}
|
|
}
|
|
|
|
/* After all mutations have been handled, we need to recompile our .sortedInputs array, but only
|
|
* if any have actually occurred.
|
|
*/
|
|
if(inputElementAdditions.length || inputElementRemovals.length) {
|
|
if(!this.keyman.util.device.touchable) {
|
|
this.listInputs();
|
|
} else if(this.keyman.util.device.touchable) { // If something was added or removed, chances are it's gonna mess up our touch-based layout scheme, so let's update the touch elements.
|
|
var domManager = this;
|
|
window.setTimeout(function() {
|
|
domManager.listInputs();
|
|
|
|
for(var k = 0; k < this.sortedInputs.length; k++) {
|
|
if(this.sortedInputs[k]['kmw_ip']) {
|
|
this.sortedInputs[k]['kmw_ip'].updateInput();
|
|
}
|
|
}
|
|
}.bind(this), 1);
|
|
}
|
|
}
|
|
}.bind(this);
|
|
|
|
/**
|
|
* Function _MutationAdditionObserved
|
|
* Scope Private
|
|
* @param {Element} Pelem A page input, textarea, or iframe element.
|
|
* Description Used by the MutationObserver event handler to properly setup any elements dynamically added to the document post-initialization.
|
|
*
|
|
*/
|
|
_MutationAdditionObserved = function(Pelem: HTMLElement) {
|
|
if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement && !this.keyman.util.device.touchable) {
|
|
//Problem: the iframe is loaded asynchronously, and we must wait for it to load fully before hooking in.
|
|
|
|
var domManager = this;
|
|
|
|
var attachFunctor = function() { // Triggers at the same time as iframe's onload property, after its internal document loads.
|
|
// Provide a minor delay to allow 'load' event handlers to set the design-mode property.
|
|
window.setTimeout(function() {
|
|
domManager.attachToControl(Pelem);
|
|
}, 1);
|
|
};
|
|
|
|
Pelem.addEventListener('load', attachFunctor);
|
|
|
|
// The following block breaks for design-mode iframes, at least in Chrome; a blank document may exist
|
|
// before the load of the desired actual document.
|
|
//
|
|
// /* If the iframe has somehow already loaded, we can't expect the onload event to be raised. We ought just
|
|
// * go ahead and perform our callback's contents.
|
|
// *
|
|
// * keymanweb.domManager.attachToControl() is now idempotent, so even if our call 'whiffs', it won't cause long-lasting
|
|
// * problems.
|
|
// */
|
|
// if(Pelem.contentDocument.readyState == 'complete') {
|
|
// window.setTimeout(attachFunctor, 1);
|
|
// }
|
|
} else {
|
|
this.attachToControl(Pelem);
|
|
}
|
|
}
|
|
|
|
// Used by the mutation event handler to properly decouple any elements dynamically removed from the document.
|
|
_MutationRemovalObserved = function(Pelem: HTMLElement) {
|
|
var element = Pelem;
|
|
if(this.keyman.util.device.touchable) {
|
|
this.disableTouchElement(Pelem);
|
|
}
|
|
|
|
this.disableInputElement(Pelem); // Remove all KMW event hooks, styling.
|
|
this.clearElementAttachment(element); // Memory management & auto de-attachment upon removal.
|
|
}
|
|
|
|
/**
|
|
* Function disableControl
|
|
* Scope Public
|
|
* @param {Element} Pelem Element to be disabled
|
|
* Description Disables a KMW control element
|
|
*/
|
|
disableControl(Pelem: HTMLElement) {
|
|
if(!this.isAttached(Pelem)) {
|
|
console.warn("KeymanWeb is not attached to element " + Pelem);
|
|
}
|
|
|
|
var cn = Pelem.className;
|
|
if(cn.indexOf('kmw-disabled') < 0) { // if not already explicitly disabled...
|
|
Pelem.className = cn ? cn + ' kmw-disabled' : 'kmw-disabled';
|
|
}
|
|
|
|
// The rest is triggered within MutationObserver code.
|
|
// See _EnablementMutationObserverCore.
|
|
}
|
|
|
|
/**
|
|
* Function enableControl
|
|
* Scope Public
|
|
* @param {Element} Pelem Element to be disabled
|
|
* Description Disables a KMW control element
|
|
*/
|
|
enableControl = function(Pelem: HTMLElement) {
|
|
if(!this.isAttached(Pelem)) {
|
|
console.warn("KeymanWeb is not attached to element " + Pelem);
|
|
}
|
|
|
|
var cn = Pelem.className;
|
|
var tagIndex = cn.indexOf('kmw-disabled');
|
|
if(tagIndex >= 0) { // if already explicitly disabled...
|
|
Pelem.className = cn.replace('kmw-disabled', '').trim();
|
|
}
|
|
|
|
// The rest is triggered within MutationObserver code.
|
|
// See _EnablementMutationObserverCore.
|
|
}
|
|
|
|
/* ------------- Page and document-level management events ------------------ */
|
|
|
|
_WindowLoad: (e: Event) => void = function(e: Event) {
|
|
//keymanweb.completeInitialization();
|
|
// Always return to top of page after a page reload
|
|
document.body.scrollTop=0;
|
|
if(typeof document.documentElement != 'undefined') {
|
|
document.documentElement.scrollTop=0;
|
|
}
|
|
}.bind(this);
|
|
|
|
/**
|
|
* Function _WindowUnload
|
|
* Scope Private
|
|
* Description Remove handlers before detaching KMW window
|
|
*/
|
|
_WindowUnload: () => void = function(this: DOMManager) {
|
|
// Allow the UI to release its own resources
|
|
this.keyman.uiManager.doUnload();
|
|
|
|
// Allow the OSK to release its own resources
|
|
if(this.keyman.osk) {
|
|
this.keyman.osk.shutdown();
|
|
if(this.keyman.osk['_Unload']) {
|
|
this.keyman.osk['_Unload'](); // I3363 (Build 301)
|
|
}
|
|
}
|
|
|
|
this.lastActiveElement = null;
|
|
}.bind(this);
|
|
|
|
/* ------ Defines independent, per-control keyboard setting behavior for the API. ------ */
|
|
|
|
/**
|
|
* Function setKeyboardForControl
|
|
* Scope Public
|
|
* @param {Element} Pelem Control element
|
|
* @param {string|null=} Pkbd Keyboard (Clears the set keyboard if set to null.)
|
|
* @param {string|null=} Plc Language Code
|
|
* Description Set default keyboard for the control
|
|
*/
|
|
setKeyboardForControl(Pelem: HTMLElement, Pkbd?: string, Plc?: string) {
|
|
/* pass null for kbd to specify no default, or '' to specify the default system keyboard. */
|
|
if(Pkbd !== null && Pkbd !== undefined) {
|
|
var index = Pkbd.indexOf("Keyboard_");
|
|
if(index < 0 && Pkbd != '') {
|
|
Pkbd = "Keyboard_" + Pkbd;
|
|
}
|
|
} else {
|
|
Plc = null;
|
|
}
|
|
|
|
if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) {
|
|
console.warn("'keymanweb.setKeyboardForControl' cannot set keyboard on iframes.");
|
|
return;
|
|
}
|
|
|
|
if(!this.isAttached(Pelem)) {
|
|
console.error("KeymanWeb is not attached to element " + Pelem);
|
|
return;
|
|
} else {
|
|
Pelem._kmwAttachment.keyboard = Pkbd;
|
|
Pelem._kmwAttachment.languageCode = Plc;
|
|
|
|
// If Pelem is the focused element/active control, we should set the keyboard in place now.
|
|
// 'kmw_ip' is the touch-alias for the original page's control.
|
|
|
|
var lastElem = this.lastActiveElement;
|
|
if(lastElem && (lastElem == Pelem || lastElem == Pelem['kmw_ip'])) {
|
|
|
|
if(Pkbd != null && Plc != null) { // Second part necessary for Closure.
|
|
this.keyman.keyboardManager.setActiveKeyboard(Pkbd, Plc);
|
|
} else {
|
|
this.keyman.keyboardManager.setActiveKeyboard(this.keyman.globalKeyboard, this.keyman.globalLanguageCode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function getKeyboardForControl
|
|
* Scope Public
|
|
* @param {Element} Pelem Control element
|
|
* @return {string|null} The independently-managed keyboard for the control.
|
|
* Description Returns the keyboard ID of the current independently-managed keyboard for this control.
|
|
* If it is currently following the global keyboard setting, returns null instead.
|
|
*/
|
|
getKeyboardForControl(Pelem: HTMLElement): string {
|
|
if(!this.isAttached(Pelem)) {
|
|
console.error("KeymanWeb is not attached to element " + Pelem);
|
|
return null;
|
|
} else {
|
|
return Pelem._kmwAttachment.keyboard;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function getLanguageForControl
|
|
* Scope Public
|
|
* @param {Element} Pelem Control element
|
|
* @return {string|null} The independently-managed keyboard for the control.
|
|
* Description Returns the language code used with the current independently-managed keyboard for this control.
|
|
* If it is currently following the global keyboard setting, returns null instead.
|
|
*/
|
|
getLanguageForControl(Pelem: HTMLElement): string {
|
|
if(!this.isAttached(Pelem)) {
|
|
console.error("KeymanWeb is not attached to element " + Pelem);
|
|
return null;
|
|
} else {
|
|
return Pelem._kmwAttachment.languageCode; // Should we have a version for the language code, too?
|
|
}
|
|
}
|
|
|
|
/* ------ End independent, per-control keyboard setting behavior definitions. ------ */
|
|
|
|
/**
|
|
* Set focus to last active target element (browser-dependent)
|
|
*/
|
|
focusLastActiveElement() {
|
|
var lastElem = this.lastActiveElement;
|
|
if(!lastElem) {
|
|
return;
|
|
}
|
|
|
|
this.keyman.uiManager.justActivated = true;
|
|
|
|
const target = Utils.getOutputTarget(lastElem);
|
|
target.focus();
|
|
}
|
|
|
|
/**
|
|
* Get the last active target element *before* KMW activated (I1297)
|
|
*
|
|
* @return {Element}
|
|
*/
|
|
get lastActiveElement(): HTMLElement {
|
|
return DOMEventHandlers.states._lastActiveElement;
|
|
}
|
|
|
|
set lastActiveElement(Pelem: HTMLElement) {
|
|
DOMEventHandlers.states._lastActiveElement = Pelem;
|
|
|
|
const osk = this.keyman.osk;
|
|
if(osk) {
|
|
if(this.lastActiveElement == null && this.activeElement == null) {
|
|
// Assigning to the property does have side-effects.
|
|
// If the property is already unset, it's best to not unset it again.
|
|
osk.activeTarget = null;
|
|
this.keyman.osk.hideNow(); // originally from a different one, seemed to serve the same role?
|
|
}
|
|
}
|
|
}
|
|
|
|
get activeElement(): HTMLElement {
|
|
return DOMEventHandlers.states._activeElement;
|
|
}
|
|
|
|
set activeElement(Pelem: HTMLElement) {
|
|
// Ensure that a TouchAliasElement is hidden whenever it is deactivated for input.
|
|
if(this.activeElement) {
|
|
if(Utils.instanceof(this.keyman.domManager.activeElement, "TouchAliasElement")) {
|
|
(this.keyman.domManager.activeElement as TouchAliasElement).hideCaret();
|
|
}
|
|
}
|
|
|
|
DOMEventHandlers.states._activeElement = Pelem;
|
|
|
|
var isActivating = this.keyman.uiManager.isActivating;
|
|
|
|
// Hide the OSK when the control is blurred, unless the UI is being temporarily selected
|
|
const osk = this.keyman.osk;
|
|
// const device = this.keyman.util.device;
|
|
|
|
if(osk) {
|
|
const target = Pelem?._kmwAttachment?.interface || null;
|
|
if(osk && (target || !isActivating)) {
|
|
// Do not unset the field if the UI is activated.
|
|
osk.activeTarget = target;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the active input element directly optionally setting focus
|
|
*
|
|
* @param {Object|string} e element id or element
|
|
* @param {boolean=} setFocus optionally set focus (KMEW-123)
|
|
*/
|
|
setActiveElement(e: string|HTMLElement, setFocus?: boolean) {
|
|
if(typeof e == "string") { // Can't instanceof string, and String is a different type.
|
|
e = document.getElementById(e);
|
|
}
|
|
|
|
if(this.keyman.isEmbedded) {
|
|
// If we're in embedded mode, auto-attach to the element specified by the page.
|
|
if(!this.isAttached(e)) {
|
|
this.attachToControl(e);
|
|
}
|
|
// Non-attached elements cannot be set as active.
|
|
} else if(!this.isAttached(e)) {
|
|
console.warn("Cannot set an element KMW is not attached to as the active element.");
|
|
return;
|
|
}
|
|
|
|
// As this is an API function, someone may pass in the base of a touch element.
|
|
// We need to respond appropriately.
|
|
e = (e['kmw_ip'] ? e['kmw_ip'] : e) as HTMLElement;
|
|
|
|
// If we're changing controls, don't forget to properly manage the keyboard settings!
|
|
// It's only an issue on 'native' (non-embedded) code paths.
|
|
if(!this.keyman.isEmbedded) {
|
|
this.keyman.touchAliasing._BlurKeyboardSettings(this.keyman.domManager.lastActiveElement);
|
|
}
|
|
|
|
// No need to reset context if we stay within the same element.
|
|
if(this.activeElement != e) {
|
|
this.keyman['resetContext'](e as HTMLElement);
|
|
}
|
|
|
|
this.activeElement = this.lastActiveElement = e;
|
|
if(!this.keyman.isEmbedded) {
|
|
this.keyman.touchAliasing._FocusKeyboardSettings(e, false);
|
|
}
|
|
|
|
// Allow external focusing KMEW-123
|
|
if(arguments.length > 1 && setFocus) {
|
|
if(this.keyman.util.device.touchable) {
|
|
var tEvent = {
|
|
clientX: 0,
|
|
clientY: 0,
|
|
target: e as HTMLElement
|
|
};
|
|
|
|
// Kinda hacky, but gets the job done.
|
|
(this.keyman.touchAliasing as DOMTouchHandlers).setFocusWithTouch(tEvent);
|
|
} else {
|
|
this.focusLastActiveElement();
|
|
}
|
|
}
|
|
|
|
// Let the keyboard do its initial group processing
|
|
//console.log('processNewContextEvent [not] called from setActiveElement');
|
|
com.keyman.singleton.core.processNewContextEvent(dom.Utils.getOutputTarget(e));
|
|
}
|
|
|
|
/** Sets the active input element only if it is presently null.
|
|
*
|
|
* @param {Element}
|
|
*/
|
|
initActiveElement(Lelem: HTMLElement) {
|
|
if(this.activeElement == null) {
|
|
this.activeElement = Lelem;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move focus to next (or previous) input or text area element on TAB
|
|
* Uses list of actual input elements
|
|
*
|
|
* Note that activeElement() on touch devices returns the DIV that overlays
|
|
* the input element, not the element itself.
|
|
*
|
|
* @param {number|boolean} bBack Direction to move (0 or 1)
|
|
*/
|
|
moveToNext(bBack: number|boolean) {
|
|
var i,t=this.sortedInputs, activeBase = this.activeElement;
|
|
var touchable = this.keyman.util.device.touchable;
|
|
|
|
if(t.length == 0) {
|
|
return;
|
|
}
|
|
|
|
// For touchable devices, get the base element of the DIV
|
|
if(touchable) {
|
|
activeBase=activeBase.base;
|
|
}
|
|
|
|
// Identify the active element in the list of inputs ordered by position
|
|
for(i=0; i<t.length; i++) {
|
|
if(t[i] == activeBase) break;
|
|
}
|
|
|
|
// Find the next (or previous) element in the list
|
|
i = bBack ? i-1 : i+1;
|
|
// Treat the list as circular, wrapping the index if necessary.
|
|
i = i >= t.length ? i-t.length : i;
|
|
i = i < 0 ? i+t.length : i;
|
|
|
|
// Move to the selected element
|
|
if(touchable) {
|
|
// Set focusing flag to prevent OSK disappearing
|
|
DOMEventHandlers.states.focusing=true;
|
|
var target=t[i]['kmw_ip'];
|
|
|
|
// Focus if next element is non-mapped
|
|
if(typeof(target) == 'undefined') {
|
|
t[i].focus();
|
|
} else { // Or reposition the caret on the input DIV if mapped
|
|
let alias = <dom.TouchAliasElement> target;
|
|
this.keyman.domManager.setActiveElement(target); // Handles both `lastActive` + `active`.
|
|
alias.setTextCaret(10000); // Safe b/c touchable == true.
|
|
alias.scrollInput(); // mousedown check
|
|
target.focus();
|
|
}
|
|
} else { // Behaviour for desktop browsers
|
|
t[i].focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move focus to user-specified element
|
|
*
|
|
* @param {string|Object} e element or element id
|
|
*
|
|
*/
|
|
moveToElement(e:string|HTMLElement) {
|
|
var i;
|
|
|
|
if(typeof(e) == "string") { // Can't instanceof string, and String is a different type.
|
|
e=document.getElementById(e);
|
|
}
|
|
|
|
if(this.keyman.util.device.touchable && e['kmw_ip']) {
|
|
e['kmw_ip'].focus();
|
|
} else {
|
|
e.focus();
|
|
}
|
|
}
|
|
|
|
/* ----------------------- Editable IFrame methods ------------------- */
|
|
|
|
/**
|
|
* Function _IsIEEditableIframe
|
|
* Scope Private
|
|
* @param {Object} Pelem Iframe element
|
|
* {boolean|number} PtestOn 1 to test if frame content is editable (TODO: unclear exactly what this is doing!)
|
|
* @return {boolean}
|
|
* Description Test if element is an IE editable IFrame
|
|
*/
|
|
_IsIEEditableIframe(Pelem: HTMLIFrameElement, PtestOn?: number) {
|
|
var Ldv, Lvalid = Pelem && (Ldv=Pelem.tagName) && Ldv.toLowerCase() == 'body' && (Ldv=Pelem.ownerDocument) && Ldv.parentWindow;
|
|
return (!PtestOn && Lvalid) || (PtestOn && (!Lvalid || Pelem.isContentEditable));
|
|
}
|
|
|
|
/**
|
|
* Function _IsMozillaEditableIframe
|
|
* Scope Private
|
|
* @param {Object} Pelem Iframe element
|
|
* @param {boolean|number} PtestOn 1 to test if 'designMode' is 'ON'
|
|
* @return {boolean}
|
|
* Description Test if element is a Mozilla editable IFrame
|
|
*/
|
|
_IsMozillaEditableIframe(Pelem: HTMLIFrameElement, PtestOn?: number) {
|
|
var Ldv, Lvalid = Pelem && (Ldv=(<any>Pelem).defaultView) && Ldv.frameElement; // Probable bug!
|
|
return (!PtestOn && Lvalid) || (PtestOn && (!Lvalid || Ldv.document.designMode.toLowerCase()=='on'));
|
|
}
|
|
|
|
/* ----------------------- Initialization methods ------------------ */
|
|
|
|
/**
|
|
* Get the user-specified (or default) font for the first mapped input or textarea element
|
|
* before applying any keymanweb styles or classes
|
|
*
|
|
* @return {string}
|
|
*/
|
|
getBaseFont() {
|
|
var util = this.keyman.util;
|
|
var ipInput = document.getElementsByTagName<'input'>('input'),
|
|
ipTextArea=document.getElementsByTagName<'textarea'>('textarea'),
|
|
n=0,fs,fsDefault='Arial,sans-serif';
|
|
|
|
// Find the first input element (if it exists)
|
|
if(ipInput.length == 0 && ipTextArea.length == 0) {
|
|
n=0;
|
|
} else if(ipInput.length > 0 && ipTextArea.length == 0) {
|
|
n=1;
|
|
} else if(ipInput.length == 0 && ipTextArea.length > 0) {
|
|
n=2;
|
|
} else {
|
|
var firstInput = ipInput[0];
|
|
var firstTextArea = ipTextArea[0];
|
|
|
|
if(firstInput.offsetTop < firstTextArea.offsetTop) {
|
|
n=1;
|
|
} else if(firstInput.offsetTop > firstTextArea.offsetTop) {
|
|
n=2;
|
|
} else if(firstInput.offsetLeft < firstTextArea.offsetLeft) {
|
|
n=1;
|
|
} else if(firstInput.offsetLeft > firstTextArea.offsetLeft) {
|
|
n=2;
|
|
}
|
|
}
|
|
|
|
// Grab that font!
|
|
switch(n) {
|
|
case 0:
|
|
fs=fsDefault;
|
|
case 1:
|
|
fs=util.getStyleValue(ipInput[0],'font-family');
|
|
case 2:
|
|
fs=util.getStyleValue(ipTextArea[0],'font-family');
|
|
}
|
|
if(typeof(fs) == 'undefined' || fs == 'monospace') {
|
|
fs=fsDefault;
|
|
}
|
|
|
|
return fs;
|
|
}
|
|
|
|
/**
|
|
* Function Initialization
|
|
* Scope Public
|
|
* @param {com.keyman.OptionType} arg object of user-defined properties
|
|
* Description KMW window initialization
|
|
*/
|
|
init: (arg: com.keyman.OptionType) => Promise<any> = function(this: DOMManager, arg): Promise<any> {
|
|
var p,opt,dTrailer,ds;
|
|
var util = this.keyman.util;
|
|
var device = util.device;
|
|
|
|
// Set callbacks for proper feedback from web-core.
|
|
this.keyman.core.keyboardProcessor.beepHandler = this.doBeep.bind(this);
|
|
this.keyman.core.keyboardProcessor.warningLogger = console.warn.bind(console);
|
|
this.keyman.core.keyboardProcessor.errorLogger = console.error.bind(console);
|
|
|
|
// Local function to convert relative to absolute URLs
|
|
// with respect to the source path, server root and protocol
|
|
var fixPath = function(p) {
|
|
if(p.length == 0) return p;
|
|
|
|
// Add delimiter if missing
|
|
if(p.substr(p.length-1,1) != '/') p = p+'/';
|
|
|
|
// Absolute
|
|
if((p.replace(/^(http)s?:.*/,'$1') == 'http')
|
|
|| (p.replace(/^(file):.*/,'$1') == 'file'))
|
|
return p;
|
|
|
|
// Absolute (except for protocol)
|
|
if(p.substr(0,2) == '//')
|
|
return this.keyman.protocol+p;
|
|
|
|
// Relative to server root
|
|
if(p.substr(0,1) == '/')
|
|
return this.keyman.rootPath+p.substr(1);
|
|
|
|
// Otherwise, assume relative to source path
|
|
return this.keyman.srcPath+p;
|
|
}.bind(this);
|
|
|
|
// Explicit (user-defined) parameter initialization
|
|
opt=this.keyman.options;
|
|
if(typeof(arg) == 'object' && arg !== null)
|
|
{
|
|
for(p in opt)
|
|
{
|
|
if(arg.hasOwnProperty(p)) opt[p] = arg[p];
|
|
}
|
|
}
|
|
|
|
// Get default paths and device options
|
|
if(opt['root'] != '') {
|
|
this.keyman.rootPath = fixPath(opt['root']);
|
|
}
|
|
|
|
// Keyboards and fonts are located with respect to the server root by default
|
|
//if(opt['keyboards'] == '') opt['keyboards'] = keymanweb.rootPath+'keyboard/';
|
|
//if(opt['fonts'] == '') opt['fonts'] = keymanweb.rootPath+'font/';
|
|
|
|
// Resources are located with respect to the engine by default
|
|
if(opt['resources'] == '') {
|
|
opt['resources'] = this.keyman.srcPath;
|
|
}
|
|
|
|
// Convert resource, keyboard and font paths to absolute URLs
|
|
opt['resources'] = fixPath(opt['resources']);
|
|
opt['keyboards'] = fixPath(opt['keyboards']);
|
|
opt['fonts'] = fixPath(opt['fonts']);
|
|
|
|
// Set default device options
|
|
this.keyman.setDefaultDeviceOptions(opt);
|
|
|
|
// Only do remainder of initialization once!
|
|
if(this.keyman.initialized) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
var keyman: KeymanBase = this.keyman;
|
|
var domManager = this;
|
|
|
|
// Do not initialize until the document has been fully loaded
|
|
if(document.readyState !== 'complete')
|
|
{
|
|
return new Promise<void>(function(resolve) {
|
|
window.setTimeout(function(){
|
|
domManager.init(arg).then(function() {
|
|
resolve();
|
|
});
|
|
}, 50);
|
|
});
|
|
}
|
|
|
|
keyman.modelManager.init();
|
|
this.keyman._MasterDocument = window.document;
|
|
|
|
/*
|
|
* Initialization of touch devices and browser interfaces must be done
|
|
* after all resources are loaded, during final stage of initialization
|
|
*/
|
|
|
|
// Set exposed initialization flag member for UI (and other) code to use
|
|
this.keyman.setInitialized(1);
|
|
|
|
// Finish keymanweb and initialize the OSK once all necessary resources are available
|
|
if(device.touchable) {
|
|
this.keyman.osk = new com.keyman.osk.AnchoredOSKView(device.coreSpec);
|
|
} else {
|
|
this.keyman.osk = new com.keyman.osk.FloatingOSKView(device.coreSpec);
|
|
}
|
|
const osk = this.keyman.osk;
|
|
|
|
// Create and save the remote keyboard loading delay indicator
|
|
util.prepareWait();
|
|
|
|
// Trigger registration of deferred keyboard stubs and keyboards
|
|
this.keyman.keyboardManager.endDeferment();
|
|
|
|
// Initialize the desktop UI
|
|
this.initializeUI();
|
|
|
|
// Exit initialization here if we're using an embedded code path.
|
|
if(this.keyman.isEmbedded) {
|
|
if(!this.keyman.keyboardManager.setDefaultKeyboard()) {
|
|
console.error("No keyboard stubs exist - cannot initialize keyboard!");
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Determine the default font for mapped elements
|
|
this.keyman.appliedFont=this.keyman.baseFont=this.getBaseFont();
|
|
|
|
// Add orientationchange event handler to manage orientation changes on mobile devices
|
|
// Initialize touch-screen device interface I3363 (Build 301)
|
|
if(device.touchable) {
|
|
this.keyman.handleRotationEvents();
|
|
}
|
|
// Initialize browser interface
|
|
|
|
if(this.keyman.options['attachType'] != 'manual') {
|
|
this._SetupDocument(document.documentElement);
|
|
}
|
|
|
|
// Create an ordered list of all input and textarea fields
|
|
this.listInputs();
|
|
|
|
// Initialize the OSK and set default OSK styles
|
|
// Note that this should *never* be called before the OSK has been initialized.
|
|
// However, it possibly may be called before the OSK has been fully defined with the current keyboard, need to check.
|
|
//osk._Load();
|
|
|
|
//document.body.appendChild(osk._Box);
|
|
|
|
//osk._Load(false);
|
|
|
|
// I3363 (Build 301)
|
|
if(device.touchable) {
|
|
const osk = keyman.osk as osk.AnchoredOSKView;
|
|
// Handle OSK touchend events (prevent propagation)
|
|
osk._Box.addEventListener('touchend',function(e){
|
|
e.stopPropagation();
|
|
}, false);
|
|
|
|
// Add a blank DIV to the bottom of the page to allow the bottom of the page to be shown
|
|
dTrailer=document.createElement('DIV');
|
|
ds=dTrailer.style;
|
|
ds.width='100%';
|
|
ds.height=(screen.width/2)+'px';
|
|
document.body.appendChild(dTrailer);
|
|
|
|
// Sets up page-default touch-based handling for activation-state management.
|
|
// These always trigger for the page, wherever a touch may occur. Does not
|
|
// prevent element-specific or OSK-key-specific handling from triggering.
|
|
const _this = this;
|
|
this.touchStartActivationHandler=function(e) {
|
|
_this.deactivateOnRelease=true;
|
|
_this.touchY=e.touches[0].screenY;
|
|
|
|
// On Chrome, scrolling up or down causes the URL bar to be shown or hidden
|
|
// according to whether or not the document is at the top of the screen.
|
|
// But when doing that, each OSK row top and height gets modified by Chrome
|
|
// looking very ugly. It would be best to hide the OSK then show it again
|
|
// when the user scroll finishes, but Chrome has no way to reliably report
|
|
// the touch end event after a move. c.f. http://code.google.com/p/chromium/issues/detail?id=152913
|
|
// The best compromise behaviour is simply to hide the OSK whenever any
|
|
// non-input and non-OSK element is touched.
|
|
_this.deactivateOnScroll=false;
|
|
if(device.OS == 'Android' && navigator.userAgent.indexOf('Chrome') > 0) {
|
|
// _this.deactivateOnScroll has the inverse of the 'true' default,
|
|
// but that fact actually facilitates the following conditional logic.
|
|
if(typeof(osk._Box) == 'undefined') return false;
|
|
if(typeof(osk._Box.style) == 'undefined') return false;
|
|
|
|
// The following tests are needed to prevent the OSK from being hidden during normal input!
|
|
let p=(e.target as HTMLElement).parentElement;
|
|
if(typeof(p) != 'undefined' && p != null) {
|
|
if(p.className.indexOf('keymanweb-input') >= 0) return false;
|
|
if(p.className.indexOf('kmw-key-') >= 0) return false;
|
|
if(typeof(p.parentElement) != 'undefined' && p.parentElement != null) {
|
|
p=p.parentElement;
|
|
if(p.className.indexOf('keymanweb-input') >= 0) return false;
|
|
if(p.className.indexOf('kmw-key-') >= 0) return false;
|
|
}
|
|
}
|
|
|
|
_this.deactivateOnScroll = true;
|
|
}
|
|
return false;
|
|
};
|
|
this.touchMoveActivationHandler = function(e) {
|
|
if(_this.deactivateOnScroll) { // Android / Chrone case.
|
|
DOMEventHandlers.states.focusing = false;
|
|
_this.activeElement = null;
|
|
}
|
|
|
|
const y = e.touches[0].screenY;
|
|
const y0 = _this.touchY;
|
|
if(y-y0 > 5 || y0-y < 5) {
|
|
_this.deactivateOnRelease = false;
|
|
}
|
|
return false;
|
|
};
|
|
this.touchEndActivationHandler = function() {
|
|
// Should not hide OSK if simply closing the language menu (30/4/15)
|
|
// or if the focusing timer (setFocusTimer) is still active.
|
|
if(_this.deactivateOnRelease && !osk['lgList'] && !DOMEventHandlers.states.focusing) {
|
|
_this.activeElement = null;
|
|
}
|
|
_this.deactivateOnRelease=false;
|
|
return false;
|
|
};
|
|
|
|
this.keyman.util.attachDOMEvent(document.body, 'touchstart', this.touchStartActivationHandler,false);
|
|
this.keyman.util.attachDOMEvent(document.body, 'touchmove', this.touchMoveActivationHandler, false);
|
|
this.keyman.util.attachDOMEvent(document.body, 'touchend', this.touchEndActivationHandler, false);
|
|
}
|
|
|
|
//document.body.appendChild(keymanweb._StyleBlock);
|
|
|
|
// Restore and reload the currently selected keyboard, selecting a default keyboard if necessary.
|
|
this.keyman.keyboardManager.restoreCurrentKeyboard();
|
|
|
|
/* Setup of handlers for dynamically-added and (eventually) dynamically-removed elements.
|
|
* Reference: https://developer.mozilla.org/en/docs/Web/API/MutationObserver
|
|
*
|
|
* We place it here so that it loads after most of the other UI loads, reducing the MutationObserver's overhead.
|
|
* Of course, we only want to dynamically add elements if the user hasn't enabled the manual attachment option.
|
|
*/
|
|
|
|
if(typeof MutationObserver == 'function') {
|
|
var observationTarget = document.querySelector('body'), observationConfig: MutationObserverInit;
|
|
if(this.keyman.options['attachType'] != 'manual') { //I1961
|
|
observationConfig = { childList: true, subtree: true};
|
|
this.attachmentObserver = new MutationObserver(this._AutoAttachObserverCore);
|
|
this.attachmentObserver.observe(observationTarget, observationConfig);
|
|
}
|
|
|
|
/*
|
|
* Setup of handlers for dynamic detection of the kmw-disabled class tag that controls enablement.
|
|
*/
|
|
observationConfig = { subtree: true, attributes: true, attributeOldValue: true, attributeFilter: ['class', 'readonly']};
|
|
this.enablementObserver = new MutationObserver(this._EnablementMutationObserverCore);
|
|
this.enablementObserver.observe(observationTarget, observationConfig);
|
|
} else {
|
|
console.warn("Your browser is outdated and does not support MutationObservers, a web feature " +
|
|
"needed by KeymanWeb to support dynamically-added elements.");
|
|
}
|
|
|
|
// Set exposed initialization flag to 2 to indicate deferred initialization also complete
|
|
|
|
/* To prevent propagation of focus & blur events from the input-scroll workaround,
|
|
* we attach top-level capturing listeners to the focus & blur events. They prevent propagation
|
|
* but NOT default behavior, allowing the scroll to complete while preventing nearly all
|
|
* possible event 'noise' that could result from the workaround.
|
|
*/
|
|
this.keyman.util.attachDOMEvent(document.body, 'focus', DOMManager.suppressFocusCheck, true);
|
|
this.keyman.util.attachDOMEvent(document.body, 'blur', DOMManager.suppressFocusCheck, true);
|
|
|
|
this.keyman.setInitialized(2);
|
|
return Promise.resolve();
|
|
}.bind(this);
|
|
|
|
/**
|
|
* Initialize the desktop user interface as soon as it is ready
|
|
*/
|
|
initializeUI() {
|
|
if(this.keyman.ui && this.keyman.ui['initialize'] instanceof Function) {
|
|
this.keyman.ui['initialize']();
|
|
// Display the OSK (again) if enabled, in order to set its position correctly after
|
|
// adding the UI to the page
|
|
this.keyman.osk.present();
|
|
} else if(this.keyman.isEmbedded) {
|
|
// UI modules aren't utilized in embedded mode. There's nothing to init, so we simply
|
|
// return instead of waiting for a UI module that will never come.
|
|
return;
|
|
} else {
|
|
window.setTimeout(this.initializeUI.bind(this),1000);
|
|
}
|
|
}
|
|
|
|
static suppressFocusCheck(e: Event) {
|
|
if(DOMEventHandlers.states._IgnoreBlurFocus) {
|
|
// Prevent triggering other blur-handling events (as possible)
|
|
e.stopPropagation();
|
|
e.cancelBubble = true;
|
|
}
|
|
// But DO perform default event behavior (actually blurring & focusing the affected element)
|
|
return true;
|
|
}
|
|
}
|
|
} |