// 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.addEventListener('touchend', touchHandlers.dragEnd, false);

      // Disable internal scroll when input element in focus
      x.addEventListener('touchmove', touchHandlers.dragInput, false);

      // 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 = {
            pageX: 0,
            pageY: 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     _IsEditableIframe
     * 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
     */
    _IsEditableIframe(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) {
        this.keyman.keyboardManager.setDefaultKeyboard();
        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;
    }
  }
}