Merge pull request #8816 from keymanapp/fix/web/app-browser-compilation

fix(web): first-draft, usable build for module-based app/browser 🧩
This commit is contained in:
Joshua Horton 2023-05-25 15:17:11 +07:00 committed by GitHub
commit 3655b9c3dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 502 additions and 541 deletions

View file

@ -9,18 +9,18 @@ configure your build environment.
The following folders contain the distribution for Keyman Engine for Web:
src Source code
src/resources/osk OSK resources for inclusion in mobile app builds;
keymanweb-osk.ttf is maintained at https://github.com/silnrsi/font-keymanweb-osk
src Source code
build/app/resources OSK + UI resources for inclusion in all build types;
keymanweb-osk.ttf is maintained at https://github.com/silnrsi/font-keymanweb-osk
build/app/web/release Fully-compiled KeymanWeb modules for release
build/app/embed/release Fully-compiled KMEA/KMEI modules for inclusion in mobile app builds
build/app/web/debug Fully-compiled but non-minified KeymanWeb modules
build/app/embed/debug Fully-compiled but non-minified KMEA/KMEI modules
build/app/browser/release Fully-compiled KeymanWeb modules for release
build/app/webview/release Fully-compiled KMEA/KMEI modules for inclusion in mobile app builds
build/app/browser/debug Fully-compiled but non-minified KeymanWeb modules
build/app/webview/debug Fully-compiled but non-minified KMEA/KMEI modules
src/samples Sample pages demonstrating ways to link with KeymanWeb
src/test/manual Test-case web-pages for various aspects of KeymanWeb functionality
src/test/auto A Node-driven test suite for automated testing of KeymanWeb
src/samples Sample pages demonstrating ways to link with KeymanWeb
src/test/manual Test-case web-pages for various aspects of KeymanWeb functionality
src/test/auto A Node-driven test suite for automated testing of KeymanWeb
**********************************************************************
@ -64,3 +64,73 @@ the former command executes.
tab.
4. In the Dev console, you can set a breakpoint in your test and refresh the
page to debug
### Approximate Overall Design
```mermaid
graph TD;
OSK[web/src/engine/osk];
KP["common/web/keyboard-processor"];
IP["common/web/input-processor"];
OSK-->KP;
IP-->KP;
Utils["common/web/utils"];
KP---->Utils;
Wordbreakers["common/models/wordbreakers"];
Models["common/models/templates"];
Models-->Utils;
LMWorker["common/web/lm-worker"];
LMWorker-->Models;
LMWorker-->Wordbreakers;
LMLayer["common/predictive-text"];
LMLayer-->LMWorker;
IP-->LMLayer;
subgraph PredText["WebWorker + its interface"]
LMLayer;
LMWorker;
Models;
Wordbreakers;
end
subgraph Headless["Fully headless components"]
direction LR
KP;
IP;
Utils;
PredText;
end
subgraph ClassicWeb["Previously unmodularized components"]
Device[web/src/engine/device-detect];
Device----->Utils;
Elements[web/src/engine/element-wrappers];
Elements-->KP;
KeyboardCache[web/src/engine/package-cache];
KeyboardCache-->IP;
DomUtils[web/src/engine/dom-utils];
DomUtils-->Utils;
OSK-->DomUtils;
OSK---->IP;
Configuration[web/src/engine/paths];
Configuration-->OSK;
CommonEngine[web/src/engine/main];
CommonEngine-->Configuration;
CommonEngine-->Device;
CommonEngine-->KeyboardCache;
CommonEngine-->OSK;
Attachment[web/src/engine/attachment];
Attachment-->DomUtils;
Attachment-->Elements;
end
subgraph WebEngine["Keyman Engine for Web (top-level libraries)"]
Browser[web/src/app/browser];
WebView[web/src/app/webview];
WebView--->CommonEngine;
Browser--->CommonEngine;
Browser-->Attachment;
end
```

View file

@ -29,7 +29,9 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \
"configure" \
"build" \
"test" \
":app/browser The form of Keyman Engine for Web for use on websites" \
":app/webview A puppetable version of KMW designed for use in a host app's WebView" \
":app/ui Builds KMW's desktop form-factor keyboard-selection UI modules" \
":engine/attachment Subset used for detecting valid page contexts for use in text editing " \
":engine/device-detect Subset used for device-detection " \
":engine/dom-utils A common subset of function used for DOM calculations, layout, etc" \
@ -95,23 +97,22 @@ builder_run_child_actions build:engine/package-cache
# Uses engine/paths, engine/device-detect, engine/package-cache, & engine/osk
builder_run_child_actions build:engine/main
# Uses all but engine/element-wrappers
# Uses all but engine/element-wrappers and engine/attachment
builder_run_child_actions build:app/webview
# Uses literally everything `engine/` above
# Is not yet compilable due to unmodularized components.
# builder_run_child_actions build:app/browser
builder_run_child_actions build:app/browser
builder_run_child_actions test
if builder_has_action build:app/browser; then
builder_die "Modularization work is not yet complete; builds dependent on this will fail."
builder_warn "Modularization work is not yet complete; consumers may find needed API or components to be missing"
fi
if builder_has_action build:app/ui; then
builder_die "Modularization work is not yet complete; builds dependent on this will fail."
fi
builder_run_child_actions test
if builder_start_action test; then
./test.sh :engine

View file

@ -40,7 +40,6 @@ builder_parse "$@"
TIER=`cat ../TIER.md`
BUILD_NUMBER=`cat ../VERSION.md`
S_KEYMAN_COM=../../s.keyman.com
if builder_start_action build; then
# Build step: since CI builds start (and should start) from scratch, run the following
@ -113,9 +112,11 @@ if builder_start_action prepare:s.keyman.com; then
# The main build products are expected to reside at the root of this folder.
BASE_PUBLISH_FOLDER="$S_KEYMAN_COM/kmw/engine/$VERSION"
echo "FOLDER: $BASE_PUBLISH_FOLDER"
mkdir -p "$BASE_PUBLISH_FOLDER"
cp -Rf build/app/web/release/* "$BASE_PUBLISH_FOLDER"
cp -Rf build/app/browser/release/* "$BASE_PUBLISH_FOLDER"
cp -Rf build/app/resources/* "$BASE_PUBLISH_FOLDER/resources"
cp -Rf build/app/ui/release/* "$BASE_PUBLISH_FOLDER"
# Third phase: tweak the sourcemaps
@ -160,12 +161,16 @@ if builder_start_action prepare:downloads.keyman.com; then
fi
fi
pushd build/app/web/release
pushd build/app/browser/release
"${COMPRESS_CMD}" $COMPRESS_ADD ../../../../$ZIP *
cd ..
"${COMPRESS_CMD}" $COMPRESS_ADD ../../../$ZIP debug
popd
pushd build/app/resources
"${COMPRESS_CMD}" $COMPRESS_ADD ../../../$ZIP *
popd
pushd build/app/ui/release
"${COMPRESS_CMD}" $COMPRESS_ADD ../../../../$ZIP *
cd ..

View file

@ -7,6 +7,28 @@
import esbuild from 'esbuild';
import { spawn } from 'child_process';
import fs from 'fs';
/*
* Refer to https://github.com/microsoft/TypeScript/issues/13721#issuecomment-307259227 -
* the `@class` emit comment-annotation is designed to facilitate tree-shaking for ES5-targeted
* down-level emits. `esbuild` doesn't look for it by default... but we can override that with
* this plugin.
*/
let es5ClassAnnotationAsPurePlugin = {
name: '@class -> __PURE__',
setup(build) {
build.onLoad({filter: /\.js$/ }, async (args) => {
let source = await fs.promises.readFile(args.path, 'utf8');
return {
// Marks any classes compiled by TS (as per the /** @class */ annotation)
// as __PURE__ in order to facilitate tree-shaking.
contents: source.replace('/** @class */', '/* @__PURE__ */ /** @class */'),
loader: 'js'
}
});
}
}
await esbuild.build({
bundle: true,
@ -14,9 +36,29 @@ await esbuild.build({
format: "iife",
nodePaths: ['../../../../node_modules'],
entryPoints: {
'index': '../../../build/app/webview/obj/main.js',
'index': '../../../build/app/browser/obj/debug-main.js',
},
outdir: '../../../build/app/webview/lib/',
tsconfig: './tsconfig.json',
target: "es5"
outfile: '../../../build/app/browser/debug/keymanweb.js',
plugins: [ es5ClassAnnotationAsPurePlugin ],
target: "es5",
treeShaking: true,
tsconfig: './tsconfig.json'
});
await esbuild.build({
bundle: true,
sourcemap: true,
minifyWhitespace: true,
minifySyntax: true,
minifyIdentifiers: false,
format: "iife",
nodePaths: ['../../../../node_modules'],
entryPoints: {
'index': '../../../build/app/browser/obj/release-main.js',
},
outfile: '../../../build/app/browser/release/keymanweb.js',
plugins: [ es5ClassAnnotationAsPurePlugin ],
target: "es5",
treeShaking: true,
tsconfig: './tsconfig.json'
});

View file

@ -22,13 +22,7 @@ SUBPROJECT_NAME=app/browser
# ################################ Main script ################################
builder_describe "Builds the Keyman Engine for Web's website-integrating version for use in non-puppeted browsers." \
"@/common/web/input-processor build" \
"@/web/src/engine/device-detect build" \
"@/web/src/engine/paths build" \
"@/web/src/engine/package-cache build" \
"@/web/src/engine/events build" \
"@/web/src/engine/osk build" \
"@/web/src/engine/element-wrappers build" \
"@/web/src/engine/attachment build" \
"@/web/src/engine/main build" \
"clean" \
"configure" \
@ -38,11 +32,16 @@ builder_describe "Builds the Keyman Engine for Web's website-integrating version
# Possible TODO?s
# "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \
builder_parse "$@"
config=release
if builder_is_debug_build; then
config=debug
fi
builder_describe_outputs \
configure /node_modules \
build /web/build/$SUBPROJECT_NAME/lib/index.js
builder_parse "$@"
build /web/build/$SUBPROJECT_NAME/$config/keymanweb.js
#### Build action definitions ####
@ -60,6 +59,9 @@ fi
if builder_start_action build; then
compile $SUBPROJECT_NAME
mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk"
cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/"
builder_finish_action success build
fi

View file

@ -3,6 +3,7 @@ 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";
import { whenDocumentReady } from "./utils/documentReady.js";
export class BrowserConfiguration extends EngineConfiguration {
private _ui: string;
@ -11,13 +12,18 @@ export class BrowserConfiguration extends EngineConfiguration {
private _alertHost?: AlertHost;
initialize(options: Required<BrowserInitOptionSpec>) {
this.initialize(options);
super.initialize(options);
this._ui = options.ui;
this._attachType = options.attachType;
if(options.useAlerts) {
this._alertHost = new AlertHost();
}
whenDocumentReady().then(() => {
if(options.useAlerts && !this.alertHost) {
this._alertHost = new AlertHost();
} else if(!options.useAlerts && this.alertHost) {
this._alertHost.shutdown();
this._alertHost = null;
}
});
}
get attachType() {

View file

@ -1,6 +1,6 @@
import { DomEventTracker } from 'keyman/engine/events';
import { KeymanEngine } from "../keymanEngine.js";
import KeymanEngine from "../keymanEngine.js";
import { FocusAssistant } from './focusAssistant.js';
// Note: in the future, it'd probably be best to have an instance per iframe window as

View file

@ -26,7 +26,7 @@ interface KeyboardCookie {
* @param {Object} Ptarg Target element
*/
function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) {
var elDir=(activeKeyboard && activeKeyboard.isRTL) ? 'rtl' : 'ltr';
const elDir = activeKeyboard?.isRTL ? 'rtl' : 'ltr';
if(Ptarg) {
if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement
@ -269,7 +269,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
// Note: is part of the keyboard activation process. Not to be called directly by published API.
activateKeyboardForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget<any>) {
let attachment = target.getElement()._kmwAttachment;
let attachment = target?.getElement()._kmwAttachment;
if(!attachment) {
// if not set with an "independent keyboard", changes the global.

View file

@ -0,0 +1,15 @@
import KeymanEngine from './keymanEngine.js'
import { SourcemappedWorker } from '@keymanapp/lexical-model-layer/web'
/**
* Determine path and protocol of executing script, setting them as
* construction defaults.
*
* This can only be done during load when the active script will be the
* last script loaded. Otherwise the script must be identified by name.
*/
var scripts = document.getElementsByTagName('script');
var ss = scripts[scripts.length-1].src;
var sPath = ss.substr(0,ss.lastIndexOf('/')+1);
window['keyman'] = new KeymanEngine(SourcemappedWorker.constructInstance(), sPath);

View file

@ -1,10 +1,9 @@
import { KeyboardKeymanGlobal } from '@keymanapp/keyboard-processor';
import { StubAndKeyboardCache } from 'keyman/engine/package-cache';
import { type OutputTarget } from 'keyman/engine/element-wrappers';
import { FloatingOSKView, OSKView } from 'keyman/engine/osk';
import { KeyboardInterface as KeyboardInterfaceBase } from 'keyman/engine/main';
import ContextManager from './contextManager.js';
import KeymanEngine from './keymanEngine.js';
export default class KeyboardInterface extends KeyboardInterfaceBase<ContextManager> {
// TBD: allowing it to be set and/or the retrieval mechanism.
@ -15,10 +14,9 @@ export default class KeyboardInterface extends KeyboardInterfaceBase<ContextMana
constructor(
_jsGlobal: any,
keymanGlobal: KeyboardKeymanGlobal,
contextManager: ContextManager,
engine: KeymanEngine,
) {
super(_jsGlobal, keymanGlobal, contextManager);
super(_jsGlobal, engine);
// Nothing else to do here... quite yet. Things may not stay that way, though.
}
@ -30,18 +28,18 @@ export default class KeyboardInterface extends KeyboardInterfaceBase<ContextMana
* Description Save keyboard focus
*/
saveFocus(): void {
this.contextManager.focusAssistant._IgnoreNextSelChange = 1;
this.engine.contextManager.focusAssistant._IgnoreNextSelChange = 1;
}
/**
* Legacy entry points (non-standard names)- included only to allow existing IME keyboards to continue to be used
*/
getLastActiveElement(): OutputTarget<any> {
return this.contextManager.lastActiveTarget;
return this.engine.contextManager.lastActiveTarget;
}
focusLastActiveElement(): void {
this.contextManager.restoreLastActiveTarget();
this.engine.contextManager.restoreLastActiveTarget();
}
//The following entry points are defined but should not normally be used in a keyboard, as OSK display is no longer determined by the keyboard

View file

@ -2,7 +2,8 @@ import { KeymanEngine as KeymanEngineBase } from 'keyman/engine/main';
import { Device as DeviceDetector } from 'keyman/engine/device-detect';
import { getAbsoluteY } from 'keyman/engine/dom-utils';
import { OutputTarget } from 'keyman/engine/element-wrappers';
import { AnchoredOSKView, FloatingOSKView, FloatingOSKViewConfiguration, OSKView } from 'keyman/engine/osk';
import { AnchoredOSKView, FloatingOSKView, FloatingOSKViewConfiguration, OSKView, TwoStateActivator } from 'keyman/engine/osk';
import { ErrorStub, KeyboardStub } from 'keyman/engine/package-cache';
import { DeviceSpec, ProcessorInitOptions, extendString } from "@keymanapp/keyboard-processor";
import { BrowserConfiguration, BrowserInitOptionDefaults, BrowserInitOptionSpec } from './configuration.js';
@ -13,11 +14,14 @@ import { FocusStateAPIObject } from './context/focusAssistant.js';
import { PageIntegrationHandlers } from './context/pageIntegrationHandlers.js';
import { LanguageMenu } from './languageMenu.js';
import { setupOskListeners } from './oskConfiguration.js';
import { whenDocumentReady } from './utils/documentReady.js';
export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, ContextManager, HardwareEventKeyboard> {
export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, ContextManager, HardwareEventKeyboard> {
touchLanguageMenu?: LanguageMenu;
private pageIntegration: PageIntegrationHandlers;
private _initialized: number = 0;
keyEventRefocus = () => {
this.contextManager.restoreLastActiveTarget();
}
@ -30,13 +34,14 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
// Scrolls the document-body to ensure that a focused element remains visible after the OSK appears.
this.contextManager.on('targetchange', (target: OutputTarget<any>) => {
const e = target?.getElement();
(this.osk.activationModel as TwoStateActivator<HTMLElement>).activationTrigger = e;
if(this.config.hostDevice.touchable) {
if(!target || !this.osk) {
if(!e || !target || !this.osk) {
return;
}
const e = target.getElement();
// Get the absolute position of the caret
const y = getAbsoluteY(e);
const t = window.pageYOffset;
@ -55,6 +60,10 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
});
}
public get initialized() {
return this._initialized;
}
protected processorConfiguration(): ProcessorInitOptions {
return {
keyboardInterface: this.interface,
@ -69,18 +78,26 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
this.config.hostDevice = device;
const totalOptions = {...BrowserInitOptionDefaults, ...options};
super.init(totalOptions);
this.config.initialize(totalOptions);
await super.init(totalOptions);
// There may be some valid mutations possible even on repeated calls?
// The original seems to allow it.
this._initialized = 1;
// Must wait for document load for further initialization.
await whenDocumentReady();
// Deferred keyboard loading + shortcutting if a different init call on the engine has
// already fully resolved.
if(this.config.deferForInitialization.hasFinalized) {
// abort! Maybe throw an error, too.
return Promise.resolve();
}
this.contextManager.initialize();
// There may be some valid mutations possible even on repeated calls?
// The original seems to allow it.
this.config.initialize(totalOptions); // will init alertHost, which requires document.body
this.contextManager.initialize(); // will seek to attach to the page, which requires document.body
const oskConfig: FloatingOSKViewConfiguration = {
hostDevice: this.config.hostDevice,
pathConfig: this.config.paths,
@ -104,6 +121,11 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
// Initialize supplementary plane string extensions
String.kmwEnableSupplementaryPlane(true);
this.config.finalizeInit();
this._initialized = 2;
}
get register() {
return this.keyboardRequisitioner.cloudQueryEngine.registerFromCloud;
}
/**
@ -162,6 +184,48 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
this.contextManager.setKeyboardForTarget(Pelem._kmwAttachment.interface, Pkbd, Plc);
}
/**
* Exposed function to load keyboards by name. One or more arguments may be used
*
* @param {any[]} args keyboard name string or keyboard metadata JSON object
* @returns {Promise<(KeyboardStub|ErrorStub)[]>} Promise of added keyboard/error stubs
*
*/
addKeyboards(...args: (any)[]) : Promise<(KeyboardStub|ErrorStub)[]> {
if (!args || !args[0] || args[0].length == 0) {
// Get the cloud keyboard catalog
return this.keyboardRequisitioner.fetchCloudCatalog().catch((errVal) => {
console.error(errVal[0].error);
return errVal;
});
} else {
let x: (string|KeyboardStub)[] = [];
if (Array.isArray(args[0])) {
x.push(...args[0]);
} else if (Array.isArray(args)) {
x.push(...args);
} else {
x.push(args);
}
return this.keyboardRequisitioner.addKeyboardArray(x);
}
}
/**
* Add default keyboards for given language(s)
*
* @param {string|string[]} arg Language name (multiple arguments allowed)
* @returns {Promise<(KeyboardStub|ErrorStub)[]>} Promise of added keyboard/error stubs
**/
['addKeyboardsForLanguage'](arg: string[]|string) : Promise<(KeyboardStub|ErrorStub)[]> {
if (typeof arg === 'string') {
return this.keyboardRequisitioner.addLanguageKeyboards(arg.split(',').map(item => item.trim()));
} else {
return this.keyboardRequisitioner.addLanguageKeyboards(arg);
}
}
/**
* Detaches all KMW event handlers attached by this instance of the engine and releases
* other related resources as appropriate.

View file

@ -2,7 +2,7 @@
import { getAbsoluteX, landscapeView } from "keyman/engine/dom-utils";
import { KeyboardStub } from "keyman/engine/package-cache";
import { KeymanEngine } from "./keymanEngine.js";
import KeymanEngine from "./keymanEngine.js";
import * as util from "./utils/index.js";
// Used by 'native'-mode KMW only - the Android and iOS embedding apps implement their own menus.

View file

@ -1 +0,0 @@
import ContextManager from './contextManager.js';

View file

@ -1,7 +1,7 @@
import { type KeyElement, OSKView, VisualKeyboard } from "keyman/engine/osk";
import { KEYMAN_VERSION } from "@keymanapp/keyman-version";
import ContextManager from "./contextManager.js";
import { KeymanEngine } from "./keymanEngine.js";
import KeymanEngine from "./keymanEngine.js";
import { LanguageMenu } from "./languageMenu.js";
export function setupOskListeners(engine: KeymanEngine, osk: OSKView, contextManager: ContextManager) {
@ -64,7 +64,8 @@ export function setupOskListeners(engine: KeymanEngine, osk: OSKView, contextMan
// On event start
focusAssistant.setMaintainingFocus(true);
// The original did nothing when the pointer left the OSK's bounds. Possible bug?
// await promise; // should we wish to change that.
await promise;
focusAssistant.setMaintainingFocus(false);
});
}

View file

@ -0,0 +1,15 @@
import KeymanEngine from './keymanEngine.js'
import { Worker } from '@keymanapp/lexical-model-layer/web'
/**
* Determine path and protocol of executing script, setting them as
* construction defaults.
*
* This can only be done during load when the active script will be the
* last script loaded. Otherwise the script must be identified by name.
*/
var scripts = document.getElementsByTagName('script');
var ss = scripts[scripts.length-1].src;
var sPath = ss.substr(0,ss.lastIndexOf('/')+1);
window['keyman'] = new KeymanEngine(Worker.constructInstance(), sPath);

View file

@ -1,346 +0,0 @@
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,15 @@
export function whenDocumentReady(): Promise<void> {
if(document.readyState === 'complete') {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const loadHandler: (e: Event) => void = () => {
window.removeEventListener('load', loadHandler);
resolve();
};
window.addEventListener('load', loadHandler);
});
}

View file

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

View file

@ -41,8 +41,7 @@ export default class KeymanEngine extends KeymanEngineBase<WebviewConfiguration,
this.config.hostDevice = device;
const totalOptions = {...WebviewInitOptionDefaults, ...options};
super.init(totalOptions);
this.config.initialize(totalOptions);
await super.init(totalOptions);
// There may be some valid mutations possible even on repeated calls?
// The original seems to allow it.

View file

@ -231,10 +231,15 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
this.emit('beforekeyboardchange', activatingKeyboard.metadata);
}
this.activateKeyboardForTarget({
keyboard: keyboard,
metadata: activatingKeyboard.metadata
}, this.keyboardTarget);
let kbdStubPair: { keyboard: Keyboard, metadata: KeyboardStub } = null;
if(keyboard) {
kbdStubPair = {
keyboard: keyboard,
metadata: activatingKeyboard.metadata
};
}
this.activateKeyboardForTarget(kbdStubPair, this.keyboardTarget);
// Only trigger `keyboardchange` events when they will affect the active context.
if(this.keyboardTarget == originalKeyboardTarget) {
@ -266,7 +271,12 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
languageCode ||= '';
// Check that the saved keyboard is currently registered
let requestedStub = this.keyboardCache.getStub(keyboardId, languageCode);
let requestedStub = null;
if(keyboardId) {
requestedStub = this.keyboardCache.getStub(keyboardId, languageCode);
} else {
languageCode == '';
}
// Mobile device addition: force selection of the first keyboard if none set
if(this.engineConfig.hostDevice.touchable && !requestedStub) {

View file

@ -1,33 +1,27 @@
import {
Keyboard,
KeyboardInterface as KeyboardInterfaceBase,
KeyboardKeymanGlobal,
} from "@keymanapp/keyboard-processor";
import { KeyboardStub, RawKeyboardStub, StubAndKeyboardCache, toUnprefixedKeyboardId as unprefixed } from 'keyman/engine/package-cache';
import { KeyboardStub, RawKeyboardStub, toUnprefixedKeyboardId as unprefixed } from 'keyman/engine/package-cache';
import { ContextManagerBase } from './contextManagerBase.js';
import { VariableStoreCookieSerializer } from "./variableStoreCookieSerializer.js";
import KeymanEngine from "./keymanEngine.js";
import { EngineConfiguration } from "./engineConfiguration.js";
export default class KeyboardInterface<ContextManagerType extends ContextManagerBase<any>> extends KeyboardInterfaceBase {
protected readonly contextManager: ContextManagerType;
private stubAndKeyboardCache: StubAndKeyboardCache;
protected readonly engine: KeymanEngine<EngineConfiguration, ContextManagerType, any>;
private stubNamespacer?: (stub: RawKeyboardStub) => void;
constructor(
_jsGlobal: any,
keymanGlobal: KeyboardKeymanGlobal,
contextManager: ContextManagerType,
engine: KeymanEngine<any, ContextManagerType, any>,
stubNamespacer?: (stub: RawKeyboardStub) => void
) {
super(_jsGlobal, keymanGlobal, new VariableStoreCookieSerializer());
this.contextManager = contextManager;
super(_jsGlobal, engine, new VariableStoreCookieSerializer());
this.engine = engine;
this.stubNamespacer = stubNamespacer;
}
setKeyboardCache(cache: StubAndKeyboardCache) {
this.stubAndKeyboardCache = cache;
}
// Preserves a keyboard's ID, even if namespaced, via script tag tagging.
preserveID(Pk: any /** a `Keyboard`'s `scriptObject` entry */) {
var trueID;
@ -59,12 +53,14 @@ export default class KeyboardInterface<ContextManagerType extends ContextManager
this.preserveID(Pk);
if(!this.stubAndKeyboardCache.isFetchingKeyboard(registeredKeyboard.id)) {
// Deliberate keyboard pre-loading via direct script-tag link on the page.
// Just load the keyboard and reset the harness's keyboard-receiver field.
this.stubAndKeyboardCache.addKeyboard(registeredKeyboard);
this.loadedKeyboard = null;
}
this.engine.config.deferForInitialization.then(() => {
if(!this.engine.keyboardRequisitioner.cache.isFetchingKeyboard(registeredKeyboard.id)) {
// Deliberate keyboard pre-loading via direct script-tag link on the page.
// Just load the keyboard and reset the harness's keyboard-receiver field.
this.engine.keyboardRequisitioner.cache.addKeyboard(registeredKeyboard);
this.loadedKeyboard = null;
}
});
}
/**
@ -89,11 +85,16 @@ export default class KeyboardInterface<ContextManagerType extends ContextManager
// https://help.keyman.com/DEVELOPER/ENGINE/WEB/2.0/guide/examples/manual-control
// (See: referenced laokeys_load.js)
const stub = new KeyboardStub(Pstub);
if(this.stubAndKeyboardCache.findMatchingStub(stub)) {
if(this.engine.keyboardRequisitioner?.cache.findMatchingStub(stub)) {
return 1;
}
this.stubAndKeyboardCache.addStub(stub);
if(!this.engine.config.deferForInitialization.hasFinalized) {
this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(stub));
} else {
this.engine.keyboardRequisitioner.cache.addStub(stub);
}
return null;
}
@ -101,8 +102,11 @@ export default class KeyboardInterface<ContextManagerType extends ContextManager
this.resetContextCache();
// As this function isn't provided a handle to an active outputTarget, we rely on
// the context manager to resolve said issue.
this.contextManager.insertText(this, Ptext, PdeadKey);
this.engine.contextManager.insertText(this, Ptext, PdeadKey);
}
// Short-hand name: necessary to do it this way due to assignment style.
KT = this.insertText;
}
(function() {

View file

@ -19,11 +19,11 @@ export default class KeymanEngine<
HardKeyboard extends HardKeyboardBase
> implements KeyboardKeymanGlobal {
readonly config: Configuration;
readonly contextManager: ContextManager;
readonly interface: KeyboardInterface<ContextManager>;
contextManager: ContextManager;
interface: KeyboardInterface<ContextManager>;
readonly core: InputProcessor;
readonly keyboardRequisitioner: KeyboardRequisitioner;
readonly modelCache: ModelCache;
keyboardRequisitioner: KeyboardRequisitioner;
modelCache: ModelCache;
protected legacyAPIEvents = new LegacyEventEmitter<LegacyAPIEvents>();
private _hardKeyboard: HardKeyboard;
@ -97,23 +97,97 @@ export default class KeymanEngine<
this.config = config;
this.contextManager = contextManager;
// Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object.
this.interface = new KeyboardInterface(window, this, this.contextManager, config.stubNamespacer);
const keyboardLoader = new KeyboardLoader(this.interface, config.applyCacheBusting);
this.keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(), this.config.paths);
this.modelCache = new ModelCache();
const kbdCache = this.keyboardRequisitioner.cache;
this.interface.setKeyboardCache(this.keyboardRequisitioner.cache);
this.interface = new KeyboardInterface(window, this, config.stubNamespacer);
this.core = new InputProcessor(config.hostDevice, worker, this.processorConfiguration());
this.core.languageProcessor.on('statechange', (state) => {
// The banner controller cannot directly trigger a layout-refresh at this time,
// so we handle that here.
this.osk.bannerController.selectBanner(state);
this.osk.refreshLayout();
this.osk?.bannerController.selectBanner(state);
this.osk?.refreshLayout();
});
this.core.keyboardProcessor.beepHandler = (target) => {
if(this.doBeep) {
this.doBeep(target);
}
}
this.contextManager.on('beforekeyboardchange', (metadata) => {
this.legacyAPIEvents.callEvent('beforekeyboardchange', {
internalName: metadata?.id,
languageCode: metadata?.langId
});
});
this.contextManager.on('keyboardchange', (kbd) => {
this.refreshModel();
this.core.activeKeyboard = kbd?.keyboard;
this.legacyAPIEvents.callEvent('keyboardchange', {
internalName: kbd?.metadata.id,
languageCode: kbd?.metadata.langId
});
// Hide OSK and do not update keyboard list if using internal keyboard (desktops).
// Condition will not be met for touch form-factors; they force selection of a
// default keyboard.
if(!kbd) {
this.osk.startHide(false);
}
if(this.osk) {
this.osk.setNeedsLayout();
this.osk.activeKeyboard = kbd;
this.osk.present();
}
});
this.contextManager.on('keyboardasyncload', (metadata) => {
/* Original implementation pre-modularization:
*
* > Force OSK display for CJK keyboards (keyboards using a pick list)
*
* A matching subcondition in the block below will ensure that the OSK activates pre-load
* for CJK keyboards. Yes, even before a CJK picker could ever show. We should be fine
* without the CJK check so long as a picker keyboard's OSK is kept activated post-load,
* when the picker actually needs to be kept persistently-active.
* `metadata` would be relevant a the CJK-check, which was based on language codes.
*
* Of course, as mobile devices don't have guaranteed physical keyboards... we need to
* keep the OSK visible for them, hence the actual block below.
*/
if(this.config.hostDevice.touchable && this.osk?.activationModel) {
this.osk.activationModel.enabled = true;
// Also note: the OSKView.mayDisable method returns false when hostDevice.touchable = false.
// The .startHide() call below will check that method before actually starting an OSK hide.
}
// Always (temporarily) hide the OSK when loading a new keyboard, to ensure
// that a failure to load doesn't leave the current OSK displayed
this.osk?.startHide(false);
});
}
async init(optionSpec: Required<InitOptionSpec>){
// There may be some valid mutations possible even on repeated calls?
// The original seems to allow it.
const config = this.config;
if(config.deferForInitialization.hasFinalized) {
// abort! Maybe throw an error, too.
return Promise.resolve();
}
config.initialize(optionSpec);
// Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object.
// All components initialized below require a properly-configured `config.paths` or similar.
const keyboardLoader = new KeyboardLoader(this.interface, config.applyCacheBusting);
this.keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(), this.config.paths);
this.modelCache = new ModelCache();
const kbdCache = this.keyboardRequisitioner.cache;
this.contextManager.configure({
resetContext: (target) => {
this.core.resetContext(target);
@ -122,12 +196,6 @@ export default class KeymanEngine<
keyboardCache: this.keyboardRequisitioner.cache
});
this.core.keyboardProcessor.beepHandler = (target) => {
if(this.doBeep) {
this.doBeep(target);
}
}
// #region Event handler wiring
this.config.on('spacebartext', () => {
// On change of spacebar-text mode, we currently need a layout refresh to update the
@ -145,6 +213,12 @@ export default class KeymanEngine<
languageCode: stub.KLC,
package: stub.KP
});
// If this is the first stub loaded, set it as active.
if(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);
}
}
if(this.config.deferForInitialization.hasFinalized) {
@ -169,61 +243,6 @@ export default class KeymanEngine<
}
});
contextManager.on('beforekeyboardchange', (metadata) => {
this.legacyAPIEvents.callEvent('beforekeyboardchange', {
internalName: metadata.id,
languageCode: metadata.langId
});
});
contextManager.on('keyboardchange', (kbd) => {
this.refreshModel();
this.core.activeKeyboard = kbd.keyboard;
this.legacyAPIEvents.callEvent('keyboardchange', {
internalName: kbd.metadata.id,
languageCode: kbd.metadata.langId
});
// Hide OSK and do not update keyboard list if using internal keyboard (desktops).
// Condition will not be met for touch form-factors; they force selection of a
// default keyboard.
if(kbd.keyboard == null && kbd.metadata == null) {
this.osk.startHide(false);
}
if(this.osk) {
this.osk.setNeedsLayout();
this.osk.activeKeyboard = kbd;
this.osk.present();
}
});
contextManager.on('keyboardasyncload', (metadata) => {
/* Original implementation pre-modularization:
*
* > Force OSK display for CJK keyboards (keyboards using a pick list)
*
* A matching subcondition in the block below will ensure that the OSK activates pre-load
* for CJK keyboards. Yes, even before a CJK picker could ever show. We should be fine
* without the CJK check so long as a picker keyboard's OSK is kept activated post-load,
* when the picker actually needs to be kept persistently-active.
* `metadata` would be relevant a the CJK-check, which was based on language codes.
*
* Of course, as mobile devices don't have guaranteed physical keyboards... we need to
* keep the OSK visible for them, hence the actual block below.
*/
if(this.config.hostDevice.touchable && this.osk?.activationModel) {
this.osk.activationModel.enabled = true;
// Also note: the OSKView.mayDisable method returns false when hostDevice.touchable = false.
// The .startHide() call below will check that method before actually starting an OSK hide.
}
// Always (temporarily) hide the OSK when loading a new keyboard, to ensure
// that a failure to load doesn't leave the current OSK displayed
this.osk?.startHide(false);
});
this.keyboardRequisitioner.cache.on('keyboardAdded', (keyboard) => {
this.legacyAPIEvents.callEvent('keyboardloaded', { keyboardName: keyboard.id });
});
@ -231,18 +250,6 @@ export default class KeymanEngine<
// #endregion
}
async init(optionSpec: Required<InitOptionSpec>){
// There may be some valid mutations possible even on repeated calls?
// The original seems to allow it.
if(this.config.deferForInitialization.hasFinalized) {
// abort! Maybe throw an error, too.
return Promise.resolve();
}
this.config.initialize(optionSpec);
}
public get hardKeyboard(): HardKeyboard {
return this._hardKeyboard;
}
@ -285,7 +292,7 @@ export default class KeymanEngine<
// is fully complete.
private refreshModel(): Promise<ModelSpec> {
const kbd = this.contextManager.activeKeyboard;
const model = this.modelCache.modelForLanguage(kbd.metadata.langId);
const model = this.modelCache.modelForLanguage(kbd?.metadata.langId);
if(this.core.activeModel != model) {
if(this.core.activeModel) {

View file

@ -728,10 +728,10 @@ export default abstract class OSKView extends EventEmitter<EventMap> implements
this._Box.appendChild(this.banner.element);
if(this.bannerView.banner) {
this.banner.banner.configureForKeyboard(this.keyboardData.keyboard, this.keyboardData.metadata);
this.banner.banner.configureForKeyboard(this.keyboardData?.keyboard, this.keyboardData?.metadata);
}
let kbdView: KeyboardView = this.keyboardView = this._GenerateKeyboardView(this.keyboardData.keyboard, this.keyboardData.metadata);
let kbdView: KeyboardView = this.keyboardView = this._GenerateKeyboardView(this.keyboardData?.keyboard, this.keyboardData?.metadata);
this._Box.appendChild(kbdView.element);
kbdView.postInsert();

View file

@ -1528,6 +1528,10 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
customStyle = customStyle + activeKeyboard.oskStyling;
this.styleSheet = createStyleSheet(customStyle); //Build 360
this.styleSheet.addEventListener('load', () => {
// Once any related fonts are loaded, we can re-adjust key-cap scaling.
this.refreshLayout();
})
this.styleSheetManager.linkStylesheet(this.styleSheet);
}

View file

@ -7,7 +7,16 @@ import {
import { PathConfiguration } from "keyman/engine/paths";
// TODO: is cleanup needed here, to use local paths instead?
import { CloudQueryEngine, type ErrorStub, KeyboardAPISpec, KeyboardStub, StubAndKeyboardCache, RawKeyboardStub, mergeAndResolveStubPromises } from "./index.js";
import {
CloudQueryEngine,
type ErrorStub,
KeyboardAPISpec,
KeyboardStub,
StubAndKeyboardCache,
RawKeyboardStub,
mergeAndResolveStubPromises,
toUnprefixedKeyboardId as unprefixed
} from "./index.js";
import { default as CloudRequesterInterface } from "./cloud/requesterInterface.js";
class CloudRequestEntry {
@ -158,7 +167,8 @@ export default class KeyboardRequisitioner {
}
// Requests not of string form never specify a specific version.
const querySpec = toQuerySpecs(incomplete.id, incomplete.langId);
// If an 'incomplete stub', we may have prefixed the keyboard ID - undo that!
const querySpec = toQuerySpecs(unprefixed(incomplete.id), incomplete.langId);
if(isUniqueRequest(this.cache, cloudList, querySpec)) {
cloudList.push(querySpec);
}
@ -262,8 +272,17 @@ export default class KeyboardRequisitioner {
return Promise.reject(errorStubs);
}
return this.cloudQueryEngine.keymanCloudRequest('&keyboardid='+cmd, false).then((result) => {
return mergeAndResolveStubPromises(result, errorStubs);
return this.cloudQueryEngine.keymanCloudRequest('&keyboardid='+cmd, false).then(async (result) => {
const results = await mergeAndResolveStubPromises(result, errorStubs);
for(let result of results) {
// If not an error stub...
if(typeof result['error'] == 'undefined') {
this.cache.addStub(result as KeyboardStub);
}
}
return results;
}, (err) => {
console.error(err);
let stub: ErrorStub = {error: err};
@ -272,6 +291,16 @@ export default class KeyboardRequisitioner {
});
}
async fetchCloudCatalog() {
try {
const stubs = await this.cloudQueryEngine.keymanCloudRequest('', false);
stubs.forEach((stub) => this.cache.addStub(stub));
return stubs;
} catch(error) {
return Promise.reject([{error: error}]);
}
}
/**
* Display warning if language name unavailable to add keyboard
* @param {string} languageName

View file

@ -53,7 +53,7 @@ export default class KeyboardStub extends KeyboardProperties {
let rx=RegExp('^(([\\.]/)|([\\.][\\.]/)|(/))|(:)');
arg1 = arg1 || '';
if(!rx.test(this.KF)) {
if(this.KF && !rx.test(this.KF)) {
this.KF = arg1 + this.KF;
}
} else {

View file

@ -58,6 +58,9 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
}
getKeyboard(keyboardID: string): Keyboard {
if(!keyboardID) {
return null;
}
const entry = this.keyboardTable[prefixed(keyboardID)];
// Unit testing may 'trip up' in the DOM, as bundled versions of a class from one bundled
@ -198,7 +201,9 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
keyboardID = arg0;
}
keyboardID = prefixed(keyboardID);
if(keyboardID) {
keyboardID = prefixed(keyboardID);
}
const stubTable = this.stubSetTable[keyboardID] ?? {};
@ -237,10 +242,10 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
let arr: KeyboardStub[] = [];
const kbdIds = Object.keys(this.stubSetTable);
for(let kbdId in kbdIds) {
for(let kbdId of kbdIds) {
let row = this.stubSetTable[kbdId];
const langIds = Object.keys(row);
for(let langId in langIds) {
for(let langId of langIds) {
arr.push(row[langId]);
}
}

View file

@ -283,7 +283,12 @@ describe("KeyboardRequisitioner", () => {
mockedRequester.request = swapFake;
const promise = keyboardRequisitioner.addLanguageKeyboards(['Khmer', 'Dzongkha']);
await promise;
try {
await promise;
} catch (e) {
// We didn't mock the actual query based on the language codes, but just knowing
// that a query was made, with the right parameters, is enough for us here.
}
assert.equal(swapFake.callCount, 2);
@ -333,4 +338,13 @@ describe("KeyboardRequisitioner", () => {
assert.strictEqual(cache.getKeyboardForStub(stub), khmer_angkor);
assert.isOk(khmer_angkor);
});
// TODO: unit tests for these.
describe.skip('fetchCloudStubs', () => {
it('fetches stubs for all supported cloud keyboards', () => {});
it('caches all fetched cloud stubs upon completion', () => {});
it('returns an error stub if unable to access the Cloud API', () => {});
});
});

View file

@ -23,7 +23,7 @@
</style>
<!-- Insert uncompiled KeymanWeb source scripts -->
<script src="../../../../build/app/web/debug/keymanweb.js" type="application/javascript"></script>
<script src="../../../../build/app/browser/debug/keymanweb.js" type="application/javascript"></script>
<!--
For desktop browsers, a script for the user interface must be inserted here.
@ -38,7 +38,8 @@
<script>
var kmw=window.keyman;
kmw.init({
attachType:'auto'
attachType:'auto',
resources:'../../resources'
}).then(function() {
loadKeyboards();
});