Skip to content

Commit 79fd7c8

Browse files
committed
fix(web): add a fetch keyboard function
This change adds a `fetchKeyboardFunc` to `DOMKeyboardLoader` that allows mobile platforms to use a different method to fetch the keyboard data. WebView's `KeymanEngine` implements fetching the URL by using `XMLHttpRequest`.
1 parent 0fd21c0 commit 79fd7c8

4 files changed

Lines changed: 48 additions & 38 deletions

File tree

web/src/app/webview/src/keymanEngine.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DeviceSpec } from 'keyman/common/web-utils';
2-
import { DefaultOutputRules, ProcessorAction } from 'keyman/engine/keyboard';
2+
import { DefaultOutputRules, DOMKeyboardLoader, ProcessorAction } from 'keyman/engine/keyboard';
33
import { KeymanEngineBase, KeyboardInterfaceBase } from 'keyman/engine/main';
44
import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk';
55
import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';
@@ -42,6 +42,10 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
4242
this.hardKeyboard = new PassthroughKeyboard(config.hardDevice);
4343
}
4444

45+
protected createKeyboardLoader(): DOMKeyboardLoader {
46+
return new DOMKeyboardLoader(this.interface, this.config.applyCacheBusting, this.fetch);
47+
}
48+
4549
async init(options: Required<WebviewInitOptionSpec>) {
4650
const device = new DeviceSpec(
4751
'native',
@@ -159,4 +163,32 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
159163
get context() {
160164
return this.contextManager.activeTextStore;
161165
}
166+
167+
/**
168+
* Fetches a resource from the specified URL.
169+
*
170+
* @param uri
171+
* @returns A response promise
172+
*
173+
* @see https://stackoverflow.com/a/63582110
174+
*
175+
* Note: Using XMLHttpRequest allows us to work around the limitations of
176+
* Fetch API's fetch() which doesn't support file:// URLs. At least in
177+
* Keyman for Android we still have to explicitly allow file URLs.
178+
*/
179+
private fetch(uri: string): Promise<Response> {
180+
return new Promise(function (resolve, reject) {
181+
const httpRequest = new XMLHttpRequest();
182+
httpRequest.onload = function () {
183+
resolve(new Response(httpRequest.response, { status: httpRequest.status }));
184+
};
185+
httpRequest.onerror = (e) => {
186+
reject(e);
187+
};
188+
httpRequest.open('GET', uri);
189+
httpRequest.responseType = "arraybuffer";
190+
httpRequest.send(null);
191+
});
192+
};
193+
162194
}

web/src/engine/src/keyboard/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export { KeyMapping } from "./keyMapping.js";
3232
export { type SystemStoreMutationHandler, MutableSystemStore, SystemStore, SystemStoreIDs, type SystemStoreDictionary } from "./systemStore.js";
3333
export { type VariableStores, VariableStoreSerializer } from "./variableStore.js";
3434

35-
export { DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js';
35+
export { type FetchFunction, DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js';
3636
export { SyntheticTextStore } from "./syntheticTextStore.js";
3737
export { TextStore } from "./textStore.js";
3838
export { TextStoreLanguageProcessorInterface } from "./textStoreLanguageProcessorInterface.js";

web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js';
77
import { KeyboardLoaderBase } from '../keyboardLoaderBase.js';
88
import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js';
99

10+
export type FetchFunction = (uri: string) => Promise<Response>;
11+
1012
export class DOMKeyboardLoader extends KeyboardLoaderBase {
1113
public readonly element: HTMLIFrameElement;
1214
private readonly performCacheBusting: boolean;
15+
private readonly fetchKeyboardFunc: FetchFunction;
1316

1417
constructor()
1518
constructor(harness: KeyboardHarness);
16-
constructor(harness: KeyboardHarness, cacheBust?: boolean)
17-
constructor(harness?: KeyboardHarness, cacheBust?: boolean) {
19+
constructor(harness: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction)
20+
constructor(harness?: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction) {
1821
if(harness && harness._jsGlobal != window) {
1922
// Copy the String typing over; preserve string extensions!
2023
harness._jsGlobal['String'] = window['String'];
@@ -27,47 +30,18 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
2730
}
2831

2932
this.performCacheBusting = cacheBust || false;
33+
const defaultFetchFunc: FetchFunction = (uri) => fetch(uri);
34+
this.fetchKeyboardFunc = fetchKeyboardFunc ?? defaultFetchFunc;
3035
}
3136

32-
/**
33-
* Fetches a resource from the specified URL. This replaces the Fetch API function
34-
* fetch() which doesn't work with file:// URLs.
35-
*
36-
* @param uri
37-
* @returns A response promise
38-
*
39-
* @see https://stackoverflow.com/a/63582110
40-
*
41-
* Note: Even with this function some browsers like Chrome will still block
42-
* file:// because of CORS, so where possible we shouldn't use file:// but
43-
* serve the files through a HTTP server. In the Keyman for Android and iOS
44-
* apps however we don't want to do this and this function allows us to work
45-
* around the limitation of the Fetch API fetch(). On Android we still have to
46-
* explicitly allow file URLs.
47-
*/
48-
private fetch(uri: string): Promise<Response> {
49-
return new Promise(function (resolve, reject) {
50-
const httpRequest = new XMLHttpRequest();
51-
httpRequest.onload = function () {
52-
resolve(new Response(httpRequest.response, { status: httpRequest.status }));
53-
};
54-
httpRequest.onerror = (e) => {
55-
reject(e);
56-
};
57-
httpRequest.open('GET', uri);
58-
httpRequest.responseType = "arraybuffer";
59-
httpRequest.send(null);
60-
});
61-
};
62-
6337
protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array> {
6438
if (this.performCacheBusting) {
6539
uri = this.cacheBust(uri);
6640
}
6741

6842
let response: Response;
6943
try {
70-
response = await this.fetch(uri);
44+
response = await this.fetchKeyboardFunc(uri);
7145
} catch (e) {
7246
throw errorBuilder.keyboardDownloadError(e);
7347
}

web/src/engine/src/main/keymanEngineBase.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ export class KeymanEngineBase<
225225
});
226226
}
227227

228+
protected createKeyboardLoader(): DOMKeyboardLoader {
229+
return new DOMKeyboardLoader(this.interface, this.config.applyCacheBusting);
230+
}
231+
228232
public async init(optionSpec: Required<InitOptionSpec>){
229233
// There may be some valid mutations possible even on repeated calls?
230234
// The original seems to allow it.
@@ -242,7 +246,7 @@ export class KeymanEngineBase<
242246

243247
// Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object.
244248
// All components initialized below require a properly-configured `config.paths` or similar.
245-
const keyboardLoader = new DOMKeyboardLoader(this.interface, config.applyCacheBusting);
249+
const keyboardLoader = this.createKeyboardLoader();
246250
this.keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(), this.config.paths);
247251
this.modelCache = new ModelCache();
248252
const kbdCache = this.keyboardRequisitioner.cache;
@@ -598,4 +602,4 @@ export class KeymanEngineBase<
598602
};
599603
}
600604

601-
// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed.
605+
// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed.

0 commit comments

Comments
 (0)