spiegel-keyman/web/source/osk/oskView.ts
Marc Durdin da5d00c284 fix(web): Non-integer OSK sizes
If the OSK had a non-integer width or height stored in the OSK cookie,
then the width and height would be parsed incorrectly and the OSK would
fail to be sized correctly on first load; this also caused a script
error and made the OSK impossible to interact with.
2022-02-12 19:06:12 +07:00

1078 lines
No EOL
36 KiB
TypeScript

// Includes the banner
/// <reference path="./bannerManager.ts" />
// Generates the visual keyboard specific to each keyboard. (class="kmw-osk-inner-frame")
/// <reference path="visualKeyboard.ts" />
// Models keyboards that present a help page, rather than a standard OSK.
/// <reference path="helpPageView.ts" />
/// <reference path="emptyView.ts" />
namespace com.keyman.osk {
export type OSKRect = {
'left'?: number,
'top'?: number,
'width'?: number,
'height'?: number,
'nosize'?: boolean,
'nomove'?: boolean
};
export enum ActivationMode {
static = "static", // For use by documentation keyboards, eventually.
manual = "manual",
automatic = "automatic"
}
export abstract class OSKView {
_Box: HTMLDivElement;
headerView: OSKViewComponent;
bannerView: BannerManager; // Which implements OSKViewComponent
keyboardView: KeyboardView; // Which implements OSKViewComponent
footerView: OSKViewComponent;
protected device: com.keyman.utils.DeviceSpec;
protected readonly hostDevice: com.keyman.utils.DeviceSpec;
private _boxBaseMouseDown: (e: MouseEvent) => boolean;
private _boxBaseTouchStart: (e: TouchEvent) => boolean;
private _boxBaseTouchEventCancel: (e: TouchEvent) => boolean;
private keyboard: keyboards.Keyboard;
private _target: text.OutputTarget;
/**
* The configured width for this OSKManager. May be `undefined` or `null`
* to allow automatic width scaling.
*/
private _width: ParsedLengthStyle;
/**
* The configured height for this OSKManager. May be `undefined` or `null`
* to allow automatic height scaling.
*/
private _height: ParsedLengthStyle;
/**
* The computed width for this OSKManager. May be null if auto sizing
* is allowed and the OSKManager is not currently in the DOM hierarchy.
*/
private _computedWidth: number;
/**
* The computed height for this OSKManager. May be null if auto sizing
* is allowed and the OSKManager is not currently in the DOM hierarchy.
*/
private _computedHeight: number;
/**
* The base font size to use for hosted `Banner`s and `VisualKeyboard`
* instances.
*/
private _baseFontSize: ParsedLengthStyle;
private needsLayout: boolean = true;
//
private _activationMode: ActivationMode = ActivationMode.automatic;
private _displayIfActive: boolean = true;
private _animatedHideTimeout: number;
constructor(deviceSpec: com.keyman.utils.DeviceSpec, hostDevice?: com.keyman.utils.DeviceSpec) {
this.device = deviceSpec;
if(!hostDevice) {
hostDevice = deviceSpec;
}
this.hostDevice = hostDevice;
// OSK initialization - create DIV and set default styles
this._Box = document.createElement('div'); // Container for OSK (Help DIV, displayed when user clicks Help icon)
this._Box.style.userSelect = 'none';
// Initializes the two constant OSKComponentView fields.
this.bannerView = new BannerManager(this.hostDevice);
this.keyboardView = null;
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
// Install the default OSK stylesheet
util.linkStyleSheet(keymanweb.getStyleSheetPath('kmwosk.css'));
this.setBaseMouseEventListeners();
if(hostDevice.touchable) {
this.setBaseTouchEventListeners();
}
// Register a listener for model change events so that we can hot-swap the banner as needed.
// Handled here b/c banner changes may trigger a need to re-layout the OSK.
const _this = this;
keymanweb.core.languageProcessor.on('statechange',
function(state: text.prediction.StateChangeEnum) {
let currentType = _this.bannerView.activeType;
_this.bannerView.selectBanner(state);
if(currentType != _this.bannerView.activeType) {
_this.refreshLayout();
}
return true;
});
}
private setBaseMouseEventListeners() {
let keymanweb = com.keyman.singleton;
this._boxBaseMouseDown = function(e) {
keymanweb.uiManager.setActivatingUI(true);
return false;
}
this._Box.addEventListener('mousedown', this._boxBaseMouseDown, false);
}
private removeBaseMouseEventListeners() {
if(this._boxBaseMouseDown) {
this._Box.removeEventListener('mousedown', this._boxBaseMouseDown, false);
this._boxBaseMouseDown = null;
}
}
private setBaseTouchEventListeners() {
// And to prevent touch event default behaviour on mobile devices
let keymanweb = com.keyman.singleton;
var cancelEventFunc = this._boxBaseTouchEventCancel = function(e) {
if(e.cancelable) {
e.preventDefault();
}
e.stopPropagation();
return false;
};
this._boxBaseTouchStart = function(e) {
keymanweb.uiManager.setActivatingUI(true);
return cancelEventFunc(e);
}
this._Box.addEventListener('touchstart', this._boxBaseTouchStart, false);
this._Box.addEventListener('touchmove', this._boxBaseTouchEventCancel, false);
this._Box.addEventListener('touchend', this._boxBaseTouchEventCancel, false);
this._Box.addEventListener('touchcancel', this._boxBaseTouchEventCancel, false);
}
private removeBaseTouchEventListeners() {
if(!this._boxBaseTouchEventCancel) {
return;
}
this._Box.removeEventListener('touchstart', this._boxBaseTouchStart, false);
this._Box.removeEventListener('touchmove', this._boxBaseTouchEventCancel, false);
this._Box.removeEventListener('touchend', this._boxBaseTouchEventCancel, false);
this._Box.removeEventListener('touchcancel', this._boxBaseTouchEventCancel, false);
this._boxBaseTouchEventCancel = null;
this._boxBaseTouchStart = null;
}
/**
* Gets and sets the IME-like interface (`OutputTarget`) to be affected by events from
* the OSK.
*
* If `activationMode` is `'conditional'`, this property's state controls the visibility
* of the OSKView.
*/
public get activeTarget(): text.OutputTarget {
return this._target;
}
public set activeTarget(targ: text.OutputTarget) {
// If already null & set to null again, take no action.
if(this._target == null && targ == null) {
return;
}
this._target = targ;
this.commonCheckAndDisplay();
}
public get targetDevice(): com.keyman.utils.DeviceSpec {
return this.device;
}
public set targetDevice(spec: com.keyman.utils.DeviceSpec) {
if(this.allowsDeviceChange(spec)) {
this.device = spec;
this.loadActiveKeyboard();
} else {
console.error("May not change target device for this OSKView type.");
}
}
protected allowsDeviceChange(newSpec: com.keyman.utils.DeviceSpec): boolean {
return false;
}
/**
* Determines the activation state model used to control presentation of the OSK.
* - `'conditional'`: Only displays if `activeTarget` is non-null - if there is an active
* target that can receive the OSK's context-manipulation events.
* - `'manual'`: Display is directly controlled by manipulating the value of `displayIfActive`.
* It may be displayed while `activeTarget` is `null`.
* - `'static'`: The OSK should be permanently displayed and may never be hidden.
*/
get activationMode(): ActivationMode {
if(!this._activationMode) {
this._activationMode = ActivationMode.automatic;
}
return this._activationMode;
}
set activationMode(mode: ActivationMode) {
this._activationMode = mode;
this.commonCheckAndDisplay();
}
/**
* Implementation of the activation modeling described in the documentation for
* `activationMode`.
*/
protected get activationConditionsMet(): boolean {
switch(this.activationMode) {
case 'manual':
return true;
case 'static':
return true;
case 'automatic':
return !!this.activeTarget;
default:
console.error("Unexpected activation mode set for the OSK.");
return false;
}
}
/**
* A property denoting whether or not the OSK should be presented if it meets its
* activation conditions.
*
* When `activationMode == 'manual'`, `displayIfActive == true` is the lone
* activation condition.
*
* Note: cannot be set to `false` if `activationMode == 'static'`.
*/
get displayIfActive(): boolean {
return this._displayIfActive;
}
set displayIfActive(flag: boolean) {
if(this.displayIfActive == flag) {
return;
}
// if is touch device or is CJK keyboard, this.displayIfActive must remain true.
if(this.keyboard?.isCJK && !flag) {
console.warn("Cannot hide display of OSK for CJK keyboards.");
flag = true;
} else if(this.hostDevice.touchable && !flag) {
console.warn("Cannot hide display of OSK when hosted on touch-based devices.");
flag = true;
} else if(this.activationMode == 'static') {
// Silently fail; it's a documentation keyboard.
// This is the primary difference between 'manual' and 'static'.
flag = true;
}
this._displayIfActive = flag;
this.commonCheckAndDisplay();
}
/**
* Used by the activation & visibility properties as a common helper; all of their
* setters rely on this function to manage presentation (showing / hiding) of the OSK.
*/
private commonCheckAndDisplay() {
if(this.activationConditionsMet && this.displayIfActive) {
this.present();
} else {
this.startHide(false);
}
}
public get vkbd(): VisualKeyboard {
if(this.keyboardView instanceof VisualKeyboard) {
return this.keyboardView;
} else {
return null;
}
}
public get banner(): BannerManager { // Maintains old reference point used by embedding apps.
return this.bannerView;
}
/**
* The configured width for this VisualKeyboard. May be `undefined` or `null`
* to allow automatic width scaling.
*/
get width(): ParsedLengthStyle {
return this._width;
}
/**
* The configured height for this VisualKeyboard. May be `undefined` or `null`
* to allow automatic height scaling.
*/
get height(): ParsedLengthStyle {
return this._height;
}
/**
* The computed width for this VisualKeyboard. May be null if auto sizing
* is allowed and the VisualKeyboard is not currently in the DOM hierarchy.
*/
get computedWidth(): number {
// Computed during layout operations; allows caching instead of continuous recomputation.
if(this.needsLayout) {
this.refreshLayout();
}
return this._computedWidth;
}
/**
* The computed height for this VisualKeyboard. May be null if auto sizing
* is allowed and the VisualKeyboard is not currently in the DOM hierarchy.
*/
get computedHeight(): number {
// Computed during layout operations; allows caching instead of continuous recomputation.
if(this.needsLayout) {
this.refreshLayout();
}
return this._computedHeight;
}
/**
* The top-level style string for the font size used by the predictive banner
* and the primary keyboard visualization elements.
*/
get baseFontSize(): string {
return this.parsedBaseFontSize?.styleString || '';
}
protected get parsedBaseFontSize(): ParsedLengthStyle {
if(!this._baseFontSize) {
let keymanweb = com.keyman.singleton;
this._baseFontSize = this.defaultFontSize(this.device, keymanweb.isEmbedded);
}
return this._baseFontSize;
}
public defaultFontSize(device: utils.DeviceSpec, isEmbedded: boolean): ParsedLengthStyle {
if(device.touchable) {
const fontScale = device.formFactor == 'phone'
? 1.6 * (isEmbedded ? 0.65 : 0.6) * 1.2 // Combines original scaling factor with one previously applied to the layer group.
: 2; // iPad or Android tablet
return ParsedLengthStyle.special(fontScale, 'em');
} else {
return this.computedHeight ? ParsedLengthStyle.inPixels(this.computedHeight / 8) : undefined;
}
}
public get activeKeyboard(): keyboards.Keyboard {
return this.keyboard;
}
public set activeKeyboard(keyboard: keyboards.Keyboard) {
this.keyboard = keyboard;
this.loadActiveKeyboard();
if(this.keyboard?.isCJK) {
this.displayIfActive = true;
}
}
private computeFrameHeight(): number {
return (this.headerView?.layoutHeight.val || 0) + (this.footerView?.layoutHeight.val || 0);
}
setSize(width?: number | LengthStyle, height?: number | LengthStyle, pending?: boolean) {
let mutatedFlag = false;
let parsedWidth: ParsedLengthStyle;
let parsedHeight: ParsedLengthStyle;
if(!width && width !== 0) {
return;
}
if(!height && height !== 0) {
return;
}
if(Number.isFinite(width as number)) {
parsedWidth = ParsedLengthStyle.inPixels(width as number);
} else {
parsedWidth = new ParsedLengthStyle(width as LengthStyle);
}
if(Number.isFinite(height as number)) {
parsedHeight = ParsedLengthStyle.inPixels(height as number);
} else {
parsedHeight = new ParsedLengthStyle(height as LengthStyle);
}
if(width && height) {
mutatedFlag = !this._width || !this._height;
mutatedFlag = mutatedFlag || parsedWidth.styleString != this._width.styleString;
mutatedFlag = mutatedFlag || parsedHeight.styleString != this._height.styleString;
this._width = parsedWidth;
this._height = parsedHeight;
}
this.needsLayout = this.needsLayout || mutatedFlag;
this.refreshLayoutIfNeeded(pending);
}
protected setNeedsLayout() {
this.needsLayout = true;
}
public refreshLayout(pending?: boolean): void {
if(!this.keyboardView) {
return;
}
// Step 1: have the necessary conditions been met?
const fixedSize = this.width && this.height && this.width.absolute && this.height.absolute;
const computedStyle = getComputedStyle(this._Box);
const isInDOM = computedStyle.height != '' && computedStyle.height != 'auto';
// Step 2: determine basic layout geometry
if(fixedSize) {
this._computedWidth = this.width.val;
this._computedHeight = this.height.val;
} else if(isInDOM) {
const parent = this._Box.offsetParent as HTMLElement;
this._computedWidth = this.width.val * (this.width.absolute ? 1 : parent.offsetWidth);
this._computedHeight = this.height.val * (this.height.absolute ? 1 : parent.offsetHeight);
} else {
// Cannot perform layout operations!
return;
}
// Must be set before any references to the .computedWidth and .computedHeight properties!
this.needsLayout = false;
// Step 3: perform layout operations.
this.banner.element.style.fontSize = this.baseFontSize;
if(this.vkbd) {
this.vkbd.fontSize = this.parsedBaseFontSize;
}
if(!pending) {
this.headerView?.refreshLayout();
this.bannerView.refreshLayout();
this.footerView?.refreshLayout();
}
if(this.vkbd) {
let availableHeight = this.computedHeight - this.computeFrameHeight();
// +5: from kmw-banner-bar's 'top' attribute when active
if(this.bannerView.height > 0) {
availableHeight -= this.bannerView.height + 5;
}
this.vkbd.setSize(this.computedWidth, availableHeight, pending);
const bs = this._Box.style;
// OSK size settings can only be reliably applied to standard VisualKeyboard
// visualizations, not to help text or empty views.
bs.width = bs.maxWidth = this.computedWidth + 'px';
bs.height = bs.maxHeight = this.computedHeight + 'px';
} else {
const bs = this._Box.style;
bs.width = 'auto';
bs.height = 'auto';
bs.maxWidth = bs.maxHeight = '';
}
let keyman = com.keyman.singleton;
keyman.alignInputs();
}
public refreshLayoutIfNeeded(pending?: boolean) {
if(this.needsLayout) {
this.refreshLayout(pending);
}
}
public abstract getDefaultWidth(): number;
public abstract getDefaultKeyboardHeight(): number;
/**
* Function _Load
* Scope Private
* Description OSK initialization when keyboard selected
*/
_Load() { // Load Help - maintained only temporarily.
let keymanweb = com.keyman.singleton;
this.activeKeyboard = keymanweb.core.activeKeyboard;
}
protected abstract postKeyboardLoad(): void;
protected abstract setBoxStyling(): void;
private loadActiveKeyboard() {
this.setBoxStyling();
if(this.vkbd) {
this.vkbd.shutdown();
}
this.keyboardView = null;
this.needsLayout = true;
// Instantly resets the OSK container, erasing / delinking the previously-loaded keyboard.
this._Box.innerHTML = '';
// Any event-cancelers would go here, after the innerHTML reset.
// Add header element to OSK only for desktop browsers
if(this.headerView) {
this._Box.appendChild(this.headerView.element);
}
// Add suggestion banner bar to OSK
this._Box.appendChild(this.banner.element);
let kbdView: KeyboardView = this.keyboardView = this._GenerateKeyboardView(this.activeKeyboard);
this._Box.appendChild(kbdView.element);
kbdView.postInsert();
// Add footer element to OSK only for desktop browsers
if(this.footerView) {
this._Box.appendChild(this.footerView.element);
}
// END: construction of the actual internal layout for the overall OSK
// Correct the classname for the (inner) OSK frame (Build 360)
var kbdID: string = (this.activeKeyboard ? this.activeKeyboard.id.replace('Keyboard_','') : '');
if(kbdID.indexOf('::') != -1) { // We used to also test if we were in embedded mode, but... whatever.
// De-namespaces the ID for use with CSS classes.
// Assumes that keyboard IDs may not contain the ':' symbol.
kbdID = kbdID.substring(kbdID.indexOf('::') + 2);
}
const kbdClassSuffix = ' kmw-keyboard-' + kbdID;
kbdView.element.className = kbdView.element.className + kbdClassSuffix;
this.banner.appendStyles();
if(this.vkbd) {
// Create the key preview (for phones)
this.vkbd.createKeyTip();
// Append a stylesheet for this keyboard for keyboard specific styles
// or if needed to specify an embedded font
this.vkbd.appendStyleSheet();
}
this.postKeyboardLoad();
}
private layerChangeHandler: text.SystemStoreMutationHandler = function(this: OSKView,
source: text.MutableSystemStore,
newValue: string) {
// This handler is also triggered on state-key state changes (K_CAPS) that
// may not actually change the layer.
if(this.vkbd) {
this.vkbd._UpdateVKShiftStyle(newValue);
}
if((this.vkbd && this.vkbd.layerId != newValue) || source.value != newValue) {
// Prevents console errors when a keyboard only displays help.
// Can occur when using SHIFT with sil_euro_latin on a desktop form-factor.
if(this.vkbd) {
this.vkbd.layerId = newValue;
// Ensure that the layer's spacebar is properly captioned.
this.vkbd.showLanguage();
}
// Ensure the keyboard view is modeling the correct state. (Correct layer, etc.)
this.keyboardView.updateState();
// We need to recalc the font size here because the layer did not have
// calculated dimensions available before it was visible
this.refreshLayout();
}
}.bind(this);
private _GenerateKeyboardView(keyboard: keyboards.Keyboard): KeyboardView {
let device = this.device;
if(this.vkbd) {
this.vkbd.shutdown();
}
this._Box.className = "";
// Case 1: since we hide the system keyboard on touch devices, we need
// to display SOMETHING that can accept input.
if(keyboard == null && !device.touchable) {
// We do not (currently) allow selecting the default system keyboard on
// touch form-factors. Likely b/c mnemonic difficulties.
return new EmptyView();
} else {
// Generate a visual keyboard from the layout (or layout default)
// Condition is false if no key definitions exist, formFactor == desktop, AND help text exists. All three.
if(keyboard && keyboard.layout(device.formFactor as utils.FormFactor)) {
return this._GenerateVisualKeyboard(keyboard);
} else if(!keyboard /* && device.touchable (implied) */) {
// Show a basic, "hollow" OSK that at least allows input, since we're
// on a touch device and hiding the system keyboard
return this._GenerateVisualKeyboard(null);
} else {
// A keyboard help-page or help-text is still a visualization, even not a standard OSK.
return new HelpPageView(keyboard);
}
}
}
/**
* Function _GenerateVisualKeyboard
* Scope Private
* @param {Object} keyboard The keyboard to visualize
* Description Generates the visual keyboard element and attaches it to KMW
*/
private _GenerateVisualKeyboard(keyboard: keyboards.Keyboard): VisualKeyboard {
let device = this.device;
// Root element sets its own classes, one of which is 'kmw-osk-inner-frame'.
let vkbd = new VisualKeyboard(keyboard, device, this.hostDevice);
// Ensure the OSK's current layer is kept up to date.
let core = com.keyman.singleton.core; // Note: will eventually be a class field.
core.keyboardProcessor.layerStore.handler = this.layerChangeHandler;
// Set box class - OS and keyboard added for Build 360
this._Box.className=device.formFactor+' '+ device.OS.toLowerCase() + ' kmw-osk-frame';
// Add primary keyboard element to OSK
return vkbd;
}
/**
* The main function for presenting the OSKView.
*
* This includes:
* - refreshing its layout
* - displaying it
* - positioning it
*/
public present(): void {
// Do not try to display OSK if no active element
if(!this.mayShow()) {
return;
}
// Ensure the keyboard view is modeling the correct state. (Correct layer, etc.)
this.keyboardView.updateState();
this._Box.style.display='block'; // Is 'none' when hidden.
// First thing after it's made visible.
this.refreshLayoutIfNeeded();
if(this.keyboardView instanceof VisualKeyboard) {
this.keyboardView.showLanguage();
}
this._Visible=true;
/* In case it's still '0' from a hide() operation.
*
* (Opacity is only modified when device.touchable = true,
* though a couple of extra conditions may apply.)
*/
this._Box.style.opacity = '1';
// If OSK still hidden, make visible only after all calculation finished
if(this._Box.style.visibility == 'hidden') {
let _this = this;
window.setTimeout(function() {
_this._Box.style.visibility = 'visible';
}, 0);
}
this.setDisplayPositioning();
}
/**
* Method usable by subclasses of OSKView to control that OSKView type's
* positioning behavior when needed by the present() method.
*/
protected abstract setDisplayPositioning();
/**
* Method used to start a potentially-asynchronous hide of the OSK.
* @param hiddenByUser `true` if this hide operation was directly requested by the user.
*/
public startHide(hiddenByUser: boolean): void {
if(!this.mayHide(hiddenByUser)) {
return;
}
if(hiddenByUser) {
// The one location outside of the `displayIfActive` property that bypasses the setter.
// Avoids needless recursion that could be triggered by it, as we're already in the
// process of hiding the OSK anyway.
this._displayIfActive = ((this.keyboard.isCJK || this.hostDevice.touchable)? true : false); // I3363 (Build 301)
}
let promise: Promise<boolean> = null;
if(this._Box && this.hostDevice.touchable && !(this.keyboardView instanceof EmptyView)) {
/**
* Note: this refactored code appears to reflect a currently-dead code path. 14.0's
* equivalent is either extremely niche or is actually inaccessible.
*/
promise = this.useHideAnimation();
} else {
promise = Promise.resolve(true);
}
const _this = this;
promise.then(function(shouldHide: boolean) {
if(shouldHide) {
_this.finalizeHide();
}
});
// Allow UI to execute code when hiding the OSK
var p={};
p['HiddenByUser']=hiddenByUser;
this.doHide(p);
// If hidden by the UI, be sure to restore the focus
if(hiddenByUser && this.activeTarget instanceof dom.targets.OutputTarget) {
this.activeTarget?.focus();
}
}
/**
* Performs the _actual_ logic and functionality involved in hiding the OSK.
*/
protected finalizeHide() {
if(document.body.className.indexOf('osk-always-visible') >= 0) {
return;
}
if(this._Box) {
let bs=this._Box.style;
bs.display = 'none';
bs.transition = '';
bs.opacity = '1';
this._Visible=false;
}
if(this.vkbd) {
this.vkbd.onHide();
}
}
/**
*
* @returns `false` if the OSK is in an invalid state for being presented to the user.
*/
protected mayShow(): boolean {
if(!this.activationConditionsMet) {
return false;
}
// Never display the OSK for desktop browsers unless KMW element is focused, and a keyboard selected
if(!this.keyboardView || this.keyboardView instanceof EmptyView || !this.displayIfActive) {
return false;
}
if(!this._Box) {
return false;
}
return true;
}
/**
*
* @param hiddenByUser
* @returns `false` if the OSK is in an invalid state for being hidden from the user.
*/
protected mayHide(hiddenByUser: boolean): boolean {
if(this.activationMode != 'automatic' && this.displayIfActive) {
return false;
}
if(!hiddenByUser && this.hostDevice.formFactor == 'desktop') {
//Allow desktop OSK to remain visible on blur if body class set
if(document.body.className.indexOf('osk-always-visible') >= 0) {
return false;
}
}
return true;
}
/**
* Applies CSS styling and handling needed to perform a fade animation when
* hiding the OSK.
*
* Note: currently reflects an effectively-dead code path, though this is
* likely not intentional. Other parts of the KMW engine seem to call hideNow()
* synchronously after each and every part of the engine that calls this function,
* cancelling the Promise.
*
* @returns A Promise denoting either cancellation of the hide (`false`) or
* completion of the hide & its animation (`true`)
*/
protected useHideAnimation(): Promise<boolean> {
const os = this._Box.style;
const _this = this;
return new Promise<boolean>(function(resolve) {
const cleanup = function() {
// TODO(lowpri): attach event listeners on create and leave them there
_this._Box.removeEventListener('transitionend', cleanup, false);
_this._Box.removeEventListener('webkitTransitionEnd', cleanup, false);
_this._Box.removeEventListener('transitioncancel', cleanup, false);
_this._Box.removeEventListener('webkitTransitionCancel', cleanup, false);
if(_this._animatedHideTimeout != 0) {
window.clearTimeout(_this._animatedHideTimeout);
}
_this._animatedHideTimeout = 0;
if(_this._Visible && _this.activationConditionsMet) {
// Leave opacity alone and clear transition if another element activated
os.transition='';
os.opacity='1';
resolve(false);
return false;
} else {
resolve(true);
return true;
}
}, startup = function() {
_this._Box.removeEventListener('transitionrun', startup, false);
_this._Box.removeEventListener('webkitTransitionRun', startup, false);
_this._Box.addEventListener('transitionend', cleanup, false);
_this._Box.addEventListener('webkitTransitionEnd', cleanup, false);
_this._Box.addEventListener('transitioncancel', cleanup, false);
_this._Box.addEventListener('webkitTransitionCancel', cleanup, false);
};
_this._Box.addEventListener('transitionrun', startup, false);
_this._Box.addEventListener('webkitTransitionRun', startup, false);
os.transition='opacity 0.5s linear 0';
os.opacity='0';
// Cannot hide the OSK smoothly using a transitioned drop, since for
// position:fixed elements transitioning is incompatible with translate3d(),
// and also does not work with top, bottom or height styles.
// Opacity can be transitioned and is probably the simplest alternative.
// We must condition on osk._Visible in case focus has since been moved to another
// input (in which case osk._Visible will be non-zero)
_this._animatedHideTimeout = window.setTimeout(cleanup,
200); // Wait a bit before starting, to allow for moving to another element
});
}
/**
* Used to synchronously hide the OSK, cancelling any async hide animations that have
* not started and immediately completing the hide of any hide ops pending completion
* of their animation.
*/
public hideNow() {
if(!this.mayHide(false) || !this._Box) {
return;
}
// Two possible uses for _animatedHideResolver:
// - _animatedHideTimeout is set: animation is waiting to start
// - _animatedHideTimeout is null: animation has already started.
// Was an animated hide waiting to start? Just cancel it.
if(this._animatedHideTimeout) {
window.clearTimeout(this._animatedHideTimeout);
this._animatedHideTimeout = 0;
}
// Was an animated hide already in progress? If so, just trigger it early.
const os = this._Box.style;
os.transition='';
os.opacity='0';
this.finalizeHide();
}
['shutdown']() {
// Disable the OSK's event handlers.
this.removeBaseMouseEventListeners();
this.removeBaseTouchEventListeners();
// Remove the OSK's elements from the document, allowing them to be properly cleaned up.
// Necessary for clean engine testing.
var _box = this._Box;
if(_box.parentElement) {
_box.parentElement.removeChild(_box);
}
}
/**
* Function getRect
* Scope Public
* @return {Object.<string,number>} Array object with position and size of OSK container
* Description Get rectangle containing KMW Virtual Keyboard
*/
['getRect'](): OSKRect { // I2405
var p: OSKRect = {};
// Always return these based upon _Box; using this.vkbd will fail to account for banner and/or
// the desktop OSK border.
p['left'] = p.left = dom.Utils.getAbsoluteX(this._Box);
p['top'] = p.top = dom.Utils.getAbsoluteY(this._Box);
p['width'] = this.computedWidth;
p['height'] = this.computedHeight;
return p;
}
/* ---- Legacy interfacing methods and fields ----
*
* The endgoal is to eliminate the need for these entirely, but extra work and care
* will be necessary to achieve said endgoal for these methods.
*
* The simplest way forward is to maintain them, then resolve them independently,
* one at a time.
*/
/**
* Display build number
*
* In the future, this should raise an event that the consuming KeymanWeb
* engine may listen for & respond to, rather than having it integrated
* as part of the OSK itself.
*/
showBuild() {
let keymanweb = com.keyman.singleton;
keymanweb.util.internalAlert('KeymanWeb Version '+keymanweb['version']+'.'+keymanweb['build']+'<br /><br />'
+'<span style="font-size:0.8em">Copyright &copy; 2021 SIL International</span>');
}
/**
* Display list of installed keyboards in pop-up menu
*
* In the future, this language menu should be defined as a UI module like the standard
* desktop UI modules. The globe key should then trigger an event to _request_ that the
* consuming engine display the active UI module's menu.
*
**/
showLanguageMenu() {
if(this.hostDevice.touchable) {
let menu = new LanguageMenu(com.keyman.singleton);
menu.show();
}
}
// OSK state fields & events
//
// These are relatively stable and may be preserved as they are.
_Visible: boolean = false;
/**
* Function enabled
* Scope Public
* @return {boolean|number} True if KMW OSK enabled
* Description Test if KMW OSK is enabled
*/
['isEnabled'](): boolean {
return this.displayIfActive;
}
/**
* Function isVisible
* Scope Public
* @return {boolean|number} True if KMW OSK visible
* Description Test if KMW OSK is actually visible
* Note that this will usually return false after any UI event that results in (temporary) loss of input focus
*/
['isVisible'](): boolean {
return this._Visible;
}
/**
* Function hide
* Scope Public
* Description Prevent display of OSK window on focus
*/
['hide']() {
this.displayIfActive = false;
this.startHide(true);
}
/**
* Description Display KMW OSK (at position set in callback to UI)
* Function show
* Scope Public
* @param {(boolean|number)=} bShow True to display, False to hide, omitted to toggle
*/
['show'](bShow: boolean) {
if(arguments.length > 0) {
this.displayIfActive = bShow;
} else {
if(this.activationConditionsMet) {
this.displayIfActive = !this.displayIfActive;
}
}
}
/**
* Allow UI to respond to OSK being shown (passing position and properties)
*
* @param {Object=} p object with coordinates and userdefined flag
* @return {boolean}
*
*/
doShow(p) {
return com.keyman.singleton.util.callEvent('osk.show',p);
}
/**
* Allow UI to update respond to OSK being hidden
*
* @param {Object=} p object with coordinates and userdefined flag
* @return {boolean}
*
*/
doHide(p) {
return com.keyman.singleton.util.callEvent('osk.hide',p);
}
/**
* Function addEventListener
* Scope Public
* @param {string} event event name
* @param {function(Object)} func event handler
* @return {boolean}
* Description Wrapper function to add and identify OSK-specific event handlers
*/
['addEventListener'](event: string, func: (obj) => boolean) {
return com.keyman.singleton.util.addEventListener('osk.'+event, func);
}
}
}