diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts index 0464d2cd9c..ad7aa49523 100644 --- a/common/web/keyboard-processor/src/index.ts +++ b/common/web/keyboard-processor/src/index.ts @@ -3,7 +3,12 @@ export * from "./keyboards/defaultLayouts.js"; export { default as Keyboard } from "./keyboards/keyboard.js"; export * from "./keyboards/keyboard.js"; export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js"; -export { default as KeyboardLoaderBase, KeyboardLoadErrorBuilder } from "./keyboards/keyboardLoaderBase.js"; +export { + default as KeyboardLoaderBase, + KeyboardLoadErrorBuilder, + KeyboardMissingError, + KeyboardScriptError +} from "./keyboards/keyboardLoaderBase.js"; export { CloudKeyboardFont, internalizeFont, diff --git a/common/web/keyboard-processor/src/keyboards/keyboardLoaderBase.ts b/common/web/keyboard-processor/src/keyboards/keyboardLoaderBase.ts index 426ac15b81..e5cb5dc3dc 100644 --- a/common/web/keyboard-processor/src/keyboards/keyboardLoaderBase.ts +++ b/common/web/keyboard-processor/src/keyboards/keyboardLoaderBase.ts @@ -18,11 +18,22 @@ export interface KeyboardLoadErrorBuilder { missingError(err: Error); } -const buildAndFinalizeError = (msg: string, err?: Error): ErrorWithCause => { - const builtError: ErrorWithCause = new Error(msg); - builtError.cause = err; +export class KeyboardScriptError extends Error { + public readonly cause; - return builtError; + constructor(msg: string, cause?: Error) { + super(msg); + this.cause = cause; + } +} + +export class KeyboardMissingError extends Error { + public readonly cause; + + constructor(msg: string, cause?: Error) { + super(msg); + this.cause = cause; + } } class UriBasedErrorBuilder implements KeyboardLoadErrorBuilder { @@ -34,12 +45,12 @@ class UriBasedErrorBuilder implements KeyboardLoadErrorBuilder { missingError(err: Error) { const msg = `Cannot find the keyboard at ${this.uri}.`; - return buildAndFinalizeError(msg, err); + return new KeyboardMissingError(msg, err); } scriptError(err: Error) { const msg = `Error registering the keyboard script at ${this.uri}; it may contain an error.`; - return buildAndFinalizeError(msg, err); + return new KeyboardScriptError(msg, err); } } @@ -53,13 +64,13 @@ class StubBasedErrorBuilder implements KeyboardLoadErrorBuilder { missingError(err: Error) { const stub = this.stub; const msg = `Cannot find the ${stub.name} keyboard for ${stub.langName} at ${stub.filename}.`; - return buildAndFinalizeError(msg, err); + return new KeyboardMissingError(msg, err); } scriptError(err: Error) { const stub = this.stub; const msg = `Error registering the ${stub.name} keyboard for ${stub.langName}; keyboard script at ${stub.filename} may contain an error.`; - return buildAndFinalizeError(msg, err); + return new KeyboardScriptError(msg, err); } } diff --git a/web/src/app/browser/src/configuration.ts b/web/src/app/browser/src/configuration.ts index 3b23ba6e72..14cd9aac9d 100644 --- a/web/src/app/browser/src/configuration.ts +++ b/web/src/app/browser/src/configuration.ts @@ -2,26 +2,30 @@ import { EngineConfiguration, InitOptionSpec, InitOptionDefaults } from "keyman/ import { OutputTarget as DOMOutputTarget } from 'keyman/engine/element-wrappers'; import { isEmptyTransform, OutputTarget, RuleBehavior } from '@keymanapp/keyboard-processor'; +import { AlertHost } from "./utils/alertHost.js"; export class BrowserConfiguration extends EngineConfiguration { private _ui: string; private _attachType: string; - private _useAlerts: boolean; + + private _alertHost?: AlertHost; initialize(options: Required) { this.initialize(options); this._ui = options.ui; this._attachType = options.attachType; - this._useAlerts = options.useAlerts; + if(options.useAlerts) { + this._alertHost = new AlertHost(); + } } get attachType() { return this._attachType; } - get shouldAlert(): boolean { - return this._useAlerts; + get alertHost(): AlertHost | undefined { + return this._alertHost; } debugReport(): Record { diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index a0fdf8d866..49561f57c3 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -1,4 +1,4 @@ -import { type Keyboard, Mock } from '@keymanapp/keyboard-processor'; +import { type Keyboard, KeyboardScriptError } from '@keymanapp/keyboard-processor'; import { type KeyboardStub } from 'keyman/engine/package-cache'; import { CookieSerializer } from 'keyman/engine/dom-utils'; import { eventOutputTarget, PageContextAttachment } from 'keyman/engine/attachment'; @@ -42,7 +42,6 @@ function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) { export default class ContextManager extends ContextManagerBase { private _activeKeyboard: {keyboard: Keyboard, metadata: KeyboardStub}; - private config: BrowserConfiguration; private cookieManager = new CookieSerializer('KeymanWeb_Keyboard'); readonly focusAssistant = new FocusAssistant(); readonly page: PageContextAttachment; @@ -60,7 +59,7 @@ export default class ContextManager extends ContextManagerBase { - // TODO: app/browser - display the loader UI if configured? - // util.wait('Installing keyboard
' + kbdName); + this.engineConfig.alertHost?.wait('Installing keyboard
' + stub.name); completion.then(() => { - // Cancel the loader UI. + this.engineConfig.alertHost?.wait(); // cancels the wait. }); }); @@ -328,6 +326,8 @@ export default class ContextManager extends ContextManagerBase {}); - } // else "We already failed, so give up." - } else { - // Fallback behavior - if on a desktop device, the user still has a physical keyboard. - // Just clear out the active keyboard & OSK. - await this.activateKeyboard('', '', false).catch(() => {}); + const fallback = async () => { + // Make sure we don't infinite-recursion should the deactivate somehow fail. + if(this.engineConfig.hostDevice.touchable) { + // Fallback behavior - if on a touch device, we need to keep a keyboard visible. + const defaultStub = this.keyboardCache.defaultStub; + if(defaultStub.id != keyboardId || defaultStub.langId != languageCode) { + await this.activateKeyboard(defaultStub.id, defaultStub.langId, true).catch(() => {}); + } // else "We already failed, so give up." + } else { + // Fallback behavior - if on a desktop device, the user still has a physical keyboard. + // Just clear out the active keyboard & OSK. + await this.activateKeyboard('', '', false).catch(() => {}); + } } - if((this.config as BrowserConfiguration).shouldAlert) { - // TODO: util.alert error report + this.engineConfig.alertHost?.wait(); // clear the wait message box, either way. + + const message = (err as Error)?.message || + 'Sorry, the ' + keyboardId + ' keyboard for ' + languageCode + ' is not currently available.'; + + if(err instanceof KeyboardScriptError) { + // We get signaled about error log messages if the site is connected to our Sentry error reporting + // system; we want to know if we have a broken keyboard that's been published. + console.error(err || message); + } else { + // If it's just internet connectivity or "file not found" issues, that's not worth reporting + // to Sentry. + console.warn(err || message); + } + + if(this.engineConfig.alertHost) { + this.engineConfig.alertHost?.alert(message, fallback); + } else { + fallback(); } throw err; // since the site-dev consumer may want to do their own error-handling. diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 51451a44ac..22bfc3dc14 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -14,7 +14,7 @@ import { PageIntegrationHandlers } from './context/pageIntegrationHandlers.js'; import { LanguageMenu } from './languageMenu.js'; import { setupOskListeners } from './oskConfiguration.js'; -export class KeymanEngine extends KeymanEngineBase { +export class KeymanEngine extends KeymanEngineBase { touchLanguageMenu?: LanguageMenu; private pageIntegration: PageIntegrationHandlers; diff --git a/web/src/app/browser/src/oskConfiguration.ts b/web/src/app/browser/src/oskConfiguration.ts index 24d160f7f7..c6804d0451 100644 --- a/web/src/app/browser/src/oskConfiguration.ts +++ b/web/src/app/browser/src/oskConfiguration.ts @@ -35,8 +35,8 @@ export function setupOskListeners(engine: KeymanEngine, osk: OSKView, contextMan }); osk.on('showBuild', () => { - internalAlert('KeymanWeb Version ' + KEYMAN_VERSION.VERSION + '

' - +'Copyright © 2007-2023 SIL International'); + engine.config.alertHost?.alert('KeymanWeb Version ' + KEYMAN_VERSION.VERSION + '

' + +'Copyright © 2007-2023 SIL International'); }); osk.on('dragMove', async (promise) => { diff --git a/web/src/app/browser/src/utilApiEndpoint.ts b/web/src/app/browser/src/utilApiEndpoint.ts new file mode 100644 index 0000000000..b1feb07c9f --- /dev/null +++ b/web/src/app/browser/src/utilApiEndpoint.ts @@ -0,0 +1,346 @@ + +import { + CookieSerializer, + createStyleSheet, + getAbsoluteX, + getAbsoluteY, + StylesheetManager + } from "keyman/engine/dom-utils"; +import { DomEventTracker } from "keyman/engine/events"; +import { BrowserConfiguration, BrowserInitOptionSpec } from "./configuration.js"; +import { getStyleValue } from "./utils/getStyleValue.js"; +import { AlertHost } from "./utils/alertHost.js"; + +/** + * Calls document.createElement for the specified node type and also applies + * 'user-select: none' styling to the new element. + * @param nodeName + * @returns + */ +export function createUnselectableElement(nodeName:E) { + const e = document.createElement(nodeName); + e.style.userSelect="none"; + return e; +} + +export class UtilApiEndpoint { + readonly config: BrowserConfiguration; + private readonly stylesheetManager: StylesheetManager; + private readonly domEventTracker: DomEventTracker; + private _alertHost: AlertHost; + + constructor(config: BrowserConfiguration) { + this.config = config; + this.stylesheetManager = new StylesheetManager(document.body, config.applyCacheBusting); + this.domEventTracker = new DomEventTracker(); + } + + readonly getAbsoluteX = getAbsoluteX; + readonly getAbsoluteY = getAbsoluteY; + + // These four were renamed, but we need to maintain their legacy names. + readonly _GetAbsoluteX = getAbsoluteX; + readonly _GetAbsoluteY = getAbsoluteY; + readonly _GetAbsolute = this.getAbsolute; + readonly toNzString = this.nzString; + + /** + * Expose the touchable state for UIs - will disable external UIs entirely + **/ + isTouchDevice(): boolean { + return this.config.hostDevice.touchable; + } + + getAbsolute(elem: HTMLElement): { x: number, y: number } { + return { + x: getAbsoluteX(elem), + y: getAbsoluteY(elem) + }; + } + + /** + * Calls document.createElement for the specified node type and also applies + * 'user-select: none' styling to the new element. + * @param nodeName + * @returns + */ + readonly createElement = createUnselectableElement; + + /** + * Function getOption + * Scope Public + * @param {string} optionName Name of option + * @param {*=} dflt Default value of option + * @return {*} + * Description Returns value of named option + */ + getOption(optionName: keyof BrowserInitOptionSpec, dflt?:any): any { + if(optionName in this.config.paths) { + return this.config.paths[optionName]; + } else if(optionName in this.config.options) { + return this.config.options[optionName]; + } else if(arguments.length > 1) { + return dflt; + } else { + return ''; + } + } + + setOption(optionName: keyof BrowserInitOptionSpec, value: any): void { + switch(optionName) { + case 'attachType': + // 16.0 & before: did nothing. + // Fixable for 17.0 with some extra work, but the changes would likely be enough to + // merit a focused PR. It's not 100% straightforward. + break; + case 'ui': + // 16.0 & before: relies on the Float UI to passively pick up on any changes. + // Only appears to be effective before the Float UI initializes. + break; + case 'useAlerts': + this.config.alertHost = (value ? new AlertHost() : null); + break; + case 'setActiveOnRegister': + this.config.activateFirstKeyboard = !!value; + break; + case 'spacebarText': + this.config.spacebarText = value; + break; + default: + throw new Error("Path-related options may not be changed after the engine has initialized."); + } + } + + /** + * Document cookie parsing for use by kernel, OSK, UI etc. + * + * @param {string=} cn cookie name (optional) + * @return {Object} array of names and strings, or array of variables and values + */ + loadCookie>(cn?: string) { + const cookie = new CookieSerializer(cn); + return cookie.load(decodeURIComponent); + } + + /** + * Standard cookie saving for use by kernel, OSK, UI etc. + * + * @param {string} cn name of cookie + * @param {Object} cv object with array of named arguments and values + */ + saveCookie>(cn: string, cv: CookieType) { + const cookie = new CookieSerializer(cn); + cookie.save(cv, encodeURIComponent); + } + + /** + * Add a stylesheet to a page programmatically, for use by the OSK, the UI or the page creator + * + * @param {string} s style string + * @return {Object} returns the object reference + **/ + addStyleSheet(s: string): HTMLStyleElement { + const styleSheet = createStyleSheet(s); + this.stylesheetManager.linkStylesheet(styleSheet); + + return styleSheet; + } + + /** + * Remove a stylesheet element + * + * @param {Object} s style sheet reference + * @return {boolean} false if element is not a style sheet + **/ + removeStyleSheet(s: HTMLStyleElement) { + return this.stylesheetManager.unlink(s); + } + + /** + * Add a reference to an external stylesheet file + * + * @param {string} s path to stylesheet file + */ + linkStyleSheet(s: string): void { + this.stylesheetManager.linkExternalSheet(s); + } + + // Possible alternative: https://www.npmjs.com/package/language-tags + // This would necessitate linking in a npm module into compiled KeymanWeb, though. + getLanguageCodes(lgCode: string): string[] { + if(lgCode.indexOf('-')==-1) { + return [lgCode]; + } else { + return lgCode.split('-'); + } + } + + /** + * Function attachDOMEvent: Note for most browsers, adds an event to a chain, doesn't stop existing events + * Scope Public + * @param {Object} Pelem Element (or IFrame-internal Document) to which event is being attached + * @param {string} Peventname Name of event without 'on' prefix + * @param {function(Object)} Phandler Event handler for event + * @param {boolean=} PuseCapture True only if event to be handled on way to target element + * Description Attaches event handler to element DOM event + */ + attachDOMEvent( + Pelem: Window, + Peventname: K, + Phandler: (ev: WindowEventMap[K]) => any, + PuseCapture?: boolean + ): void; + attachDOMEvent( + Pelem: Document, + Peventname: K, + Phandler: (ev: DocumentEventMap[K]) => any, + PuseCapture?: boolean + ): void; + attachDOMEvent( + Pelem: HTMLElement, + Peventname: K, + Phandler: (ev: HTMLElementEventMap[K]) => any, + PuseCapture?: boolean + ): void; + attachDOMEvent(Pelem: EventTarget, Peventname: string, Phandler: (Object) => boolean, PuseCapture?: boolean): void { + // TS can't quite track the type inference forwarding here. + this.domEventTracker.attachDOMEvent(Pelem as any, Peventname as any, Phandler, PuseCapture); + } + + /** + * Function detachDOMEvent + * Scope Public + * @param {Object} Pelem Element from which event is being detached + * @param {string} Peventname Name of event without 'on' prefix + * @param {function(Object)} Phandler Event handler for event + * @param {boolean=} PuseCapture True if event was being handled on way to target element + * Description Detaches event handler from element [to prevent memory leaks] + */ + detachDOMEvent( + Pelem: Window, + Peventname: K, + Phandler: (ev: WindowEventMap[K]) => any, + PuseCapture?: boolean + ): void; + detachDOMEvent( + Pelem: Document, + Peventname: K, + Phandler: (ev: DocumentEventMap[K]) => any, + PuseCapture?: boolean + ): void; + detachDOMEvent( + Pelem: HTMLElement, + Peventname: K, + Phandler: (ev: HTMLElementEventMap[K]) => any, + PuseCapture?: boolean + ): void; + detachDOMEvent(Pelem: EventTarget, Peventname: string, Phandler: (Object) => boolean, PuseCapture?: boolean): void { + // TS can't quite track the type inference forwarding here. + this.domEventTracker.detachDOMEvent(Pelem as any, Peventname as any, Phandler, PuseCapture); + } + + getStyleValue = getStyleValue; + + private get alertHost(): AlertHost { + if(this.config.alertHost) { + return this.config.alertHost; + } else if(!this._alertHost) { + // Lazy init: if KMW is set to not show alerts, we try not to initialize the alert host. + // If the .alert API is called, though, we have no choice. + this._alertHost = new AlertHost(); + } + + return this._alertHost; + } + + alert(s: string, fn: () => void) { + this.alertHost.alert(s, fn); + } + + /** + * Function toNzString + * Scope Public + * @param {*} item variable to test + * @param {?*=} dflt default value + * @return {*} + * Description Test if a variable is null, false, empty string, or undefined, and return as string + */ + nzString(item: any, dflt: string): string { + // // ... is this whole thing essentially just: + // return '' + (item || dflt || ''); + // // ? + + let dfltValue = ''; + if(arguments.length > 1) { + dfltValue = dflt; + } + + if(typeof(item) == 'undefined') { + return dfltValue; + } + + if(item == null) { + return dfltValue; + } + + if(item == 0 || item == '') { + return dfltValue; + } + + return ''+item; + } + + /** + * Function toNumber + * Scope Public + * @param {string} s numeric string + * @param {number} dflt default value + * @return {number} + * Description Return string converted to integer or default value + */ + toNumber(s: string, dflt: number): number { + const x = parseInt(s,10); + return isNaN(x) ? dflt : x; + } + + /** + * Function toNumber + * Scope Public + * @param {string} s numeric string + * @param {number} dflt default value + * @return {number} + * Description Return string converted to real value or default value + */ + toFloat(s: string, dflt: number): number { + const x = parseFloat(s); + return isNaN(x) ? dflt : x; + } + + /** + * Function rgba + * Scope Public + * @param {Object} s element style object + * @param {number} r red value, 0-255 + * @param {number} g green value, 0-255 + * @param {number} b blue value, 0-255 + * @param {number} a opacity value, 0-1.0 + * @return {string} background colour style string + * Description Browser-independent alpha-channel management + */ + rgba(s: HTMLStyleElement, r:number, g:number, b:number, a:number): string { + let bgColor='transparent'; + try { + bgColor='rgba('+r+','+g+','+b+','+a+')'; + } catch(ex) { + bgColor='rgb('+r+','+g+','+b+')'; + } + + return bgColor; + } + + shutdown() { + this.stylesheetManager?.unlinkAll(); + this.domEventTracker?.shutdown(); + this._alertHost?.shutdown(); + } +} \ No newline at end of file diff --git a/web/src/app/browser/src/utils/alertHost.ts b/web/src/app/browser/src/utils/alertHost.ts new file mode 100644 index 0000000000..fa01cc9dd0 --- /dev/null +++ b/web/src/app/browser/src/utils/alertHost.ts @@ -0,0 +1,145 @@ +/** + * Encapsulates the emulated dialog used by KMW to signal alerts and keyboard-load + * waiting messages to the user. + * + * The intent is for this to be a direct child element of `document.body`. + */ +export class AlertHost { + /** + * The "background" element - a full-page shim _behind_ the alert / wait box that assists + * in emulating a modal state. + * + * This element uses `display: none` by default, and as the root element, this keeps the others + * hidden by default as well. + */ + private readonly bg: HTMLDivElement; + + /** + * The "wait box" element - the top-level element comprising the wait/message box itself. + */ + private readonly lb: HTMLDivElement; + + /** + * The "wait text" / "alert text" element - the element containing any message text associated + * with the alert or wait being signaled. + */ + private readonly lt: HTMLDivElement; + + /** + * The "wait graphic" - the spinny-bar graphic used during keyboard-load waiting messages. + */ + private readonly gr: HTMLDivElement; + + /** + * The top-right 'close'/'x' button. + */ + private readonly bx: HTMLDivElement; + + private dismiss?: () => void; + private pending: boolean = false; + + // Formerly known as `this.waiting` when within `keyman.util`. + public get rootElement(): HTMLDivElement { + return this.bg; + } + + /** + * Prepares, but does not display, the background and keyboard loading wait message box + * Should not be called before options are defined during initialization + **/ + constructor() { + // "background" - the clickable shim BEHIND the 'dialog' + const bg = this.bg = document.createElement('div'), + lb = this.lb = document.createElement('div'), // 'wait box' - the main actual message box / 'dialog' element + lt = this.lt = document.createElement('div'), // 'wait text' // or alert text - but the host for message text + gr = this.gr = document.createElement('div'), // 'wait graphic' - the little 'please wait' spinny / faux progress bar + bx = this.bx = document.createElement('div'); // the little 'close' top-right x button. + + bg.className='kmw-wait-background'; + lb.className='kmw-wait-box'; + this.dismiss=null; + lt.className='kmw-wait-text'; + gr.className='kmw-wait-graphic'; + bx.className='kmw-alert-close'; + + // Close alert if anywhere in box is touched, since close box is too small on mobiles + lb.onmousedown = lb.onclick = (e) => { + // Ignore if waiting, only handle for alert + if(bx.style.display == 'block') { + bg.style.display='none'; + if(this.dismiss) { + this.dismiss(); + } + } + }; + + lb.addEventListener('touchstart', lb.onclick, false); + bg.onmousedown = bg.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + } + bg.addEventListener('touchstart', bg.onclick, false); + lb.appendChild(bx); // [0] + lb.appendChild(lt); // [1] + lb.appendChild(gr); // [2] + bg.appendChild(lb); + + document.body.appendChild(bg); + } + + /** + * Customized wait display + * + * @param {string|boolean} s displayed text + */ + wait(s?: string) { + // Keyboards loaded with page are initialized before the page is ready, + // so cannot use the wait indicator (and don't need it, anyway) + const bg = this.bg; + if(typeof(bg) == 'undefined' || bg == null) { + return; + } + + if(s) { + this.pending=true; + // In practice, is typically: "Installing keyboard", with line 2 = keyboard name. + window.setTimeout(() => { + if(this.pending) { + window.scrollTo(0,0); + this.bx.style.display='none'; + this.lt.className='kmw-wait-text'; + this.lt.innerHTML=s; + this.gr.style.display='block'; + bg.style.display='block'; + } + }, 1000); + } else { + if(this.pending) { + this.lt.innerHTML=''; + this.pending=false; + bg.style.display='none'; + } + } + } + + /** + * Customized alert. + * + * @param {string} s alert text + * @param {function()=} fn function to call when alert dismissed + */ + alert(s: string, fn?: () => void): void { + const bg = this.bg; + this.bx.style.display='block'; + this.lt.className='kmw-alert-text'; + this.lt.innerHTML=s; + this.bg.style.display='none'; + bg.style.display='block'; + this.dismiss = arguments.length > 1 ? fn : null; + } + + shutdown() { + this.bg.parentNode.removeChild(this.bg); + } +} \ No newline at end of file diff --git a/web/src/app/browser/src/utils/index.ts b/web/src/app/browser/src/utils/index.ts index 81677c4b6f..42b203addc 100644 --- a/web/src/app/browser/src/utils/index.ts +++ b/web/src/app/browser/src/utils/index.ts @@ -1,3 +1,4 @@ +export { AlertHost } from './alertHost.js'; export { _CreateElement } from './createElement.js'; export { getStyleValue } from './getStyleValue.js'; export { getViewportScale } from './getViewportScale.js'; \ No newline at end of file diff --git a/web/src/app/web/kmwnative.ts b/web/src/app/web/kmwnative.ts index b192f07bb5..b7b9cc5482 100644 --- a/web/src/app/web/kmwnative.ts +++ b/web/src/app/web/kmwnative.ts @@ -35,40 +35,6 @@ if(!window['keyman']['initialized']) { } } - /** - * Customized wait display - * - * @param {string|boolean} s displayed text (or false) - */ - util.wait = function(s) { - // Keyboards loaded with page are initialized before the page is ready, - // so cannot use the wait indicator (and don't need it, anyway) - // Do not display if a blocking cloud server error has occurred (to prevent multiple errors) - var bg=this.waiting; - if(typeof(bg) == 'undefined' || bg == null || keymanweb.warned) { - return; - } - - var nn=bg.firstChild.childNodes; - if(s) { - bg.pending=true; - window.setTimeout(function() { - if(bg.pending && keymanweb.options.useAlerts) { - window.scrollTo(0,0); - nn[0].style.display='none'; - nn[1].className='kmw-wait-text'; nn[1].innerHTML=s; - nn[2].style.display='block'; - bg.style.display='block'; - } - },1000); - } else { - if(bg.pending) { - nn[1].innerHTML=''; - bg.pending=false; bg.style.display='none'; - } - } - } - // Get default style sheet path keymanweb.getStyleSheetPath=function(ssName) { var ssPath = util['getOption']('resources')+'osk/'+ssName; diff --git a/web/src/app/webview/src/keymanEngine.ts b/web/src/app/webview/src/keymanEngine.ts index b8389ab7fa..abf4344c72 100644 --- a/web/src/app/webview/src/keymanEngine.ts +++ b/web/src/app/webview/src/keymanEngine.ts @@ -10,7 +10,7 @@ import PassthroughKeyboard from './passthroughKeyboard.js'; import { buildEmbeddedGestureConfig, setupEmbeddedListeners } from './oskConfiguration.js'; import { SubkeyDelegator } from './osk/subkeyDelegator.js'; -export default class KeymanEngine extends KeymanEngineBase { +export default class KeymanEngine extends KeymanEngineBase { // Ideally, we would be able to auto-detect `sourceUri`: https://stackoverflow.com/a/60244278. // But it's too new of a feature to utilize... and also expects to be in a module, when this may // be compiled down to an IIFE. diff --git a/web/src/engine/main/src/keymanEngine.ts b/web/src/engine/main/src/keymanEngine.ts index 95e251b4ae..a525ecc6e3 100644 --- a/web/src/engine/main/src/keymanEngine.ts +++ b/web/src/engine/main/src/keymanEngine.ts @@ -14,10 +14,11 @@ import { EventNames, EventListener, LegacyEventEmitter } from "keyman/engine/eve import DOMCloudRequester from "keyman/engine/package-cache/dom-requester"; export default class KeymanEngine< + Configuration extends EngineConfiguration, ContextManager extends ContextManagerBase, HardKeyboard extends HardKeyboardBase > implements KeyboardKeymanGlobal { - readonly config: EngineConfiguration; + readonly config: Configuration; readonly contextManager: ContextManager; readonly interface: KeyboardInterface; readonly core: InputProcessor; @@ -92,7 +93,7 @@ export default class KeymanEngine< * @param config * @param contextManager */ - constructor(worker: Worker, config: EngineConfiguration, contextManager: ContextManager) { + constructor(worker: Worker, config: Configuration, contextManager: ContextManager) { this.config = config; this.contextManager = contextManager; diff --git a/web/src/engine/namespaced-main/kmwutils.ts b/web/src/engine/namespaced-main/kmwutils.ts index 8c38f09aa1..1c41349e43 100644 --- a/web/src/engine/namespaced-main/kmwutils.ts +++ b/web/src/engine/namespaced-main/kmwutils.ts @@ -336,99 +336,9 @@ namespace com.keyman { } } - /** - * Customized alert. - * - * @param {string} s alert text - * @param {function()=} fn function to call when alert dismissed - */ - alert(s: string, fn?: () => void): void { - var bg = this.waiting, nn=bg.firstChild.childNodes; - (nn[0] as HTMLElement).style.display='block'; - (nn[1] as HTMLElement).className='kmw-alert-text'; - (nn[1] as HTMLElement).innerHTML=s; - (nn[2] as HTMLElement).style.display='none'; - bg.style.display='block'; - bg.dismiss = arguments.length > 1 ? fn : null; - } - - // Stub definition to be fleshed out depending upon native/embedded mode. - wait(s: string|boolean): void { - - } - - /** - * Customized internal alert. This is enabled/disabled by the option flag 'useAlerts' - * - * @param {string} s alert text - * @param {function()=} fn function to call when alert dismissed - */ - internalAlert(s: string, fn?: () => void): void { - if (this.keyman.options.useAlerts) { - this.alert(s, fn); - } - } - - /** - * Prepare the background and keyboard loading wait message box - * Should not be called before options are defined during initialization - **/ - prepareWait(): void { - var bg: HTMLDivElement = document.createElement('DIV'), - lb=document.createElement('DIV'), - lt=document.createElement('DIV'), - gr=document.createElement('DIV'), - bx=document.createElement('DIV'); - - bg.className='kmw-wait-background'; - lb.className='kmw-wait-box'; - bg.dismiss=null; - lt.className='kmw-wait-text'; - gr.className='kmw-wait-graphic'; - bx.className='kmw-alert-close'; - - // Close alert if anywhere in box is touched, since close box is too small on mobiles - lb.onmousedown=lb.onclick=function(e) { - // Ignore if waiting, only handle for alert - if(bx.style.display == 'block') { - bg.style.display='none'; - if(bg.dismiss) { - bg.dismiss(); - } - } - }; - - lb.addEventListener('touchstart', lb.onclick, false); - bg.onmousedown=bg.onclick=function(e) { - e.preventDefault(); - e.stopPropagation(); - - } - bg.addEventListener('touchstart', bg.onclick, false); - lb.appendChild(bx); - lb.appendChild(lt); - lb.appendChild(gr); - bg.appendChild(lb); - document.body.appendChild(bg); - this.waiting=bg; - } - shutdown() { // Remove all event-handler references rooted in KMW events. this.events = {}; - - // Remove all events linking to elements of the original, unaltered page. - // This should sever any still-existing page ties to this instance of KMW, - // allowing browser GC to do its thing. - for(let event of this.domEvents) { - this.detachDOMEvent(event.Pelem, event.Peventname, event.Phandler, event.PuseCapture); - } - - // alt / modularized form for the above: given `eventTracker: DomEventTracker`, call - // `eventTracker.shutdown()`. - - // Remove any KMW-added DOM element clutter. - this.waiting.parentNode.removeChild(this.waiting); } /**