diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index d1a40be69ae..65a9a74f944 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -886,10 +886,12 @@ private static void copyAssets(Context context) { // Copy default keyboard font copyAsset(context, KMDefault_KeyboardFont, "", true); + // Includes something needed up to Chrome 61, which has full ES5 support. + // Thus, this one isn't legacy-only. + copyAsset(context, KMFilename_JSPolyfill2, "", true); if(legacyMode) { copyAsset(context, KMFilename_JSPolyfill, "", true); - copyAsset(context, KMFilename_JSPolyfill2, "", true); copyAsset(context, KMFilename_JSPolyfill3, "", true); } @@ -1638,7 +1640,7 @@ public static boolean removeKeyboard(Context context, int position) { public static boolean isDefaultKey(String key) { return ( - key != null && + key != null && key.equals(KMString.format("%s_%s", KMDefault_LanguageID, KMDefault_KeyboardID))); } @@ -2084,11 +2086,11 @@ public static Point getWindowSize(Context context) { wm.getDefaultDisplay().getSize(size); return size; } - + WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); return new Point( windowMetrics.getBounds().width(), - windowMetrics.getBounds().height()); + windowMetrics.getBounds().height()); } public static float getWindowDensity(Context context) { diff --git a/common/web/keyboard-processor/src/keyboards/loaders/domKeyboardLoader.ts b/common/web/keyboard-processor/src/keyboards/loaders/domKeyboardLoader.ts index ea9cddecd42..72ddc9e5b38 100644 --- a/common/web/keyboard-processor/src/keyboards/loaders/domKeyboardLoader.ts +++ b/common/web/keyboard-processor/src/keyboards/loaders/domKeyboardLoader.ts @@ -6,6 +6,28 @@ import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder import { ManagedPromise } from '@keymanapp/web-utils'; +/** + * Add this function as an event listener for the main `window`'s 'error' event + * in order to filter out the sanitized error generated by a failed + * script-loading attempt. + * + * Added to address #11962. + * @param err + */ +export function keyboardScriptErrorFilterer(err: ErrorEvent) { + if(/script error/i.test(err.message) && !err.filename) { + // Tends to be reached on Android devices; some sort of 'security' is + // enacted that obscures the actual error details. + err.preventDefault(); + err.stopPropagation(); + } else if(/modifierCodes/.test(err.message) && /undefined/.test(err.message)) { + // Tends to be reached for Keyman Engine for Web in the browser for + // locally-hosted files. + err.preventDefault(); + err.stopPropagation(); + } +} + export class DOMKeyboardLoader extends KeyboardLoaderBase { public readonly element: HTMLIFrameElement; private readonly performCacheBusting: boolean; diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 917ee0fee9b..41abdfda6cb 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -485,8 +485,9 @@ export default class ContextManager extends ContextManagerBase { + public async activateKeyboard(keyboardId: string, languageCode?: string, saveCookie?: boolean, silenceErrorLogging?: boolean): Promise { saveCookie ||= false; + silenceErrorLogging ||= false; const originalKeyboardTarget = this.currentKeyboardSrcTarget(); // If someone tries to activate a keyboard before we've had a chance to load it, @@ -538,14 +539,16 @@ export default class ContextManager extends ContextManagerBase { touchLanguageMenu?: LanguageMenu; @@ -219,6 +220,13 @@ export default class KeymanEngine extends KeymanEngineBase keyboardScriptErrorFilterer(event); + window.addEventListener('error', errorFilterer); /* Attempt to restore the user's last-used keyboard from their previous session. The method auto-loads the default stub if one is available and the last-used keyboard @@ -234,7 +242,19 @@ export default class KeymanEngine extends KeymanEngineBase this.contextManager.restoreSavedKeyboard(savedKeyboardStr)); + } + /* in case of failed fetch due to network error or bad URI; we must still let the OSK init. */ + } finally { + window.removeEventListener('error', errorFilterer); + }; const firstKbdConfig = { keyboardToActivate: this.contextManager.activeKeyboard diff --git a/web/src/app/webview/src/keymanEngine.ts b/web/src/app/webview/src/keymanEngine.ts index d78ce2bad8e..f8d6cbc67fd 100644 --- a/web/src/app/webview/src/keymanEngine.ts +++ b/web/src/app/webview/src/keymanEngine.ts @@ -1,4 +1,4 @@ -import { DefaultRules, DeviceSpec, RuleBehavior, Keyboard } from '@keymanapp/keyboard-processor' +import { DefaultRules, DeviceSpec, RuleBehavior, Keyboard, KeyboardScriptError } from '@keymanapp/keyboard-processor' import { KeymanEngine as KeymanEngineBase, KeyboardInterface } from 'keyman/engine/main'; import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk'; import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils'; @@ -8,6 +8,7 @@ import { WebviewConfiguration, WebviewInitOptionDefaults, WebviewInitOptionSpec import ContextManager, { ContextHost } from './contextManager.js'; import PassthroughKeyboard from './passthroughKeyboard.js'; import { buildEmbeddedGestureConfig, setupEmbeddedListeners } from './oskConfiguration.js'; +import { keyboardScriptErrorFilterer } from '@keymanapp/keyboard-processor/dom-keyboard-loader'; export default class KeymanEngine extends KeymanEngineBase { // Ideally, we would be able to auto-detect `sourceUri`: https://stackoverflow.com/a/60244278. @@ -81,14 +82,30 @@ export default class KeymanEngine extends KeymanEngineBase this.setActiveKeyboard(firstStub.id, firstStub.langId)); + } else { + throw e; + } + } finally { + window.removeEventListener('error', keyboardScriptErrorFilterer); + } } const keyboardConfig: ViewConfiguration['keyboardToActivate'] = firstKeyboard ? { diff --git a/web/src/engine/main/src/contextManagerBase.ts b/web/src/engine/main/src/contextManagerBase.ts index e3fabc728f4..e62b239ea7f 100644 --- a/web/src/engine/main/src/contextManagerBase.ts +++ b/web/src/engine/main/src/contextManagerBase.ts @@ -236,11 +236,11 @@ export abstract class ContextManagerBase // still async-loading, we should go with the later setting - the preloaded one. this.findAndPopActivation(this.currentKeyboardSrcTarget()); - const activatingKeyboard = this.prepareKeyboardForActivation(keyboardId, languageCode); + let activatingKeyboard = this.prepareKeyboardForActivation(keyboardId, languageCode); const originalKeyboardTarget = this.currentKeyboardSrcTarget(); - const keyboard = await activatingKeyboard.keyboard; + let keyboard = await activatingKeyboard.keyboard; if(keyboard == null && activatingKeyboard.metadata) { // The activation was async and was cancelled - either by `beforeKeyboardChange` first-pass // cancellation or because a different keyboard was requested before completion of the async load. @@ -357,17 +357,19 @@ export abstract class ContextManagerBase let combinedPromise = Promise.race([keyboardPromise, timeoutPromise]); - // Ensure the async-load Promise completes properly. + // Ensure the async-load Promise completes properly, allowing us to + // consistently clear keyboard-loading alerts. combinedPromise.then(() => { completionPromise.resolve(null); - // Prevent any 'unhandled Promise rejection' events that may otherwise occur from the timeout promise. + // Prevent any 'unhandled Promise rejection' events that may otherwise + // occur from the timeout promise. timeoutPromise.catch(() => {}); - }); - combinedPromise.catch((err) => { + }).catch((err) => { completionPromise.resolve(err); - throw err; }); + // Any errors from the keyboard-loading process will continue to be + // forwarded through the main returned Promise. return combinedPromise; }); diff --git a/web/src/engine/main/src/engineConfiguration.ts b/web/src/engine/main/src/engineConfiguration.ts index eb870c2daf1..2cd5cf39052 100644 --- a/web/src/engine/main/src/engineConfiguration.ts +++ b/web/src/engine/main/src/engineConfiguration.ts @@ -17,6 +17,7 @@ export class EngineConfiguration extends EventEmitter { public hostDevice: DeviceSpec; readonly sourcePath: string; readonly deferForInitialization: ManagedPromise; + deferForOsk: ManagedPromise; private _paths: PathConfiguration; public activateFirstKeyboard: boolean; @@ -39,6 +40,7 @@ export class EngineConfiguration extends EventEmitter { this.sourcePath = sourcePath; this.hostDevice = device; this.deferForInitialization = new ManagedPromise(); + this.deferForOsk = new ManagedPromise(); } initialize(options: Required) { diff --git a/web/src/engine/main/src/keymanEngine.ts b/web/src/engine/main/src/keymanEngine.ts index 890574e2840..f35ce32852b 100644 --- a/web/src/engine/main/src/keymanEngine.ts +++ b/web/src/engine/main/src/keymanEngine.ts @@ -1,5 +1,5 @@ -import { type Keyboard, KeyboardKeymanGlobal, ProcessorInitOptions } from "@keymanapp/keyboard-processor"; -import { DOMKeyboardLoader as KeyboardLoader } from "@keymanapp/keyboard-processor/dom-keyboard-loader"; +import { type Keyboard, KeyboardKeymanGlobal, ProcessorInitOptions, ManagedPromise, KeyboardScriptError } from "@keymanapp/keyboard-processor"; +import { DOMKeyboardLoader as KeyboardLoader, keyboardScriptErrorFilterer } from "@keymanapp/keyboard-processor/dom-keyboard-loader"; import { InputProcessor, PredictionContext } from "@keymanapp/input-processor"; import { OSKView } from "keyman/engine/osk"; import { KeyboardRequisitioner, ModelCache, ModelSpec, toUnprefixedKeyboardId as unprefixed } from "keyman/engine/package-cache"; @@ -263,7 +263,7 @@ export default class KeymanEngine< }); kbdCache.on('stubadded', (stub) => { - let eventRaiser = () => { + let eventRaiser = async () => { // The corresponding event is needed in order to update UI modules as new keyboard stubs "come online". this.legacyAPIEvents.callEvent('keyboardregistered', { internalName: stub.KI, @@ -275,8 +275,27 @@ export default class KeymanEngine< // If this is the first stub loaded, set it as active. if(this.config.activateFirstKeyboard && this.keyboardRequisitioner.cache.defaultStub == stub) { - // Note: leaving this out is super-useful for debugging issues that occur when no keyboard is active. - this.contextManager.activateKeyboard(stub.id, stub.langId, true); + // Note: this can trigger before the OSK is first built; such a + // situation can present a problem for debug-mode keyboards (as + // certain values are accessed through `keyman.osk`). + // + // Other areas also use the same filter function, so we create a distinct function + // unique to this location for our listener. + const errorFilterer = (event: ErrorEvent) => keyboardScriptErrorFilterer(event); + window.addEventListener('error', errorFilterer); + + try { + // Note: leaving this out is super-useful for debugging issues that occur when no keyboard is active. + await this.contextManager.activateKeyboard(stub.id, stub.langId, true); + } catch (err) { + if(err instanceof KeyboardScriptError) { + this.config.deferForOsk.then(() => this.setActiveKeyboard(stub.id, stub.langId)); + } else { + throw err; + } + } finally { + window.removeEventListener('error', errorFilterer); + } } } @@ -360,6 +379,13 @@ export default class KeymanEngine< } value.on('keyevent', this.keyEventListener); this.core.keyboardProcessor.layerStore.handler = value.layerChangeHandler; + + // Trigger any deferred operations that require a present OSK. + // For example, debug-mode keyboards use APIs available through the OSK. + this.config.deferForOsk.resolve(); + } else { + // If the OSK was cleared, we may need to restore deferment for debug-mode keyboards. + this.config.deferForOsk = new ManagedPromise(); } } diff --git a/web/src/engine/package-cache/src/stubAndKeyboardCache.ts b/web/src/engine/package-cache/src/stubAndKeyboardCache.ts index 9fd70e74724..a2e4a46c6b0 100644 --- a/web/src/engine/package-cache/src/stubAndKeyboardCache.ts +++ b/web/src/engine/package-cache/src/stubAndKeyboardCache.ts @@ -168,10 +168,16 @@ export default class StubAndKeyboardCache extends EventEmitter { // Overrides the built-in ID in case of keyboard namespacing. kbd.scriptObject["KI"] = keyboardID; this.addKeyboard(kbd); - }).catch((err) => { + return kbd; + }).catch(() => { delete this.keyboardTable[keyboardID]; - throw err; - }) + // Do NOT throw; the returned `promise` will throw the error as well; + // it will be handled via that pathway. + // + // Otherwise, we get duplicate errors... and become unable to silence + // the error here even if we're silencing it elsewhere (say, when + // trying to load a debug-mode keyboard too early). + }); return promise; } diff --git a/web/src/test/manual/web/chirality/index.html b/web/src/test/manual/web/chirality/index.html index aed29c6c95d..e877fc93dd0 100644 --- a/web/src/test/manual/web/chirality/index.html +++ b/web/src/test/manual/web/chirality/index.html @@ -34,24 +34,24 @@ --> + + + - - - - +

KeymanWeb Sample Page - Chirality Testing

This page is designed to stress-test the new support for chiral modifiers and state keys.

Be sure to reference the developer console for additional feedback.

diff --git a/web/src/test/manual/web/unminified.html b/web/src/test/manual/web/unminified.html index 1e6b7496efe..d50353bfe12 100644 --- a/web/src/test/manual/web/unminified.html +++ b/web/src/test/manual/web/unminified.html @@ -34,19 +34,17 @@ --> + + + - - -