spiegel-keyman/web/source/osk/bannerManager.ts

292 lines
9.7 KiB
TypeScript

/// <reference path="banner.ts" />
/// <reference path="oskViewComponent.ts" />
namespace com.keyman.osk {
/**
* This object is used to specify options by both `BannerManager.getOptions`
* and `BannerManager.setOptions`. Refer to the latter for specification of
* each field.
*/
export interface BannerOptions {
alwaysShow?: boolean;
mayPredict?: boolean;
mayCorrect?: boolean;
imagePath?: string;
}
export type BannerType = "blank" | "image" | "suggestion";
/**
* The `BannerManager` module is designed to serve as a manager for the
* different `Banner` types.
* To facilitate this, it will provide a root element property that serves
* as a container for any active `Banner`, helping KMW to avoid needless
* DOM element shuffling.
*
* Goals for the `BannerManager`:
*
* * It will be exposed as `keyman.osk.banner` and will provide the following API:
* * `getOptions`, `setOptions` - refer to the `BannerOptions` class for details.
* * This provides a persistent point that the web page designers and our
* model apps can utilize and can communicate with.
* * These API functions are designed for live use and will allow
* _hot-swapping_ the `Banner` instance; they're not initialization-only.
* * Disabling the `Banner` (even for suggestions) outright with
* `enablePredictions == false` will auto-unload any loaded predictive model
* from `ModelManager` and setting it to `true` will revert this.
* * This should help to avoid wasting computational resources.
* * It will listen to ModelManager events and automatically swap Banner
* instances as appropriate:
* * The option `persistentBanner == true` is designed to replicate current
* iOS system keyboard behavior.
* * When true, an `ImageBanner` will be displayed.
* * If false, it will be replaced with a `BlankBanner` of zero height,
* corresponding to our current default lack of banner.
* * It will not automatically set `persistentBanner == true`;
* this must be set by the iOS app, and only under the following conditions:
* * `keyman.isEmbedded == true`
* * `device.OS == 'ios'`
* * Keyman is being used as the system keyboard within an app that
* needs to reserve this space (i.e: Keyman for iOS),
* rather than as its standalone app.
*/
export class BannerManager implements OSKViewComponent {
private _activeType: BannerType;
private _options: BannerOptions = {};
private bannerContainer: HTMLDivElement;
private activeBanner: Banner;
private alwaysShow: boolean;
private imagePath?: string = "";
private readonly hostDevice: utils.DeviceSpec;
public static readonly DEFAULT_OPTIONS: BannerOptions = {
alwaysShow: false,
mayPredict: true,
mayCorrect: true,
imagePath: ""
}
constructor(hostDevice: utils.DeviceSpec) {
// Step 1 - establish the container element. Must come before this.setOptions.
this.constructContainer();
this.hostDevice = hostDevice;
// Initialize with the default options -
// any 'manually set' options come post-construction.
// This will also automatically set the default banner in place.
this.setOptions(BannerManager.DEFAULT_OPTIONS);
}
/**
* Constructs the <div> element used to contain hot-swapped `Banner` instances.
*/
private constructContainer(): HTMLDivElement {
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
let d = util._CreateElement('div');
d.id = "keymanweb_banner_container";
d.className = "kmw-banner-container";
return this.bannerContainer = d;
}
/**
* Returns the `Banner`-containing div element used to facilitate hot-swapping.
*/
public get element(): HTMLDivElement {
return this.bannerContainer;
}
/**
* This function corresponds to `keyman.osk.banner.getOptions`.
*
* Gets the current control settings in use by `BannerManager`.
*/
public getOptions(): BannerOptions {
let retObj = {};
for(let key in this._options) {
retObj[key] = this._options[key];
}
return retObj;
}
/**
* This function corresponds to `keyman.osk.banner.setOptions`.
*
* Sets options used to tweak the automatic `Banner`
* control logic used by `BannerManager`.
* @param optionSpec An object specifying one or more of the following options:
* * `persistentBanner` (boolean) When `true`, ensures that a `Banner`
* is always displayed, even when no predictive model exists
* for the active language.
*
* Default: `false`
* * `imagePath` (URL string) Specifies the file path to use for an
* `ImageBanner` when `persistentBanner` is `true` and no predictive model exists.
*
* Default: `''`.
* * `enablePredictions` (boolean) Turns KMW predictions
* on (when `true`) and off (when `false`).
*
* Default: `true`.
*/
public setOptions(optionSpec: BannerOptions) {
let keyman = com.keyman.singleton;
for(let key in optionSpec) {
switch(key) {
// Each defined option may require specialized handling.
case 'alwaysShow':
// Determines the banner type to activate.
this.alwaysShow = optionSpec[key];
break;
case 'mayPredict':
// If this toggles our internal flag, it will generate events
// that reconfigures the banner (and internal engine state) appropriately.
keyman.core.languageProcessor.mayPredict = optionSpec[key]
break;
case 'mayCorrect':
keyman.core.languageProcessor.mayCorrect = optionSpec[key];
break;
case 'imagePath':
// Determines the image file to use for ImageBanners.
this.imagePath = optionSpec[key];
break;
default:
// Invalid option specified!
}
this._options[key] = optionSpec[key];
// If no banner instance exists yet, go with a safe, blank initialization.
if(!this.activeBanner) {
this.selectBanner('inactive');
}
}
}
/**
* Applies any stylesheets needed by specific `Banner` instances.
*/
public appendStyles() {
if(this.activeBanner) {
this.activeBanner.appendStyleSheet();
}
}
/**
* Sets the active `Banner` to the specified type, regardless of
* existing management logic settings.
*
* @param type `'blank' | 'image' | 'suggestion'` - A plain-text string
* representing the type of `Banner` to set active.
* @param height - Optional banner height in pixels.
*/
public setBanner(type: BannerType, height?: number) {
var banner: Banner;
switch(type) {
case 'blank':
banner = new BlankBanner();
break;
case 'image':
banner = new ImageBanner(this.imagePath, Banner.DEFAULT_HEIGHT);
break;
case 'suggestion':
banner = new SuggestionBanner(this.hostDevice, height);
break;
default:
throw new Error("Invalid type specified for the banner!");
}
this._activeType = type;
if(banner) {
this._setBanner(banner);
banner.activate();
}
}
/**
* Handles `LanguageProcessor`'s `'statechange'` events,
* allowing logic to automatically hot-swap `Banner`s as needed.
* @param state
*/
selectBanner(state: text.prediction.StateChangeEnum) {
// Only display a SuggestionBanner when LanguageProcessor states it is active.
if(state == 'active') {
this.setBanner('suggestion');
} else if(state == 'inactive') {
if(this.alwaysShow) {
this.setBanner('image');
} else {
this.setBanner('blank');
}
} else if(state == 'configured') {
let suggestionBanner = this.activeBanner as SuggestionBanner;
if(suggestionBanner.postConfigure) {
// Triggers the initially-displayed suggestions.
suggestionBanner.postConfigure();
}
}
}
/**
* Internal method used by the public API `setBanner`. `setBanner`
* translates the string parameter into a new instance consumed by this method.
* @param banner The `Banner` instance to set as active.
*/
private _setBanner(banner: Banner) {
if(this.activeBanner) {
if(banner == this.activeBanner) {
return;
} else {
let prevBanner = this.activeBanner;
prevBanner.deactivate();
this.bannerContainer.replaceChild(banner.getDiv(), prevBanner.getDiv());
}
}
this.activeBanner = banner;
this.bannerContainer.appendChild(banner.getDiv());
// Don't forget to adjust the OSK in case we're now using a blank Banner!
// Null guard b/c this function can be trigggered during OSK initialization.
let keyman = com.keyman.singleton;
if(keyman['osk']) {
keyman['osk'].refreshLayout();
}
}
public get activeType(): BannerType {
return this._activeType;
}
/**
* Gets the height (in pixels) of the active `Banner` instance.
*/
public get height(): number {
if(this.activeBanner) {
return this.activeBanner.height;
} else {
return 0;
}
}
/**
* Sets the height (in pixels) of the active 'Banner' instance.
*/
public set height(h: number) {
if (this.activeBanner) {
this.activeBanner.height = h;
}
}
public get layoutHeight(): ParsedLengthStyle {
return ParsedLengthStyle.inPixels(this.height);
}
public refreshLayout() {};
}
}