spiegel-keyman/web/source/dom/domManager.ts
Marc Durdin 0c0bb420b5
Merge pull request #5963 from keymanapp/feat/web/3621-newcontext-group
feat(web): Start of Sentence support - part 1 🕊
2022-01-17 12:04:29 +11:00

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;
}
}
}