-
-
Notifications
You must be signed in to change notification settings - Fork 103
/
kmwembedded.ts
426 lines (367 loc) · 15.6 KB
/
kmwembedded.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// Since 'web' compilation is the path recognized by VSCode, we need to make references here to prevent TS errors.
// References the base Keyman object (and consequently, the rest of the core objects).
/// <reference path="kmwbase.ts" />
/// <reference path="osk/embedded/keytip.ts" />
/// <reference path="osk/embedded/pendingLongpress.ts" />
// KeymanWeb 11.0
// Copyright 2019 SIL International
/*****************************************/
/* */
/* Embedded application-specific code */
/* */
/*****************************************/
namespace com.keyman.osk {
VisualKeyboard.prototype.optionKey = function(this: VisualKeyboard, e: KeyElement, keyName: string, keyDown: boolean) {
let keyman = com.keyman.singleton;
if(keyName.indexOf('K_LOPT') >= 0) {
if(keyDown) {
this.menuEvent = e;
if(typeof keyman['showKeyboardList'] == 'function') {
keyman['showKeyboardList']();
}
} else {
if(this.menuEvent) {
this.highlightKey(this.menuEvent, false);
}
if(typeof(window['menuKeyUp']) == 'function') {
window['menuKeyUp']();
}
this.menuEvent = null;
}
} else if(keyName.indexOf('K_ROPT') >= 0) {
if(keyDown) {
this.highlightKey(e,false);
if(typeof keyman['hideKeyboard'] == 'function') {
keyman['hideKeyboard']();
}
}
}
};
// iOS now relies upon native-mode popup key management, so we only implement these hybrid-targeted
// methods when embedding in Android.
let device = com.keyman.singleton.util.device;
if(device.OS == 'Android') { // assumption - if this file is being loaded, keyman.isEmbedded == true.
// Send the subkey array to iOS, with centre,top of base key position
/**
* Create a popup key array natively
*
* @param {Object} key base key element
*/
VisualKeyboard.prototype.startLongpress = function(this: VisualKeyboard, key: KeyElement): PendingGesture {
if(typeof(window['oskCreatePopup']) == 'function') {
var xBase = dom.Utils.getAbsoluteX(key) - dom.Utils.getAbsoluteX(this.kbdDiv) + key.offsetWidth/2,
yBase = dom.Utils.getAbsoluteY(key);
// #3718: No longer prepend base key to subkey array
window['oskCreatePopup'](key['subKeys'], xBase, yBase, key.offsetWidth, key.offsetHeight);
return new embedded.PendingLongpress(this, key);
} else {
// When embedded within our Android app, we expect the `oskCreatePopup` function to
// exist; all subkey control is delegated to the app.
//
// No function = big problem.
console.error("Missing `oskCreatePopup` function for engine integration.");
return null;
}
};
// Create a keytip (dummy call - actual keytip handled by native code)
VisualKeyboard.prototype.createKeyTip = function(this: VisualKeyboard) {
if(com.keyman.singleton.util.device.formFactor == 'phone') {
this.keytip = new osk.embedded.KeyTip(window['oskCreateKeyPreview'], window['oskClearKeyPreview']);
}
};
}
SuggestionManager.prototype.platformHold = function(this: SuggestionManager, suggestionObj: BannerSuggestion, isCustom: boolean) {
// Parallels VisualKeyboard.prototype.touchHold, but for predictive suggestions instead of keystrokes.
let suggestionEle = suggestionObj.div;
// Need to know if it's a <keep> option or not!
var xBase = dom.Utils.getAbsoluteX(suggestionEle) - dom.Utils.getAbsoluteX(suggestionEle.parentElement) + suggestionEle.offsetWidth/2,
yBase = dom.Utils.getAbsoluteY(suggestionEle) - dom.Utils.getAbsoluteY(suggestionEle.parentElement);
window['suggestionPopup'](suggestionObj.suggestion, isCustom, xBase, yBase, suggestionEle.offsetWidth, suggestionEle.offsetHeight);
}
}
namespace com.keyman.text {
let nativeForBaseKeys = DefaultOutput.forBaseKeys;
// Overrides the 'native'-mode implementation with in-app friendly defaults prioritized over 'native' defaults.
DefaultOutput.forBaseKeys = function(Lkc: KeyEvent): string {
let Codes = com.keyman.text.Codes;
let code = Lkc.Lcode;
// Intentionally not assigning K_TAB or K_ENTER so KMW will pass them back
// to the mobile apps to handle (insert characters or navigate forms).
if (code == Codes.keyCodes.K_oE2) {
// Using defaults of English US layout for the 102nd key
if (Lkc.Lmodifiers == Codes.modifierCodes['SHIFT']) {
return '|';
} else {
return '\\';
}
}
// Use 'native'-mode defaults, determining the character from the OSK
return nativeForBaseKeys(Lkc);
}
}
(function() {
// Declare KeymanWeb and related objects
var keymanweb=window['keyman'], util=keymanweb['util'],device=util.device;
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='phone';
if(navigator && navigator.userAgent && navigator.userAgent.indexOf('iPad') >= 0) device.formFactor='tablet';
if(device.app.indexOf('Mobile') >= 0) device.formFactor='phone';
if(device.app.indexOf('Tablet') >= 0) device.formFactor='tablet';
device.browser='native';
};
// Get default style sheet path
keymanweb.getStyleSheetPath = function(ssName) {
return keymanweb.rootPath+ssName;
};
// 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.
};
util.alert = function() {
// Empty stub - this function should not be implemented or used within embedded code routes.
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
*
* @param {Object} Ptarg Target element
*/
keymanweb.domManager._SetTargDir = function(Ptarg){};
/**
* Use rotation events to adjust OSK element positions and scaling if necessary
*/
keymanweb.handleRotationEvents = function() {};
/**
* Caret position always determined from the active (but hidden) element
*
* @return {boolean}
**/
keymanweb.isPositionSynthesized = function()
{
return false;
};
/**
* correctOSKTextSize handles rotation event -- currently rebuilds keyboard and adjusts font sizes
*/
keymanweb['correctOSKTextSize']=function() {
keymanweb.osk?.refreshLayout();
};
/**
* Register a lexical model
*
* @param {com.keyman.text.prediction.ModelSpec} model Spec of the lexical model
*/
keymanweb['registerModel']=function(model: com.keyman.text.prediction.ModelSpec) {
keymanweb.modelManager.register(model);
};
/**
* Function called by Android and iOS when a device-implemented keyboard popup
* is displayed or hidden. As this is controlled by the app, we use it as a
* trigger for 'embedded'-mode gesture state management.
*
* @param {boolean} isVisible
*
**/
keymanweb['popupVisible'] = function(isVisible) {
let osk = keymanweb.osk;
let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator;
let pendingLongpress = osk.vkbd.pendingSubkey;
/*
* If a longpress popup was visible, but is no longer, this means that the
* associated longpress gesture was cancelled. It is possible for the base
* key to emit if selected at this time; detection of this is managed by
* the `SubkeyDelegator` class.
*/
if(!isVisible) {
if(gesture) {
gesture.resolve(null);
osk.vkbd.subkeyGesture = null;
} else if(pendingLongpress) {
pendingLongpress.cancel();
osk.vkbd.pendingSubkey = null;
}
}
/*
* If the popup was not visible, but now is, that means our previously-pending
* longpress is now 'realized' (complete). The OSK relies upon this state
* information, which will be properly updated by `resolve`.
*
* Prominent uses of such state info helps prevent change of base key, key
* previews, and key output from occurring while a subkey popup remains active.
*/
if(isVisible && pendingLongpress) {
// Fulfills the first-stage promise.
pendingLongpress.resolve();
}
};
/**
* Return position of language menu key to KeymanTouch
*
* @return {string} comma-separated x,y,w,h of language menu key
*
**/
keymanweb['touchMenuPos'] = function() {
let osk = keymanweb.osk;
if(osk == null || osk.vkbd == null || osk.vkbd.lgKey == null) {
return '';
}
var key: HTMLElement = osk.vkbd.lgKey;
// A CSS change of kmd-key-square from position:fixed to position:static was needed
// for Android 4.3 to display the OSK correctly, but resulted in the position of
// the menu key not being returned correctly. The following line gets the
// key element, instead of the key-square element, fixes this. It should be
// removed again when the key-square elements are all removed as planned.
if(typeof key.firstChild != 'undefined' && key.firstChild != null && util.hasClass(key.firstChild,'kmw-key')) {
key = <HTMLElement> key.firstChild;
}
var w=key.offsetWidth,
h=key.offsetHeight,
// Since the full OSKManager '_Box' is displayed within the keyboards' WebViews,
// these calculations should be performed with respect to that, rather than osk.vkbd.kbdDiv.
x=dom.Utils.getAbsoluteX(key) - dom.Utils.getAbsoluteX(osk._Box) + w/2,
y=dom.Utils.getAbsoluteY(key) - dom.Utils.getAbsoluteY(osk._Box);
return x+','+y+','+w+','+h;
};
/**
* Accept an external key ID (from KeymanTouch) and pass to the keyboard mapping
*
* @param {string} keyName key identifier which could contain a display layer and a "functional" layer
* e.g: 'shift-K_E+rightalt-shift'
**/
keymanweb['executePopupKey'] = function(keyName: string) {
let osk = keymanweb.osk;
var origArg = keyName;
if(!keymanweb.core.activeKeyboard || !osk.vkbd) {
return false;
}
/* Clear any pending (non-popup) key */
osk.vkbd.keyPending = null;
// Changes for Build 353 to resolve KMEI popup key issues
keyName=keyName.replace('popup-',''); //remove popup prefix if present (unlikely)
// Regex for 'display layer'-'virtual key name'+'optional functional layer'
// Can't just split on '-' because some layers like ctrl-shift contain it.
// Virtual key name starts with T_, K_, or U_
// matches[1]: displayLayer (not used)
// matches[2]: keyId
// matches[3]: optional functionalLayer
let matches = keyName.match(/^(.+)-([TKU]_[^+]+)\+?(.+)?$/);
if (matches == null) {
return false;
}
keyName = matches[2] + (matches[3] ? '+' + matches[3] : '');
// Note: this assumes Lelem is properly attached and has an element interface.
// Currently true in the Android and iOS apps.
var Lelem=keymanweb.domManager.lastActiveElement;
keymanweb.domManager.initActiveElement(Lelem);
// This should be set if we're within this method... but it's best to guard against nulls here, just in case.
if(osk.vkbd.subkeyGesture) {
let gesture = osk.vkbd.subkeyGesture as com.keyman.osk.embedded.SubkeyDelegator;
gesture.resolve(keyName);
osk.vkbd.subkeyGesture = null;
} else {
console.warn("No base key exists for the subkey being executed: '" + origArg + "'");
}
};
/**
* API endpoint for hardware keystroke events from Android external keyboards
*
* @param {number} code key identifier
* @param {number} shift shift state (0x01=left ctrl 0x02=right ctrl 0x04=left alt 0x08=right alt
* 0x10=shift 0x20=ctrl 0x40=alt)
* @param {number} lstates lock state (0x0200=no caps 0x0400=num 0x0800=no num 0x1000=scroll 0x2000=no scroll locks)
* @return {boolean} false when KMW _has_ fully handled the event and true when not.
**/
keymanweb['executeHardwareKeystroke'] = function(code, shift, lstates = 0): boolean {
if(!keymanweb.core.activeKeyboard || code == 0) {
return false;
}
// Clear any pending (non-popup) key
keymanweb.osk.vkbd.keyPending = null;
// Note: this assumes Lelem is properly attached and has an element interface.
// Currently true in the Android and iOS apps.
var Lelem = keymanweb.domManager.lastActiveElement;
keymanweb.domManager.initActiveElement(Lelem);
// Check the virtual key
var Lkc: com.keyman.text.KeyEvent = {
Lmodifiers: shift,
vkCode: code,
Lcode: code,
Lstates: lstates,
LisVirtualKey: true,
kName: '',
device: keymanweb.util.physicalDevice.coreSpec, // As we're executing a hardware keystroke.
isSynthetic: false
};
try {
// Now that we've manually constructed a proper keystroke-sourced KeyEvent, pass control
// off to the processor for its actual execution.
// Should return 'false' when KMW _has_ fully handled the event and 'true' when not.
const ruleBehavior: com.keyman.text.RuleBehavior = keymanweb.core.processKeyEvent(Lkc, com.keyman.dom.Utils.getOutputTarget(Lelem));
return !ruleBehavior || ruleBehavior.triggerKeyDefault;
} catch (err) {
console.error(err.message, err);
return false;
}
};
})();