// Includes KMW-added property declaration extensions for HTML elements. /// // Includes a promise polyfill (needed for IE) /// // Defines the web-page interface object. /// // Defines the core text processor. /// // Extends KeyboardInterface with DOM-oriented offerings. /// // Defines the web-page interface object. /// // Includes KMW-added property declaration extensions for HTML elements. /// // Defines keyboard management classes. /// // Defines KMW's hotkey management object. /// // Defines the ui management code that tracks UI activation and such. /// // Defines OSK management code. /// /// /// // Defines the model manager. /// /*** KeymanWeb 14.0 Copyright 2017-2021 SIL International ***/ namespace com.keyman { export enum SpacebarText { KEYBOARD = 'keyboard', LANGUAGE = 'language', LANGUAGE_KEYBOARD = 'languageKeyboard', BLANK = 'blank' }; export interface OptionType { root?: string; resources?: string; keyboards?: string; fonts?: string; attachType?: 'auto' | 'manual' | ''; // If blank or undefined, attachType will be assigned to "auto" or "manual" ui?: string; setActiveOnRegister?: string; // TODO: Convert to boolean. Option loader needs to be able to receive this as a string or boolean // Determines the default text shown on the spacebar, if undefined, LANGUAGE_KEYBOARD spacebarText?: SpacebarText; // Determines whether or not KeymanWeb should display its own alert messages useAlerts?: boolean; } export class KeymanBase { _IE = 0; // browser version identification _MasterDocument = null; // Document with controller (to allow iframes to distinguish local/master control) _HotKeys = []; // Array of document-level hotkey objects warned = false; // Warning flag (to prevent multiple warnings) baseFont = 'sans-serif'; // Default font for mapped input elements appliedFont = ''; // Chain of fonts to be applied to mapped input elements fontCheckTimer = null; // Timer for testing loading of embedded fonts srcPath = ''; // Path to folder containing executing keymanweb script rootPath = ''; // Path to server root protocol = ''; // Protocol used for the KMW script. mustReloadKeyboard = false;// Force keyboard refreshing even if already loaded globalKeyboard = null; // Indicates the currently-active keyboard for controls without independent keyboard settings. globalLanguageCode = null; // Indicates the language code corresponding to `globalKeyboard`. isEmbedded = false; // Indicates if the KeymanWeb instance is embedded within a mobile app. // Blocks full page initialization when set to `true`. refocusTimer = 0; // Tracks a timeout event that aids of OSK modifier/state key tracking when the document loses focus. initialized: number; // Signals the initialization state of the KeymanWeb system. isHeadless = false; // Indicates that KMW lacks any access to the DOM. Nothing yet implemented for '= true'. 'build' = 300; // TS needs this to be defined within the class. _BrowserIsSafari: boolean; // A legacy browser-check variable. // Used as placeholders during initialization. // The corresponding class properties should be dropped after a refactor; // this is an intermediate solution while doing the big conversion. static _srcPath: string; static _rootPath: string; static _protocol: string; // Internal objects ['util']: Util; ['osk']: com.keyman.osk.OSKView; ['ui']: any; keyboardManager: keyboards.KeyboardManager; domManager: dom.DOMManager; hotkeyManager: HotkeyManager; uiManager: UIManager; core: text.InputProcessor; modelManager: text.prediction.ModelManager; touchAliasing: dom.DOMEventHandlers; // Defines default option values options: OptionType = { root: '', resources: '', keyboards: '', fonts: '', attachType: '', ui: null, setActiveOnRegister: 'true', // TODO: convert to boolean spacebarText: SpacebarText.LANGUAGE_KEYBOARD, // Determines whether or not KeymanWeb should display its own alert messages useAlerts: true }; // Stub functions (defined later in code only if required) setDefaultDeviceOptions(opt: OptionType){} getStyleSheetPath(s){return s;} getKeyboardPath(f, p?){return f;} KC_(n, ln, Pelem){return '';} handleRotationEvents(){} // Will serve as an API function for a workaround, in case of future touch-alignment issues. ['alignInputs'](eleList?: HTMLElement[]){} hideInputs() {}; namespaceID(Pstub) {}; preserveID(Pk) {}; setInitialized(val: number) { this.initialized = this['initialized'] = val; } refreshElementContent = null; // ------------- constructor() { // Allow internal minification of the public modules. this.util = this['util'] = new Util(this); this.ui = this['ui'] = {}; this.keyboardManager = new keyboards.KeyboardManager(this); this.domManager = new dom.DOMManager(this); this.hotkeyManager = new HotkeyManager(this); this.uiManager = new UIManager(this); // I732 START - Support for European underlying keyboards #1 var baseLayout: string; if(typeof(window['KeymanWeb_BaseLayout']) !== 'undefined') { baseLayout = window['KeymanWeb_BaseLayout']; } else { baseLayout = 'us'; } this._BrowserIsSafari = (navigator.userAgent.indexOf('AppleWebKit') >= 0); // I732 END - Support for European underlying keyboards #1 this.core = new text.InputProcessor(this.util.device.coreSpec, { baseLayout: baseLayout, variableStoreSerializer: new dom.VariableStoreCookieSerializer() }); // Used by the embedded apps. this['interface'] = this.core.keyboardInterface; this.modelManager = new text.prediction.ModelManager(); this.osk = this['osk'] = null; // Load properties from their static variants. this['build'] = com.keyman.environment.BUILD; this.srcPath = KeymanBase._srcPath; this.rootPath = KeymanBase._rootPath; this.protocol = KeymanBase._protocol; this['version'] = com.keyman.environment.VERSION; this['helpURL'] = 'http://help.keyman.com/go'; this.setInitialized(0); // Signals that a KMW load has occurred in order to prevent double-loading. this['loaded'] = true; } delayedInit() { // Track the selected Event-handling object. this.touchAliasing = this.util.device.touchable ? this.domManager.touchHandlers : this.domManager.nonTouchHandlers; } /** * Triggers a KeymanWeb engine shutdown to facilitate a full system reset. * This function is designed for use with KMW unit-testing, which reloads KMW * multiple times to test the different initialization paths. */ ['shutdown']() { // Disable page focus/blur events, which can sometimes trigger and cause parallel KMW instances in testing. this.util.detachDOMEvent(window, 'focus', this['pageFocusHandler'], false); this.util.detachDOMEvent(window, 'blur', this['pageFocusHandler'], false); this.domManager.shutdown(); this.osk.shutdown(); this.util.shutdown(); this.keyboardManager.shutdown(); this.core.languageProcessor.shutdown(); if(this.ui && this.ui.shutdown) { this.ui.shutdown(); } dom.DOMEventHandlers.states = new dom.CommonDOMStates(); } /** * Returns a generalized metadata object about the state of KMW for use with error reporting. */ ['getDebugInfo']() { let metadata = { attachType: this.options.attachType, device: this.util.device, initialized: this.initialized, isEmbedded: this.isEmbedded, ui: this.ui ? this.ui.name : null } if(this.util.device.touchable) { metadata.ui = 'touch'; } return metadata; } /** * Expose font testing to allow checking that SpecialOSK or custom font has * been correctly loaded by browser * * @param {string} fName font-family name * @return {boolean} true if available **/ ['isFontAvailable'](fName: string): boolean { return this.util.checkFont({'family':fName}); } /** * Function addEventListener * Scope Public * @param {string} event event to handle * @param {function(Event)} func event handler function * @return {boolean} value returned by util.addEventListener * Description Wrapper function to add and identify KeymanWeb-specific event handlers */ ['addEventListener'](event: string, func): boolean { return this.util.addEventListener('kmw.'+event, func); } /** * Function _GetEventObject * Scope Private * @param {Event=} e Event object if passed by browser * @return {Event|null} Event object * Description Gets the event object from the window when using Internet Explorer * and handles getting the event correctly in frames */ _GetEventObject(e: E) { // I2404 - Attach to controls in IFRAMEs if (!e) { e = window.event as E; if(!e) { var elem: HTMLElement = this.domManager.lastActiveElement; if(elem) { let doc = elem.ownerDocument; var win: Window; if(doc) { win = doc.defaultView; } if(!win) { return null; } e = win.event as E; } } } return e; } /** * Function _push * Scope Private * @param {Array} Parray Array * @param {*} Pval Value to be pushed or appended to array * @return {Array} Returns extended array * Description Push (if possible) or append a value to an array */ _push(Parray: T[], Pval: T) { if(Parray.push) { Parray.push(Pval); } else { Parray=Parray.concat(Pval); } return Parray; } // Base object API definitions /** * Function attachToControl * Scope Public * @param {Element} Pelem Element to which KMW will be attached * Description Attaches KMW to control (or IFrame) */ ['attachToControl'](Pelem: HTMLElement) { this.domManager.attachToControl(Pelem); } /** * Function detachFromControl * Scope Public * @param {Element} Pelem Element from which KMW will detach * Description Detaches KMW from a control (or IFrame) */ ['detachFromControl'](Pelem: HTMLElement) { this.domManager.detachFromControl(Pelem); } /** * Exposed function to load keyboards by name. One or more arguments may be used * * @param {any[]} args keyboard name string or keyboard metadata JSON object * @returns {Promise<(KeyboardStub|ErrorStub)[]>} Promise of added keyboard/error stubs * */ ['addKeyboards'](...args: any[]) : Promise<(com.keyman.keyboards.KeyboardStub|com.keyman.keyboards.ErrorStub)[]> { if (!args || !args[0] || args[0].length == 0) { // Get the cloud keyboard catalog return this.keyboardManager.keymanCloudRequest('',false).catch(error => { console.error(error); return Promise.reject([{error: error}]); }); } else { let x: (string|com.keyman.keyboards.KeyboardStub)[] = []; if (Array.isArray(args[0])) { args[0].forEach(a => x.push(a)); } else if (Array.isArray(args)) { args.forEach(a => x.push(a)); } else { x.push(args); } return this.keyboardManager.addKeyboardArray(x); } } /** * Add default keyboards for given language(s) * * @param {string|string[]} arg Language name (multiple arguments allowed) * @returns {Promise<(KeyboardStub|ErrorStub)[]>} Promise of added keyboard/error stubs **/ ['addKeyboardsForLanguage'](arg: string[]|string) : Promise<(com.keyman.keyboards.KeyboardStub|com.keyman.keyboards.ErrorStub)[]> { if (typeof arg === 'string') { return this.keyboardManager.addLanguageKeyboards(arg.split(',').map(item => item.trim())); } else { return this.keyboardManager.addLanguageKeyboards(arg); } } /** * Call back from cloud for adding keyboard metadata * * @param {Object} x metadata object **/ ['register'](x) { this.keyboardManager.register(x); } /** * 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, force?) { return this.keyboardManager.removeKeyboards(x); } /** * 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 { return this.keyboardManager.setActiveKeyboard(PInternalName,PLgCode); } /** * Function getActiveKeyboard * Scope Public * @return {string} Name of active keyboard * Description Return internal name of currently active keyboard */ ['getActiveKeyboard'](): string { return this.keyboardManager.getActiveKeyboardName(); } /** * Function getActiveLanguage * Scope Public * @param {boolean=} true to retrieve full language name, false/undefined to retrieve code. * @return {string} language code * Description Return language code for currently selected language */ ['getActiveLanguage'](fullName?: boolean): string { return this.keyboardManager.getActiveLanguage(fullName); } ['isAttached'](x: HTMLElement): boolean { return this.domManager.isAttached(x); } /** * Function isCJK * Scope Public * @param {Object=} k0 * @return {boolean} * Description Tests if active keyboard (or specified keyboard script object, as optional argument) * uses a pick list (Chinese, Japanese, Korean, etc.) * (This function accepts either keyboard structure.) */ ['isCJK'](k0?) { var kbd: keyboards.Keyboard; if(k0) { kbd = new keyboards.Keyboard(k0); } else { kbd = this.core.activeKeyboard; } return kbd && kbd.isCJK; } /** * Function isChiral * Scope Public * @param {string|Object=} k0 * @return {boolean} * Description Tests if the active keyboard (or optional argument) uses chiral modifiers. */ ['isChiral'](k0?) { var kbd: keyboards.Keyboard; if(k0) { kbd = new keyboards.Keyboard(k0); } else { kbd = this.core.activeKeyboard; } return kbd.isChiral; } /** * Get keyboard meta data for the selected keyboard and language * * @param {string} PInternalName Internal name of keyboard * @param {string=} PlgCode language code * @return {Object} Details of named keyboard * **/ ['getKeyboard'](PInternalName: string, PlgCode?: string) { var Ln, Lrn; var kbdList = this.keyboardManager.getDetailedKeyboards(); for(Ln=0; Ln < kbdList.length; Ln++) { Lrn = kbdList[Ln]; if(Lrn['InternalName'] == PInternalName || Lrn['InternalName'] == "Keyboard_" + PInternalName) { if(arguments.length < 2) { return Lrn; } if(Lrn['LanguageCode'] == PlgCode) { return Lrn; } } } return null; } /** * Get array of available keyboard stubs * * @return {Array} Array of available keyboards * */ ['getKeyboards']() { return this.keyboardManager.getDetailedKeyboards(); } /** * 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']() { return this.keyboardManager.getSavedKeyboard(); } /** * Function Initialization * Scope Public * @param {Object} arg object array of user-defined properties * Description KMW window initialization */ ['init'](arg): Promise { return this.domManager.init(arg); } /** * Function resetContext * Scope Public * @param {Object} e The element whose context should be cleared. If null, the currently-active element will be chosen. * Description Reverts the OSK to the default layer, clears any processing caches and modifier states, * and clears deadkeys and prediction-processing states on the active element (if it exists) */ ['resetContext'](e?: HTMLElement) { let elem = e; if(!elem) { elem = this.domManager.activeElement; } let outputTarget = dom.Utils.getOutputTarget(elem); if(outputTarget) { outputTarget.resetContext(); } this.core.resetContext(outputTarget); }; /** * Function setNumericLayer * Scope Public * Description Set OSK to numeric layer if it exists */ ['setNumericLayer']() { this.core.keyboardProcessor.setNumericLayer(this.util.device.coreSpec); }; /** * Function disableControl * Scope Public * @param {Element} Pelem Element to be disabled * Description Disables a KMW control element */ ['disableControl'](Pelem: HTMLElement) { this.domManager.disableControl(Pelem); } /** * Function enableControl * Scope Public * @param {Element} Pelem Element to be disabled * Description Disables a KMW control element */ ['enableControl'](Pelem: HTMLMapElement) { this.domManager.enableControl(Pelem); } /** * Function setKeyboardForControl * Scope Public * @param {Element} Pelem Control element * @param {string|null=} Pkbd Keyboard (Clears the set keyboard if set to null.) * @param {string|null=} Plc Language Code * Description Set default keyboard for the control */ ['setKeyboardForControl'](Pelem: HTMLElement, Pkbd?: string, Plc?: string) { this.domManager.setKeyboardForControl(Pelem, Pkbd, Plc); } /** * Function getKeyboardForControl * Scope Public * @param {Element} Pelem Control element * @return {string|null} The independently-managed keyboard for the control. * Description Returns the keyboard ID of the current independently-managed keyboard for this control. * If it is currently following the global keyboard setting, returns null instead. */ ['getKeyboardForControl'](Pelem) { this.domManager.getKeyboardForControl(Pelem); } /** * Function getLanguageForControl * Scope Public * @param {Element} Pelem Control element * @return {string|null} The independently-managed keyboard for the control. * Description Returns the language code used with the current independently-managed keyboard for this control. * If it is currently following the global keyboard setting, returns null instead. */ ['getLanguageForControl'](Pelem) { this.domManager.getLanguageForControl(Pelem); } /** * Set focus to last active target element (browser-dependent) */ ['focusLastActiveElement']() { this.domManager.focusLastActiveElement(); } /** * Get the last active target element *before* KMW activated (I1297) * * @return {Object} */ ['getLastActiveElement']() { return this.domManager.lastActiveElement; } /** * Set the active input element directly optionally setting focus * * @param {Object|string} e element id or element * @param {boolean=} setFocus optionally set focus (KMEW-123) **/ ['setActiveElement'](e: string|HTMLElement, setFocus: boolean) { return this.domManager.setActiveElement(e, setFocus); } /** * Move focus to user-specified element * * @param {string|Object} e element or element id * **/ ['moveToElement'](e: string|HTMLElement) { this.domManager.moveToElement(e); } /** * Function addHotkey * Scope Public * @param {number} keyCode * @param {number} shiftState * @param {function(Object)} handler * Description Add hot key handler to array of document-level hotkeys triggered by key up event */ ['addHotKey'](keyCode: number, shiftState: number, handler: () => void) { this.hotkeyManager.addHotKey(keyCode, shiftState, handler); } /** * Function removeHotkey * Scope Public * @param {number} keyCode * @param {number} shiftState * Description Remove a hot key handler from array of document-level hotkeys triggered by key up event */ ['removeHotKey'](keyCode: number, shiftState: number) { this.hotkeyManager.removeHotkey(keyCode, shiftState); } /** * Function getUIState * Scope Public * @return {Object.} * Description Return object with activation state of UI: * activationPending (bool): KMW being activated * activated (bool): KMW active */ ['getUIState'](): UIState { return this.uiManager.getUIState(); } /** * Set or clear the IsActivatingKeymanWebUI flag (exposed function) * * @param {(boolean|number)} state Activate (true,false) */ ['activatingUI'](state: boolean) { this.uiManager.setActivatingUI(state); } // Functions that might be added later ['beepKeyboard']: () => void; /** * @param {number} dn Number of pre-caret characters to delete * @param {string} s Text to insert * @param {number=} dr Number of post-caret characters to delete */ ['oninserttext']: (dn: number, s: string, dr?: number) => void; /** * Create copy of the OSK that can be used for embedding in documentation or help * The currently active keyboard will be returned if PInternalName is null * * @param {string} PInternalName internal name of keyboard, with or without Keyboard_ prefix * @param {number} Pstatic static keyboard flag (unselectable elements) * @param {string=} argFormFactor layout form factor, defaulting to 'desktop' * @param {(string|number)=} argLayerId name or index of layer to show, defaulting to 'default' * @return {Object} DIV object with filled keyboard layer content */ ['BuildVisualKeyboard'](PInternalName, Pstatic, argFormFactor, argLayerId): HTMLElement { let PKbd: com.keyman.keyboards.Keyboard = null; if(PInternalName != null) { var p=PInternalName.toLowerCase().replace('keyboard_',''); var keyboardsList = this.keyboardManager.keyboards; for(let Ln=0; Ln