mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-12 17:07:31 +00:00
1770 lines
62 KiB
TypeScript
1770 lines
62 KiB
TypeScript
/// <reference path="../kmwbase.ts" />
|
|
|
|
namespace com.keyman.keyboards {
|
|
class CloudRequestEntry {
|
|
id: string;
|
|
language?: string;
|
|
version?: string;
|
|
|
|
constructor(id: string, language?: string) {
|
|
this.id = id;
|
|
this.language = language;
|
|
}
|
|
|
|
toString(): string {
|
|
var kbid=this.id;
|
|
var lgid='';
|
|
var kvid='';
|
|
|
|
if(this.language) {
|
|
kbid=kbid+'@'+this.language;
|
|
if(this.version) {
|
|
kbid=kbid+'@'+this.version;
|
|
}
|
|
} else {
|
|
if(this.version) {
|
|
kbid=kbid+'@@'+this.version;
|
|
}
|
|
}
|
|
|
|
//TODO: add specifier validation...
|
|
|
|
return kbid;
|
|
}
|
|
}
|
|
|
|
class KeyboardFont {
|
|
'family': string;
|
|
'files': string;
|
|
'path': string;
|
|
|
|
constructor(fontObj: any, fontPath: string) {
|
|
this['family'] = fontObj['family'];
|
|
this['files'] = fontObj['source'];
|
|
this['path'] = fontPath;
|
|
}
|
|
}
|
|
|
|
export class KeyboardStub {
|
|
'KI': string;
|
|
'KLC': string;
|
|
|
|
'KN': string;
|
|
'KL': string;
|
|
'KR': string;
|
|
'KRC': string;
|
|
|
|
'KF': string;
|
|
|
|
'KFont': KeyboardFont;
|
|
'KOskFont': KeyboardFont;
|
|
|
|
'displayName': string; // Display name as shown on spacebar
|
|
|
|
// Used when loading a stub's keyboard.
|
|
asyncLoader?: any;
|
|
|
|
constructor(id: string, langCode: string) {
|
|
this['KI'] = 'Keyboard_' + id;
|
|
this['KLC'] = langCode;
|
|
}
|
|
|
|
/**
|
|
* Utility to convert stubs to KeyboardStub[]
|
|
* @param arg
|
|
* @returns (KeyboardStub|ErrorStub)[]
|
|
*/
|
|
public static toStubs(arg: any): (KeyboardStub|ErrorStub)[] {
|
|
let errorMsg: string = '';
|
|
if (!arg) {
|
|
errorMsg = "Stub undefined";
|
|
} else if (!arg.id) {
|
|
errorMsg = "KeyboardStub has undefined id";
|
|
} else if (!arg.languages) {
|
|
errorMsg = "KeyboardStub has undefined languages"
|
|
}
|
|
if (errorMsg != '') {
|
|
return [{error: new Error(errorMsg)}];
|
|
}
|
|
|
|
// Extract all the languages
|
|
let languages: any[] = [];
|
|
if (typeof arg.languages === 'object') {
|
|
languages.push(arg.languages);
|
|
} else {
|
|
arg.languages.foreach(language => {
|
|
languages.push(language);
|
|
});
|
|
}
|
|
|
|
let stubs: KeyboardStub[] = [];
|
|
languages.forEach(language => {
|
|
let stub: KeyboardStub = new KeyboardStub(arg.id, language.id);
|
|
|
|
if (arg.name) {
|
|
stub['KN'] = arg.name
|
|
}
|
|
if (arg.filename) {
|
|
stub['KF'] = arg.filename;
|
|
}
|
|
if (arg.displayName) {
|
|
stub['displayName'] = arg.displayName;
|
|
}
|
|
|
|
if (language.name) {
|
|
stub['KL'] = language.name;
|
|
}
|
|
if (language.region) {
|
|
stub['KR'] = language.region;
|
|
}
|
|
|
|
// Can ignore ['KRC'] RegionCodes used by the Toolbar UI
|
|
|
|
if (language.font) {
|
|
stub['KFont'] = language.font;
|
|
}
|
|
if (language.oskFont) {
|
|
stub['KOskFont'] = language.oskFont;
|
|
}
|
|
|
|
stubs.push(stub);
|
|
})
|
|
|
|
return stubs;
|
|
}
|
|
}
|
|
|
|
// Information about a keyboard that fails to get added
|
|
export interface ErrorStub {
|
|
keyboard?: {
|
|
id: string;
|
|
name: string;
|
|
},
|
|
language?: {
|
|
id?: string;
|
|
name?: string;
|
|
}
|
|
|
|
error: Error;
|
|
}
|
|
export class KeyboardTag {
|
|
stores: {[text: string]: text.ComplexKeyboardStore} = {};
|
|
}
|
|
|
|
export interface KeyboardChangeData {
|
|
['internalName']: string;
|
|
['languageCode']: string;
|
|
['indirect']: boolean;
|
|
}
|
|
|
|
// For when the API call straight-up times out.
|
|
const CLOUD_TIMEOUT_ERR = "The Cloud API request timed out.";
|
|
// Currently cannot distinguish between "no matching keyboard" and other script-load errors.
|
|
const CLOUD_MALFORMED_OBJECT_ERR = "Could not find a keyboard with that ID.";
|
|
// Represents unspecified errors that occur when registering the results of a successful API call.
|
|
const CLOUD_STUB_REGISTRATION_ERR = "The Cloud API failed to find an appropriate keyboard.";
|
|
// Represents custom, specified KMW errors that occur when registering the results of a successful API call.
|
|
const CLOUD_REGISTRATION_ERR = "Error occurred while registering keyboards: ";
|
|
|
|
const MISSING_KEYBOARD = function(kbdid: string) {
|
|
return kbdid + ' keyboard not found.';
|
|
}
|
|
|
|
interface RegistrationPromiseTuple {
|
|
resolve: (stubs: KeyboardStub[]) => void;
|
|
reject: (err: Error) => void;
|
|
}
|
|
|
|
export class KeyboardManager {
|
|
// Language regions as defined by cloud server
|
|
static readonly regions = ['World','Africa','Asia','Europe','South America','North America','Oceania','Central America','Middle East'];
|
|
static readonly regionCodes = ['un','af','as','eu','sa','na','oc','ca','me'];
|
|
|
|
keymanweb: KeymanBase;
|
|
|
|
activeStub: KeyboardStub = null;
|
|
keyboardStubs: KeyboardStub[] = [];
|
|
|
|
// For deferment of adding keyboards until keymanweb initializes
|
|
deferment: Promise<void> = null;
|
|
endDeferment:() => void;
|
|
|
|
// The following was not actually utilized within KeymanWeb; I think it's handled via different logic.
|
|
// See setDefaultKeyboard() below.
|
|
dfltStub = null; // First keyboard stub loaded - default for touch-screen devices, ignored on desktops
|
|
|
|
keyboards: any[] = [];
|
|
|
|
/**
|
|
* Holds the 'resolve' function for Promises built by keymanCloudRequest.
|
|
* These should be cleared when the Promise is fulfilled.
|
|
*/
|
|
registrationResolvers: {[timeoutID: number] : RegistrationPromiseTuple} = {};
|
|
|
|
languageListPromise: Promise<(KeyboardStub|ErrorStub)[]> = null;
|
|
|
|
languageList: any[] = null; // List of keyboard languages available for KeymanCloud
|
|
|
|
linkedScripts: HTMLScriptElement[] = [];
|
|
|
|
constructor(kmw: KeymanBase) {
|
|
this.keymanweb = kmw;
|
|
|
|
let _this = this;
|
|
this.deferment = new Promise(function(resolve) {
|
|
_this.endDeferment = resolve;
|
|
});
|
|
}
|
|
|
|
getActiveKeyboardName(): string {
|
|
let core = com.keyman.singleton.core;
|
|
return core.activeKeyboard ? core.activeKeyboard.id : '';
|
|
}
|
|
|
|
getActiveLanguage(fullName?: boolean): string {
|
|
if(this.activeStub == null) {
|
|
return '';
|
|
} else if(fullName) {
|
|
return this.activeStub['KL'];
|
|
} else {
|
|
return this.activeStub['KLC'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get an associative array of keyboard identification strings
|
|
* This was defined as an array, so is kept that way, but
|
|
* Javascript treats it as an object anyway
|
|
*
|
|
* @param {Object} Lstub Keyboard stub object
|
|
* @param {Object} Lkbd Keyboard script object
|
|
* @return {Object} Copy of keyboard identification strings
|
|
*
|
|
*/
|
|
private _GetKeyboardDetail = function(Lstub: KeyboardStub, Lkbd: any /* KeyboardScriptObject */) { // I2078 - Full keyboard detail
|
|
var Lr={};
|
|
Lr['Name'] = Lstub['KN'];
|
|
Lr['InternalName'] = Lstub['KI'];
|
|
Lr['LanguageName'] = Lstub['KL']; // I1300 - Add support for language names
|
|
Lr['LanguageCode'] = Lstub['KLC']; // I1702 - Add support for language codes, region names, region codes, country names and country codes
|
|
Lr['RegionName'] = Lstub['KR'];
|
|
Lr['RegionCode'] = Lstub['KRC'];
|
|
Lr['CountryName'] = Lstub['KC'];
|
|
Lr['CountryCode'] = Lstub['KCC'];
|
|
Lr['KeyboardID'] = Lstub['KD'];
|
|
Lr['Font'] = Lstub['KFont'];
|
|
Lr['OskFont'] = Lstub['KOskFont'];
|
|
Lr['HasLoaded'] = !!Lkbd;
|
|
|
|
Lr['IsRTL'] = Lkbd ? !!Lkbd['KRTL'] : null;
|
|
return Lr;
|
|
}
|
|
|
|
/**
|
|
* Get array of available keyboard stubs
|
|
*
|
|
* @return {Array} Array of available keyboards
|
|
*
|
|
*/
|
|
getDetailedKeyboards() {
|
|
var Lr = [], Ln, Lstub, Lrn;
|
|
|
|
for(Ln=0; Ln < this.keyboardStubs.length; Ln++) { // I1511 - array prototype extended
|
|
Lstub = this.keyboardStubs[Ln];
|
|
|
|
// In Chrome, (including on Android), Array.prototype.find() requires Chrome 45.
|
|
// This is a later version than the default on our oldest-supported Android devices.
|
|
const Lkbd = this.keyboards.find(k => k['KI'] == Lstub['KI']);
|
|
Lrn = this._GetKeyboardDetail(Lstub, Lkbd); // I2078 - Full keyboard detail
|
|
Lr=this.keymanweb._push(Lr,Lrn); // TODO: Resolve without need for the cast.
|
|
}
|
|
return Lr;
|
|
}
|
|
|
|
/**
|
|
* Register a fully specified keyboard (add meta-data for each language) immediately
|
|
*
|
|
* @param {Object} arg
|
|
* @returns {boolean}
|
|
**/
|
|
addStub(arg: any): boolean {
|
|
if(typeof(arg['id']) != 'string') {
|
|
return false;
|
|
}
|
|
|
|
if(typeof(arg['language']) != "undefined") {
|
|
console.warn("The 'language' property for keyboard stubs has been deprecated. Please use the 'languages' property instead.");
|
|
arg['languages'] = arg['language'];
|
|
}
|
|
|
|
if(typeof(arg['languages']) == 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
// Default the keyboard name to its id, capitalized
|
|
if(typeof(arg['name']) != 'string') {
|
|
arg['name'] = arg['id'].replace('_',' ');
|
|
arg['name'] = arg['name'].substr(0,1).toUpperCase()+arg['name'].substr(1);
|
|
}
|
|
|
|
var lgArg=arg['languages'];
|
|
var lgList=[], i, lg;
|
|
if(typeof(lgArg.length) == 'undefined') {
|
|
lgList[0] = lgArg;
|
|
} else {
|
|
lgList = lgArg;
|
|
}
|
|
|
|
var localOptions={
|
|
'keyboardBaseUri': this.keymanweb.options['keyboards'],
|
|
'fontBaseUri': this.keymanweb.options['fonts']
|
|
};
|
|
|
|
// Add a stub for each correctly specified language
|
|
for(i=0; i<lgList.length; i++) {
|
|
this.mergeStub(arg, lgList[i], localOptions);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create or update a keyboard meta-data 'stub' during keyboard registration
|
|
*
|
|
* Cross-reference with https://help.keyman.com/developer/engine/web/11.0/reference/core/addKeyboards.
|
|
*
|
|
* @param {Object} kp (partial) keyboard meta-data object (`spec` object)
|
|
* @param {Object} lp language object (`spec.languages` object)
|
|
* @param {Object} options KeymanCloud callback options
|
|
**/
|
|
mergeStub(kp: any, lp: any, options) {
|
|
var sp: KeyboardStub = this.findStub(kp['id'], lp['id']);
|
|
var isNew: boolean = false;
|
|
|
|
if(sp == null) {
|
|
sp= new KeyboardStub(kp['id'], lp['id']);
|
|
this.keyboardStubs.push(sp);
|
|
isNew = true;
|
|
}
|
|
|
|
// Accept region as number (from Cloud server), code, or name
|
|
var region=lp['region'], rIndex=0;
|
|
if(typeof(region) == 'number') {
|
|
if(region < 1 || region > 9) {
|
|
rIndex = 0;
|
|
} else {
|
|
rIndex = region-1;
|
|
}
|
|
} else if(typeof(region) == 'string') {
|
|
var list = (region.length == 2 ? KeyboardManager.regionCodes : KeyboardManager.regions);
|
|
for(var i=0; i<list.length; i++) {
|
|
if(region.toLowerCase() == list[i].toLowerCase()) {
|
|
rIndex=i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var rx: RegExp;
|
|
|
|
sp['KL'] = (typeof sp['KL'] === 'undefined') ? lp['name'] : sp['KL'];
|
|
sp['KR'] = (typeof sp['KR'] === 'undefined') ? KeyboardManager.regions[rIndex] : sp['KR'];
|
|
sp['KRC'] = (typeof sp['KRC'] === 'undefined') ? KeyboardManager.regionCodes[rIndex] : sp['KRC'];
|
|
sp['KN'] = (typeof sp['KN'] === 'undefined') ? kp['name'] : sp['KN'];
|
|
sp['displayName'] = (typeof sp['displayName'] === 'undefined') ? kp['displayName'] : sp['displayName'];
|
|
|
|
if(typeof(sp['KF']) == 'undefined') {
|
|
rx=RegExp('^(([\\.]/)|([\\.][\\.]/)|(/))|(:)');
|
|
sp['KF'] = kp['filename'];
|
|
|
|
if(!rx.test(sp['KF'])) {
|
|
sp['KF'] = options['keyboardBaseUri']+sp['KF'];
|
|
}
|
|
}
|
|
|
|
// Font path defined by cloud entry
|
|
var fontPath=options['fontBaseUri'];
|
|
|
|
// or overridden locally, in page source
|
|
if(this.keymanweb.options['fonts'] != '') {
|
|
fontPath=this.keymanweb.options['fonts'];
|
|
rx=new RegExp('^https?\\:');
|
|
if(!rx.test(fontPath)) {
|
|
if(fontPath.substr(0,2) == '//') {
|
|
fontPath = this.keymanweb.protocol + fontPath;
|
|
} else if(fontPath.substr(0,1) == '/') {
|
|
fontPath = this.keymanweb.rootPath + fontPath.substr(1);
|
|
} else {
|
|
fontPath = this.keymanweb.rootPath + fontPath;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
this.keymanweb.options.fonts=fontPath;
|
|
}
|
|
|
|
// Add font specifiers where necessary and not overridden by user
|
|
if(typeof(lp['font']) != 'undefined') {
|
|
sp['KFont'] = (typeof sp['KFont'] === 'undefined') ? new KeyboardFont(lp['font'], fontPath) : sp['KFont'];
|
|
}
|
|
|
|
// Fixed OSK font issue Github #7 (9/1/2015)
|
|
if(typeof(lp['oskFont']) != 'undefined') {
|
|
sp['KOskFont'] = (typeof sp['KOskFont'] === 'undefined') ? new KeyboardFont(lp['oskFont'], fontPath) : sp['KOskFont'];
|
|
}
|
|
|
|
// Update the UI
|
|
this.doKeyboardRegistered(sp['KI'],sp['KL'],sp['KN'],sp['KLC'],sp['KP']);
|
|
|
|
// If we have no activeStub because there were no stubs, set the new keyboard as active.
|
|
// Do not trigger on merges.
|
|
if(!this.activeStub && isNew && this.keyboardStubs.length == 1 && this.keymanweb.options['setActiveOnRegister']=='true') {
|
|
// #676: We call _SetActiveKeyboard so we can avoid overwriting
|
|
// cookies that determine our active keyboard at page load time
|
|
this.doBeforeKeyboardChange(sp['KI'], sp['KLC']);
|
|
this._SetActiveKeyboard(sp['KI'], sp['KLC'], false);
|
|
this.doKeyboardChange(sp['KI'], sp['KLC']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find a keyboard stub by id in the registered keyboards list
|
|
*
|
|
* @param {string} kid internal keyboard id (without 'Keyboard_' prefix)
|
|
* @param {string} lgid language code
|
|
*
|
|
**/
|
|
findStub(kid: string, lgid: string): KeyboardStub {
|
|
var i;
|
|
for(i=0; i<this.keyboardStubs.length; i++) {
|
|
if((this.keyboardStubs[i]['KI'] == 'Keyboard_'+kid) && (this.keyboardStubs[i]['KLC'] == lgid)) {
|
|
return this.keyboardStubs[i];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Called on the embedded path at the end of its initialization.
|
|
setDefaultKeyboard() {
|
|
if(this.keyboardStubs.length > 0) {
|
|
// Select the first stub as our active keyboard.
|
|
this._SetActiveKeyboard(this.keyboardStubs[0]['KI'], this.keyboardStubs[0]['KLC']);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allow to change active keyboard by (internal) keyboard name
|
|
*
|
|
* @param {string} PInternalName Internal name
|
|
* @param {string} PLgCode Language code
|
|
*/
|
|
setActiveKeyboard(PInternalName: string, PLgCode: string): Promise<void> {
|
|
//TODO: This does not make sense: the callbacks should be in _SetActiveKeyboard, not here,
|
|
// since this is always called FROM the UI, which should not need notification.
|
|
// If UI callbacks are needed at all, they should be within _SetActiveKeyboard
|
|
|
|
// Skip on embedded which namespaces packageID::Keyboard_keyboardID
|
|
if(!this.keymanweb.isEmbedded && PInternalName && PInternalName.indexOf("Keyboard_") != 0) {
|
|
PInternalName = "Keyboard_" + PInternalName;
|
|
}
|
|
|
|
this.doBeforeKeyboardChange(PInternalName,PLgCode);
|
|
let p: Promise<void> = this._SetActiveKeyboard(PInternalName,PLgCode,true);
|
|
if(this.keymanweb.domManager.lastActiveElement != null) {
|
|
this.keymanweb.domManager.focusLastActiveElement(); // TODO: Resolve without need for the cast.
|
|
}
|
|
// If we ever allow PLgCode to be set by default, we can auto-detect the language code
|
|
// after the _SetActiveKeyboard call.
|
|
// if(!PLgCode && (<KeymanBase>keymanweb).keyboardManager.activeStub) {
|
|
// PLgCode = (<KeymanBase>keymanweb).keyboardManager.activeStub['KLC'];
|
|
// }
|
|
const _this = this;
|
|
p.then(function() {
|
|
// Only mark the keyboard as having changed once the setActiveKeyboard op
|
|
// is successful.
|
|
_this.doKeyboardChange(PInternalName, PLgCode);
|
|
});
|
|
|
|
p.catch(error => {
|
|
// Rejection indicates a failure of the keyboard to load.
|
|
//
|
|
// In case p's rejection is never caught, throwing this error will generate logs that shows up
|
|
// in Sentry or in the console, with useful information for debugging either way.
|
|
throw new Error("Unable to load keyboard with internal name \"" + PInternalName + "\", language code \"" + PLgCode + "\": " + error);
|
|
});
|
|
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* Change active keyboard to keyboard selected by (internal) name and language code
|
|
*
|
|
* Test if selected keyboard already loaded, and simply update active stub if so.
|
|
* Otherwise, insert a script to download and insert the keyboard from the repository
|
|
* or user-indicated file location.
|
|
*
|
|
* Note that the test-case oriented 'recorder' stubs this method to provide active
|
|
* keyboard stub information. If changing this function, please ensure the recorder is
|
|
* not affected.
|
|
*
|
|
* @param {string} PInternalName
|
|
* @param {string=} PLgCode
|
|
* @param {boolean=} saveCookie
|
|
*/
|
|
_SetActiveKeyboard(PInternalName: string, PLgCode?: string, saveCookie?: boolean): Promise<void> {
|
|
var n, Ln;
|
|
|
|
let keyman = com.keyman.singleton;
|
|
|
|
var util = keyman.util;
|
|
var osk = keyman.osk;
|
|
|
|
let activeKeyboard = keyman.core.activeKeyboard;
|
|
|
|
// Set default language code
|
|
if(arguments.length < 2 || (!PLgCode)) {
|
|
PLgCode='---';
|
|
}
|
|
|
|
// Check that the saved keyboard is currently registered
|
|
for(n=0; n < this.keyboardStubs.length; n++) {
|
|
if(PInternalName == this.keyboardStubs[n]['KI']) {
|
|
if(PLgCode == this.keyboardStubs[n]['KLC'] || PLgCode == '---') break;
|
|
}
|
|
}
|
|
|
|
// Mobile device addition: force selection of the first keyboard if none set
|
|
if(util.device.touchable && (PInternalName == '' || PInternalName == null || n >= this.keyboardStubs.length)) {
|
|
if(this.keyboardStubs.length != 0) {
|
|
PInternalName=this.keyboardStubs[0]['KI'];
|
|
PLgCode=this.keyboardStubs[0]['KLC'];
|
|
}
|
|
}
|
|
|
|
// Save name of keyboard (with language code) as a cookie
|
|
if(arguments.length > 2 && saveCookie) {
|
|
this.saveCurrentKeyboard(PInternalName,PLgCode);
|
|
}
|
|
|
|
// Check if requested keyboard and stub are currently active
|
|
if(this.activeStub && activeKeyboard && activeKeyboard.id == PInternalName
|
|
&& this.activeStub['KI'] == PInternalName //this part of test should not be necessary, but keep anyway
|
|
&& this.activeStub['KLC'] == PLgCode && !this.keymanweb.mustReloadKeyboard
|
|
) return Promise.resolve();
|
|
|
|
// Check if current keyboard matches requested keyboard, but not stub
|
|
if(activeKeyboard && (activeKeyboard.id == PInternalName)) {
|
|
// If so, simply update the active stub
|
|
for(Ln=0; Ln<this.keyboardStubs.length; Ln++) {
|
|
if((this.keyboardStubs[Ln]['KI'] == PInternalName)
|
|
&& (this.keyboardStubs[Ln]['KLC'] == PLgCode)) {
|
|
this.activeStub = this.keyboardStubs[Ln];
|
|
|
|
// Append a stylesheet for this keyboard for keyboard specific styles
|
|
// or if needed to specify an embedded font
|
|
osk.vkbd?.appendStyleSheet();
|
|
|
|
// Re-initializate OSK before returning if required
|
|
if(this.keymanweb.mustReloadKeyboard) {
|
|
activeKeyboard.refreshLayouts();
|
|
if(osk) {
|
|
osk._Load();
|
|
}
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
}
|
|
|
|
keyman.core.activeKeyboard = null;
|
|
this.activeStub = null;
|
|
|
|
// Hide OSK and do not update keyboard list if using internal keyboard (desktops)
|
|
if(PInternalName == '') {
|
|
osk.startHide(false);
|
|
|
|
if(!this.keymanweb.isEmbedded) {
|
|
util.wait(false);
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Determine if the keyboard was previously loaded but is not active and use the prior load if so.
|
|
for(Ln=0; Ln<this.keyboards.length; Ln++) { // I1511 - array prototype extended
|
|
if(this.keyboards[Ln]['KI'] == PInternalName) {
|
|
keyman.core.activeKeyboard = new Keyboard(this.keyboards[Ln]);
|
|
// As a rotation may have occurred since the keyboard was swapped out,
|
|
// we should refresh its layouts.
|
|
keyman.core.activeKeyboard.refreshLayouts();
|
|
this.keymanweb.domManager._SetTargDir(this.keymanweb.domManager.lastActiveElement); // I2077 - LTR/RTL timing
|
|
|
|
// and update the active stub
|
|
for(var Ls=0; Ls<this.keyboardStubs.length; Ls++) {
|
|
if((this.keyboardStubs[Ls]['KI'] == PInternalName) &&
|
|
(this.keyboardStubs[Ls]['KLC'] == PLgCode || PLgCode == '---')) {
|
|
this.activeStub = this.keyboardStubs[Ls];
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we've reached this point, this is the first load request for the requested keyboard.
|
|
if(keyman.core.activeKeyboard == null) {
|
|
for(Ln=0; Ln<this.keyboardStubs.length; Ln++) { // I1511 - array prototype extended
|
|
if((this.keyboardStubs[Ln]['KI'] == PInternalName)
|
|
&& ((this.keyboardStubs[Ln]['KLC'] == PLgCode) || (PLgCode == '---'))) {
|
|
// Force OSK display for CJK keyboards (keyboards using a pick list)
|
|
if(this.isCJK(this.keyboardStubs[Ln]) || util.device.touchable) {
|
|
osk.displayIfActive = true;
|
|
}
|
|
|
|
// Create a script to load from the server - when it finishes loading, it will register itself,
|
|
// detect that it is active, and focus as appropriate. The second test is needed to allow recovery from a failed script load
|
|
|
|
// Ensure we're not already loading the keyboard.
|
|
if(!this.keyboardStubs[Ln].asyncLoader) {
|
|
// Always (temporarily) hide the OSK when loading a new keyboard, to ensure that a failure to load doesn't leave the current OSK displayed
|
|
if(osk) {
|
|
osk.startHide(false);
|
|
}
|
|
|
|
var loadingStub = this.keyboardStubs[Ln];
|
|
// Tag the stub so that we don't double-load the keyboard!
|
|
loadingStub.asyncLoader = {};
|
|
|
|
var kbdName = loadingStub['KN'];
|
|
var lngName = loadingStub['KL'];
|
|
kbdName = kbdName.replace(/\s*keyboard\s*/i, '');
|
|
|
|
// Setup our default error-messaging callback if it should be implemented.
|
|
loadingStub.asyncLoader.callback = function(altString, msgType) {
|
|
var msg = altString || 'Sorry, the '+kbdName+' keyboard for '+lngName+' is not currently available.';
|
|
|
|
// Thanks, Closure errors.
|
|
if(!this.keymanweb.isEmbedded) {
|
|
util.wait(false);
|
|
util.internalAlert(altString || msg, function() {
|
|
this.keymanweb['setActiveKeyboard'](''); // The API call!
|
|
}.bind(this));
|
|
}
|
|
|
|
switch(msgType) { // in case we extend this later.
|
|
case 'err':
|
|
console.error(msg);
|
|
break;
|
|
case 'warn':
|
|
default:
|
|
console.warn(msg);
|
|
break;
|
|
}
|
|
|
|
if(Ln > 0) {
|
|
var Ps = this.keyboardStubs[0];
|
|
this._SetActiveKeyboard(Ps['KI'], Ps['KLC'], true);
|
|
}
|
|
}.bind(this);
|
|
|
|
loadingStub.asyncLoader.timer = window.setTimeout(loadingStub.asyncLoader.callback, 10000);
|
|
|
|
//Display the loading delay bar (Note: only append 'keyboard' if not included in name.)
|
|
if(!this.keymanweb.isEmbedded) {
|
|
util.wait('Installing keyboard<br/>' + kbdName);
|
|
}
|
|
|
|
// Installing the script immediately does not work reliably if two keyboards are
|
|
// loaded in succession if there is any delay in downloading the script.
|
|
// It works much more reliably if deferred (KMEW-101, build 356)
|
|
// The effect of a delay can also be tested, for example, by setting the timeout to 5000
|
|
var manager = this;
|
|
loadingStub.asyncLoader.promise = new Promise<void>(function(resolve, reject) {
|
|
window.setTimeout(function(){
|
|
manager.installKeyboard(resolve, reject, loadingStub);
|
|
},0);
|
|
});
|
|
}
|
|
this.activeStub=this.keyboardStubs[Ln];
|
|
return this.keyboardStubs[Ln].asyncLoader.promise;
|
|
}
|
|
}
|
|
this.keymanweb.domManager._SetTargDir(this.keymanweb.domManager.lastActiveElement); // I2077 - LTR/RTL timing
|
|
}
|
|
|
|
// Initialize the OSK (provided that the base code has been loaded)
|
|
if(osk) {
|
|
osk._Load();
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* Install a keyboard script that has been downloaded from a keyboard server
|
|
* Operates as the core of a Promise, hence the 'resolve' and 'reject' parameters.
|
|
*
|
|
* @param {Object} kbdStub keyboard stub to be loaded.
|
|
*
|
|
**/
|
|
installKeyboard(resolve: () => void, reject: (message?: string) => void, kbdStub: KeyboardStub) {
|
|
var util = this.keymanweb.util;
|
|
var osk = this.keymanweb.osk;
|
|
|
|
var Lscript = util._CreateElement('script');
|
|
Lscript.charset="UTF-8"; // KMEW-89
|
|
Lscript.type = 'text/javascript';
|
|
|
|
// Preserve any namespaced IDs by use of the script's id tag attribute!
|
|
if(this.keymanweb.isEmbedded) {
|
|
Lscript.id = kbdStub['KI'];
|
|
}
|
|
|
|
var kbdFile = kbdStub['KF'];
|
|
var kbdLang = kbdStub['KL'];
|
|
var kbdName = kbdStub['KN'];
|
|
|
|
const scriptSrc = this.keymanweb.getKeyboardPath(kbdFile);
|
|
|
|
var manager = this;
|
|
let core = com.keyman.singleton.core;
|
|
|
|
// Add a handler for cases where the new <script> block fails to load.
|
|
Lscript.addEventListener('error', function() {
|
|
if(kbdStub.asyncLoader.timer !== null) {
|
|
// Clear the timeout timer.
|
|
window.clearTimeout(kbdStub.asyncLoader.timer);
|
|
kbdStub.asyncLoader.timer = null;
|
|
}
|
|
|
|
// We already know the load has failed... why wait?
|
|
const msg = 'Cannot find the ' + kbdName + ' keyboard for ' + kbdLang + ' at ' + scriptSrc + '.';
|
|
kbdStub.asyncLoader.callback(msg, 'warn');
|
|
kbdStub.asyncLoader = null;
|
|
|
|
reject(msg);
|
|
});
|
|
|
|
|
|
// The load event will activate a newly-loaded keyboard if successful and report an error if it is not.
|
|
Lscript.addEventListener('load', function() {
|
|
if(kbdStub.asyncLoader.timer !== null) {
|
|
// Clear the timeout timer.
|
|
window.clearTimeout(kbdStub.asyncLoader.timer);
|
|
kbdStub.asyncLoader.timer = null;
|
|
}
|
|
|
|
// To determine if the load was successful, we'll need to check the keyboard array for our desired keyboard.
|
|
// Test if keyboard already loaded
|
|
var kbd = manager.getKeyboardByID(kbdStub['KI']);
|
|
if(kbd) { // Is cleared upon a successful load.
|
|
|
|
//Activate keyboard, if it's still the active stub.
|
|
if(kbdStub == manager.activeStub) {
|
|
manager.doBeforeKeyboardChange(kbd['KI'],kbdStub['KLC']);
|
|
core.activeKeyboard=new Keyboard(kbd);
|
|
|
|
if(manager.keymanweb.domManager.lastActiveElement != null) { // TODO: Resolve without need for the cast.
|
|
manager.keymanweb.uiManager.justActivated = true; // TODO: Resolve without need for the cast.
|
|
manager.keymanweb.domManager._SetTargDir(manager.keymanweb.domManager.lastActiveElement);
|
|
}
|
|
|
|
manager.saveCurrentKeyboard(kbd['KI'], kbdStub['KLC']);
|
|
|
|
// Prepare and show the OSK for this keyboard
|
|
if(osk) {
|
|
osk._Load();
|
|
}
|
|
|
|
if(manager.keymanweb.domManager.lastActiveElement != null) {
|
|
core.processNewContextEvent(dom.Utils.getOutputTarget(manager.keymanweb.domManager.lastActiveElement));
|
|
}
|
|
}
|
|
|
|
// Remove the wait message, if defined
|
|
if(!manager.keymanweb.isEmbedded) {
|
|
util.wait(false);
|
|
}
|
|
|
|
kbdStub.asyncLoader = null;
|
|
resolve();
|
|
// A handler portion for cases where the new <script> block loads, but fails to process.
|
|
} else { // Output error messages even when embedded - they're useful when debugging the apps and KMEA/KMEI engines.
|
|
const msg = 'Error registering the ' + kbdName + ' keyboard for ' + kbdLang + '; keyboard script at ' + scriptSrc + ' may contain an error.';
|
|
kbdStub.asyncLoader.callback(msg, 'error');
|
|
kbdStub.asyncLoader = null;
|
|
reject(msg);
|
|
}
|
|
}, false);
|
|
|
|
// Some browsers may instantly start loading the file when assigned to an element, so we do this after the rest
|
|
// of our setup. This method is not relocated here (yet) b/c it varies based upon 'native' vs 'embedded'.
|
|
Lscript.src = scriptSrc;
|
|
|
|
try {
|
|
document.body.appendChild(Lscript);
|
|
}
|
|
catch(ex) {
|
|
try {
|
|
document.getElementsByTagName('head')[0].appendChild(Lscript);
|
|
} catch(ex2) {
|
|
reject('Error registering script ' + scriptSrc + ': ' + ex2);
|
|
return;
|
|
}
|
|
}
|
|
this.linkedScripts.push(Lscript);
|
|
}
|
|
|
|
/* TODO: why not use util.loadCookie and saveCookie?? */
|
|
|
|
/**
|
|
* Function saveCurrentKeyboard
|
|
* Scope Private
|
|
* @param {string} PInternalName name of keyboard
|
|
* @param {string} PLgCode language code
|
|
* Description Saves current keyboard as a cookie
|
|
*/
|
|
private saveCurrentKeyboard(PInternalName: string, PLgCode: string) {
|
|
var s = "current="+PInternalName+":"+PLgCode;
|
|
this.keymanweb.util.saveCookie('KeymanWeb_Keyboard',{'current':PInternalName+':'+PLgCode});
|
|
|
|
// Additionally, make sure we save the (upcoming) per-control keyboard settings.
|
|
// This allows us to ensure the keyboard is set correctly without waiting for focus event
|
|
// triggers - very helpful for automated testing.
|
|
if(!this.keymanweb.isEmbedded) {
|
|
this.keymanweb.touchAliasing._BlurKeyboardSettings(this.keymanweb.domManager.lastActiveElement, PInternalName, PLgCode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore the most recently used keyboard, if still available
|
|
*/
|
|
restoreCurrentKeyboard() {
|
|
var stubs = this.keyboardStubs, i, n=stubs.length;
|
|
let core = com.keyman.singleton.core;
|
|
|
|
// Do nothing if no stubs loaded
|
|
if(stubs.length < 1) return;
|
|
|
|
// If no saved keyboard, default to US English, else first loaded stub
|
|
var d=this.getSavedKeyboard();
|
|
var t=d.split(':');
|
|
|
|
// Identify the stub with the saved keyboard
|
|
t=d.split(':');
|
|
if(t.length < 2) t[1]='';
|
|
|
|
// This loop is needed to select the correct stub when several apply to a given keyboard
|
|
// TODO: There should be a better way!
|
|
for(i=0; i<n; i++)
|
|
{
|
|
if(stubs[i]['KI'] == t[0] && (stubs[i]['KLC'] == t[1] || t[1] == '')) break;
|
|
}
|
|
|
|
// Sets the default stub (as specified with the `getSavedKeyboard` call) as active.
|
|
// if((i < n) || (device.touchable && (this.activeKeyboard == null)))
|
|
if((i < n) || (core.activeKeyboard == null))
|
|
{
|
|
this._SetActiveKeyboard(t[0],t[1],false);
|
|
this.keymanweb.globalKeyboard = t[0];
|
|
this.keymanweb.globalLanguageCode = t[1];
|
|
|
|
this.doKeyboardChange(t[0],t[1]); // And update the UI if necessary
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the cookie for the name and language code of the most recently active keyboard
|
|
*
|
|
* Defaults to US English, but this needs to be user-set in later revision (TODO)
|
|
*
|
|
* @return {string} InternalName:LanguageCode
|
|
**/
|
|
getSavedKeyboard(): string {
|
|
var v = this.keymanweb.util.loadCookie('KeymanWeb_Keyboard');
|
|
|
|
if(typeof(v['current']) != 'string') {
|
|
return 'Keyboard_us:eng';
|
|
}
|
|
|
|
// Check that the requested keyboard is included in the available keyboard stubs
|
|
var n, stubs = this.keyboardStubs,kd;
|
|
|
|
for(n=0; n<stubs.length; n++) {
|
|
kd=stubs[n]['KI']+':'+stubs[n]['KLC'];
|
|
if(kd == v['current']) return kd;
|
|
}
|
|
|
|
// Default to US English if available (but don't assume it is first)
|
|
for(n=0; n<stubs.length; n++) {
|
|
kd=stubs[n]['KI']+':'+stubs[n]['KLC'];
|
|
if(kd == 'Keyboard_us:eng') return kd;
|
|
}
|
|
|
|
// Otherwise use the first keyboard stub
|
|
if(stubs.length > 0) {
|
|
return stubs[0]['KI']+':'+stubs[0]['KLC'];
|
|
}
|
|
|
|
// Or US English if no stubs loaded (should never happen)
|
|
return 'Keyboard_us:eng';
|
|
}
|
|
|
|
/**
|
|
* Function isCJK
|
|
* Scope Public
|
|
* @param {Object=} k0
|
|
* @return {boolean}
|
|
* Description Tests if the keyboard stub uses a pick list (Chinese, Japanese, Korean, etc.)
|
|
* (This function accepts either keyboard structure.)
|
|
*/
|
|
isCJK(k: KeyboardStub) { // I3363 (Build 301)
|
|
var lg: string;
|
|
if(typeof(k['KLC']) != 'undefined') {
|
|
lg = k['KLC'];
|
|
} else if(typeof(k['LanguageCode']) != 'undefined') {
|
|
lg = k['LanguageCode'];
|
|
}
|
|
|
|
return ((lg == 'cmn') || (lg == 'jpn') || (lg == 'kor'));
|
|
}
|
|
|
|
/**
|
|
* Function _getKeyboardByID
|
|
* Scope Private
|
|
* @param {string} keyboardID
|
|
* @return {Object|null}
|
|
* Description Returns the internal, registered keyboard object - not the stub, but the keyboard itself.
|
|
*/
|
|
private getKeyboardByID(keyboardID: string):any {
|
|
var Li;
|
|
for(Li=0; Li<this.keyboards.length; Li++) {
|
|
if(keyboardID == this.keyboards[Li]['KI']) {
|
|
return this.keyboards[Li];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/* ------------------------------------------------------------
|
|
* Definitions for adding, removing, and requesting keyboards.
|
|
* ------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Function isUniqueRequest
|
|
* Scope Private
|
|
* @param {Object} tEntry
|
|
* Description Checks to ensure that the stub isn't already loaded within KMW or subject
|
|
* to an already-pending request.
|
|
*/
|
|
isUniqueRequest(cloudList: {id: string, language?: string}[], tEntry: CloudRequestEntry) {
|
|
var k;
|
|
|
|
if(this.findStub(tEntry.id, tEntry.language) == null) {
|
|
for(k=0; k < cloudList.length; k++) {
|
|
if(cloudList[k].id == tEntry['id'] && cloudList[k].language == tEntry.language) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns a Promise of the merged keyboard stubs and error stubs.
|
|
*
|
|
* If the keyboard stub array is empty, will return a rejected Promise,
|
|
* otherwise returns a resolved Promise.
|
|
*
|
|
* @param keyboardStubs array of keyboard stubs to merge.
|
|
* @param errorStubs array of error stubs to merge.
|
|
* @returns resolved or rejected promise with merged array of stubs.
|
|
*/
|
|
private mergeAndResolveStubPromises(keyboardStubs: (KeyboardStub|ErrorStub)[], errorStubs: ErrorStub[]) :
|
|
Promise<(KeyboardStub|ErrorStub)[]> {
|
|
if (errorStubs.length == 0) {
|
|
return Promise.resolve(keyboardStubs);
|
|
} if (keyboardStubs.length == 0) {
|
|
return Promise.reject(errorStubs);
|
|
} else {
|
|
// Merge this with errorStubs
|
|
let result: (KeyboardStub|ErrorStub)[] = keyboardStubs;
|
|
return Promise.resolve(result.concat(errorStubs));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build 362: addKeyboardArray() link to Cloud. One or more arguments may be used
|
|
*
|
|
* @param x keyboard name string or keyboard metadata JSON object
|
|
* @returns resolved or rejected promise with merged array of stubs.
|
|
*/
|
|
async addKeyboardArray(x: (string|KeyboardStub)[]): Promise<(KeyboardStub|ErrorStub)[]> {
|
|
let errorStubs: ErrorStub[] = [];
|
|
|
|
// Ensure keymanweb is initialized before continuing to add keyboards
|
|
if(!this.keymanweb.initialized) {
|
|
await this.deferment;
|
|
}
|
|
|
|
// Ignore empty array passed as argument
|
|
if(x.length == 0) {
|
|
let stub: ErrorStub = {error: new Error("No keyboards to add")}
|
|
errorStubs.push(stub);
|
|
// Normally reject error, but this can be a warning
|
|
return Promise.resolve(errorStubs);
|
|
}
|
|
|
|
// Create a temporary array of metadata objects from the arguments used
|
|
var i,j,cmd='',comma='';
|
|
var cloudList: CloudRequestEntry[] = [];
|
|
let keyboardStubs: KeyboardStub[] = [];
|
|
var tEntry: CloudRequestEntry;
|
|
|
|
for(i=0; i<x.length; i++) {
|
|
if(typeof(x[i]) == 'string' && (<string>x[i]).length > 0) {
|
|
var pList=(<string>x[i]).split('@'),lList=[''];
|
|
if(pList[0].toLowerCase() == 'english') {
|
|
pList[0] = 'us';
|
|
}
|
|
|
|
if(pList.length > 1) {
|
|
lList=pList[1].split(',');
|
|
}
|
|
|
|
for(j=0; j<lList.length; j++) {
|
|
tEntry = new CloudRequestEntry(pList[0]);
|
|
|
|
if(lList[j] != '') {
|
|
tEntry.language=lList[j];
|
|
}
|
|
|
|
if(pList.length > 2) {
|
|
tEntry.version=pList[2];
|
|
}
|
|
|
|
// If we've already registered or requested a stub for this keyboard-language pairing,
|
|
// don't bother with a cloud request.
|
|
if(this.isUniqueRequest(cloudList, tEntry)) {
|
|
cloudList.push(tEntry);
|
|
}
|
|
}
|
|
}
|
|
if(typeof(x[i]) == 'object' && x[i] != null) {
|
|
// Register any local keyboards immediately:
|
|
// - must specify filename, keyboard name, language codes, region codes
|
|
// - no request will be sent to cloud
|
|
if(typeof(x[i]['filename']) == 'string') {
|
|
if(!this.addStub(x[i])) {
|
|
this.keymanweb.util.internalAlert('To use a custom keyboard, you must specify file name, keyboard name, language, language code and region code.');
|
|
}
|
|
} else {
|
|
if(x[i]['language']) {
|
|
console.warn("The 'language' property for keyboard stubs has been deprecated. Please use the 'languages' property instead.");
|
|
x[i]['languages'] = x[i]['language'];
|
|
}
|
|
|
|
lList=x[i]['languages'];
|
|
if (!lList) {
|
|
let msg = 'To use keyboard \'' + x[i]['id'] + '\', you must specify languages.';
|
|
let e: ErrorStub = {
|
|
keyboard: {
|
|
id : x[i]['id'],
|
|
name: x[i]['name']
|
|
},
|
|
error: new Error(msg)
|
|
};
|
|
errorStubs.push(e);
|
|
continue;
|
|
}
|
|
|
|
//Array or single entry?
|
|
if(typeof(lList.length) == 'number') {
|
|
for(j=0; j<lList.length; j++) {
|
|
tEntry = new CloudRequestEntry(x[i]['id'], x[i]['languages'][j]['id']);
|
|
if(this.isUniqueRequest(cloudList, tEntry)) {
|
|
cloudList.push(tEntry);
|
|
}
|
|
}
|
|
} else { // Single language element
|
|
tEntry = new CloudRequestEntry(x[i]['id'], x[i]['languages']['id']);
|
|
if(this.isUniqueRequest(cloudList, tEntry)) {
|
|
cloudList.push(tEntry);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert stub from one-to-many KeyboardStub[]
|
|
let convertedStubs = KeyboardStub.toStubs(x[i]);
|
|
convertedStubs.forEach(s => {
|
|
if (s instanceof KeyboardStub) {
|
|
keyboardStubs.push(s);
|
|
} else {
|
|
errorStubs.push(s);
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Return if all keyboards being registered are local and fully specified
|
|
try {
|
|
if(cloudList.length == 0) {
|
|
return this.mergeAndResolveStubPromises(keyboardStubs, errorStubs);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
// Update the keyboard metadata list from keyman.com - build the command
|
|
cmd='&keyboardid=';
|
|
for(i=0; i<cloudList.length; i++) {
|
|
cmd=cmd+comma+cloudList[i].toString();
|
|
comma=',';
|
|
}
|
|
|
|
// Request keyboard metadata from the Keyman Cloud keyboard metadata server
|
|
try {
|
|
let result: (KeyboardStub|ErrorStub)[]|Error = await this.keymanCloudRequest(cmd,false);
|
|
return this.mergeAndResolveStubPromises(result, errorStubs);
|
|
} catch(err) {
|
|
// We don't have keyboard info for this ErrorStub
|
|
console.error(err);
|
|
let stub: ErrorStub = {error: err};
|
|
errorStubs.push(stub);
|
|
return Promise.reject(errorStubs);
|
|
}
|
|
|
|
// no keyboards added so return empty stub
|
|
return Promise.resolve(errorStubs);
|
|
}
|
|
|
|
/**
|
|
* Register a keyboard for each associated language
|
|
*
|
|
* @param {Object} kp Keyboard Object or Object array
|
|
* @param {Object} options keymanCloud callback options
|
|
* @param {number} nArg keyboard index in argument array
|
|
*
|
|
**/
|
|
registerLanguagesForKeyboard(kp, options, nArg:number) {
|
|
var i,j,id,nDflt=0,kbId='';
|
|
|
|
// Do not attempt to process badly formatted requests
|
|
if(typeof(kp) == 'undefined') {
|
|
return;
|
|
}
|
|
|
|
if(typeof(options['keyboardid']) == 'string') {
|
|
kbId = options['keyboardid'].split(',')[nArg];
|
|
}
|
|
|
|
// When keyboards requested by language code, several keyboards may be returned as an array
|
|
if(typeof(kp.length) == 'number') {
|
|
// If language code is suffixed by $, register all keyboards for this language
|
|
if(kp.length == 1 || kbId.substr(-1,1) == '$' || kbId == '') {
|
|
for(i=0; i<kp.length; i++) {
|
|
this.registerLanguagesForKeyboard(kp[i],options,nArg);
|
|
}
|
|
}
|
|
// Register the default keyboard for the language code
|
|
// Until a default is defined, the default will be the Windows keyboard,
|
|
// that is, the keyboard named for the language (exception: English:US), or the
|
|
// first keyboard found.
|
|
else {
|
|
for(i=0; i<kp.length; i++) {
|
|
id=kp[i].id.toLowerCase();
|
|
if(id == 'us') {
|
|
id='english';
|
|
}
|
|
|
|
for(j=0; j<kp[i]['languages'].length; j++) {
|
|
if(id == kp[i]['languages'][j]['name'].toLowerCase()) {
|
|
nDflt = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.registerLanguagesForKeyboard(kp[nDflt],options,nArg);
|
|
}
|
|
} else { // Otherwise, process a single keyboard for the specified languages
|
|
// May need to filter returned stubs by language
|
|
var lgCode=kbId.split('@')[1];
|
|
if(typeof(lgCode) == 'string') {
|
|
lgCode=lgCode.replace(/\$$/,'');
|
|
}
|
|
|
|
// Can only add keyboard stubs for defined languages
|
|
var ll=kp['languages'];
|
|
if(typeof(ll) != 'undefined') {
|
|
if(typeof(ll.length) == 'number') {
|
|
for(i=0; i<ll.length; i++) {
|
|
if(typeof(lgCode) == 'undefined' || ll[i]['id'] == lgCode) {
|
|
this.mergeStub(kp,ll[i],options);
|
|
}
|
|
}
|
|
} else {
|
|
this.mergeStub(kp,ll,options);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call back from cloud for adding keyboard metadata
|
|
*
|
|
* @param {Object} x metadata object
|
|
**/
|
|
register(x) {
|
|
const promiseid = x['timerid'];
|
|
|
|
let result: KeyboardStub[] | Error;
|
|
try {
|
|
result = this._registerCore(x);
|
|
} catch(err) {
|
|
result = new Error(CLOUD_REGISTRATION_ERR + err);
|
|
}
|
|
|
|
if(promiseid) {
|
|
const promiseFuncs = this.registrationResolvers[promiseid];
|
|
window.clearTimeout(promiseid);
|
|
|
|
if(promiseFuncs) {
|
|
try {
|
|
if(result instanceof Error) {
|
|
promiseFuncs.reject(result as Error);
|
|
} else {
|
|
promiseFuncs.resolve(result as KeyboardStub[]);
|
|
}
|
|
} finally {
|
|
delete this.registrationResolvers[promiseid];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call back from cloud for adding keyboard metadata
|
|
*
|
|
* @param {Object} x metadata object
|
|
**/
|
|
private _registerCore(x): KeyboardStub[] | Error { // TODO (#5044): should return heterogenous type; allow array of stubs.
|
|
var options=x['options'];
|
|
let currentKeyboardStubsCount = this.keyboardStubs.length;
|
|
|
|
// Indicate if unable to register keyboard
|
|
if(typeof(x['error']) == 'string') {
|
|
// Currently unreachable (24 May 2021 - API returns a 404; returned 'script' does not call register)
|
|
var badName='';
|
|
if(typeof(x['keyboardid']) == 'string') {
|
|
badName = x['keyboardid'].substr(0,1).toUpperCase()+x['keyboardid'].substr(1);
|
|
}
|
|
|
|
return new Error(MISSING_KEYBOARD(badName));
|
|
}
|
|
|
|
// Ignore callback unless the context is defined
|
|
if(typeof(options) == 'undefined' || typeof(options['context']) == 'undefined') {
|
|
return new Error(CLOUD_MALFORMED_OBJECT_ERR);
|
|
}
|
|
|
|
// Register each keyboard for the specified language codes
|
|
if(options['context'] == 'keyboard') {
|
|
var i,kp=x['keyboard'];
|
|
// Process array of keyboard definitions
|
|
if(typeof(kp.length) == 'number') {
|
|
for(i=0; i<kp.length; i++) {
|
|
// Note: if an invalid language code is specified, the elements here may be
|
|
// empty arrays. Will not report an error if so.
|
|
this.registerLanguagesForKeyboard(kp[i],options,i);
|
|
}
|
|
} else { // Process a single keyboard definition
|
|
this.registerLanguagesForKeyboard(kp,options,0);
|
|
}
|
|
} else if(options['context'] == 'language') { // Download the full list of supported keyboard languages
|
|
this.languageList = x['languages'];
|
|
}
|
|
|
|
return this.keyboardStubs.slice(currentKeyboardStubsCount);
|
|
}
|
|
|
|
/**
|
|
* Internal handler for processing keyboard registration, used only by `register`
|
|
*
|
|
* @param {string[]} languages Array of language names
|
|
* @returns {Promise<(KeyboardStub|ErrorStub)[]>} Promise of added keyboard stubs
|
|
**/
|
|
addLanguageKeyboards(languages: string[]): Promise<(KeyboardStub|ErrorStub)[]> {
|
|
let errorStubs: ErrorStub[] = [];
|
|
|
|
// Defer registering keyboards by language until the language list has been loaded
|
|
if (this.languageList == null) {
|
|
if (this.languageListPromise == null) {
|
|
this.languageListPromise = this.keymanCloudRequest('',true).catch((error) => {
|
|
// If promise is not error, then... (needs an error guard)
|
|
console.error(error);
|
|
errorStubs.push({error: error});
|
|
return errorStubs;
|
|
});
|
|
}
|
|
|
|
let _this = this;
|
|
return new Promise<(KeyboardStub|ErrorStub)[]>(function(resolve, reject) {
|
|
if (_this.languageListPromise) {
|
|
// 1: wait for the language list to be loaded properly
|
|
return _this.languageListPromise.then(function() {
|
|
// 2: perform the actual query, now that we can find the language code
|
|
resolve(_this.addLanguageKeyboards(languages));
|
|
}).catch(function(error) {
|
|
reject(error);
|
|
});
|
|
} else {
|
|
resolve(_this.addLanguageKeyboards(languages));
|
|
}
|
|
}).then(function(result) {
|
|
if (result instanceof Error) {
|
|
throw result;
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
|
|
// Identify and register each keyboard by language name
|
|
let cmd = '';
|
|
for(let i=0; i<languages.length; i++) {
|
|
let lgName = languages[i].toLowerCase();
|
|
let addAll = (lgName.substr(-1,1) == '$');
|
|
if(addAll) {
|
|
lgName = lgName.substr(0,lgName.length-1);
|
|
}
|
|
|
|
let languageFound: boolean = false;
|
|
for(let j=0; j<this.languageList.length; j++) {
|
|
if(lgName == this.languageList[j]['name'].toLowerCase()) {
|
|
if(cmd != '') {
|
|
cmd = cmd + ',';
|
|
}
|
|
|
|
cmd = cmd+'@'+this.languageList[j]['id'];
|
|
if(addAll) {
|
|
cmd = cmd + '$';
|
|
}
|
|
|
|
languageFound = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!languageFound) {
|
|
// Construct response array of errors (failed-query keyboards)
|
|
// that will be merged with stubs (successfully-queried keyboards)
|
|
let stub: ErrorStub = {language: {name: lgName}, error: new Error(this.alertLanguageUnavailable(lgName))};
|
|
errorStubs.push(stub);
|
|
}
|
|
}
|
|
|
|
if(cmd == '') {
|
|
// No command so return errors
|
|
return Promise.reject(errorStubs);
|
|
}
|
|
|
|
return this.keymanCloudRequest('&keyboardid='+cmd, false).then((result) => {
|
|
return this.mergeAndResolveStubPromises(result, errorStubs);
|
|
}, (err) => {
|
|
console.error(err);
|
|
let stub: ErrorStub = {error: err};
|
|
errorStubs.push(stub);
|
|
return Promise.reject(errorStubs);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Request keyboard metadata from the Keyman Cloud keyboard metadata server
|
|
*
|
|
* @param {string} cmd command string
|
|
* @param {boolean?} byLanguage if true, context=languages, else context=keyboards
|
|
* @returns {Promise<(KeyboardStub[]>} Promise of added keyboard stubs
|
|
**/
|
|
keymanCloudRequest(cmd: string, byLanguage?: boolean): Promise<KeyboardStub[]> {
|
|
var kbdManager = this;
|
|
var keymanweb = this.keymanweb;
|
|
|
|
// Some basic support toward #5044, but definitely not a full solution toward it.
|
|
// Wraps the cloud API keyboard-stub request in a Promise, allowing response on network
|
|
// and/or parser errors. Also detects when `register` returns due to an error case that
|
|
// does not throw errors. (There are a few such "empty" `return` statements there.)
|
|
const URL='https://api.keyman.com/cloud/4.0/'
|
|
+ ((arguments.length > 1) && byLanguage ? 'languages' : 'keyboards');
|
|
|
|
let promise = new Promise(function(resolve: (result: KeyboardStub[]) => void, reject: (error: Error) => void) {
|
|
const Lscript: HTMLScriptElement = keymanweb.util._CreateElement('script');
|
|
|
|
const queryConfig = '?jsonp=keyman.register&languageidtype=bcp47&version='+keymanweb['version'];
|
|
|
|
// Set callback timer
|
|
const timeoutID = window.setTimeout(function() {
|
|
delete kbdManager.registrationResolvers[timeoutID];
|
|
reject(new Error(CLOUD_TIMEOUT_ERR));
|
|
} ,10000);
|
|
|
|
// Save the resolve / reject functions.
|
|
kbdManager.registrationResolvers[timeoutID] = {
|
|
resolve: resolve,
|
|
reject: reject
|
|
};
|
|
|
|
const tFlag='&timerid='+ timeoutID;
|
|
|
|
Lscript.onload = function(event: Event) {
|
|
window.clearTimeout(timeoutID);
|
|
// This case should only happen if a returned, otherwise-valid keyboard
|
|
// script does not ever call `register`. Also provides default handling
|
|
// should `register` fail to report results/failure correctly.
|
|
if(kbdManager.registrationResolvers[timeoutID]) {
|
|
try {
|
|
reject(new Error(CLOUD_STUB_REGISTRATION_ERR));
|
|
} finally {
|
|
delete kbdManager.registrationResolvers[timeoutID];
|
|
}
|
|
}
|
|
};
|
|
|
|
// Note: at this time (24 May 2021), this is also happens for "successful"
|
|
// API calls where there is no matching keyboard ID.
|
|
//
|
|
// The returned 'error' JSON object is sent with an HTML error code (404)
|
|
// and does not call `keyman.register`. Even if it did the latter, the
|
|
// 404 code would likely prevent the returned script's call.
|
|
Lscript.onerror = function(event: string | Event, source?: string,
|
|
lineno?: number, colno?: number, error?: Error) {
|
|
window.clearTimeout(timeoutID);
|
|
try {
|
|
let msg = CLOUD_MALFORMED_OBJECT_ERR;
|
|
if(error) {
|
|
msg = msg + ": " + error.message;
|
|
}
|
|
reject(new Error(msg));
|
|
} finally {
|
|
delete kbdManager.registrationResolvers[timeoutID];
|
|
}
|
|
}
|
|
|
|
Lscript.src = URL + queryConfig + cmd + tFlag;
|
|
|
|
try {
|
|
document.body.appendChild(Lscript);
|
|
} catch(ex) {
|
|
document.getElementsByTagName('head')[0].appendChild(Lscript);
|
|
}
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Display warning if language name unavailable to add keyboard
|
|
* @param {string} languageName
|
|
* @returns string of Error message
|
|
*/
|
|
private alertLanguageUnavailable(languageName: string): string {
|
|
let msg = 'No keyboards are available for '+ languageName + '. '
|
|
+'Does it have another language name?';
|
|
this.keymanweb.util.internalAlert(msg);
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* Display warning if Keyman Cloud server fails to respond
|
|
*
|
|
* @param {string} cmd command string sent to Cloud
|
|
*
|
|
*/
|
|
private serverUnavailable(cmd: string): void {
|
|
this.keymanweb.util.internalAlert(cmd == '' ? 'Unable to connect to Keyman Cloud server!' : cmd);
|
|
this.keymanweb.warned=true;
|
|
}
|
|
|
|
/**
|
|
* Build 362: removeKeyboards() remove keyboard from list of available keyboards
|
|
*
|
|
* @param {string} x keyboard name string
|
|
* @param {boolean} force When true, also drops the cached keyboard object
|
|
*
|
|
*/
|
|
removeKeyboards(x: string, force?: boolean) {
|
|
if(arguments.length == 0) {
|
|
return false;
|
|
}
|
|
|
|
var i, j;
|
|
var success = true, activeRemoved = false, anyRemoved = false;;
|
|
|
|
for(i=0; i<arguments.length; i++) {
|
|
for(j=this.keyboardStubs.length-1; j>=0; j--) {
|
|
if('Keyboard_'+arguments[i] == this.keyboardStubs[j]['KI']) {
|
|
if('Keyboard_'+arguments[i] == this.getActiveKeyboardName()) {
|
|
activeRemoved = true;
|
|
}
|
|
|
|
anyRemoved = true;
|
|
this.keyboardStubs.splice(j,1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(j < 0) {
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
for(i=0; i<arguments.length; i++) {
|
|
for(j=this.keyboards.length-1; j>=0; j--) {
|
|
if('Keyboard_'+arguments[i] == this.keyboards[j]['KI']) {
|
|
this.keyboards.splice(j, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(activeRemoved) {
|
|
if(this.keyboardStubs.length > 0) {
|
|
// Always reset to the first remaining keyboard
|
|
this._SetActiveKeyboard(this.keyboardStubs[0]['KI'],this.keyboardStubs[0]['KLC'],true);
|
|
} else {
|
|
this._SetActiveKeyboard('', '', false);
|
|
}
|
|
// This is likely to be triggered by a UI call of some sort, and we need to treat
|
|
// this call as such to properly maintain the globalKeyboard setting.
|
|
this.keymanweb.uiManager.justActivated = true;
|
|
}
|
|
|
|
if(anyRemoved) {
|
|
// Update the UI keyboard menu
|
|
this.doKeyboardUnregistered();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* Function _registerKeyboard KR
|
|
* Scope Public
|
|
* @param {Object} Pk Keyboard object
|
|
* Description Register and load the keyboard
|
|
*/
|
|
async _registerKeyboard(Pk) {
|
|
// Ensure keymanweb is initialized before continuing to register keyboards
|
|
if(!this.keymanweb.initialized) {
|
|
await this.deferment;
|
|
}
|
|
|
|
if(Pk['_kmw']) {
|
|
console.error("The keyboard _kmw property is a reserved field for engine use only; this keyboard is invalid.");
|
|
return;
|
|
} else {
|
|
Pk['_kmw'] = new KeyboardTag();
|
|
}
|
|
|
|
var Li,Lstub;
|
|
|
|
// For package namespacing with KMEA/KMEI.
|
|
if(this.keymanweb.isEmbedded) {
|
|
this.keymanweb.preserveID(Pk);
|
|
}
|
|
|
|
// Check if the active stub refers to this keyboard, else find applicable stub
|
|
|
|
var Ps=this.activeStub;
|
|
var savedActiveStub = this.activeStub;
|
|
if(!Ps || !('KI' in Ps) || (Ps['KI'] != Pk['KI'])) {
|
|
// Find the first stub for this keyboard
|
|
for(Lstub=0; Lstub < this.keyboardStubs.length; Lstub++) { // I1511 - array prototype extended
|
|
Ps=this.keyboardStubs[Lstub];
|
|
if(Pk['KI'] == Ps['KI']) {
|
|
break;
|
|
}
|
|
|
|
Ps=null;
|
|
}
|
|
}
|
|
|
|
// Build 369: ensure active stub defined when loading local keyboards
|
|
if(this.activeStub == null && Ps != null) {
|
|
this.activeStub = Ps;
|
|
}
|
|
|
|
// Register the stub for this language (unless it is already registered)
|
|
// keymanweb.KRS(Ps?Ps:Pk);
|
|
|
|
// Test if keyboard already loaded
|
|
for(Li=0; Li<this.keyboards.length; Li++) {
|
|
if(Pk['KI'] == this.keyboards[Li]['KI']) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Append to keyboards array
|
|
this.keyboards=this.keymanweb._push(this.keyboards, Pk); // TODO: Resolve without need for the cast.
|
|
|
|
// Execute any external (UI) code needed after loading keyboard
|
|
this.doKeyboardLoaded(Pk['KI']);
|
|
|
|
// Restore the originally-active stub to its prior state. No need to change it permanently.
|
|
this.activeStub = savedActiveStub;
|
|
}
|
|
|
|
/**
|
|
* Add the basic keyboard parameters (keyboard stub) to the array of keyboard stubs
|
|
* If no language code is specified in a keyboard it cannot be registered,
|
|
* and a keyboard stub must be registered before the keyboard is loaded
|
|
* for the keyboard to be usable.
|
|
*
|
|
* @param {Object} Pstub Keyboard stub object
|
|
* @return {Promise<?boolean>} 1 if already registered, else null
|
|
*/
|
|
async _registerStub(Pstub): Promise<boolean> {
|
|
Pstub = { ... Pstub}; // shallow clone the stub object
|
|
// Ensure keymanweb is initialized before continuing to register stub
|
|
if(!this.keymanweb.initialized) {
|
|
await this.deferment;
|
|
}
|
|
|
|
// The default stub is always the first keyboard stub loaded [and will be ignored by desktop browsers - not for beta, anyway]
|
|
if(this.dfltStub == null) {
|
|
this.dfltStub=Pstub;
|
|
//if(device.formFactor == 'desktop') return 1; //Needs further thought before release
|
|
}
|
|
|
|
// If no language code has been defined, and no stub has been registered for this keyboard, register with empty string as the language code
|
|
if(this.keymanweb.isEmbedded) {
|
|
this.keymanweb.namespaceID(Pstub);
|
|
} // else leave undefined. It's nice to condition upon.
|
|
if(typeof(Pstub['KLC']) == 'undefined') {
|
|
Pstub['KLC'] = '';
|
|
}
|
|
if(typeof(Pstub['KL']) == 'undefined') {
|
|
Pstub['KL'] = 'undefined';
|
|
}
|
|
|
|
// Register stub (add to KeyboardStubs array)
|
|
this.keyboardStubs=this.keymanweb._push(this.keyboardStubs, Pstub); // TODO: Resolve without need for the cast.
|
|
|
|
// TODO: Need to distinguish between initial loading of a large number of stubs and any subsequent loading.
|
|
// UI initialization should not be needed for each registration, only at end.
|
|
// Reload this keyboard if it was the last active keyboard and
|
|
// make any changes needed by UI for new keyboard stub
|
|
// (Uncommented for Build 360)
|
|
this.doKeyboardRegistered(Pstub['KI'],Pstub['KL'],Pstub['KN'],Pstub['KLC'],Pstub['KP']);
|
|
|
|
// If we have no activeStub because there were no stubs, set the new keyboard as active.
|
|
// Do not trigger on merges.
|
|
if(!this.activeStub && this.dfltStub == Pstub && this.keyboardStubs.length == 1 && this.keymanweb.options['setActiveOnRegister']=='true') {
|
|
this.setActiveKeyboard(Pstub['KI'], Pstub['KLC']);
|
|
}
|
|
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
/*
|
|
* Last part - the events.
|
|
*/
|
|
|
|
/**
|
|
* Execute external (UI) code needed on registering keyboard, used
|
|
* to update each UIs language menu
|
|
*
|
|
* Note that the argument object is not at present used by any UI,
|
|
* since the menu is always fully recreated when needed, but the arguments
|
|
* remain defined to allow for possible use in future (Aug 2014)
|
|
*
|
|
* @param {string} _internalName
|
|
* @param {string} _language
|
|
* @param {string} _keyboardName
|
|
* @param {string} _languageCode
|
|
* @param {string=} _packageID Used by KMEA/KMEI to track .kmp related info.
|
|
* @return {boolean}
|
|
*/
|
|
doKeyboardRegistered(_internalName: string, _language: string, _keyboardName: string,
|
|
_languageCode: string, _packageID?: string): boolean {
|
|
var p={'internalName':_internalName,'language':_language,'keyboardName':_keyboardName,'languageCode':_languageCode};
|
|
|
|
// Utilized only by our embedded codepaths.
|
|
if(_packageID) {
|
|
p['package'] = _packageID;
|
|
}
|
|
return this.keymanweb.util.callEvent('kmw.keyboardregistered',p);
|
|
}
|
|
|
|
/**
|
|
* Execute external (UI) code to rebuild menu when deregistering keyboard
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
|
|
doKeyboardUnregistered(): boolean {
|
|
var p={};
|
|
return this.keymanweb.util.callEvent('kmw.keyboardregistered',p);
|
|
}
|
|
|
|
/**
|
|
* Execute external (UI) code needed on loading keyboard
|
|
*
|
|
* @param {string} _internalName
|
|
* @return {boolean}
|
|
*/
|
|
doKeyboardLoaded(_internalName: string): boolean {
|
|
var p={};
|
|
p['keyboardName']=_internalName;
|
|
return this.keymanweb.util.callEvent('kmw.keyboardloaded', p);
|
|
}
|
|
|
|
/**
|
|
* Function doBeforeKeyboardChange
|
|
* Scope Private
|
|
* @param {string} _internalName
|
|
* @param {string} _languageCode
|
|
* @return {boolean}
|
|
* Description Execute external (UI) code needed before changing keyboard
|
|
*/
|
|
doBeforeKeyboardChange(_internalName: string, _languageCode: string): boolean {
|
|
var p={};
|
|
p['internalName']=_internalName;
|
|
p['languageCode']=_languageCode;
|
|
return this.keymanweb.util.callEvent('kmw.beforekeyboardchange',p);
|
|
}
|
|
|
|
/**
|
|
* Execute external (UI) code needed *after* changing keyboard
|
|
*
|
|
* @param {string} _internalName
|
|
* @param {string} _languageCode
|
|
* @param {boolean=} _indirect
|
|
* @return {boolean}
|
|
*/
|
|
doKeyboardChange(_internalName: string, _languageCode: string, _indirect?:boolean): boolean {
|
|
var p: KeyboardChangeData = {
|
|
'internalName': _internalName,
|
|
'languageCode': _languageCode,
|
|
'indirect': (arguments.length > 2 ? _indirect : false)
|
|
}
|
|
|
|
return this.keymanweb.util.callEvent('kmw.keyboardchange', p);
|
|
}
|
|
|
|
shutdown() {
|
|
for(let script of this.linkedScripts) {
|
|
if(script.remove) {
|
|
script.remove();
|
|
} else if(script.parentNode) {
|
|
script.parentNode.removeChild(script);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|