Merge pull request #8813 from keymanapp/refactor/web/alerts-and-waits

refactor(web): util.alert() & util.wait() modularization 🧩
This commit is contained in:
Joshua Horton 2023-05-23 14:26:06 +07:00 • committed by GitHub
commit 0c5351a2fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 570 additions and 162 deletions

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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> {

View file

@ -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.

View file

@ -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;

View file

@ -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 &copy; 2007-2023 SIL International</span>');
engine.config.alertHost?.alert('KeymanWeb Version ' + KEYMAN_VERSION.VERSION + '<br /><br />'
+'<span style="font-size:0.8em">Copyright &copy; 2007-2023 SIL International</span>');
});
osk.on('dragMove', async (promise) => {

View 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();
}
}

View 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);
}
}

View file

@ -1,3 +1,4 @@
export { AlertHost } from './alertHost.js';
export { _CreateElement } from './createElement.js';
export { getStyleValue } from './getStyleValue.js';
export { getViewportScale } from './getViewportScale.js';

View file

@ -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;

View file

@ -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.

View file

@ -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;

View file

@ -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);
}
/**