From 482e743313f3e11452079d9fb519336fd1315ddb Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 4 Oct 2022 11:01:13 +0700 Subject: [PATCH 1/6] feat(web): adds mutationobserver to detect inputmode change, stores setting --- web/source/dom/domManager.ts | 57 +++++++++++++++++++++++++++++++++++- web/source/kmwtypedefs.ts | 7 ++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index accd402c80..46a621739d 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -55,6 +55,11 @@ namespace com.keyman.dom { */ enablementObserver: MutationObserver; + /** + * Tracks changes in inputmode state. + */ + inputModeObserver: MutationObserver; + /** * Tracks a list of event-listening elements. * @@ -99,6 +104,9 @@ namespace com.keyman.dom { if(this.attachmentObserver) { this.attachmentObserver.disconnect(); } + if(this.inputModeObserver) { + this.inputModeObserver.disconnect(); + } for(let input of this.inputList) { this.disableInputElement(input); @@ -233,6 +241,7 @@ namespace com.keyman.dom { if(!this.isAttached(Pelem)) { this.setupElementAttachment(Pelem); + Pelem._kmwAttachment.inputMode = Pelem.inputMode ?? 'text'; Pelem.inputMode = 'none'; } @@ -264,7 +273,13 @@ namespace com.keyman.dom { */ disableTouchElement(Pelem: HTMLElement) { // Do not check for the element being officially disabled - it's also used for detachment. - Pelem.inputMode = 'text'; + const intendedInputMode = Pelem._kmwAttachment.inputMode; + + this.disableInputModeObserver(); + Pelem.inputMode = intendedInputMode; + this.enableInputModeObserver(); + + this.setupNonKMWTouchElement(Pelem); } /** @@ -907,6 +922,30 @@ namespace com.keyman.dom { } }.bind(this); + _InputModeObserverCore = function(this: DOMManager, mutations: MutationRecord[]) { + const keyman = com.keyman.singleton; + // Prevent infinite recursion from any changes / updates made within the observation handler. + this.disableInputModeObserver(); + try { + for(const mutation of mutations) { + const target = mutation.target as HTMLElement; + if(!(this as DOMManager).isAttached(target)) { + continue; + } + + const newValue = target.inputMode; + + target._kmwAttachment.inputMode = newValue; + + if(keyman.util.device.touchable) { + target.inputMode = 'none'; + } + } + } finally { + this.enableInputModeObserver(); + } + }.bind(this); + /** * Function _MutationAdditionObserved * Scope Private @@ -1611,6 +1650,12 @@ namespace com.keyman.dom { observationConfig = { subtree: true, attributes: true, attributeOldValue: true, attributeFilter: ['class', 'readonly']}; this.enablementObserver = new MutationObserver(this._EnablementMutationObserverCore); this.enablementObserver.observe(observationTarget, observationConfig); + + /** + * Setup of handlers for dynamic detection of change in inputMode state. + */ + this.inputModeObserver = new MutationObserver(this._InputModeObserverCore); + this.enableInputModeObserver(); } else { console.warn("Your browser is outdated and does not support MutationObservers, a web feature " + "needed by KeymanWeb to support dynamically-added elements."); @@ -1630,6 +1675,16 @@ namespace com.keyman.dom { return Promise.resolve(); }.bind(this); + enableInputModeObserver() { + const observationTarget = document.querySelector('body'); + const observationConfig = { subtree: true, attributes: true, attributeFilter: ['inputmode']}; + this.inputModeObserver.observe(observationTarget, observationConfig); + } + + disableInputModeObserver() { + this.inputModeObserver.disconnect(); + } + /** * Initialize the desktop user interface as soon as it is ready */ diff --git a/web/source/kmwtypedefs.ts b/web/source/kmwtypedefs.ts index f819e490c2..09aa23bca3 100644 --- a/web/source/kmwtypedefs.ts +++ b/web/source/kmwtypedefs.ts @@ -17,12 +17,17 @@ namespace com.keyman { /** * Tracks if the control has an aliased control for touch functionality. - * + * * Future note - could be changed to track the DOMEventHandler instance used by this control; * this may be useful for an eventual hybrid touch/non-touch implementation. */ touchEnabled: boolean; + /** + * Tracks the inputmode originally set by the webpage. + */ + inputMode?: string; + constructor(eleInterface: dom.targets.OutputTarget, kbd: string, touch?: boolean) { this.interface = eleInterface; this.keyboard = kbd; From 089077d86dfe8f6ad555d24f76dab10e708f3dfc Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 4 Oct 2022 12:52:00 +0700 Subject: [PATCH 2/6] chore(web): minor cleanup --- web/source/dom/domManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index 46a621739d..bbf130907f 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -929,7 +929,7 @@ namespace com.keyman.dom { try { for(const mutation of mutations) { const target = mutation.target as HTMLElement; - if(!(this as DOMManager).isAttached(target)) { + if(!this.isAttached(target)) { continue; } From ce6c05a2fba84a2b53a05d06218176355546b09f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 10 Oct 2022 09:54:48 +0700 Subject: [PATCH 3/6] fix(web): inputMode masking on control enablement, not just attachment --- web/source/dom/domManager.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index bbf130907f..c4c8ef3b61 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -241,8 +241,6 @@ namespace com.keyman.dom { if(!this.isAttached(Pelem)) { this.setupElementAttachment(Pelem); - Pelem._kmwAttachment.inputMode = Pelem.inputMode ?? 'text'; - Pelem.inputMode = 'none'; } // Set font for base element @@ -322,6 +320,11 @@ namespace com.keyman.dom { var baseElement = isAlias ? Pelem['base'] : Pelem; if(!this.isKMWDisabled(baseElement)) { + this.disableInputModeObserver(); + Pelem._kmwAttachment.inputMode = Pelem.inputMode ?? 'text'; + Pelem.inputMode = 'none'; + this.enableInputModeObserver(); + if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) { this._AttachToIframe(Pelem); } else { From 773a619ccf417c8c359fa272a48c8f582a6d9ee3 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 10 Oct 2022 14:15:54 +0700 Subject: [PATCH 4/6] chore(web): null catches on observer toggle methods --- web/source/dom/domManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index c4c8ef3b61..c99c75bd21 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -1681,11 +1681,11 @@ namespace com.keyman.dom { enableInputModeObserver() { const observationTarget = document.querySelector('body'); const observationConfig = { subtree: true, attributes: true, attributeFilter: ['inputmode']}; - this.inputModeObserver.observe(observationTarget, observationConfig); + this.inputModeObserver?.observe(observationTarget, observationConfig); } disableInputModeObserver() { - this.inputModeObserver.disconnect(); + this.inputModeObserver?.disconnect(); } /** From f80fec1280650dd792739aee5ea221a055bf4b5c Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 14 Oct 2022 10:58:30 +0700 Subject: [PATCH 5/6] fix(web): must attach before saving inputmode --- web/source/dom/domManager.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index c99c75bd21..424f275f2f 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -317,27 +317,24 @@ namespace com.keyman.dom { * 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(!this.isKMWDisabled(Pelem)) { this.disableInputModeObserver(); - Pelem._kmwAttachment.inputMode = Pelem.inputMode ?? 'text'; - Pelem.inputMode = 'none'; this.enableInputModeObserver(); if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) { this._AttachToIframe(Pelem); } else { - if(!isAlias) { - this.setupElementAttachment(Pelem); - } + this.setupElementAttachment(Pelem); - baseElement.className = baseElement.className ? baseElement.className + ' keymanweb-font' : 'keymanweb-font'; + Pelem._kmwAttachment.inputMode = Pelem.inputMode ?? 'text'; // This spot. + Pelem.inputMode = 'none'; + + Pelem.className = Pelem.className ? Pelem.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); + this.keyman.util.attachDOMEvent(Pelem,'focus', this.getHandlers(Pelem)._ControlFocus); + this.keyman.util.attachDOMEvent(Pelem,'blur', this.getHandlers(Pelem)._ControlBlur); + this.keyman.util.attachDOMEvent(Pelem,'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; From 15e4e3745c386fe4aada5a8e8709b32e32e89d1b Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 17 Oct 2022 08:28:46 +0700 Subject: [PATCH 6/6] chore(web): PR cleanup per review --- web/source/dom/domManager.ts | 38 ++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index 424f275f2f..6d5946edff 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -244,7 +244,7 @@ namespace com.keyman.dom { } // Set font for base element - this.enableInputElement(Pelem, false); + this.enableInputElement(Pelem); // Superimpose custom input fields for each input or textarea, unless readonly or disabled @@ -271,11 +271,13 @@ namespace com.keyman.dom { */ disableTouchElement(Pelem: HTMLElement) { // Do not check for the element being officially disabled - it's also used for detachment. - const intendedInputMode = Pelem._kmwAttachment.inputMode; + if(this.isAttached(Pelem)) { + const intendedInputMode = Pelem._kmwAttachment.inputMode; - this.disableInputModeObserver(); - Pelem.inputMode = intendedInputMode; - this.enableInputModeObserver(); + this.disableInputModeObserver(); + Pelem.inputMode = intendedInputMode; + this.enableInputModeObserver(); + } this.setupNonKMWTouchElement(Pelem); } @@ -316,20 +318,19 @@ namespace com.keyman.dom { * 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) { + enableInputElement(Pelem: HTMLElement) { if(!this.isKMWDisabled(Pelem)) { - this.disableInputModeObserver(); - this.enableInputModeObserver(); - if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) { this._AttachToIframe(Pelem); } else { this.setupElementAttachment(Pelem); - Pelem._kmwAttachment.inputMode = Pelem.inputMode ?? 'text'; // This spot. + Pelem._kmwAttachment.inputMode = Pelem.inputMode ?? 'text'; + this.disableInputModeObserver(); Pelem.inputMode = 'none'; + this.enableInputModeObserver(); - Pelem.className = Pelem.className ? Pelem.className + ' keymanweb-font' : 'keymanweb-font'; + Pelem.classList.add('keymanweb-font'); this.inputList.push(Pelem); this.keyman.util.attachDOMEvent(Pelem,'focus', this.getHandlers(Pelem)._ControlFocus); @@ -922,6 +923,15 @@ namespace com.keyman.dom { } }.bind(this); + /** + * The core method for a MutationObserver that checks for changes to the `.inputMode` property + * of controls that KMW is attached to in touch mode. + * + * In touch mode, KMW requires that their `.inputMode` property be set to 'none' in order + * to hide the device's default OSK. That said, we should still aim to honor the setting + * and restore it if and when detachment occurs. Should we ever support intents, we'll want + * to utilize the incoming value for use with that feature too. + */ _InputModeObserverCore = function(this: DOMManager, mutations: MutationRecord[]) { const keyman = com.keyman.singleton; // Prevent infinite recursion from any changes / updates made within the observation handler. @@ -933,9 +943,7 @@ namespace com.keyman.dom { continue; } - const newValue = target.inputMode; - - target._kmwAttachment.inputMode = newValue; + target._kmwAttachment.inputMode = target.inputMode; if(keyman.util.device.touchable) { target.inputMode = 'none'; @@ -1677,7 +1685,7 @@ namespace com.keyman.dom { enableInputModeObserver() { const observationTarget = document.querySelector('body'); - const observationConfig = { subtree: true, attributes: true, attributeFilter: ['inputmode']}; + const observationConfig = { subtree: true, attributes: true, attributeFilter: ['inputmode'] }; this.inputModeObserver?.observe(observationTarget, observationConfig); }