Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change(web): facilitates differentiated configuration objects for KMW variants 🧩 #8458

Merged
merged 12 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

9 changes: 8 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,14 @@ if builder_start_action clean; then
builder_finish_action success clean
fi

# Do not call child actions for configure - they all do the same thing, and it can take a while.

if builder_start_action configure; then
verify_npm_setup

builder_finish_action success configure
fi
jahorton marked this conversation as resolved.
Show resolved Hide resolved

## 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
}
35 changes: 25 additions & 10 deletions web/src/app/webview/src/keymanEngine.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't you just search for ::? Although wouldn't it be better for the namespace to be separated from KI rather than mutating input data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although wouldn't it be better for the namespace to be separated from KI rather than mutating input data?

I could see an argument for that, but the pre-modular namespacing system worked by prepending the package name as part of the ID. I opted to maintain that.

There's then the issue of Package_id::Keyboard_kbd_id vs Keyboard_Package_id::kbd_id due to the whole Keyboard_ ID-prefixing prepend bit that's been in KMW since forever. The function highlighted in the code block above is designed to ensure that it's always the former pattern and never the latter by detecting the latter and converting it over while leaving the former intact.

Though... I'm not 100% sure that Package-prepended keyboards actually use the Keyboard_ bit too in the existing implementation.

Copy link
Contributor Author

@jahorton jahorton Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmation for that last point...

(from the current master)

// Skip on embedded which namespaces packageID::Keyboard_keyboardID

Contrast with...

(also from the current master)

// 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'];
}
}

Any incoming keyboard IDs are already mutated to have a prepended package ID as part of the keyboard's ID itself.

We need to make sure that the Keyboard_ applies at the location we want, hence the if-condition seen above.

// 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());
Comment on lines +15 to +26
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block best showcases the main reason for the change in constructor parameter ordering: app/webview (and eventually app/browser) will construct their own Configuration object in their respective KeymanEngine constructor. Each derived-class constructor may take additional parameters, but the first parameter - for worker - will be consistent for both.

The reason: Worker can't be specified at this level if we wish to provide separate debug vs release build products. The non-minified + sourcemapped variant of the worker should be used for debug, while the minified + unsourcemapped variant would be used for release. These will be specified by the top-level entry point for both debug and release esbuild bundling configurations.


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

initialize() {
super.initialize();
init(options: Required<WebviewInitOptionSpec>) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay, a keyman.init for the modularized app/webview! Well, the start of one, anyway...

let device = new DeviceSpec('native', options.embeddingApp.indexOf('Tablet') >= 0 ? 'tablet' : 'phone', this.config.hostDevice.OS, true);
jahorton marked this conversation as resolved.
Show resolved Hide resolved
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