Skip to content

Commit

Permalink
Merge pull request #9987 from keymanapp/chore/web/separate-banner-sou…
Browse files Browse the repository at this point in the history
…rce-files

chore(web): splits banner.ts into separate files per banner type 🎏
  • Loading branch information
jahorton authored Nov 16, 2023
2 parents 4daba25 + 0d79658 commit e6e89d7
Show file tree
Hide file tree
Showing 8 changed files with 648 additions and 637 deletions.
480 changes: 1 addition & 479 deletions web/src/engine/osk/src/banner/banner.ts

Large diffs are not rendered by default.

157 changes: 157 additions & 0 deletions web/src/engine/osk/src/banner/bannerController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { DeviceSpec } from '@keymanapp/web-utils';
import type { PredictionContext, StateChangeEnum } from '@keymanapp/input-processor';
import { ImageBanner } from './imageBanner.js';
import { SuggestionBanner } from './suggestionBanner.js';
import { BannerView, BannerOptions, BannerType } from './bannerView.js';
import { Banner } from './banner.js';
import { BlankBanner } from './blankBanner.js';

export class BannerController {
private _activeType: BannerType;
private _options: BannerOptions = {};
private container: BannerView;
private alwaysShow: boolean;
private imagePath?: string = "";

private predictionContext?: PredictionContext;

private readonly hostDevice: DeviceSpec;

public static readonly DEFAULT_OPTIONS: BannerOptions = {
alwaysShow: false,
imagePath: ""
}

constructor(bannerView: BannerView, hostDevice: DeviceSpec, predictionContext?: PredictionContext) {
// Step 1 - establish the container element. Must come before this.setOptions.
this.hostDevice = hostDevice;
this.container = bannerView;
this.predictionContext = predictionContext;

// Initialize with the default options - any 'manually set' options come post-construction.
// This will also automatically set the default banner in place.
this.setOptions(BannerController.DEFAULT_OPTIONS);
}

/**
* 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) {
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 '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.container.banner) {
this.selectBanner('inactive');
}
}
}

/**
* 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) {
var banner: Banner;

let oldBanner = this.container.banner;
if(oldBanner instanceof SuggestionBanner) {
this.predictionContext.off('update', oldBanner.onSuggestionUpdate);
}

switch(type) {
case 'blank':
banner = new BlankBanner();
break;
case 'image':
banner = new ImageBanner(this.imagePath, this.container.activeBannerHeight);
break;
case 'suggestion':
let suggestBanner = banner = new SuggestionBanner(this.hostDevice, this.container.activeBannerHeight);
suggestBanner.predictionContext = this.predictionContext;
suggestBanner.events.on('apply', (selection) => this.predictionContext.accept(selection.suggestion));

this.predictionContext.on('update', suggestBanner.onSuggestionUpdate);
break;
default:
throw new Error("Invalid type specified for the banner!");
}

this._activeType = type;

if(banner) {
this.container.banner = banner;
}
}

/**
* Handles `LanguageProcessor`'s `'statechange'` events,
* allowing logic to automatically hot-swap `Banner`s as needed.
* @param state
*/
selectBanner(state: StateChangeEnum) {
// Only display a SuggestionBanner when LanguageProcessor states it is active.
if(state == 'active' || state == 'configured') {
this.setBanner('suggestion');
} else if(state == 'inactive') {
if(this.alwaysShow) {
this.setBanner('image');
} else {
this.setBanner('blank');
}
}
}

public get activeType(): BannerType {
return this._activeType;
}
}
161 changes: 5 additions & 156 deletions web/src/engine/osk/src/banner/bannerView.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import EventEmitter from 'eventemitter3';

import { Banner, BlankBanner, ImageBanner, SuggestionBanner } from './banner.js';
import { createUnselectableElement } from 'keyman/engine/dom-utils';

import { Banner } from './banner.js';
import OSKViewComponent from '../components/oskViewComponent.interface.js';
import { ParsedLengthStyle } from '../lengthStyle.js';

import { DeviceSpec } from '@keymanapp/web-utils';
import type { PredictionContext, StateChangeEnum } from '@keymanapp/input-processor';
import { createUnselectableElement } from 'keyman/engine/dom-utils';
import { BlankBanner } from './blankBanner.js';

/**
* This object is used to specify options by both `BannerManager.getOptions`
Expand Down Expand Up @@ -58,7 +57,7 @@ interface BannerViewEventMap {
* needs to reserve this space (i.e: Keyman for iOS),
* rather than as its standalone app.
*/
export default class BannerView implements OSKViewComponent {
export class BannerView implements OSKViewComponent {
private bannerContainer: HTMLDivElement;
private activeBanner: Banner;
private _activeBannerHeight: number = Banner.DEFAULT_HEIGHT;
Expand Down Expand Up @@ -160,154 +159,4 @@ export default class BannerView implements OSKViewComponent {
}

public refreshLayout() {};
}

export class BannerController {
private _activeType: BannerType;
private _options: BannerOptions = {};
private container: BannerView;
private alwaysShow: boolean;
private imagePath?: string = "";

private predictionContext?: PredictionContext;

private readonly hostDevice: DeviceSpec;

public static readonly DEFAULT_OPTIONS: BannerOptions = {
alwaysShow: false,
imagePath: ""
}

constructor(bannerView: BannerView, hostDevice: DeviceSpec, predictionContext?: PredictionContext) {
// Step 1 - establish the container element. Must come before this.setOptions.
this.hostDevice = hostDevice;
this.container = bannerView;
this.predictionContext = predictionContext;

// Initialize with the default options - any 'manually set' options come post-construction.
// This will also automatically set the default banner in place.
this.setOptions(BannerController.DEFAULT_OPTIONS);
}

/**
* 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) {
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 '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.container.banner) {
this.selectBanner('inactive');
}
}
}

/**
* 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) {
var banner: Banner;

let oldBanner = this.container.banner;
if(oldBanner instanceof SuggestionBanner) {
this.predictionContext.off('update', oldBanner.onSuggestionUpdate);
}

switch(type) {
case 'blank':
banner = new BlankBanner();
break;
case 'image':
banner = new ImageBanner(this.imagePath, this.container.activeBannerHeight);
break;
case 'suggestion':
let suggestBanner = banner = new SuggestionBanner(this.hostDevice, this.container.activeBannerHeight);
suggestBanner.predictionContext = this.predictionContext;
suggestBanner.events.on('apply', (selection) => this.predictionContext.accept(selection.suggestion));

this.predictionContext.on('update', suggestBanner.onSuggestionUpdate);
break;
default:
throw new Error("Invalid type specified for the banner!");
}

this._activeType = type;

if(banner) {
this.container.banner = banner;
}
}

/**
* Handles `LanguageProcessor`'s `'statechange'` events,
* allowing logic to automatically hot-swap `Banner`s as needed.
* @param state
*/
selectBanner(state: StateChangeEnum) {
// Only display a SuggestionBanner when LanguageProcessor states it is active.
if(state == 'active' || state == 'configured') {
this.setBanner('suggestion');
} else if(state == 'inactive') {
if(this.alwaysShow) {
this.setBanner('image');
} else {
this.setBanner('blank');
}
}
}

public get activeType(): BannerType {
return this._activeType;
}
}
12 changes: 12 additions & 0 deletions web/src/engine/osk/src/banner/blankBanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Banner } from "./banner.js";

/**
* Function BlankBanner
* Description A banner of height 0 that should not be shown
*/
export class BlankBanner extends Banner {

constructor() {
super(0);
}
}
Loading

0 comments on commit e6e89d7

Please sign in to comment.