mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-22 00:07:46 +00:00
Merge pull request #9752 from keymanapp/refactor/web/banner-gesture-integration
refactor(web): banner integration with the new gesture engine 🐵
This commit is contained in:
commit
101daa0f2c
17 changed files with 298 additions and 1194 deletions
|
|
@ -15,7 +15,7 @@ export const pluginForDowncompiledClassTreeshaking: esbuild.Plugin = {
|
|||
return {
|
||||
// Marks any classes compiled by TS (as per the /** @class */ annotation)
|
||||
// as __PURE__ in order to facilitate tree-shaking.
|
||||
contents: source.replace('/** @class */', '/* @__PURE__ */ /** @class */'),
|
||||
contents: source.replaceAll('/** @class */', '/** @__PURE__ */'),
|
||||
loader: 'js'
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ import EventEmitter from 'eventemitter3';
|
|||
import { DeviceSpec } from '@keymanapp/web-utils';
|
||||
import { Keyboard, KeyboardProperties } from '@keymanapp/keyboard-processor';
|
||||
import { type PredictionContext } from '@keymanapp/input-processor';
|
||||
import InputEventEngine, { InputEventEngineConfig } from '../input/event-interpreter/inputEventEngine.js';
|
||||
import MouseEventEngine from '../input/event-interpreter/mouseEventEngine.js';
|
||||
import TouchEventEngine from '../input/event-interpreter/touchEventEngine.js';
|
||||
import UITouchHandlerBase from '../input/event-interpreter/uiTouchHandlerBase.js';
|
||||
|
||||
|
||||
import {
|
||||
GestureRecognizer,
|
||||
GestureRecognizerConfiguration,
|
||||
GestureSource,
|
||||
InputSample,
|
||||
PaddedZoneSource
|
||||
} from '@keymanapp/gesture-recognizer';
|
||||
|
||||
import { BANNER_GESTURE_SET } from './bannerGestureSet.js';
|
||||
|
||||
import { createUnselectableElement } from 'keyman/engine/dom-utils';
|
||||
|
||||
|
|
@ -49,7 +56,7 @@ export abstract class Banner {
|
|||
* @return {boolean} true if the banner styling changed
|
||||
* Description Update the height and display styling of the banner
|
||||
*/
|
||||
private update() : boolean {
|
||||
protected update() : boolean {
|
||||
let ds = this.div.style;
|
||||
let currentHeightStyle = ds.height;
|
||||
let currentDisplayStyle = ds.display;
|
||||
|
|
@ -233,6 +240,18 @@ export class BannerSuggestion {
|
|||
this.display = display;
|
||||
}
|
||||
|
||||
public highlight(on: boolean) {
|
||||
const elem = this.div;
|
||||
let classes = elem.className;
|
||||
let cs = ' ' + SuggestionBanner.TOUCHED_CLASS;
|
||||
|
||||
if(on && classes.indexOf(cs) < 0) {
|
||||
elem.className=classes+cs;
|
||||
} else {
|
||||
elem.className=classes.replace(cs,'');
|
||||
}
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return !this._suggestion;
|
||||
}
|
||||
|
|
@ -287,7 +306,7 @@ export class SuggestionBanner extends Banner {
|
|||
private options : BannerSuggestion[] = [];
|
||||
private hostDevice: DeviceSpec;
|
||||
|
||||
private manager: SuggestionInputManager;
|
||||
private gestureEngine: GestureRecognizer<BannerSuggestion>;
|
||||
|
||||
private _predictionContext: PredictionContext;
|
||||
|
||||
|
|
@ -299,13 +318,11 @@ export class SuggestionBanner extends Banner {
|
|||
this.hostDevice = hostDevice;
|
||||
|
||||
this.getDiv().className = this.getDiv().className + ' ' + SuggestionBanner.BANNER_CLASS;
|
||||
|
||||
this.buildInternals(false);
|
||||
|
||||
this.manager = new SuggestionInputManager(this.getDiv());
|
||||
this.events = this.manager.events;
|
||||
this.events = new EventEmitter<SuggestionInputEventMap>(); //this.manager.events;
|
||||
|
||||
this.setupInputHandling();
|
||||
this.gestureEngine = this.setupInputHandling();
|
||||
}
|
||||
|
||||
buildInternals(rtl: boolean) {
|
||||
|
|
@ -342,34 +359,118 @@ export class SuggestionBanner extends Banner {
|
|||
}
|
||||
}
|
||||
|
||||
private setupInputHandling() {
|
||||
let inputEngine: InputEventEngine;
|
||||
if(this.hostDevice.touchable) { // /*&& ('ontouchstart' in window)*/ // Except Chrome emulation doesn't set this.
|
||||
// Not to mention, it's rather redundant.
|
||||
inputEngine = this.touchEventConfig;
|
||||
} else {
|
||||
inputEngine = this.mouseEventConfig;
|
||||
private setupInputHandling(): GestureRecognizer<BannerSuggestion> {
|
||||
|
||||
const findTargetFrom = (e: HTMLElement): HTMLDivElement => {
|
||||
try {
|
||||
if(e) {
|
||||
if(e.classList.contains('kmw-suggest-option')) {
|
||||
return e as HTMLDivElement;
|
||||
}
|
||||
if(e.parentElement && e.parentElement.classList.contains('kmw-suggest-option')) {
|
||||
return e.parentElement as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
} catch(ex) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
inputEngine.registerEventHandlers();
|
||||
const config: GestureRecognizerConfiguration<BannerSuggestion> = {
|
||||
targetRoot: this.getDiv(),
|
||||
maxRoamingBounds: new PaddedZoneSource(this.getDiv(), [-0.333 * this.height]),
|
||||
// touchEventRoot: this.element, // is the default
|
||||
itemIdentifier: (sample, target: HTMLElement) => {
|
||||
let bestMatch: BannerSuggestion = null;
|
||||
let bestDist = Number.MAX_VALUE;
|
||||
|
||||
this.manager.events.on('highlight', (suggestion, on) => {
|
||||
const elem = suggestion.div;
|
||||
let classes = elem.className;
|
||||
let cs = ' ' + SuggestionBanner.TOUCHED_CLASS;
|
||||
for(const option of this.options) {
|
||||
const optionBounding = option.div.getBoundingClientRect();
|
||||
|
||||
if(on && classes.indexOf(cs) < 0) {
|
||||
elem.className=classes+cs;
|
||||
} else {
|
||||
elem.className=classes.replace(cs,'');
|
||||
if(optionBounding.left <= sample.clientX && sample.clientX < optionBounding.right) {
|
||||
return option;
|
||||
} else {
|
||||
const dist = (sample.clientX < optionBounding.left ? -1 : 1) * (sample.clientX - optionBounding.left);
|
||||
|
||||
if(dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestMatch = option;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
};
|
||||
|
||||
const engine = new GestureRecognizer<BannerSuggestion>(BANNER_GESTURE_SET, config);
|
||||
|
||||
const sourceTracker: {
|
||||
source: GestureSource<BannerSuggestion>,
|
||||
roamingHighlightHandler: (sample: InputSample<BannerSuggestion>) => void,
|
||||
suggestion: BannerSuggestion
|
||||
} = {
|
||||
source: null,
|
||||
roamingHighlightHandler: null,
|
||||
suggestion: null
|
||||
};
|
||||
|
||||
engine.on('inputstart', (source) => {
|
||||
// The banner does not support multi-touch - if one is still current, block all others.
|
||||
if(sourceTracker.source) {
|
||||
source.terminate(true);
|
||||
return;
|
||||
}
|
||||
|
||||
sourceTracker.source = source;
|
||||
sourceTracker.roamingHighlightHandler = (sample) => {
|
||||
// Maintain highlighting
|
||||
const suggestion = sample.item;
|
||||
|
||||
if(suggestion != sourceTracker.suggestion) {
|
||||
sourceTracker.suggestion.highlight(false);
|
||||
suggestion.highlight(true);
|
||||
sourceTracker.suggestion = suggestion;
|
||||
}
|
||||
};
|
||||
sourceTracker.suggestion = source.currentSample.item;
|
||||
|
||||
|
||||
source.currentSample.item.highlight(true);
|
||||
|
||||
const terminationHandler = () => {
|
||||
sourceTracker.suggestion.highlight(false);
|
||||
sourceTracker.source = null;
|
||||
sourceTracker.roamingHighlightHandler = null;
|
||||
sourceTracker.suggestion = null;
|
||||
}
|
||||
|
||||
source.path.on('complete', terminationHandler);
|
||||
source.path.on('invalidated', terminationHandler);
|
||||
source.path.on('step', sourceTracker.roamingHighlightHandler);
|
||||
});
|
||||
|
||||
this.manager.events.on('apply', (option) => {
|
||||
if(this.predictionContext) {
|
||||
this.predictionContext.accept(option.suggestion);
|
||||
}
|
||||
engine.on('recognizedgesture', (sequence) => {
|
||||
// The actual result comes in via the sequence's `stage` event.
|
||||
sequence.once('stage', (result) => {
|
||||
const suggestion = result.item; // Should also == sourceTracker.suggestion.
|
||||
if(suggestion) {
|
||||
this.predictionContext.accept(suggestion.suggestion);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
protected update() {
|
||||
const result = super.update();
|
||||
|
||||
// Ensure the banner's extended recognition zone is based on proper, up-to-date layout info.
|
||||
// Note: during banner init, `this.gestureEngine` may only be defined after
|
||||
// the first call to this setter!
|
||||
(this.gestureEngine?.config.maxRoamingBounds as PaddedZoneSource)?.updatePadding([-0.333 * this.height]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) {
|
||||
|
|
@ -389,36 +490,6 @@ export class SuggestionBanner extends Banner {
|
|||
this.onSuggestionUpdate(this.currentSuggestions); // restore suggestions
|
||||
}
|
||||
|
||||
private get mouseEventConfig() {
|
||||
const config: InputEventEngineConfig = {
|
||||
targetRoot: this.getDiv(),
|
||||
// document.body is the event root b/c we need to track the mouse if it leaves
|
||||
// the VisualKeyboard's hierarchy.
|
||||
eventRoot: document.body,
|
||||
inputStartHandler: this.manager.touchStart.bind(this.manager),
|
||||
inputMoveHandler: this.manager.touchMove.bind(this.manager),
|
||||
inputEndHandler: this.manager.touchEnd.bind(this.manager),
|
||||
coordConstrainedWithinInteractiveBounds: function() { return true; }
|
||||
};
|
||||
|
||||
return new MouseEventEngine(config);
|
||||
}
|
||||
|
||||
private get touchEventConfig() {
|
||||
const config: InputEventEngineConfig = {
|
||||
targetRoot: this.getDiv(),
|
||||
// document.body is the event root b/c we need to track the mouse if it leaves
|
||||
// the VisualKeyboard's hierarchy.
|
||||
eventRoot: this.getDiv(),
|
||||
inputStartHandler: this.manager.touchStart.bind(this.manager),
|
||||
inputMoveHandler: this.manager.touchMove.bind(this.manager),
|
||||
inputEndHandler: this.manager.touchEnd.bind(this.manager),
|
||||
coordConstrainedWithinInteractiveBounds: function() { return true; }
|
||||
};
|
||||
|
||||
return new TouchEventEngine(config);
|
||||
}
|
||||
|
||||
public get predictionContext(): PredictionContext {
|
||||
return this._predictionContext;
|
||||
}
|
||||
|
|
@ -454,117 +525,4 @@ interface SuggestionInputEventMap {
|
|||
highlight: (bannerSuggestion: BannerSuggestion, state: boolean) => void,
|
||||
apply: (bannerSuggestion: BannerSuggestion) => void;
|
||||
hold: (bannerSuggestion: BannerSuggestion) => void;
|
||||
}
|
||||
|
||||
class SuggestionInputManager extends UITouchHandlerBase<HTMLDivElement> {
|
||||
public readonly events = new EventEmitter<SuggestionInputEventMap>();
|
||||
|
||||
private eventDisablePromise: Promise<any>;
|
||||
|
||||
platformHold: (suggestion: BannerSuggestion, isCustom: boolean) => void;
|
||||
|
||||
//#region Touch handling implementation
|
||||
findTargetFrom(e: HTMLElement): HTMLDivElement {
|
||||
try {
|
||||
if(e) {
|
||||
if(e.classList.contains('kmw-suggest-option')) {
|
||||
return e as HTMLDivElement;
|
||||
}
|
||||
if(e.parentElement && e.parentElement.classList.contains('kmw-suggest-option')) {
|
||||
return e.parentElement as HTMLDivElement;
|
||||
}
|
||||
// if(e.firstChild && util.hasClass(<HTMLElement> e.firstChild,'kmw-suggest-option')) {
|
||||
// return e.firstChild as HTMLDivElement;
|
||||
// }
|
||||
}
|
||||
} catch(ex) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected highlight(t: HTMLDivElement, on: boolean): void {
|
||||
let suggestion = t['suggestion'] as BannerSuggestion;
|
||||
|
||||
// Never highlight an empty suggestion button.
|
||||
if(suggestion.isEmpty()) {
|
||||
on = false;
|
||||
}
|
||||
|
||||
this.events.emit('highlight', suggestion, on);
|
||||
}
|
||||
|
||||
protected select(t: HTMLDivElement): void {
|
||||
this.events.emit('apply', t['suggestion'] as BannerSuggestion);
|
||||
}
|
||||
|
||||
//#region Long-press support
|
||||
protected hold(t: HTMLDivElement): void {
|
||||
// let suggestionObj = t['suggestion'] as BannerSuggestion;
|
||||
//
|
||||
// // Is this the <keep> suggestion? It's never in this.currentSuggestions, so check against that.
|
||||
// let isCustom = this.currentSuggestions.indexOf(suggestionObj.suggestion) == -1;
|
||||
|
||||
this.events.emit('hold', t['suggestion'] as BannerSuggestion);
|
||||
}
|
||||
protected clearHolds(): void {
|
||||
// Temp, pending implementation of suggestion longpress submenus
|
||||
// - nothing to clear without them -
|
||||
|
||||
// only really used in native-KMW
|
||||
}
|
||||
|
||||
protected hasModalPopup(): boolean {
|
||||
return this.eventsBlocked;
|
||||
}
|
||||
|
||||
protected dealiasSubTarget(target: HTMLDivElement): HTMLDivElement {
|
||||
return target;
|
||||
}
|
||||
|
||||
protected hasSubmenu(t: HTMLDivElement): boolean {
|
||||
// Temp, pending implementation of suggestion longpress submenus
|
||||
|
||||
// Only really used by native-KMW - see kmwnative's highlightSubKeys func.
|
||||
return false;
|
||||
}
|
||||
|
||||
protected isSubmenuActive(): boolean {
|
||||
// Temp, pending implementation of suggestion longpress submenus
|
||||
|
||||
// Utilized only by native-KMW - it parallels hasModalPopup() in purpose.
|
||||
return false;
|
||||
}
|
||||
|
||||
protected displaySubmenuFor(target: HTMLDivElement) {
|
||||
// Utilized only by native-KMW to show submenus.
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
//#endregion
|
||||
//#endregion
|
||||
|
||||
public get eventsBlocked(): boolean {
|
||||
return !!this.eventDisablePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended for use by the mobile apps, which sometimes 'takes over' touch handling.
|
||||
* For such cases, input should be blocked within KMW when the apps are managing an
|
||||
* ongoing touch-hold for any other interaction.
|
||||
*
|
||||
* Formerly:
|
||||
```
|
||||
let keyman = com.keyman.singleton;
|
||||
return keyman['osk'].vkbd.subkeyGesture && keyman.isEmbedded;
|
||||
```
|
||||
*/
|
||||
public temporarilyBlockEvents(promise: Promise<void>) { // TODO: ensure connection for embedded mode!
|
||||
this.eventDisablePromise = promise; // Will require routing; this class is not exported!
|
||||
promise.finally(() => {
|
||||
this.eventDisablePromise = null;
|
||||
})
|
||||
}
|
||||
|
||||
constructor(div: HTMLElement) {
|
||||
// TODO: Determine appropriate CSS styling names, etc.
|
||||
super(div, Banner.BANNER_CLASS, SuggestionBanner.TOUCHED_CLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
web/src/engine/osk/src/banner/bannerGestureSet.ts
Normal file
27
web/src/engine/osk/src/banner/bannerGestureSet.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { deepCopy } from '@keymanapp/web-utils';
|
||||
|
||||
import {
|
||||
gestures,
|
||||
GestureModelDefs,
|
||||
InputSample
|
||||
} from '@keymanapp/gesture-recognizer';
|
||||
|
||||
import { BannerSuggestion } from './banner.js';
|
||||
import { SimpleTapModelWithReset } from "../input/gestures/specsForLayout.js";
|
||||
|
||||
export const BannerSimpleTap: gestures.specs.GestureModel<BannerSuggestion> = {
|
||||
...deepCopy(SimpleTapModelWithReset),
|
||||
resolutionAction: {
|
||||
type: 'complete',
|
||||
item: 'current'
|
||||
}
|
||||
};
|
||||
|
||||
export const BANNER_GESTURE_SET: GestureModelDefs<BannerSuggestion> = {
|
||||
gestures: [
|
||||
BannerSimpleTap
|
||||
],
|
||||
sets: {
|
||||
default: [SimpleTapModelWithReset.id]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
import GlobeHint from "../globehint.interface.js";
|
||||
import PendingGesture from "../input/gestures/pendingGesture.interface.js";
|
||||
import { KeyElement } from "../keyElement.js";
|
||||
import KeyTip from "../keytip.interface.js";
|
||||
import VisualKeyboard from "../visualKeyboard.js";
|
||||
|
||||
export default interface EmbeddedGestureConfig {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ export { type KeyElement } from './keyElement.js';
|
|||
export { type default as OSKBaseKey } from './keyboard-layout/oskBaseKey.js';
|
||||
export { type default as GlobeHint } from './globehint.interface.js';
|
||||
export { type default as KeyTip } from './keytip.interface.js';
|
||||
export { type default as PendingGesture } from './input/gestures/pendingGesture.interface.js';
|
||||
export { type default as RealizedGesture } from './input/gestures/realizedGesture.interface.js';
|
||||
export { type default as InputEventCoordinate } from './input/inputEventCoordinate.js';
|
||||
export { type default as EmbeddedGestureConfig } from './config/embeddedGestureConfig.js';
|
||||
|
||||
export { default as Activator, StaticActivator } from './views/activator.js';
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import InputEventCoordinate from '../inputEventCoordinate.js';
|
||||
|
||||
export type InputHandler = (coord: InputEventCoordinate) => void;
|
||||
|
||||
export interface InputEventEngineConfig {
|
||||
/**
|
||||
* Specifies the element that input listeners should be attached to.
|
||||
*/
|
||||
readonly eventRoot: HTMLElement;
|
||||
/**
|
||||
* Specifies the most specific common ancestor element of any event target
|
||||
* that the InputEventEngine should consider.
|
||||
*/
|
||||
readonly targetRoot: HTMLElement;
|
||||
|
||||
readonly coordConstrainedWithinInteractiveBounds: (coord: InputEventCoordinate) => boolean;
|
||||
|
||||
readonly inputStartHandler?: InputHandler;
|
||||
readonly inputMoveHandler?: InputHandler;
|
||||
readonly inputMoveCancelHandler?: InputHandler;
|
||||
readonly inputEndHandler?: InputHandler;
|
||||
}
|
||||
|
||||
export default abstract class InputEventEngine {
|
||||
protected readonly config: InputEventEngineConfig;
|
||||
|
||||
public constructor(config: InputEventEngineConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
abstract registerEventHandlers();
|
||||
abstract unregisterEventHandlers();
|
||||
|
||||
onInputStart(coord: InputEventCoordinate) {
|
||||
if(this.config.inputStartHandler) {
|
||||
this.config.inputStartHandler(coord);
|
||||
}
|
||||
}
|
||||
|
||||
onInputMove(coord: InputEventCoordinate) {
|
||||
if(this.config.inputMoveHandler) {
|
||||
this.config.inputMoveHandler(coord);
|
||||
}
|
||||
}
|
||||
|
||||
onInputMoveCancel(coord: InputEventCoordinate) {
|
||||
if(this.config.inputMoveCancelHandler) {
|
||||
this.config.inputMoveCancelHandler(coord);
|
||||
}
|
||||
}
|
||||
|
||||
onInputEnd(coord: InputEventCoordinate) {
|
||||
if(this.config.inputEndHandler) {
|
||||
this.config.inputEndHandler(coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import InputEventEngine, { InputEventEngineConfig } from './inputEventEngine.js';
|
||||
import InputEventCoordinate from '../inputEventCoordinate.js';
|
||||
|
||||
export default class MouseEventEngine extends InputEventEngine {
|
||||
private readonly _mouseStart: typeof MouseEventEngine.prototype.onMouseStart;
|
||||
private readonly _mouseMove: typeof MouseEventEngine.prototype.onMouseMove;
|
||||
private readonly _mouseEnd: typeof MouseEventEngine.prototype.onMouseEnd;
|
||||
|
||||
private hasActiveClick: boolean = false;
|
||||
private ignoreSequence: boolean = false;
|
||||
|
||||
public constructor(config: InputEventEngineConfig) {
|
||||
super(config);
|
||||
|
||||
this._mouseStart = this.onMouseStart.bind(this);
|
||||
this._mouseMove = this.onMouseMove.bind(this);
|
||||
this._mouseEnd = this.onMouseEnd.bind(this);
|
||||
}
|
||||
|
||||
registerEventHandlers() {
|
||||
this.config.eventRoot.addEventListener('mousedown', this._mouseStart, true);
|
||||
this.config.eventRoot.addEventListener('mousemove', this._mouseMove, false);
|
||||
// The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'.
|
||||
this.config.eventRoot.addEventListener('mouseup', this._mouseEnd, true);
|
||||
}
|
||||
|
||||
unregisterEventHandlers() {
|
||||
this.config.eventRoot.removeEventListener('mousedown', this._mouseStart, true);
|
||||
this.config.eventRoot.removeEventListener('mousemove', this._mouseMove, false);
|
||||
this.config.eventRoot.removeEventListener('mouseup', this._mouseEnd, true);
|
||||
}
|
||||
|
||||
private preventPropagation(e: MouseEvent) {
|
||||
// Standard event maintenance
|
||||
e.preventDefault();
|
||||
e.cancelBubble=true;
|
||||
e.returnValue=false; // I2409 - Avoid focus loss for visual keyboard events
|
||||
|
||||
if(typeof e.stopImmediatePropagation == 'function') {
|
||||
e.stopImmediatePropagation();
|
||||
} else if(typeof e.stopPropagation == 'function') {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseStart(event: MouseEvent) {
|
||||
if(!this.config.targetRoot.contains(event.target as Node)) {
|
||||
this.ignoreSequence = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.preventPropagation(event);
|
||||
this.onInputStart(InputEventCoordinate.fromEvent(event));
|
||||
this.hasActiveClick = true;
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
if(this.ignoreSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coord = InputEventCoordinate.fromEvent(event);
|
||||
|
||||
if(!event.buttons) {
|
||||
if(this.hasActiveClick) {
|
||||
this.hasActiveClick = false;
|
||||
this.onInputMoveCancel(coord);
|
||||
}
|
||||
return;
|
||||
} else if(!this.hasActiveClick) {
|
||||
// Can interfere with OSK drag-handlers (title bar, resize bar) otherwise.
|
||||
return;
|
||||
}
|
||||
|
||||
this.preventPropagation(event);
|
||||
|
||||
if(this.config.coordConstrainedWithinInteractiveBounds(coord)) {
|
||||
this.onInputMove(coord);
|
||||
} else {
|
||||
this.onInputMoveCancel(coord);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnd(event: MouseEvent) {
|
||||
if(this.ignoreSequence) {
|
||||
this.ignoreSequence = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!event.buttons) {
|
||||
this.hasActiveClick = false;
|
||||
}
|
||||
this.onInputEnd(InputEventCoordinate.fromEvent(event));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import InputEventEngine, { InputEventEngineConfig } from './inputEventEngine.js';
|
||||
import InputEventCoordinate from '../inputEventCoordinate.js';
|
||||
|
||||
export default class TouchEventEngine extends InputEventEngine {
|
||||
private readonly _touchStart: typeof TouchEventEngine.prototype.onTouchStart;
|
||||
private readonly _touchMove: typeof TouchEventEngine.prototype.onTouchMove;
|
||||
private readonly _touchEnd: typeof TouchEventEngine.prototype.onTouchEnd;
|
||||
|
||||
public constructor(config: InputEventEngineConfig) {
|
||||
super(config);
|
||||
|
||||
this._touchStart = this.onTouchStart.bind(this);
|
||||
this._touchMove = this.onTouchMove.bind(this);
|
||||
this._touchEnd = this.onTouchEnd.bind(this);
|
||||
}
|
||||
|
||||
registerEventHandlers() {
|
||||
this.config.eventRoot.addEventListener('touchstart', this._touchStart, true);
|
||||
this.config.eventRoot.addEventListener('touchmove', this._touchMove, false);
|
||||
// The listener below fails to capture when performing automated testing checks in Chrome emulation unless 'true'.
|
||||
this.config.eventRoot.addEventListener('touchend', this._touchEnd, true);
|
||||
}
|
||||
|
||||
unregisterEventHandlers() {
|
||||
this.config.eventRoot.removeEventListener('touchstart', this._touchStart, true);
|
||||
this.config.eventRoot.removeEventListener('touchmove', this._touchMove, false);
|
||||
this.config.eventRoot.removeEventListener('touchend', this._touchEnd, true);
|
||||
}
|
||||
|
||||
private preventPropagation(e: TouchEvent) {
|
||||
// Standard event maintenance
|
||||
e.preventDefault();
|
||||
e.cancelBubble=true;
|
||||
|
||||
if(typeof e.stopImmediatePropagation == 'function') {
|
||||
e.stopImmediatePropagation();
|
||||
} else if(typeof e.stopPropagation == 'function') {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
onTouchStart(event: TouchEvent) {
|
||||
this.onInputStart(InputEventCoordinate.fromEvent(event));
|
||||
}
|
||||
|
||||
onTouchMove(event: TouchEvent) {
|
||||
this.preventPropagation(event);
|
||||
const coord = InputEventCoordinate.fromEvent(event);
|
||||
|
||||
if(this.config.coordConstrainedWithinInteractiveBounds(coord)) {
|
||||
this.onInputMove(coord);
|
||||
} else {
|
||||
this.onInputMoveCancel(coord);
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd(event: TouchEvent) {
|
||||
this.onInputEnd(InputEventCoordinate.fromEvent(event));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,429 +0,0 @@
|
|||
import InputEventCoordinate from "../inputEventCoordinate.js";
|
||||
import { getAbsoluteY } from 'keyman/engine/dom-utils';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class ScrollState {
|
||||
// While we don't currently track y-coordinates here, the class is designed
|
||||
// to permit tracking them with minimal extra effort if we ever decide to do so.
|
||||
x: number;
|
||||
totalLength = 0;
|
||||
|
||||
// The amount of coordinate 'noise' allowed during a scroll-enabled touch allowed
|
||||
// before interpreting the currently-ongoing touch command as having scrolled.
|
||||
static readonly HAS_SCROLLED_FUDGE_FACTOR = 10;
|
||||
|
||||
constructor(coord: InputEventCoordinate) {
|
||||
this.x = coord.x;
|
||||
|
||||
this.totalLength = 0;
|
||||
}
|
||||
|
||||
updateTo(coord: InputEventCoordinate): {deltaX: number} {
|
||||
let x = this.x;
|
||||
this.x = coord.x;
|
||||
|
||||
let deltas = {deltaX: this.x - x};
|
||||
this.totalLength += Math.abs(deltas.deltaX);
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
public get hasScrolled(): boolean {
|
||||
// Allow an accidental fudge-factor for overflow element noise during a touch, but not much.
|
||||
return this.totalLength > ScrollState.HAS_SCROLLED_FUDGE_FACTOR;
|
||||
}
|
||||
}
|
||||
|
||||
export default abstract class UITouchHandlerBase<Target extends HTMLElement> {
|
||||
private rowClassMatch: string;
|
||||
private selectedTargetMatch: string;
|
||||
private baseElement: HTMLElement;
|
||||
|
||||
private touchX: number;
|
||||
private touchY: number;
|
||||
private touchCount: number;
|
||||
|
||||
private currentTarget: Target;
|
||||
|
||||
private scrollTouchState: ScrollState;
|
||||
private pendingTarget: Target;
|
||||
|
||||
constructor(baseElement: HTMLElement, rowClassMatch: string, selectedTargetMatch: string) {
|
||||
this.baseElement = baseElement;
|
||||
this.rowClassMatch = rowClassMatch;
|
||||
this.selectedTargetMatch = selectedTargetMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the internally-preferred target element or submenu target element.
|
||||
* @param e The DOM element that actually received the touch event.
|
||||
* May be parent, child, or the actually-desired element itself.
|
||||
*/
|
||||
abstract findTargetFrom(e: HTMLElement): Target;
|
||||
|
||||
/**
|
||||
* Highlights the target element as visual feedback representing
|
||||
* a pending touch.
|
||||
* @param t The `Target` to highlight
|
||||
* @param state `true` to apply highlighting, `false` to remove it.
|
||||
*/
|
||||
protected abstract highlight(t: Target, state: boolean): void;
|
||||
|
||||
/**
|
||||
* Called whenever the touch-handling analysis determines that the Target has been selected
|
||||
* @param t The `Target` to activate/execute.
|
||||
*/
|
||||
protected abstract select(t: Target): void;
|
||||
|
||||
/**
|
||||
* Requests info on whether or not the indicated `Target` has subkeys or a submenu.
|
||||
* @param t A `Target`.
|
||||
*/
|
||||
protected abstract hasSubmenu(t: Target): boolean;
|
||||
|
||||
/**
|
||||
* Indicates that the user is maintaining a `Touch` on the specified `Target`.
|
||||
* Popups and-or longpress menus may be appropriate.
|
||||
* @param t The `Target` being held.
|
||||
*/
|
||||
protected abstract hold(t: Target): void;
|
||||
|
||||
/**
|
||||
* Signals that any popup elements (previews, subkey views, etc) should be cancelled.
|
||||
*/
|
||||
protected abstract clearHolds(): void;
|
||||
|
||||
/**
|
||||
* Requests a boolean indicating whether or not the UI is currently displaying any input-blocking popup elements.
|
||||
* Embedded mode should return `true` when the app is displaying popup menus.
|
||||
*/
|
||||
protected abstract hasModalPopup(): boolean;
|
||||
|
||||
/**
|
||||
* Designed to support highlighting of prepended base keys on phone form-factor subkey menus.
|
||||
* @param target The base element with a potential subkey menu alias.
|
||||
* @returns The aliased submenu version of the `Target`, or the original `Target` if no alias exists.
|
||||
*/
|
||||
protected abstract dealiasSubTarget(target: Target): Target;
|
||||
|
||||
/**
|
||||
* Should return true whenever a 'native'-mode submenu (or subkey) display is active.
|
||||
*/
|
||||
protected abstract isSubmenuActive(): boolean;
|
||||
|
||||
/**
|
||||
* For 'native' mode - requests that the submenu for the indicated `Target` be instantly displayed.
|
||||
* @param target The base element with a potential submenu
|
||||
*/
|
||||
protected abstract displaySubmenuFor(target: Target);
|
||||
|
||||
/**
|
||||
* Identify the key nearest to (but NOT under) the touch point if at the end of a key row,
|
||||
* but return null more than about 0.6 key width from the nearest key.
|
||||
*
|
||||
* @param {Object} coord A pre-analyzed input coordinate
|
||||
* @param {Object} t HTML object at touch point
|
||||
* @param {boolean} omitCurrent Omits any target directly under the touch point.
|
||||
* @return {Object} nearest key to touch point
|
||||
*
|
||||
**/
|
||||
private findTargetFromTouch(coord: InputEventCoordinate, t: HTMLElement, forMove: boolean): Target {
|
||||
var x = coord.x;
|
||||
|
||||
// Get the UI row beneath touch point (SuggestionBanner div, 'kmw-key-row' if OSK, ...)
|
||||
while(t && t.className !== undefined && t.className.indexOf(this.rowClassMatch) < 0) {
|
||||
t = <HTMLElement> t.parentNode;
|
||||
}
|
||||
if(!t) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find minimum distance from any key
|
||||
var k: number, bestMatch=0, dxMax=24, dxMin=100000, x1: number, x2: number;
|
||||
for(k = 0; k < t.childNodes.length; k++) {
|
||||
let childNode = t.childNodes[k] as HTMLElement;
|
||||
|
||||
if(this.isInvalidTarget(this.findTargetFrom(childNode))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
x1 = childNode.offsetLeft;
|
||||
x2 = x1 + childNode.offsetWidth;
|
||||
|
||||
// If it lies completely to the right and is the closest so far
|
||||
let dxRight = x1 - x;
|
||||
if(dxRight >= 0 && dxRight < dxMin) {
|
||||
bestMatch = k;
|
||||
dxMin = dxRight;
|
||||
}
|
||||
|
||||
// If it lies completely to the left and is the closest so far
|
||||
let dxLeft = x - x2;
|
||||
if(dxLeft >= 0 && dxLeft < dxMin) {
|
||||
bestMatch = k;
|
||||
dxMin = dxLeft;
|
||||
}
|
||||
|
||||
// If it is neither completely to the left nor completely to the right,
|
||||
// it's under the cursor. Stop the search!
|
||||
if(dxLeft < 0 && dxRight < 0) {
|
||||
return this.findTargetFrom(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
if(dxMin < 100000) {
|
||||
t = <HTMLElement> t.childNodes[bestMatch];
|
||||
x1 = t.offsetLeft;
|
||||
x2 = x1 + t.offsetWidth;
|
||||
|
||||
// Limit extended touch area to the larger of 0.6 of the potential target's width and 24 px
|
||||
if(t.offsetWidth > 40) {
|
||||
dxMax = 0.6 * t.offsetWidth;
|
||||
}
|
||||
|
||||
if(((x1 - x) >= 0 && (x1 - x) < dxMax) || ((x - x2) >= 0 && (x - x2) < dxMax)) {
|
||||
return this.findTargetFrom(t);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
findBestTarget(coord: InputEventCoordinate, forMove?: boolean) {
|
||||
var eventTarget: HTMLElement;
|
||||
|
||||
if(forMove) {
|
||||
const clientX = coord.x + document.body.scrollLeft;
|
||||
const clientY = coord.y + document.body.scrollTop;
|
||||
eventTarget = document.elementFromPoint(clientX, clientY) as HTMLElement;
|
||||
} else {
|
||||
eventTarget = coord.target as HTMLElement;
|
||||
}
|
||||
|
||||
let target = this.findTargetFrom(eventTarget);
|
||||
|
||||
// Should refactor this multi-check a bit for more overall reliability.
|
||||
if(!target) {
|
||||
// We didn't find a direct target, so we should look for the closest possible one.
|
||||
// Filters out invalid targets.
|
||||
target = this.findTargetFromTouch(coord, eventTarget, forMove);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether or not a `Target` should be considered invalid. Needed by the OSK for
|
||||
* hidden keys.
|
||||
* @param target A `Target` element to be validated.
|
||||
*/
|
||||
protected isInvalidTarget(target: Target): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
touchStart(coord: InputEventCoordinate) {
|
||||
// Determine the selected Target, manage state.
|
||||
this.currentTarget = this.findBestTarget(coord);
|
||||
this.touchX = coord.x;
|
||||
this.touchY = coord.y;
|
||||
|
||||
// If popup stuff, immediately return.
|
||||
|
||||
this.touchCount = coord.activeInputCount;
|
||||
|
||||
if(!this.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Establish scroll tracking.
|
||||
let shouldScroll = (this.currentTarget.clientWidth < this.currentTarget.scrollWidth);
|
||||
this.scrollTouchState = shouldScroll ? new ScrollState(coord) : null;
|
||||
|
||||
// Alright, Target acquired! Now to use it:
|
||||
|
||||
// Highlight the touched key
|
||||
this.highlight(this.currentTarget,true);
|
||||
|
||||
// If used by the OSK, the special function keys need immediate action
|
||||
// Add a `checkForImmediates()` to facilitate this.
|
||||
if(this.pendingTarget) {
|
||||
this.highlight(this.pendingTarget, false);
|
||||
this.select(this.pendingTarget);
|
||||
this.clearHolds();
|
||||
// Decrement the number of unreleased touch points to prevent
|
||||
// sending the keystroke again when the key is actually released
|
||||
this.touchCount--;
|
||||
} else {
|
||||
// If this key has subkey, start timer to display subkeys after delay, set up release
|
||||
this.hold(this.currentTarget);
|
||||
}
|
||||
this.pendingTarget = this.currentTarget;
|
||||
}
|
||||
|
||||
touchEnd(coord: InputEventCoordinate): void {
|
||||
// Prevent incorrect multi-touch behaviour if native or device popup visible
|
||||
let t = this.currentTarget;
|
||||
|
||||
if(this.isSubmenuActive() || this.hasModalPopup()) {
|
||||
// Ignore release if a multiple touch
|
||||
if(coord.activeInputCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel (but do not execute) pending key if neither a popup key or the base key
|
||||
if(t == null || t.id.indexOf('popup') < 0) {
|
||||
if (this.pendingTarget) {
|
||||
this.highlight(this.pendingTarget,false);
|
||||
}
|
||||
this.clearHolds();
|
||||
this.pendingTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Test if moved off screen (effective release point must be corrected for touch point horizontal speed)
|
||||
// This is not completely effective and needs some tweaking, especially on Android
|
||||
var x = coord.x;
|
||||
var beyondEdge = ((x < 2 && this.touchX > 5) || (x > window.innerWidth - 2 && this.touchX < window.innerWidth - 5));
|
||||
|
||||
if(this.scrollTouchState) {
|
||||
beyondEdge = beyondEdge || this.scrollTouchState.hasScrolled;
|
||||
}
|
||||
|
||||
// Save then decrement current touch count
|
||||
var tc=this.touchCount;
|
||||
if(this.touchCount > 0) {
|
||||
this.touchCount--;
|
||||
}
|
||||
|
||||
// Process and clear highlighting of pending target
|
||||
if(this.pendingTarget) {
|
||||
this.highlight(this.pendingTarget,false);
|
||||
|
||||
// Output character unless moved off key
|
||||
if(this.pendingTarget.className.indexOf('hidden') < 0 && tc > 0 && !beyondEdge) {
|
||||
this.select(this.pendingTarget);
|
||||
}
|
||||
this.clearHolds();
|
||||
this.pendingTarget = null;
|
||||
// Always clear highlighting of current target on release (multi-touch)
|
||||
} else {
|
||||
t = this.findBestTarget(coord);
|
||||
|
||||
if(t) {
|
||||
this.highlight(t,false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OSK touch move event handler
|
||||
*
|
||||
* @param {Object} coord A pre-analyzed input coordinate
|
||||
*
|
||||
**/
|
||||
touchMove(coord: InputEventCoordinate) : void {
|
||||
//let keyman = com.keyman.singleton;
|
||||
|
||||
// Do not attempt to support reselection of target key for overlapped keystrokes
|
||||
if(coord.activeInputCount > 1 || this.touchCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.currentTarget && this.scrollTouchState != null) {
|
||||
let deltaX = this.scrollTouchState.updateTo(coord).deltaX;
|
||||
this.currentTarget.scrollLeft -= window.devicePixelRatio * deltaX;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get touch position
|
||||
var y = coord.y;
|
||||
|
||||
// Move target key and highlighting
|
||||
var key0 = this.pendingTarget,
|
||||
key1 = this.findBestTarget(coord, true); // For the OSK, this ALSO gets subkeys.
|
||||
|
||||
// If option should not be selectable, how do we re-target?
|
||||
|
||||
|
||||
// Do not move over keys if device popup visible
|
||||
if(this.hasModalPopup()) {
|
||||
if(key0) {
|
||||
this.highlight(key0,false);
|
||||
}
|
||||
this.pendingTarget=null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the popup duplicate of the base key if a phone with a visible popup array
|
||||
key1 = this.dealiasSubTarget(key1);
|
||||
|
||||
// Identify current touch position (to manage off-key release)
|
||||
this.currentTarget = key1;
|
||||
|
||||
// Clear previous key highlighting
|
||||
if(key0 && key1 && key1 !== key0) {
|
||||
this.highlight(key0,false);
|
||||
}
|
||||
|
||||
// Code below directly related to subkeys should only be triggered within 'native' mode.
|
||||
// The embedded version instead passes info to the apps to produce their own subkeys in-app.
|
||||
|
||||
// If popup is visible, need to move over popup, not over main keyboard
|
||||
if(key1 && this.hasSubmenu(key1)) {
|
||||
//this.highlightSubKeys(key1,x,y);
|
||||
|
||||
// Currently only used by the banner... which currently does not do submenus.
|
||||
// // Native-mode: show popup keys immediately if touch moved up towards key array (KMEW-100, Build 353)
|
||||
// if(!keyman.isEmbedded && (this.touchY-y > 5) && !this.isSubmenuActive()) {
|
||||
// // Instantly show the submenu.
|
||||
// this.displaySubmenuFor(key1);
|
||||
// }
|
||||
|
||||
// Once a subkey array is displayed, do not allow changing the base key.
|
||||
// Keep that array visible and accept no other options until the touch ends.
|
||||
if(key1 && key1.id.indexOf('popup') < 0) { // TODO: reliant on 'popup' in .id
|
||||
return;
|
||||
}
|
||||
|
||||
// Highlight the base key on devices that do not append it to the subkey array.
|
||||
if(key1 && key1.className.indexOf(this.selectedTargetMatch) < 0) {
|
||||
this.highlight(key1,true);
|
||||
}
|
||||
// Cancel touch if moved up and off keyboard, unless popup keys visible
|
||||
} else {
|
||||
let base = this.baseElement;
|
||||
let top = getAbsoluteY(base);
|
||||
let height = base.offsetHeight;
|
||||
let yMin = Math.max(5, top - 0.25 * height);
|
||||
let yMax = (top + height) + 0.25 * height;
|
||||
if(key0 && (coord.y < yMin || coord.y > yMax)) {
|
||||
this.highlight(key0,false);
|
||||
this.clearHolds();
|
||||
this.pendingTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the target key, if any, by the new target key
|
||||
// Do not replace a null target, as that indicates the key has already been released
|
||||
if(key1 && this.pendingTarget) {
|
||||
this.pendingTarget = key1;
|
||||
}
|
||||
|
||||
if(this.pendingTarget) {
|
||||
if(key1 && (key0 != key1 || key1.className.indexOf(this.selectedTargetMatch) < 0)) {
|
||||
this.highlight(key1,true);
|
||||
}
|
||||
}
|
||||
|
||||
if(key0 && key1 && (key1 != key0) && (key1.id != '')) {
|
||||
// Display the touch-hold keys (after a pause)
|
||||
this.hold(key1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Codes, KeyEvent } from '@keymanapp/keyboard-processor';
|
||||
import { type KeyElement } from '../../../keyElement.js';
|
||||
import VisualKeyboard from '../../../visualKeyboard.js';
|
||||
import PendingGesture from '../pendingGesture.interface.js';
|
||||
// import PendingGesture from '../pendingGesture.interface.js';
|
||||
|
||||
export enum PendingMultiTapState { Waiting, Realized, Cancelled };
|
||||
/**
|
||||
|
|
@ -9,7 +9,7 @@ export enum PendingMultiTapState { Waiting, Realized, Cancelled };
|
|||
* (based on key id substring in the case of the shift key), within a
|
||||
* specified timeout period.
|
||||
*/
|
||||
export default class PendingMultiTap implements PendingGesture {
|
||||
export default class PendingMultiTap {
|
||||
public readonly vkbd: VisualKeyboard;
|
||||
public readonly baseKey: KeyElement;
|
||||
public readonly count: number;
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import type RealizedGesture from './realizedGesture.interface.js';
|
||||
import { type KeyElement } from '../../keyElement.js';
|
||||
|
||||
/**
|
||||
* Used for evaluating potential gestures. Classes adhering to this interface
|
||||
* should be instantiated whenever the (implied) state-machine allows a new
|
||||
* touch event to mark the start of a potential new gesture.
|
||||
*
|
||||
* For example, whenever a user touches a base key and there are no "realized"
|
||||
* (fully-completed, but as-of-yet unresolved) gestures, that state allows the
|
||||
* start of a potential new longpress event.
|
||||
*
|
||||
* The role of the `PendingGesture` is complete whenever all touch-events and
|
||||
* conditions necessary for a modeled gesture have been met. As this point,
|
||||
* it should be `resolve`d, fulfilling its `promise`. This results in a
|
||||
* `RealizedGesture` appropriate for the gesture type that is used to obtain
|
||||
* the final `KeyEvent` for the overall gesture sequence.
|
||||
*
|
||||
* For example, a "longpress" is considered resolved once the user has maintained
|
||||
* an active, stationary touch point on the same key for a sufficiently long
|
||||
* period without releasing it.
|
||||
* * Were it released earlier, that would result in selection of a base key.
|
||||
*
|
||||
* Alternatively, a "flick" might be considered resolved if:
|
||||
* * a user has rapidly moved a touch point in a consistent direction
|
||||
* * for a long enough distance
|
||||
* * and _then_ releases that touch point within a short timeframe.
|
||||
*
|
||||
* The pending gesture should only `resolve` to a realized gesture once
|
||||
* _all_ such conditions are met, confirming that this specific gesture,
|
||||
* and _only_ this specific gesture, could have resulted from the active
|
||||
* touch sequence.
|
||||
*
|
||||
* The `RealizedGesture` that results and is 'returned' via the Promise will
|
||||
* be handled by the `VisualKeyboard` class, which will retrieve and forward
|
||||
* any `KeyEvent` that results from the overall gesture input sequence.
|
||||
*
|
||||
* @see `RealizedGesture`
|
||||
*/
|
||||
export default interface PendingGesture {
|
||||
readonly baseKey: KeyElement;
|
||||
readonly promise?: Promise<RealizedGesture>;
|
||||
|
||||
cancel(): void;
|
||||
resolve?(): void;
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import InputEventCoordinate from '../inputEventCoordinate.js';
|
||||
import { type KeyElement } from '../../keyElement.js';
|
||||
import { type KeyEvent } from '@keymanapp/keyboard-processor';
|
||||
|
||||
/**
|
||||
* Implementations of this interface allow individual types of gestures to
|
||||
* specify any additional user interaction and functionality (which may
|
||||
* include UI elements) appropriate for obtaining a key event that may be
|
||||
* produced by the modeled gesture type. These should only be instantiated
|
||||
* once the associated `PendingLongpress` is no longer 'pending' - once it
|
||||
* has become clear that the input touch-event sequence could only correspond
|
||||
* to the modeled gesture.
|
||||
*
|
||||
* For example, when a longpress gesture completes - and hence, the user has
|
||||
* kept their finger stationary on the same key for a long enough period -
|
||||
* we display a popup view presenting subkeys corresponding to the gesture's
|
||||
* underlying element. This popup view accepts touch input and completes only
|
||||
* upon release of the ongoing touch sequence.
|
||||
*
|
||||
* Gestures are events that occur over intervals of time, and since some of them
|
||||
* will require time and user interaction after becoming 'realized', these cases
|
||||
* will be inherently async. The simplest way to model this is with `Promise`s.
|
||||
*
|
||||
* If appropriate for the modeled gesture type, an implementation may supply an
|
||||
* instantly-resolving `Promise`. This may be appropriate for modeling "flick"
|
||||
* or "swipe" gestures in the future, which may require no additional input once
|
||||
* such a gesture is fully realized.
|
||||
*/
|
||||
export default interface RealizedGesture {
|
||||
readonly baseKey: KeyElement;
|
||||
readonly promise: Promise<KeyEvent>;
|
||||
|
||||
clear(): void;
|
||||
isVisible(): boolean;
|
||||
updateTouch(input: InputEventCoordinate): void;
|
||||
}
|
||||
|
|
@ -95,11 +95,11 @@ export function gestureSetForLayout(layerGroup: OSKLayerGroup, params: GesturePa
|
|||
}
|
||||
};
|
||||
|
||||
const simpleTapModel: GestureModel = deepCopy(layout.hasFlicks ? SimpleTapModel : SimpleTapModelWithReset);
|
||||
const longpressModel: GestureModel = deepCopy(layout.hasFlicks ? basicLongpressModel(params) : longpressModelWithShortcut(params));
|
||||
const simpleTapModel: GestureModel<KeyElement> = deepCopy(layout.hasFlicks ? SimpleTapModel : SimpleTapModelWithReset);
|
||||
const longpressModel: GestureModel<KeyElement> = deepCopy(layout.hasFlicks ? basicLongpressModel(params) : longpressModelWithShortcut(params));
|
||||
|
||||
// #region Functions for implementing and/or extending path initial-state checks
|
||||
function withKeySpecFiltering(model: GestureModel, contactIndices: number | number[]) {
|
||||
function withKeySpecFiltering(model: GestureModel<KeyElement>, contactIndices: number | number[]) {
|
||||
// Creates deep copies of the model specifications that are safe to customize to the
|
||||
// keyboard layout.
|
||||
model = deepCopy(model);
|
||||
|
|
@ -125,7 +125,7 @@ export function gestureSetForLayout(layerGroup: OSKLayerGroup, params: GesturePa
|
|||
return model;
|
||||
}
|
||||
|
||||
function withLayerChangeItemFix(model: GestureModel, contactIndices: number | number[]) {
|
||||
function withLayerChangeItemFix(model: GestureModel<KeyElement>, contactIndices: number | number[]) {
|
||||
// Creates deep copies of the model specifications that are safe to customize to the
|
||||
// keyboard layout.
|
||||
model = deepCopy(model);
|
||||
|
|
@ -206,7 +206,9 @@ export function gestureSetForLayout(layerGroup: OSKLayerGroup, params: GesturePa
|
|||
|
||||
// #region Definition of models for paths comprising gesture-stage models
|
||||
|
||||
type ContactModel = specs.ContactModel<KeyElement>;
|
||||
// Note: as specified below, none of the raw specs actually need access to KeyElement typing.
|
||||
|
||||
type ContactModel = specs.ContactModel<any>;
|
||||
|
||||
export const InstantContactRejectionModel: ContactModel = {
|
||||
itemPriority: 0,
|
||||
|
|
@ -337,14 +339,18 @@ export const SubkeySelectContactModel: ContactModel = {
|
|||
// #endregion
|
||||
|
||||
// #region Gesture-stage model definitions
|
||||
type GestureModel = specs.GestureModel<KeyElement>;
|
||||
|
||||
// Note: as specified below, most of the raw specs actually need access to KeyElement typing.
|
||||
// That only becomes relevant with some of the modifier functions in the `gestureSetForLayout`
|
||||
// func at the top.
|
||||
type GestureModel<Type> = specs.GestureModel<Type>;
|
||||
|
||||
// TODO: customization of the gesture models depending upon properties of the keyboard.
|
||||
// - has flicks? no longpress shortcut, also no longpress reset(?)
|
||||
// - modipress: keyboard-specific modifier keys - which may require inspection of a
|
||||
// key's properties.
|
||||
|
||||
export const SpecialKeyStartModel: GestureModel = {
|
||||
export const SpecialKeyStartModel: GestureModel<KeyElement> = {
|
||||
id: 'special-key-start',
|
||||
resolutionPriority: 0,
|
||||
contacts : [
|
||||
|
|
@ -376,7 +382,7 @@ export const SpecialKeyStartModel: GestureModel = {
|
|||
}
|
||||
}
|
||||
|
||||
export const SpecialKeyEndModel: GestureModel = {
|
||||
export const SpecialKeyEndModel: GestureModel<any> = {
|
||||
id: 'special-key-end',
|
||||
resolutionPriority: 0,
|
||||
contacts : [
|
||||
|
|
@ -397,7 +403,7 @@ export const SpecialKeyEndModel: GestureModel = {
|
|||
/**
|
||||
* The flickless, roaming-touch-less version.
|
||||
*/
|
||||
export function basicLongpressModel(params: GestureParams): GestureModel {
|
||||
export function basicLongpressModel(params: GestureParams): GestureModel<any> {
|
||||
return {
|
||||
id: 'longpress',
|
||||
resolutionPriority: 0,
|
||||
|
|
@ -427,7 +433,7 @@ export function basicLongpressModel(params: GestureParams): GestureModel {
|
|||
* For use when a layout doesn't have flicks; has the up-flick shortcut
|
||||
* and facilitates roaming-touch.
|
||||
*/
|
||||
export function longpressModelWithShortcut(params: GestureParams): GestureModel {
|
||||
export function longpressModelWithShortcut(params: GestureParams): GestureModel<any> {
|
||||
return {
|
||||
...basicLongpressModel(params),
|
||||
|
||||
|
|
@ -473,7 +479,7 @@ export function longpressModelWithShortcut(params: GestureParams): GestureModel
|
|||
* For use when a layout doesn't have flicks; has the up-flick shortcut
|
||||
* and facilitates roaming-touch.
|
||||
*/
|
||||
export function longpressModelWithRoaming(params: GestureParams): GestureModel {
|
||||
export function longpressModelWithRoaming(params: GestureParams): GestureModel<any> {
|
||||
return {
|
||||
...basicLongpressModel(params),
|
||||
|
||||
|
|
@ -517,7 +523,7 @@ export function longpressModelWithRoaming(params: GestureParams): GestureModel {
|
|||
}
|
||||
|
||||
|
||||
export const MultitapModel: GestureModel = {
|
||||
export const MultitapModel: GestureModel<any> = {
|
||||
id: 'multitap',
|
||||
resolutionPriority: 2,
|
||||
contacts: [
|
||||
|
|
@ -547,7 +553,7 @@ export const MultitapModel: GestureModel = {
|
|||
}
|
||||
}
|
||||
|
||||
export const SimpleTapModel: GestureModel = {
|
||||
export const SimpleTapModel: GestureModel<any> = {
|
||||
id: 'simple-tap',
|
||||
resolutionPriority: 1,
|
||||
contacts: [
|
||||
|
|
@ -570,7 +576,7 @@ export const SimpleTapModel: GestureModel = {
|
|||
}
|
||||
}
|
||||
|
||||
export const SimpleTapModelWithReset: GestureModel = {
|
||||
export const SimpleTapModelWithReset: GestureModel<any> = {
|
||||
...SimpleTapModel,
|
||||
rejectionActions: {
|
||||
item: {
|
||||
|
|
@ -580,7 +586,7 @@ export const SimpleTapModelWithReset: GestureModel = {
|
|||
}
|
||||
}
|
||||
|
||||
export const SubkeySelectModel: GestureModel = {
|
||||
export const SubkeySelectModel: GestureModel<any> = {
|
||||
id: 'subkey-select',
|
||||
resolutionPriority: 0,
|
||||
contacts: [
|
||||
|
|
@ -608,7 +614,7 @@ export const SubkeySelectModel: GestureModel = {
|
|||
sustainWhenNested: true
|
||||
}
|
||||
|
||||
export const ModipressStartModel: GestureModel = {
|
||||
export const ModipressStartModel: GestureModel<KeyElement> = {
|
||||
id: 'modipress-start',
|
||||
resolutionPriority: 5,
|
||||
contacts: [
|
||||
|
|
@ -641,7 +647,7 @@ export const ModipressStartModel: GestureModel = {
|
|||
}
|
||||
}
|
||||
|
||||
export const ModipressEndModel: GestureModel = {
|
||||
export const ModipressEndModel: GestureModel<any> = {
|
||||
id: 'modipress-end',
|
||||
resolutionPriority: 5,
|
||||
contacts: [
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
/**
|
||||
* Represents the current location of the current cursor / touchpoint during
|
||||
* an ongoing OSK input event. This class standardizes to .pageX (document)
|
||||
* coordinates, rather than .clientX (viewport) coordinates.
|
||||
*/
|
||||
export default class InputEventCoordinate {
|
||||
public readonly x: number;
|
||||
public readonly y: number;
|
||||
|
||||
private readonly source: MouseEvent | TouchEvent;
|
||||
|
||||
public constructor(x: number, y: number, source?: MouseEvent | TouchEvent) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
if(source) {
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a MouseEvent or TouchEvent into the base coordinates needed
|
||||
// by the mouse-dragging operations.
|
||||
public static fromEvent(e: MouseEvent | TouchEvent) {
|
||||
let coordSource: MouseEvent | Touch;
|
||||
|
||||
// Desktop Safari versions as recent as 14.1 do not support TouchEvents.
|
||||
// So, just in case, a two-fold conditional check to avoid issues with a direct
|
||||
// 'instanceof' against the type.
|
||||
if(window['TouchEvent'] && e instanceof TouchEvent) {
|
||||
coordSource = e.changedTouches[0];
|
||||
} else if(e['changedTouches']) {
|
||||
coordSource = e['changedTouches'][0] as Touch;
|
||||
} else {
|
||||
coordSource = e as MouseEvent;
|
||||
}
|
||||
|
||||
// For MouseEvents, .pageX is slightly less supported in older browsers when
|
||||
// compared to .clientX. They're about equally supported for TouchEvents.
|
||||
if (coordSource.pageX) {
|
||||
return new InputEventCoordinate(coordSource.pageX, coordSource.pageY, e);
|
||||
} else if (coordSource.clientX) {
|
||||
const x = coordSource.clientX + document.body.scrollLeft;
|
||||
const y = coordSource.clientY + document.body.scrollTop;
|
||||
|
||||
return new InputEventCoordinate(x, y, e);
|
||||
} else {
|
||||
return new InputEventCoordinate(null, null, e);
|
||||
}
|
||||
}
|
||||
|
||||
public get activeInputCount(): number {
|
||||
// May not be an ACTUAL touch event during unit tests.
|
||||
if(window['TouchEvent'] && this.source['touches'] !== undefined && this.source['touches'] !== null) {
|
||||
return this.source['touches'].length;
|
||||
} else {
|
||||
const event = this.source as MouseEvent;
|
||||
return event.buttons > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public get target() {
|
||||
return this.source?.target;
|
||||
}
|
||||
|
||||
public get isFromTouch(): boolean {
|
||||
return !this.isFromMouse;
|
||||
}
|
||||
|
||||
public get isFromMouse(): boolean {
|
||||
return this.source instanceof MouseEvent;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,56 @@
|
|||
import InputEventCoordinate from './inputEventCoordinate.js';
|
||||
|
||||
type MouseHandler = (this: GlobalEventHandlers, ev: MouseEvent) => any;
|
||||
|
||||
/**
|
||||
* Represents the current location of the current cursor / touchpoint during
|
||||
* an ongoing contact-point event series. This class standardizes to .pageX
|
||||
* (document) coordinates, rather than .clientX (viewport) coordinates.
|
||||
*/
|
||||
class InputEventCoordinate {
|
||||
public readonly x: number;
|
||||
public readonly y: number;
|
||||
|
||||
private readonly source: MouseEvent | TouchEvent;
|
||||
|
||||
public constructor(x: number, y: number, source?: MouseEvent | TouchEvent) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
if(source) {
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a MouseEvent or TouchEvent into the base coordinates needed
|
||||
// by the mouse-dragging operations.
|
||||
public static fromEvent(e: MouseEvent | TouchEvent) {
|
||||
let coordSource: MouseEvent | Touch;
|
||||
|
||||
// Desktop Safari versions as recent as 14.1 do not support TouchEvents.
|
||||
// So, just in case, a two-fold conditional check to avoid issues with a direct
|
||||
// 'instanceof' against the type.
|
||||
if(window['TouchEvent'] && e instanceof TouchEvent) {
|
||||
coordSource = e.changedTouches[0];
|
||||
} else if(e['changedTouches']) {
|
||||
coordSource = e['changedTouches'][0] as Touch;
|
||||
} else {
|
||||
coordSource = e as MouseEvent;
|
||||
}
|
||||
|
||||
// For MouseEvents, .pageX is slightly less supported in older browsers when
|
||||
// compared to .clientX. They're about equally supported for TouchEvents.
|
||||
if (coordSource.pageX) {
|
||||
return new InputEventCoordinate(coordSource.pageX, coordSource.pageY, e);
|
||||
} else if (coordSource.clientX) {
|
||||
const x = coordSource.clientX + document.body.scrollLeft;
|
||||
const y = coordSource.clientY + document.body.scrollTop;
|
||||
|
||||
return new InputEventCoordinate(x, y, e);
|
||||
} else {
|
||||
return new InputEventCoordinate(null, null, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to store the page's original mouse handlers and properties
|
||||
* when temporarily overridden by OSK moving or resizing handlers due
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import TouchLayoutFlick = TouchLayout.TouchLayoutFlick;
|
|||
import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';
|
||||
|
||||
import { getFontSizeStyle } from '../fontSizeUtils.js';
|
||||
import InputEventCoordinate from '../input/inputEventCoordinate.js';
|
||||
import specialChars from '../specialCharacters.js';
|
||||
import buttonClassNames from '../buttonClassNames.js';
|
||||
|
||||
|
|
@ -379,19 +378,6 @@ export default abstract class OSKKey {
|
|||
return t;
|
||||
}
|
||||
|
||||
public isUnderTouch(input: InputEventCoordinate): boolean {
|
||||
let x = input.x;
|
||||
let y = input.y;
|
||||
|
||||
let btn = this.btn;
|
||||
let x0 = getAbsoluteX(btn);
|
||||
let y0 = getAbsoluteY(btn);
|
||||
let x1 = x0 + btn.offsetWidth;
|
||||
let y1 = y0 + btn.offsetHeight;
|
||||
|
||||
return (x > x0 && x < x1 && y > y0 && y < y1);
|
||||
}
|
||||
|
||||
public refreshLayout(vkbd: VisualKeyboard) {
|
||||
// space bar may not define the text span!
|
||||
if(this.label) {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import {
|
|||
import { createStyleSheet, getAbsoluteX, getAbsoluteY, StylesheetManager } from 'keyman/engine/dom-utils';
|
||||
|
||||
import GlobeHint from './globehint.interface.js';
|
||||
import InputEventCoordinate from './input/inputEventCoordinate.js';
|
||||
import KeyboardView from './components/keyboardView.interface.js';
|
||||
import { type KeyElement, getKeyFrom } from './keyElement.js';
|
||||
import KeyTip from './keytip.interface.js';
|
||||
|
|
@ -37,8 +36,6 @@ import OSKKey from './keyboard-layout/oskKey.js';
|
|||
import OSKLayer from './keyboard-layout/oskLayer.js';
|
||||
import OSKLayerGroup from './keyboard-layout/oskLayerGroup.js';
|
||||
import { LengthStyle, ParsedLengthStyle } from './lengthStyle.js';
|
||||
import PendingGesture from './input/gestures/pendingGesture.interface.js';
|
||||
import RealizedGesture from './input/gestures/realizedGesture.interface.js';
|
||||
import { defaultFontSize, getFontSizeStyle } from './fontSizeUtils.js';
|
||||
import PendingMultiTap, { PendingMultiTapState } from './input/gestures/browser/pendingMultiTap.js';
|
||||
import InternalKeyTip from './input/gestures/browser/keytip.js';
|
||||
|
|
@ -180,7 +177,6 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
|
||||
// State-related properties
|
||||
keyPending: KeyElement;
|
||||
touchPending: InputEventCoordinate;
|
||||
deleteKey: KeyElement;
|
||||
deleting: number; // Tracks a timer id for repeated deletions.
|
||||
nextLayer: string;
|
||||
|
|
@ -192,7 +188,6 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
};
|
||||
|
||||
// Touch-tracking properties
|
||||
initTouchCoord: InputEventCoordinate;
|
||||
touchCount: number;
|
||||
currentTarget: KeyElement;
|
||||
|
||||
|
|
@ -205,9 +200,6 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
|
||||
activeGestures: GestureHandler[] = [];
|
||||
|
||||
pendingSubkey: PendingGesture;
|
||||
subkeyGesture: RealizedGesture;
|
||||
|
||||
// Multi-tap gesture management
|
||||
pendingMultiTap: PendingMultiTap;
|
||||
|
||||
|
|
@ -773,108 +765,6 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
|
||||
//#region Input handling start
|
||||
|
||||
/**
|
||||
* Determines a "fuzzy boundary" area around the OSK within which active mouse and
|
||||
* touch events will be maintained, even if their coordinates lie outside of the OSK's
|
||||
* true visual bounds.
|
||||
* @returns A `BoundingRect`, in `.pageX` / `.pageY` coordinates.
|
||||
*/
|
||||
private getInteractiveBoundingRect(): BoundingRect {
|
||||
// Determine the important geometric values involved
|
||||
let oskX = getAbsoluteX(this.element);
|
||||
let oskY = getAbsoluteY(this.element);
|
||||
|
||||
// Determine the out-of-bounds threshold at which touch-cancellation should automatically occur.
|
||||
// Assuming square key-squares, we'll use 1/3 the height of a row for bounds detection
|
||||
// for both dimensions.
|
||||
const rowCount = this.currentLayer.rows.length;
|
||||
const buffer = (0.333 * this.height / rowCount);
|
||||
|
||||
// Determine the OSK's boundaries and the boundaries of the page / view.
|
||||
// These values are needed in .pageX / .pageY coordinates for the final calcs.
|
||||
let boundingRect: BoundingRect = {
|
||||
left: oskX - buffer,
|
||||
right: oskX + this.width + buffer,
|
||||
top: oskY - buffer,
|
||||
bottom: oskY + this.height + buffer
|
||||
};
|
||||
|
||||
return boundingRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts a potential "interactive boundary" definition by enforcing an
|
||||
* "event cancellation zone" near screen boundaries that are not directly adjacent
|
||||
* to the ongoing input event's initial coordinate.
|
||||
*
|
||||
* This facilitates modeling of conventional cancellation gestures where a user would
|
||||
* drag the mouse or touch point off the OSK, as mouse and touch event handlers receive
|
||||
* no input beyond screen boundaries.
|
||||
*
|
||||
* @param baseBounds The baseline interactive bounding area to be adjusted
|
||||
* @param startCoord The initial coordinate of a currently-ongoing input event
|
||||
* @returns
|
||||
*/
|
||||
private applyScreenMarginBoundsThresholding(baseBounds: BoundingRect,
|
||||
startCoord: InputEventCoordinate): BoundingRect {
|
||||
// Determine the needed linear translation to screen coordinates.
|
||||
const xDelta = window.screenLeft - window.pageXOffset;
|
||||
const yDelta = window.screenTop - window.pageYOffset;
|
||||
|
||||
let adjustedBounds: BoundingRect = { ...baseBounds };
|
||||
|
||||
// Also translate the initial touch's screen coord, as it affects our bounding box logic.
|
||||
const initScreenCoord = new InputEventCoordinate(startCoord.x + xDelta, startCoord.y + yDelta);
|
||||
|
||||
// Detection: is the OSK aligned with any screen boundaries?
|
||||
// If so, create a 'fuzzy' zone around the edges not near the initial touch point that allow
|
||||
// move-based cancellation.
|
||||
|
||||
// If the initial input screen-coord is at least 5 pixels from the screen's left AND
|
||||
// the OSK's left boundary is within 2 pixels from the screen's left...
|
||||
if (initScreenCoord.x >= 5 && baseBounds.left + xDelta <= 2) {
|
||||
adjustedBounds.left = 2 - xDelta; // new `leftBound` is set to 2 pixels from the screen's left.
|
||||
}
|
||||
|
||||
if (initScreenCoord.x <= screen.width - 5 && baseBounds.right + xDelta >= screen.width - 2) {
|
||||
adjustedBounds.right = (screen.width - 2) - xDelta; // new `rightBound` 2px from screen's right.
|
||||
}
|
||||
|
||||
if (initScreenCoord.y >= 5 && baseBounds.top + yDelta <= 2) {
|
||||
adjustedBounds.top = 2 - yDelta;
|
||||
}
|
||||
|
||||
if (initScreenCoord.y <= screen.height - 5 && baseBounds.bottom + yDelta >= screen.height - 2) {
|
||||
adjustedBounds.bottom = (screen.height - 2) - yDelta;
|
||||
}
|
||||
|
||||
return adjustedBounds;
|
||||
}
|
||||
|
||||
detectWithinInteractiveBounds(coord: InputEventCoordinate): boolean {
|
||||
// Shortcuts the method during unit testing, as we don't currently
|
||||
// provide coordinate values in its synthetic events.
|
||||
if (coord.x === null && coord.y === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const baseBoundingRect = this.getInteractiveBoundingRect();
|
||||
let adjustedBoundingRect = baseBoundingRect;
|
||||
if(this.initTouchCoord) {
|
||||
this.applyScreenMarginBoundsThresholding(baseBoundingRect, this.initTouchCoord);
|
||||
}
|
||||
|
||||
// Now to check where the input coordinate lies in relation to the final bounding box!
|
||||
|
||||
if (coord.x < adjustedBoundingRect.left || coord.x > adjustedBoundingRect.right) {
|
||||
return false;
|
||||
} else if (coord.y < adjustedBoundingRect.top || coord.y > adjustedBoundingRect.bottom) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * The main OSK touch start event handler
|
||||
// *
|
||||
|
|
@ -1571,88 +1461,78 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all supported gestures given a base key and the triggering touch coordinates.
|
||||
* @param key The gesture's base key
|
||||
* @param touch The starting touch coordinates for the gesture
|
||||
* @returns
|
||||
*/
|
||||
initGestures(key: KeyElement, input: InputEventCoordinate) {
|
||||
// /**
|
||||
// * Initializes all supported gestures given a base key and the triggering touch coordinates.
|
||||
// * @param key The gesture's base key
|
||||
// * @param touch The starting touch coordinates for the gesture
|
||||
// * @returns
|
||||
// */
|
||||
// initGestures(key: KeyElement, input: InputEventCoordinate) {
|
||||
|
||||
if (this.pendingMultiTap) {
|
||||
switch (this.pendingMultiTap.incrementTouch(key)) {
|
||||
case PendingMultiTapState.Cancelled:
|
||||
this.pendingMultiTap = null;
|
||||
break;
|
||||
case PendingMultiTapState.Realized:
|
||||
// Don't initialize any other gestures if the
|
||||
// multi tap is realized; we cleanup on touch
|
||||
// release because we need to cancel the base
|
||||
// key action
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if (this.pendingMultiTap) {
|
||||
// switch (this.pendingMultiTap.incrementTouch(key)) {
|
||||
// case PendingMultiTapState.Cancelled:
|
||||
// this.pendingMultiTap = null;
|
||||
// break;
|
||||
// case PendingMultiTapState.Realized:
|
||||
// // Don't initialize any other gestures if the
|
||||
// // multi tap is realized; we cleanup on touch
|
||||
// // release because we need to cancel the base
|
||||
// // key action
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!this.pendingMultiTap && PendingMultiTap.isValidTarget(this, key)) {
|
||||
// We are only going to support double-tap on Shift
|
||||
// in Keyman 15, so we pass in the constant count = 2
|
||||
this.pendingMultiTap = new PendingMultiTap(this, key, 2);
|
||||
this.pendingMultiTap.timeout.then(() => {
|
||||
this.pendingMultiTap = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
// if (!this.pendingMultiTap && PendingMultiTap.isValidTarget(this, key)) {
|
||||
// // We are only going to support double-tap on Shift
|
||||
// // in Keyman 15, so we pass in the constant count = 2
|
||||
// this.pendingMultiTap = new PendingMultiTap(this, key, 2);
|
||||
// this.pendingMultiTap.timeout.then(() => {
|
||||
// this.pendingMultiTap = null;
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Updates all currently-pending and activated gestures.
|
||||
*
|
||||
* @param currentKey The key currently underneath the most recent touch coordinate
|
||||
* @param previousKey The previously-selected key
|
||||
* @param input The current mouse or touch coordinate for the gesture
|
||||
* @returns true if should fully capture input, false if input should 'fall through'.
|
||||
*/
|
||||
updateGestures(currentKey: KeyElement, previousKey: KeyElement, input: InputEventCoordinate): boolean {
|
||||
let key0 = previousKey;
|
||||
let key1 = currentKey;
|
||||
// /**
|
||||
// * Updates all currently-pending and activated gestures.
|
||||
// *
|
||||
// * @param currentKey The key currently underneath the most recent touch coordinate
|
||||
// * @param previousKey The previously-selected key
|
||||
// * @param input The current mouse or touch coordinate for the gesture
|
||||
// * @returns true if should fully capture input, false if input should 'fall through'.
|
||||
// */
|
||||
// updateGestures(currentKey: KeyElement, previousKey: KeyElement, input: InputEventCoordinate): boolean {
|
||||
// let key0 = previousKey;
|
||||
// let key1 = currentKey;
|
||||
|
||||
if(!currentKey && this.pendingMultiTap) {
|
||||
this.pendingMultiTap.cancel();
|
||||
this.pendingMultiTap = null;
|
||||
}
|
||||
// if(!currentKey && this.pendingMultiTap) {
|
||||
// this.pendingMultiTap.cancel();
|
||||
// this.pendingMultiTap = null;
|
||||
// }
|
||||
|
||||
// Clear previous key highlighting, allow subkey controller to highlight as appropriate.
|
||||
if (this.subkeyGesture) {
|
||||
if (key0) {
|
||||
key0.key.highlight(false);
|
||||
}
|
||||
this.subkeyGesture.updateTouch(input);
|
||||
// // Clear previous key highlighting, allow subkey controller to highlight as appropriate.
|
||||
// if (this.subkeyGesture) {
|
||||
// if (key0) {
|
||||
// key0.key.highlight(false);
|
||||
// }
|
||||
// this.subkeyGesture.updateTouch(input);
|
||||
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
// this.keyPending = null;
|
||||
// this.touchPending = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
// return true;
|
||||
// }
|
||||
|
||||
this.currentTarget = null;
|
||||
// this.currentTarget = null;
|
||||
|
||||
// If there is an active popup menu (which can occur from the previous block),
|
||||
// a subkey popup exists; do not allow base key output.
|
||||
if (this.subkeyGesture || this.pendingSubkey) {
|
||||
return true;
|
||||
}
|
||||
// // If there is an active popup menu (which can occur from the previous block),
|
||||
// // a subkey popup exists; do not allow base key output.
|
||||
// if (this.subkeyGesture || this.pendingSubkey) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private getLongpressFlickThreshold(): number {
|
||||
const rowHeight = this.currentLayer.rowHeight;
|
||||
|
||||
// If larger than 5 (and it likely is), new threshold = 1/4 the std. key height.
|
||||
const proportionalThreshold = rowHeight / 4;
|
||||
|
||||
// 5 - the longpress-flick triggering threshold before 15.0.
|
||||
return Math.max(proportionalThreshold, 5);
|
||||
}
|
||||
// return false;
|
||||
// }
|
||||
|
||||
optionKey(e: KeyElement, keyName: string, keyDown: boolean) {
|
||||
if (keyName.indexOf('K_LOPT') >= 0) {
|
||||
|
|
@ -1729,7 +1609,6 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
}
|
||||
|
||||
this.keyPending = null;
|
||||
this.touchPending = null;
|
||||
|
||||
this.keytip?.show(null, false, this);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue