mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-11 11:25:34 +00:00
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:
commit
3655b9c3dc
29 changed files with 502 additions and 541 deletions
|
|
@ -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
|
||||
```
|
||||
13
web/build.sh
13
web/build.sh
|
|
@ -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
|
||||
|
||||
|
|
|
|||
11
web/ci.sh
11
web/ci.sh
|
|
@ -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 ..
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
15
web/src/app/browser/src/debug-main.ts
Normal file
15
web/src/app/browser/src/debug-main.ts
Normal 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);
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
import ContextManager from './contextManager.js';
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
15
web/src/app/browser/src/release-main.ts
Normal file
15
web/src/app/browser/src/release-main.ts
Normal 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);
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
15
web/src/app/browser/src/utils/documentReady.ts
Normal file
15
web/src/app/browser/src/utils/documentReady.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue