From 478b8ff294f9e9edd300485efc6f5bc1db78c626 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 24 Apr 2023 10:50:09 +0700 Subject: [PATCH 1/8] chore(web): _ControlFocus, _ControlBlur, remaining focus-management flag modularization --- web/src/app/browser/src/contextManager.ts | 61 +++++++++++++++++------ 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index ec192938a3..fc1f49cc26 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -16,6 +16,42 @@ interface KeyboardCookie { current: string; } +/** + * Given a DOM event related to an KMW-attached element, this function determines + * the corresponding OutputTarget. + * @param e + * @returns + */ +function eventOutputTarget(e: Event) { + // Step 1: given the event target... + let Ltarg: HTMLElement = e?.target as HTMLElement; + if (Ltarg == null) { + return null; + } + // ... determine the element expected to hold the KMW attachment object based on + // its typing, properties, etc. + + // if(Ltarg['body']) { + // Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes. + // } + + if (Ltarg.nodeType == 3) { // defeat Safari bug + Ltarg = Ltarg.parentNode as HTMLElement; + } + + // Verify that the element does correspond to a remappable input field + if(nestedInstanceOf(Ltarg, "HTMLInputElement")) { + const et=(Ltarg as HTMLInputElement).type.toLowerCase(); + if(!(et == 'text' || et == 'search')) { + return null; + } + } + + // Step 2: With the most likely host element determined, obtain the corresponding OutputTarget + // instance. + return Ltarg._kmwAttachment.interface; +} + /** * Set target element text direction (LTR or RTL), but only if the element is empty * @@ -485,6 +521,8 @@ export default class ContextManager extends ContextManagerBase Date: Mon, 24 Apr 2023 12:47:08 +0700 Subject: [PATCH 2/8] chore(web): modularization of blur, focus, and text-change events --- web/src/app/browser/src/contextManager.ts | 68 ++++++++--------------- 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index fc1f49cc26..65c7300e2f 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -16,42 +16,6 @@ interface KeyboardCookie { current: string; } -/** - * Given a DOM event related to an KMW-attached element, this function determines - * the corresponding OutputTarget. - * @param e - * @returns - */ -function eventOutputTarget(e: Event) { - // Step 1: given the event target... - let Ltarg: HTMLElement = e?.target as HTMLElement; - if (Ltarg == null) { - return null; - } - // ... determine the element expected to hold the KMW attachment object based on - // its typing, properties, etc. - - // if(Ltarg['body']) { - // Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes. - // } - - if (Ltarg.nodeType == 3) { // defeat Safari bug - Ltarg = Ltarg.parentNode as HTMLElement; - } - - // Verify that the element does correspond to a remappable input field - if(nestedInstanceOf(Ltarg, "HTMLInputElement")) { - const et=(Ltarg as HTMLInputElement).type.toLowerCase(); - if(!(et == 'text' || et == 'search')) { - return null; - } - } - - // Step 2: With the most likely host element determined, obtain the corresponding OutputTarget - // instance. - return Ltarg._kmwAttachment.interface; -} - /** * Set target element text direction (LTR or RTL), but only if the element is empty * @@ -99,6 +63,13 @@ export default class ContextManager extends ContextManagerBase { + // TODO: set up attachment-listeners here that can add necessary event-hooks + // for focus management here! + + this.page.install(this.engineConfig.attachType == 'manual'); + }); } get apiEvents(): LegacyEventEmitter { @@ -521,8 +492,6 @@ export default class ContextManager extends ContextManagerBase Date: Tue, 25 Apr 2023 15:37:50 +0700 Subject: [PATCH 3/8] chore(web): ContextManager attachment handlers for focus-event hooks --- web/src/app/browser/src/contextManager.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 65c7300e2f..4fd66fc2e8 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -63,13 +63,6 @@ export default class ContextManager extends ContextManagerBase { - // TODO: set up attachment-listeners here that can add necessary event-hooks - // for focus management here! - - this.page.install(this.engineConfig.attachType == 'manual'); - }); } get apiEvents(): LegacyEventEmitter { @@ -88,6 +81,7 @@ export default class ContextManager extends ContextManagerBase { const device = this.engineConfig.hostDevice; + const noPropagation = (event: Event) => event.stopPropagation() // For any elements being attached, or being enabled after having been disabled... From 18e50656a342d9471ec2d47e9273e0e6a58f338f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 26 Apr 2023 12:22:20 +0700 Subject: [PATCH 4/8] chore(web): modularizes base-page suppressFocusCheck --- .../src/context/pageIntegrationHandlers.ts | 48 +++++++++++++++++++ web/src/app/browser/src/contextManager.ts | 1 - web/src/app/browser/src/keymanEngine.ts | 9 ++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 web/src/app/browser/src/context/pageIntegrationHandlers.ts diff --git a/web/src/app/browser/src/context/pageIntegrationHandlers.ts b/web/src/app/browser/src/context/pageIntegrationHandlers.ts new file mode 100644 index 0000000000..a9ab15295c --- /dev/null +++ b/web/src/app/browser/src/context/pageIntegrationHandlers.ts @@ -0,0 +1,48 @@ +import { DomEventTracker } from 'keyman/engine/events'; + +import { KeymanEngine } from "../keymanEngine.js"; + +// Note: in the future, it'd probably be best to have an instance per iframe window as +// well as the top-level window. This was not done in or before KMW 16.0 though, so +// we'll leave that out for now within the initial modular form of app/browser KMW in 17.0. +export class PageIntegrationHandlers { + private readonly window: Window; + private readonly engine: KeymanEngine; + private readonly domEventTracker = new DomEventTracker(); + + constructor(window: Window, engine: KeymanEngine) { + this.window = window; + this.engine = engine; + + this.attachHandlers(); + } + + suppressFocusCheck: (e: Event) => boolean = (e) => { + if(this.engine.contextManager.focusAssistant._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; + } + + private attachHandlers() { + const eventTracker = this.domEventTracker; + /* + * 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. + */ + eventTracker.attachDOMEvent(document.body, 'focus', this.suppressFocusCheck, true); + eventTracker.attachDOMEvent(document.body, 'blur', this.suppressFocusCheck, true); + } + + public shutdown() { + const eventTracker = this.domEventTracker; + + eventTracker.detachDOMEvent(document.body, 'focus', this.suppressFocusCheck, true); + eventTracker.detachDOMEvent(document.body, 'blur', this.suppressFocusCheck, true); + } +} \ No newline at end of file diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 4fd66fc2e8..9f6d2d5789 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -46,7 +46,6 @@ export default class ContextManager extends ContextManagerBase('KeymanWeb_Keyboard'); readonly focusAssistant = new FocusAssistant(); readonly page: PageContextAttachment; - private mostRecentTarget: OutputTarget; private currentTarget: OutputTarget; diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 5da9365dd4..c317f2ba64 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -10,12 +10,14 @@ import ContextManager from './contextManager.js'; import DefaultBrowserRules from './defaultBrowserRules.js'; import KeyEventKeyboard from './keyEventKeyboard.js'; import { FocusStateAPIObject } from './context/focusAssistant.js'; +import { PageIntegrationHandlers } from './context/pageIntegrationHandlers.js'; import { setupOskListeners } from './oskConfiguration.js'; export class KeymanEngine extends KeymanEngineBase { keyEventRefocus = () => { this.contextManager.restoreLastActiveTarget(); } + private pageIntegration: PageIntegrationHandlers; constructor(worker: Worker, sourceUri: string) { const config = new BrowserConfiguration(sourceUri); // currently set to perform device auto-detect. @@ -92,6 +94,7 @@ export class KeymanEngine extends KeymanEngineBase Date: Wed, 26 Apr 2023 12:28:10 +0700 Subject: [PATCH 5/8] chore(web): modularizes base-page touch handlers for focus / scroll interactions --- .../src/context/pageIntegrationHandlers.ts | 123 +++++++++++++++++- .../engine/namespaced-main/dom/domManager.ts | 82 ------------ 2 files changed, 117 insertions(+), 88 deletions(-) diff --git a/web/src/app/browser/src/context/pageIntegrationHandlers.ts b/web/src/app/browser/src/context/pageIntegrationHandlers.ts index a9ab15295c..6da17c5b83 100644 --- a/web/src/app/browser/src/context/pageIntegrationHandlers.ts +++ b/web/src/app/browser/src/context/pageIntegrationHandlers.ts @@ -1,6 +1,7 @@ import { DomEventTracker } from 'keyman/engine/events'; import { KeymanEngine } from "../keymanEngine.js"; +import { FocusAssistant } from './focusAssistant.js'; // Note: in the future, it'd probably be best to have an instance per iframe window as // well as the top-level window. This was not done in or before KMW 16.0 though, so @@ -10,6 +11,25 @@ export class PageIntegrationHandlers { private readonly engine: KeymanEngine; private readonly domEventTracker = new DomEventTracker(); + /** + * Used together with `deactivateOnRelease` to determine the distance of vertical scrolls; + * if sufficiently far at any point, we avoid deactivating the current context when it ends. + */ + private touchY: number; + + /** + * Used together with `touchY` to determine the distance of vertical scrolls; + * if sufficiently far at any point, we avoid deactivating the current context when it ends. + */ + private deactivateOnRelease: boolean; + + /** + * Used on certain browser/OS combinations (e.g. Chrome on Android) to prevent odd behaviors + * that arise when URL bars scroll into view during an ongoing scroll, as this can impede + * proper / smooth positioning of the OSK. (Deactivating the active target also hides the OSK.) + */ + private deactivateOnScroll: boolean; + constructor(window: Window, engine: KeymanEngine) { this.window = window; this.engine = engine; @@ -17,8 +37,12 @@ export class PageIntegrationHandlers { this.attachHandlers(); } - suppressFocusCheck: (e: Event) => boolean = (e) => { - if(this.engine.contextManager.focusAssistant._IgnoreBlurFocus) { + private get focusAssistant(): FocusAssistant { + return this.engine.contextManager.focusAssistant; + } + + private suppressFocusCheck: (e: Event) => boolean = (e) => { + if(this.focusAssistant._IgnoreBlurFocus) { // Prevent triggering other blur-handling events (as possible) e.stopPropagation(); e.cancelBubble = true; @@ -27,22 +51,109 @@ export class PageIntegrationHandlers { return true; } + // 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. + + private touchStartActivationHandler: (e: TouchEvent) => boolean = (e) => { + const osk = this.engine.osk; + if(!osk) { + return false; + } + const device = this.engine.config.hostDevice; + + 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' && device.browser == 'chrome') { + // 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('kmw-key-') >= 0) return false; + if(typeof(p.parentElement) != 'undefined' && p.parentElement != null) { + p=p.parentElement; + if(p.className.indexOf('kmw-key-') >= 0) return false; + } + } + + this.deactivateOnScroll = true; + } + return false; + }; + + private touchMoveActivationHandler: (e: TouchEvent) => boolean = (e) => { + if(this.deactivateOnScroll) { // Android / Chrone case. + this.focusAssistant.focusing = false; + this.engine.contextManager.deactivateCurrentTarget(); + } + + const y = e.touches[0].screenY; + const y0 = this.touchY; + if(y-y0 > 5 || y0-y < 5) { + this.deactivateOnRelease = false; + } + return false; + }; + + private touchEndActivationHandler: (e: TouchEvent) => boolean = (e) => { + // Should not hide OSK if simply closing the language menu (30/4/15) + // or if the focusing timer (focusAssistant.setFocusTimer) is still active. + if(this.deactivateOnRelease && !osk['lgList'] && !this.focusAssistant.focusing) { + this.engine.contextManager.deactivateCurrentTarget(); + } + this.deactivateOnRelease=false; + return false; + }; + private attachHandlers() { const eventTracker = this.domEventTracker; + const device = this.engine.config.hostDevice; + const docBody = this.window.document.body; + /* * 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. */ - eventTracker.attachDOMEvent(document.body, 'focus', this.suppressFocusCheck, true); - eventTracker.attachDOMEvent(document.body, 'blur', this.suppressFocusCheck, true); + eventTracker.attachDOMEvent(docBody, 'focus', this.suppressFocusCheck, true); + eventTracker.attachDOMEvent(docBody, 'blur', this.suppressFocusCheck, true); + + if(device.touchable) { + eventTracker.attachDOMEvent(docBody, 'touchstart', this.touchStartActivationHandler,false); + eventTracker.attachDOMEvent(docBody, 'touchmove', this.touchMoveActivationHandler, false); + eventTracker.attachDOMEvent(docBody, 'touchend', this.touchEndActivationHandler, false); + } } public shutdown() { const eventTracker = this.domEventTracker; + const device = this.engine.config.hostDevice; + const docBody = this.window.document.body; - eventTracker.detachDOMEvent(document.body, 'focus', this.suppressFocusCheck, true); - eventTracker.detachDOMEvent(document.body, 'blur', this.suppressFocusCheck, true); + // See `attachHandlers` for the purpose behind all handlers listed here. + + eventTracker.detachDOMEvent(docBody, 'focus', this.suppressFocusCheck, true); + eventTracker.detachDOMEvent(docBody, 'blur', this.suppressFocusCheck, true); + + if(device.touchable) { + eventTracker.detachDOMEvent(docBody, 'touchstart', this.touchStartActivationHandler,false); + eventTracker.detachDOMEvent(docBody, 'touchmove', this.touchMoveActivationHandler, false); + eventTracker.detachDOMEvent(docBody, 'touchend', this.touchEndActivationHandler, false); + } } } \ No newline at end of file diff --git a/web/src/engine/namespaced-main/dom/domManager.ts b/web/src/engine/namespaced-main/dom/domManager.ts index ca427f912a..3af45ee7bc 100644 --- a/web/src/engine/namespaced-main/dom/domManager.ts +++ b/web/src/engine/namespaced-main/dom/domManager.ts @@ -499,70 +499,6 @@ namespace com.keyman.dom { 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('kmw-key-') >= 0) return false; - if(typeof(p.parentElement) != 'undefined' && p.parentElement != null) { - p=p.parentElement; - 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); @@ -572,14 +508,6 @@ namespace com.keyman.dom { // 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); @@ -648,15 +576,5 @@ namespace com.keyman.dom { 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; - } } } \ No newline at end of file From d16927f7126c32627a3eb3a2e9a40c0dc09af4e4 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 26 Apr 2023 12:40:03 +0700 Subject: [PATCH 6/8] chore(web): modularizes pageFocusHandler --- .../src/context/pageIntegrationHandlers.ts | 20 ++++++++++++++++++- web/src/engine/namespaced-main/keymanweb.ts | 4 ---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/web/src/app/browser/src/context/pageIntegrationHandlers.ts b/web/src/app/browser/src/context/pageIntegrationHandlers.ts index 6da17c5b83..e853fbfce6 100644 --- a/web/src/app/browser/src/context/pageIntegrationHandlers.ts +++ b/web/src/app/browser/src/context/pageIntegrationHandlers.ts @@ -41,7 +41,7 @@ export class PageIntegrationHandlers { return this.engine.contextManager.focusAssistant; } - private suppressFocusCheck: (e: Event) => boolean = (e) => { + private suppressFocusCheck: (e: FocusEvent) => boolean = (e) => { if(this.focusAssistant._IgnoreBlurFocus) { // Prevent triggering other blur-handling events (as possible) e.stopPropagation(); @@ -51,6 +51,18 @@ export class PageIntegrationHandlers { return true; } + /** + * Reset context when entering or exiting the active element. + * Will also trigger OSK shift state / layer reset. + **/ + private pageFocusHandler: (e: FocusEvent) => boolean = () => { + if(!this.focusAssistant.maintainingFocus && this.engine.osk?.vkbd) { + this.engine.contextManager.deactivateCurrentTarget(); + this.engine.contextManager.resetContext(); + } + return false; + } + // 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. @@ -124,6 +136,9 @@ export class PageIntegrationHandlers { const device = this.engine.config.hostDevice; const docBody = this.window.document.body; + eventTracker.attachDOMEvent(this.window, 'focus', this.pageFocusHandler, false); + eventTracker.attachDOMEvent(this.window, 'blur', this.pageFocusHandler, false); + /* * 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 @@ -147,6 +162,9 @@ export class PageIntegrationHandlers { // See `attachHandlers` for the purpose behind all handlers listed here. + eventTracker.detachDOMEvent(this.window, 'focus', this.pageFocusHandler, false); + eventTracker.detachDOMEvent(this.window, 'blur', this.pageFocusHandler, false); + eventTracker.detachDOMEvent(docBody, 'focus', this.suppressFocusCheck, true); eventTracker.detachDOMEvent(docBody, 'blur', this.suppressFocusCheck, true); diff --git a/web/src/engine/namespaced-main/keymanweb.ts b/web/src/engine/namespaced-main/keymanweb.ts index 88cef3e2b1..d650dc4fa2 100644 --- a/web/src/engine/namespaced-main/keymanweb.ts +++ b/web/src/engine/namespaced-main/keymanweb.ts @@ -117,10 +117,6 @@ if(!window['keyman']['initialized']) { util.attachDOMEvent(document, 'keyup', keymanweb.hotkeyManager._Process, false); - // We need to track this handler, as it causes... interesting... interactions during testing in certain browsers. - util.attachDOMEvent(window, 'focus', keymanweb.pageFocusHandler, false); // I775 - util.attachDOMEvent(window, 'blur', keymanweb.pageFocusHandler, false); // I775 - // Initialize supplementary plane string extensions String.kmwEnableSupplementaryPlane(true); From 953973c55356a8a1b15712b49f80d19702f83940 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 26 Apr 2023 12:40:46 +0700 Subject: [PATCH 7/8] chore(web): modularizes window load/unload handlers, nearby init + shutdown code --- .../src/context/pageIntegrationHandlers.ts | 32 +++++++++++++++++++ web/src/app/browser/src/keymanEngine.ts | 13 +++++++- .../engine/namespaced-main/dom/domManager.ts | 31 ------------------ web/src/engine/namespaced-main/keymanweb.ts | 11 ------- web/src/engine/namespaced-main/kmwbase.ts | 24 -------------- 5 files changed, 44 insertions(+), 67 deletions(-) diff --git a/web/src/app/browser/src/context/pageIntegrationHandlers.ts b/web/src/app/browser/src/context/pageIntegrationHandlers.ts index e853fbfce6..36bd089056 100644 --- a/web/src/app/browser/src/context/pageIntegrationHandlers.ts +++ b/web/src/app/browser/src/context/pageIntegrationHandlers.ts @@ -131,6 +131,26 @@ export class PageIntegrationHandlers { return false; }; + + private _WindowLoad: (e: Event) => void = () => { + // Always return to top of page after a page reload + document.body.scrollTop=0; + if(typeof document.documentElement != 'undefined') { + document.documentElement.scrollTop=0; + } + } + + /** + * Function _WindowUnload + * Scope Private + * Description Remove handlers before detaching KMW window + */ + private _WindowUnload: () => void = () => { + // Future note: should restrict this to anything for the corresponding document if on a + // child iframe, not the whole engine. + this.engine.shutdown(); + } + private attachHandlers() { const eventTracker = this.domEventTracker; const device = this.engine.config.hostDevice; @@ -153,6 +173,12 @@ export class PageIntegrationHandlers { eventTracker.attachDOMEvent(docBody, 'touchmove', this.touchMoveActivationHandler, false); eventTracker.attachDOMEvent(docBody, 'touchend', this.touchEndActivationHandler, false); } + + eventTracker.attachDOMEvent(window, 'load', this._WindowLoad, false); + eventTracker.attachDOMEvent(window, 'unload', this._WindowUnload,false); + + // TODO: Hotkey module stuff. Is not yet modularized. + // eventTracker.attachDOMEvent(document, 'keyup', this.engine.hotkeyManager._Process, false); } public shutdown() { @@ -173,5 +199,11 @@ export class PageIntegrationHandlers { eventTracker.detachDOMEvent(docBody, 'touchmove', this.touchMoveActivationHandler, false); eventTracker.detachDOMEvent(docBody, 'touchend', this.touchEndActivationHandler, false); } + + eventTracker.detachDOMEvent(window, 'load', this._WindowLoad, false); + eventTracker.detachDOMEvent(window, 'unload', this._WindowUnload,false); + + // TODO: Hotkey module stuff. + // eventTracker.detachDOMEvent(document, 'keyup', this.engine.hotkeyManager._Process, false); } } \ No newline at end of file diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index c317f2ba64..6538c68537 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -3,7 +3,7 @@ import { Device as DeviceDetector } from 'keyman/engine/device-detect'; import { getAbsoluteY } from 'keyman/engine/dom-utils'; import { OutputTarget } from 'keyman/engine/element-wrappers'; import { AnchoredOSKView, FloatingOSKView, FloatingOSKViewConfiguration, OSKView } from 'keyman/engine/osk'; -import { DeviceSpec, ProcessorInitOptions } from "@keymanapp/keyboard-processor"; +import { DeviceSpec, ProcessorInitOptions, extendString } from "@keymanapp/keyboard-processor"; import { BrowserConfiguration, BrowserInitOptionDefaults, BrowserInitOptionSpec } from './configuration.js'; import ContextManager from './contextManager.js'; @@ -96,6 +96,8 @@ export class KeymanEngine extends KeymanEngineBase 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. ------ */ /** diff --git a/web/src/engine/namespaced-main/keymanweb.ts b/web/src/engine/namespaced-main/keymanweb.ts index d650dc4fa2..57d1901d23 100644 --- a/web/src/engine/namespaced-main/keymanweb.ts +++ b/web/src/engine/namespaced-main/keymanweb.ts @@ -103,22 +103,11 @@ if(!window['keyman']['initialized']) { keymanweb.debugElement=null; var dbg=keymanweb.debug; - keymanweb.delayedInit(); - //TODO: find all references to next three routines and disambiguate!! // Complete page initialization only after the page is fully loaded, including any embedded fonts // This avoids the need to use a timer to test for the fonts - util.attachDOMEvent(window, 'load', keymanweb.domManager._WindowLoad,false); - util.attachDOMEvent(window, 'unload', keymanweb.domManager._WindowUnload,false); // added fourth argument (default value) - // *** I3319 Supplementary Plane modifications - end new code - - util.attachDOMEvent(document, 'keyup', keymanweb.hotkeyManager._Process, false); - - // Initialize supplementary plane string extensions - String.kmwEnableSupplementaryPlane(true); - })(); } \ No newline at end of file diff --git a/web/src/engine/namespaced-main/kmwbase.ts b/web/src/engine/namespaced-main/kmwbase.ts index f69e6be286..ac32535495 100644 --- a/web/src/engine/namespaced-main/kmwbase.ts +++ b/web/src/engine/namespaced-main/kmwbase.ts @@ -176,22 +176,6 @@ namespace com.keyman { this['loaded'] = true; } - delayedInit() { - // Track the selected Event-handling object. - this.touchAliasing = this.util.device.touchable ? this.domManager.touchHandlers : this.domManager.nonTouchHandlers; - } - - /** - * Reset context when entering or exiting the active element. - * Will also trigger OSK shift state / layer reset. - **/ - pageFocusHandler = () => { - if(!focusAssistant.maintainingFocus && this.osk?.vkbd) { - this.core.resetContext(null); - } - return false; - } - /** * Triggers a KeymanWeb engine shutdown to facilitate a full system reset. * This function is designed for use with KMW unit-testing, which reloads KMW @@ -199,14 +183,6 @@ namespace com.keyman { */ ['shutdown']() { // Disable page focus/blur events, which can sometimes trigger and cause parallel KMW instances in testing. - this.util.detachDOMEvent(window, 'focus', this.pageFocusHandler, false); - this.util.detachDOMEvent(window, 'blur', this.pageFocusHandler, false); - - this.domManager.shutdown(); - this.osk.shutdown(); - this.util.shutdown(); - this.keyboardManager.shutdown(); - this.core.languageProcessor.shutdown(); if(this.ui && this.ui.shutdown) { this.ui.shutdown(); From b5df853cabc791615b13913e965cc0616c85bb5f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 26 Apr 2023 12:45:04 +0700 Subject: [PATCH 8/8] docs(web): bookkeeping for modularization progress --- web/src/app/browser/src/keymanEngine.ts | 3 +++ web/src/engine/namespaced-main/dom/domManager.ts | 11 +++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 6538c68537..b918621df4 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -94,6 +94,9 @@ export class KeymanEngine extends KeymanEngineBase