Skip to content

Commit

Permalink
Merge pull request #7934 from keymanapp/feat/web/fancy-suggestion-banner
Browse files Browse the repository at this point in the history
feat(web): suggestion banner UI enhancements - suggestion expanding + scrollable banner
  • Loading branch information
jahorton authored Jan 22, 2024
2 parents a9ef4d7 + 9917086 commit e00dbe8
Show file tree
Hide file tree
Showing 11 changed files with 1,050 additions and 178 deletions.
12 changes: 11 additions & 1 deletion web/src/engine/osk/src/banner/banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { createUnselectableElement } from 'keyman/engine/dom-utils';

export abstract class Banner {
private _height: number; // pixels
private _width: number; // pixels
private div: HTMLDivElement;

public static DEFAULT_HEIGHT: number = 37; // pixels; embedded apps can modify
Expand Down Expand Up @@ -47,6 +48,15 @@ export abstract class Banner {
this.update();
}

public get width(): number {
return this._width;
}

public set width(width: number) {
this._width = width;
this.update();
}

/**
* Function update
* @return {boolean} true if the banner styling changed
Expand Down Expand Up @@ -88,7 +98,7 @@ export abstract class Banner {
* Function getDiv
* Scope Public
* @returns {HTMLElement} Base element of the banner
* Description Returns the HTMLElelemnt of the banner
* Description Returns the HTMLElement of the banner
*/
public getDiv(): HTMLElement {
return this.div;
Expand Down
21 changes: 21 additions & 0 deletions web/src/engine/osk/src/banner/bannerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BannerView } from './bannerView.js';
import { Banner } from './banner.js';
import { BlankBanner } from './blankBanner.js';
import { HTMLBanner } from './htmlBanner.js';
import { Keyboard, KeyboardProperties } from '@keymanapp/keyboard-processor';

export class BannerController {
private container: BannerView;
Expand All @@ -16,6 +17,9 @@ export class BannerController {

private _inactiveBanner: Banner;

private keyboard: Keyboard;
private keyboardStub: KeyboardProperties;

/**
* Builds a banner for use when predictions are not active, supporting a single image.
*/
Expand Down Expand Up @@ -92,6 +96,23 @@ export class BannerController {
selectBanner(state: StateChangeEnum) {
// Only display a SuggestionBanner when LanguageProcessor states it is active.
this.activateBanner(state == 'active' || state == 'configured');

if(this.keyboard) {
this.container.banner.configureForKeyboard(this.keyboard, this.keyboardStub);
}
}

/**
* Allows banners to adapt based on the active keyboard and related properties, such as
* associated fonts.
* @param keyboard
* @param keyboardProperties
*/
public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) {
this.keyboard = keyboard;
this.keyboardStub = keyboardProperties;

this.container.banner.configureForKeyboard(keyboard, keyboardProperties);
}

public shutdown() {
Expand Down
46 changes: 46 additions & 0 deletions web/src/engine/osk/src/banner/bannerScrollState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { InputSample } from "@keymanapp/gesture-recognizer";

/**
* The amount of coordinate 'noise' allowed during a scroll-enabled touch
* before interpreting the currently-ongoing touch command as having scrolled.
*/
const HAS_SCROLLED_FUDGE_FACTOR = 10;

/**
* This class was added to facilitate scroll handling for overflow-x elements, though it could
* be extended in the future to accept overflow-y if needed.
*
* This is necessary because of the OSK's need to use `.preventDefault()` for stability; that
* same method blocks native handling of overflow scrolling for touch browsers.
*/
export class BannerScrollState {
totalLength = 0;

baseCoord: InputSample<any>;
curCoord: InputSample<any>;
baseScrollLeft: number;

constructor(coord: InputSample<any>, baseScrollLeft: number) {
this.baseCoord = coord;
this.curCoord = coord;
this.baseScrollLeft = baseScrollLeft;

this.totalLength = 0;
}

updateTo(coord: InputSample<any>): number {
let prevCoord = this.curCoord;
this.curCoord = coord;

let delta = this.baseCoord.targetX - this.curCoord.targetX + this.baseScrollLeft;
// Track the total amount of scrolling used, even if just a pixel-wide back and forth wiggle.
this.totalLength += Math.abs(this.curCoord.targetX - prevCoord.targetX);

return delta;
}

public get hasScrolled(): boolean {
// Allow an accidental fudge-factor for overflow element noise during a touch, but not much.
return this.totalLength > HAS_SCROLLED_FUDGE_FACTOR;
}
}
47 changes: 14 additions & 33 deletions web/src/engine/osk/src/banner/bannerView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,8 @@ interface BannerViewEventMap {
}

/**
* 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.
* The `BannerView` module is designed to serve as the hot-swap container for the
* different `Banner` types, helping KMW to avoid needless DOM element shuffling.
*/
export class BannerView implements OSKViewComponent {
private bannerContainer: HTMLDivElement;
Expand Down Expand Up @@ -161,5 +131,16 @@ export class BannerView implements OSKViewComponent {
return ParsedLengthStyle.inPixels(this.height);
}

public refreshLayout() {};
public get width(): number | undefined {
return this.currentBanner?.width;
}

public set width(w: number) {
if(this.currentBanner) {
this.currentBanner.width = w;
}
}

public refreshLayout() {
}
}
Loading

0 comments on commit e00dbe8

Please sign in to comment.