mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-29 19:17:41 +00:00
Merge pull request #8813 from keymanapp/refactor/web/alerts-and-waits
refactor(web): util.alert() & util.wait() modularization 🧩
This commit is contained in:
commit
0c5351a2fd
13 changed files with 570 additions and 162 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BrowserInitOptionSpec>) {
|
||||
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<string, any> {
|
||||
|
|
|
|||
|
|
@ -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<BrowserConfiguration> {
|
||||
private _activeKeyboard: {keyboard: Keyboard, metadata: KeyboardStub};
|
||||
private config: BrowserConfiguration;
|
||||
private cookieManager = new CookieSerializer<KeyboardCookie>('KeymanWeb_Keyboard');
|
||||
readonly focusAssistant = new FocusAssistant();
|
||||
readonly page: PageContextAttachment;
|
||||
|
|
@ -60,7 +59,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
this._eventsObj = eventsClosure;
|
||||
|
||||
this.page = new PageContextAttachment(window.document, {
|
||||
hostDevice: this.config.hostDevice
|
||||
hostDevice: this.engineConfig.hostDevice
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -70,11 +69,10 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
|
||||
initialize(): void {
|
||||
this.on('keyboardasyncload', (stub, completion) => {
|
||||
// TODO: app/browser - display the loader UI if configured?
|
||||
// util.wait('Installing keyboard<br/>' + kbdName);
|
||||
this.engineConfig.alertHost?.wait('Installing keyboard<br/>' + 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<BrowserConfigurat
|
|||
try {
|
||||
let result = await super.activateKeyboard(keyboardId, languageCode, saveCookie);
|
||||
|
||||
this.engineConfig.alertHost?.wait(); // clear any pending waits.
|
||||
|
||||
if(saveCookie && !originalKeyboardTarget) { // if the active target uses global keyboard settings
|
||||
this.cookieManager.save({current: `${keyboardId}:${languageCode}`});
|
||||
}
|
||||
|
|
@ -344,21 +344,40 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
} catch(err) {
|
||||
// non-embedded: if keyboard activation failed, deactivate the keyboard.
|
||||
|
||||
// Make sure we don't infinite-recursion should the deactivate somehow fail.
|
||||
if(this.config.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(() => {});
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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<ContextManager, HardwareEventKeyboard> {
|
||||
export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, ContextManager, HardwareEventKeyboard> {
|
||||
touchLanguageMenu?: LanguageMenu;
|
||||
private pageIntegration: PageIntegrationHandlers;
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ export function setupOskListeners(engine: KeymanEngine, osk: OSKView, contextMan
|
|||
});
|
||||
|
||||
osk.on('showBuild', () => {
|
||||
internalAlert('KeymanWeb Version ' + KEYMAN_VERSION.VERSION + '<br /><br />'
|
||||
+'<span style="font-size:0.8em">Copyright © 2007-2023 SIL International</span>');
|
||||
engine.config.alertHost?.alert('KeymanWeb Version ' + KEYMAN_VERSION.VERSION + '<br /><br />'
|
||||
+'<span style="font-size:0.8em">Copyright © 2007-2023 SIL International</span>');
|
||||
});
|
||||
|
||||
osk.on('dragMove', async (promise) => {
|
||||
|
|
|
|||
346
web/src/app/browser/src/utilApiEndpoint.ts
Normal file
346
web/src/app/browser/src/utilApiEndpoint.ts
Normal file
|
|
@ -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<E extends keyof HTMLElementTagNameMap>(nodeName:E) {
|
||||
const e = document.createElement<E>(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<CookieType extends Record<keyof CookieType, string | number | boolean>>(cn?: string) {
|
||||
const cookie = new CookieSerializer<CookieType>(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<CookieType extends Record<keyof CookieType, string | number | boolean>>(cn: string, cv: CookieType) {
|
||||
const cookie = new CookieSerializer<CookieType>(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<K extends keyof WindowEventMap>(
|
||||
Pelem: Window,
|
||||
Peventname: K,
|
||||
Phandler: (ev: WindowEventMap[K]) => any,
|
||||
PuseCapture?: boolean
|
||||
): void;
|
||||
attachDOMEvent<K extends keyof DocumentEventMap>(
|
||||
Pelem: Document,
|
||||
Peventname: K,
|
||||
Phandler: (ev: DocumentEventMap[K]) => any,
|
||||
PuseCapture?: boolean
|
||||
): void;
|
||||
attachDOMEvent<K extends keyof HTMLElementEventMap>(
|
||||
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<K extends keyof WindowEventMap>(
|
||||
Pelem: Window,
|
||||
Peventname: K,
|
||||
Phandler: (ev: WindowEventMap[K]) => any,
|
||||
PuseCapture?: boolean
|
||||
): void;
|
||||
detachDOMEvent<K extends keyof DocumentEventMap>(
|
||||
Pelem: Document,
|
||||
Peventname: K,
|
||||
Phandler: (ev: DocumentEventMap[K]) => any,
|
||||
PuseCapture?: boolean
|
||||
): void;
|
||||
detachDOMEvent<K extends keyof HTMLElementEventMap>(
|
||||
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();
|
||||
}
|
||||
}
|
||||
145
web/src/app/browser/src/utils/alertHost.ts
Normal file
145
web/src/app/browser/src/utils/alertHost.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { AlertHost } from './alertHost.js';
|
||||
export { _CreateElement } from './createElement.js';
|
||||
export { getStyleValue } from './getStyleValue.js';
|
||||
export { getViewportScale } from './getViewportScale.js';
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ContextManager, PassthroughKeyboard> {
|
||||
export default class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, ContextManager, PassthroughKeyboard> {
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -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<any>,
|
||||
HardKeyboard extends HardKeyboardBase
|
||||
> implements KeyboardKeymanGlobal {
|
||||
readonly config: EngineConfiguration;
|
||||
readonly config: Configuration;
|
||||
readonly contextManager: ContextManager;
|
||||
readonly interface: KeyboardInterface<ContextManager>;
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = <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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue