Skip to content

Commit

Permalink
Merge pull request #8458 from keymanapp/change/web/engine-configuration
Browse files Browse the repository at this point in the history
change(web): facilitates differentiated configuration objects for KMW variants 🧩
  • Loading branch information
jahorton authored Mar 27, 2023
2 parents 7259157 + d2f4e98 commit 40c6c14
Show file tree
Hide file tree
Showing 26 changed files with 308 additions and 258 deletions.
2 changes: 1 addition & 1 deletion common/test/resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"@keymanapp/resources-gosh": "*",
"@types/node": "^10.17.21",
"chai": "^4.3.4",
"typescript": "^4.5.4"
"typescript": "^4.9.5"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { ManagedPromise } from '@keymanapp/web-utils';

export class DOMKeyboardLoader extends KeyboardLoaderBase {
public readonly element: HTMLIFrameElement;
private readonly performCacheBusting: boolean;

constructor()
constructor(harness: KeyboardHarness);
constructor(harness?: KeyboardHarness) {
constructor(harness: KeyboardHarness, cacheBust?: boolean)
constructor(harness?: KeyboardHarness, cacheBust?: boolean) {
if(harness && harness._jsGlobal != window) {
// Copy the String typing over; preserve string extensions!
harness._jsGlobal['String'] = window['String'];
Expand All @@ -22,11 +24,17 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
} else {
super(harness);
}

this.performCacheBusting = cacheBust || false;
}

protected loadKeyboardInternal(uri: string): Promise<Keyboard> {
const promise = new ManagedPromise<Keyboard>();

if(this.performCacheBusting) {
uri = this.cacheBust(uri);
}

try {
const document = this.harness._jsGlobal.document;
const script = document.createElement('script');
Expand Down Expand Up @@ -55,4 +63,11 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {

return promise.corePromise;
}

private cacheBust(uri: string) {
// Our WebView version directly sets the keyboard path, and it may replace the file
// after KMW has loaded. We need cache-busting to prevent the new version from
// being ignored.
return uri + "?v=" + (new Date()).getTime(); /*cache buster*/
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion web/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ builder_parse "$@"
# We can run all clean & configure actions at once without much issue.

builder_run_child_actions clean
builder_run_child_actions configure

## Clean actions

Expand All @@ -69,6 +68,8 @@ if builder_start_action clean; then
builder_finish_action success clean
fi

builder_run_child_actions configure

## Build actions

builder_run_child_actions build:engine/device-detect
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/browser/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ builder_describe "Builds the Keyman Engine for Web's website-integrating version

builder_describe_outputs \
configure /node_modules \
build /web/$SUBPROJECT_NAME/lib/index.mjs
build /web/build/$SUBPROJECT_NAME/lib/index.js

builder_parse "$@"

Expand Down
37 changes: 37 additions & 0 deletions web/src/app/browser/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { EngineConfiguration, InitOptionSpec, InitOptionDefaults } from "keyman/engine/main";

export class BrowserConfiguration extends EngineConfiguration {
private _ui: string;
private _attachType: string;

initialize(options: Required<BrowserInitOptionSpec>) {
this.initialize(options);

this._ui = options.ui;
this._attachType = options.attachType;
}

get attachType() {
return this._attachType;
}

debugReport(): Record<string, any> {
const baseReport = super.debugReport();
baseReport.attachType = this.attachType;
baseReport.ui = this._ui;
baseReport.keymanEngine = 'app/browser';

return baseReport;
}
}

export interface BrowserInitOptionSpec extends InitOptionSpec {
ui?: string;
attachType?: 'auto' | 'manual' | ''; // If blank or undefined, attachType will be assigned to "auto" or "manual"
}

export const BrowserInitOptionDefaults: Required<BrowserInitOptionSpec> = {
ui: '',
attachType: '',
...InitOptionDefaults
}
7 changes: 3 additions & 4 deletions web/src/app/browser/src/keymanEngine.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { KeymanEngine as KeymanEngineBase } from 'keyman/engine/main';
import { EngineConfiguration, KeymanEngine as KeymanEngineBase } from 'keyman/engine/main';
import { ProcessorInitOptions } from "@keymanapp/keyboard-processor";
import { Configuration } from "keyman/engine/configuration";

import ContextManager from './contextManager.js';
import DefaultOutput from './defaultOutput.js';
import KeyEventKeyboard from './keyEventKeyboard.js';

export class KeymanEngine extends KeymanEngineBase<ContextManager, KeyEventKeyboard> {
constructor(config: Configuration, worker: Worker) {
super(config, worker, new ContextManager());
constructor(worker: Worker, config: EngineConfiguration) {
super(worker, config, new ContextManager());
}

protected processorConfiguration(): ProcessorInitOptions {
Expand Down
87 changes: 1 addition & 86 deletions web/src/app/embed/kmwembedded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,90 +9,15 @@

(function() {
// Declare KeymanWeb and related objects
var keymanweb=window['keyman'], util=keymanweb['util'],device=util.device;
var keymanweb=window['keyman'], util=keymanweb['util'];
var dom = com.keyman.dom;

// Allow definition of application name
keymanweb.options['app']='';

// Flag to control refreshing of a keyboard that is already loaded
keymanweb.mustReloadKeyboard = true;

// Skip full page initialization - skips native-mode only code
keymanweb.isEmbedded = true;

// Set default device options
keymanweb.setDefaultDeviceOptions = function(opt: com.keyman.OptionType) {
opt['attachType'] = 'manual';
device.app=opt['app'];
device.touchable=true;
device.formFactor = device.app.indexOf('Tablet') >= 0 ? 'tablet' : 'phone';
device.browser='native';
};

// Get default style sheet path
keymanweb.getStyleSheetPath = function(ssName) {
return keymanweb.rootPath+ssName;
};

keymanweb.linkStylesheetResources = function() {
const keyman = keymanweb as KeymanBase;
let util = keyman.util;

// Install the globe-hint stylesheet.
util.linkStyleSheet(keymanweb.getStyleSheetPath('globe-hint.css'));

// For now, the OSK will handle linking of the main OSK stylesheet separately.
}

// Get KMEI, KMEA keyboard path (overrides default function, allows direct app control of paths)
keymanweb.getKeyboardPath = function(Lfilename, packageID) {
return Lfilename + "?v=" + (new Date()).getTime(); /*cache buster*/
};

// Establishes keyboard namespacing.
keymanweb.namespaceID = function(Pstub) {
if(typeof(Pstub['KP']) != 'undefined') {
// An embedded use case wants to utilize package-namespacing.
Pstub['KI'] = Pstub['KP'] + "::" + Pstub['KI'];
}
}

// In conjunction with the KeyboardManager's installKeyboard method and script IDs, preserves a keyboard's
// namespaced ID.
keymanweb.preserveID = function(Pk) {
var trueID;

// Find the currently-executing script tag; KR is called directly from each keyboard's definition script.
if(document.currentScript) {
trueID = document.currentScript.id;
} else {
var scripts = document.getElementsByTagName('script');
var currentScript = scripts[scripts.length-1];

trueID = currentScript.id;
}

// Final check that the script tag is valid and appropriate for the loading keyboard.
if(trueID.indexOf(Pk['KI']) != -1) {
Pk['KI'] = trueID; // Take the script's version of the ID, which may include package namespacing.
} else {
console.error("Error when registering keyboard: current SCRIPT tag's ID does not match!");
}
}

/**
* Force reload of resource
*
* @param {string} s unmodified URL
* @return {string} modified URL
*/
util.unCached = function(s) {
var t=(new Date().getTime());
s = s + '?v=' + t;
return s;
};

util.wait = function() {
// Empty stub - this function should not be implemented or used within embedded code routes.
console.warn("util.wait() call attempted in embedded mode!"); // Sends log message to embedding app.
Expand All @@ -103,16 +28,6 @@
console.warn("util.alert() call attempted in embedded mode!"); // Sends log message to embedding app.
};

/**
* Refresh element content after change of text (if required)
*
* @param {Object} Pelem input element
*/
keymanweb.refreshElementContent = function(Pelem)
{
if('ontextchange' in keymanweb) keymanweb['ontextchange'](Pelem);
};

/**
* Set target element text direction (LTR or RTL): not functional for KMEI, KMEA
*
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/webview/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ builder_describe "Builds the Keyman Engine for Web's puppetable version designed

builder_describe_outputs \
configure /node_modules \
build /web/$SUBPROJECT_NAME/lib/index.mjs
build /web/build/$SUBPROJECT_NAME/lib/index.js

builder_parse "$@"

Expand Down
35 changes: 35 additions & 0 deletions web/src/app/webview/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { EngineConfiguration, InitOptionSpec, InitOptionDefaults } from "keyman/engine/main";

export class WebviewConfiguration extends EngineConfiguration {
private _embeddingApp: string;

initialize(options: Required<WebviewInitOptionSpec>) {
this.initialize(options);

this._embeddingApp = options.embeddingApp;
}

get embeddingApp() {
return this._embeddingApp;
}

debugReport(): Record<string, any> {
const baseReport = super.debugReport();
baseReport.embeddingApp = this.embeddingApp;
baseReport.keymanEngine = 'app/webview';

return baseReport;
}
}

export interface WebviewInitOptionSpec extends InitOptionSpec {
/**
* May be used to denote the name of the embedding application
*/
embeddingApp?: string;
}

export const WebviewInitOptionDefaults: Required<WebviewInitOptionSpec> = {
embeddingApp: '',
...InitOptionDefaults
}
41 changes: 31 additions & 10 deletions web/src/app/webview/src/keymanEngine.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
import { DeviceSpec } from '@keymanapp/keyboard-processor'
import { KeymanEngine as KeymanEngineBase } from 'keyman/engine/main';
import { Configuration } from "keyman/engine/configuration";
import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk';
import { toPrefixedKeyboardId, toUnprefixedKeyboardId } from 'keyman/engine/keyboard-cache';

import { WebviewConfiguration, WebviewInitOptionDefaults, WebviewInitOptionSpec } from './configuration.js';
import ContextManager from './contextManager.js';
import PassthroughKeyboard from './passthroughKeyboard.js';
import { buildEmbeddedGestureConfig, setupEmbeddedListeners } from './oskConfiguration.js';

export class KeymanEngine extends KeymanEngineBase<ContextManager, PassthroughKeyboard> {
constructor(config: Configuration, worker: Worker) {
// TODO: set the old `namespacedID` function in a new Configuration property
// for use within `KeyboardInterface.registerStub` when available.
//
// Or... just build it in and configure via boolean flag?
super(config, worker, new ContextManager());
// Ideally, we would be able to auto-detect `sourceUri`: https://stackoverflow.com/a/60244278.
// But it's too new of a feature to utilize... and also expects to be in a module, when this may
// be compiled down to an IIFE.
constructor(worker: Worker, sourceUri: string) {
const config = new WebviewConfiguration(sourceUri); // currently set to perform device auto-detect.
config.stubNamespacer = (stub) => {
// If the package has not yet been applied as namespacing...
if(stub.KP && stub.KI.indexOf(`${stub.KP}::`) == -1) {
// Apply namespacing. To make 100% sure that we don't muck up internal prefixing,
// we ensure it is applied consistently in the manner specified below.
stub.KI = toPrefixedKeyboardId(`${stub.KP}::${toUnprefixedKeyboardId(stub.KI)}`);
}
}

super(worker, config, new ContextManager());

this.hardKeyboard = new PassthroughKeyboard(config.hardDevice);
}

initialize() {
super.initialize();
init(options: Required<WebviewInitOptionSpec>) {
let device = new DeviceSpec(
'native',
options.embeddingApp.indexOf('Tablet') >= 0 ? 'tablet' : 'phone',
this.config.hostDevice.OS,
true
);

this.config.hostDevice = device;

super.init({...WebviewInitOptionDefaults, ...options});

const oskConfig: ViewConfiguration = {
hostDevice: this.config.hostDevice,
pathConfig: this.config.paths,
// When hosted in a WebView, we never hide the Web OSK without hiding the hosting WebView.
activator: new StaticActivator(),
embeddedGestureConfig: buildEmbeddedGestureConfig(this.config.softDevice)
embeddedGestureConfig: buildEmbeddedGestureConfig(this.config.softDevice),
doCacheBusting: true
}

this.osk = new AnchoredOSKView(oskConfig);
Expand Down
Loading

0 comments on commit 40c6c14

Please sign in to comment.