chore(web): removes old namespaced version of osk source files

This commit is contained in:
Joshua A. Horton 2023-01-20 15:18:09 +07:00
parent d987444683
commit f6d79964d6
37 changed files with 0 additions and 8484 deletions

View file

@ -1,307 +0,0 @@
// Includes KMW-added property declaration extensions for HTML elements.
/// <reference path="../kmwexthtml.ts" />
// Includes the touch-mode language picker UI.
/// <reference path="languageMenu.ts" />
/// <reference path="lengthStyle.ts" />
// Defines desktop-centric OSK positioning + sizing behavior
/// <reference path="layouts/targetedFloatLayout.ts" />
/// <reference path="oskView.ts" />
/***
KeymanWeb 10.0
Copyright 2017 SIL International
***/
namespace com.keyman.osk {
type OSKPos = {'left'?: number, 'top'?: number};
export class AnchoredOSKView extends OSKView {
desktopLayout: layouts.TargetedFloatLayout;
// OSK positioning fields
x: number;
y: number;
private isResizing: boolean = false;
// Key code definition aliases for legacy keyboards (They expect window['keyman']['osk'].___)
modifierCodes = text.Codes.modifierCodes;
modifierBitmasks = text.Codes.modifierBitmasks;
stateBitmasks = text.Codes.stateBitmasks;
keyCodes = text.Codes.keyCodes;
public constructor(modeledDevice: utils.DeviceSpec) {
super(modeledDevice);
document.body.appendChild(this._Box);
let keymanweb = com.keyman.singleton;
if(keymanweb.isEmbedded) {
this.activationMode == ActivationMode.manual;
}
}
/**
* Function _Unload
* Scope Private
* Description Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)
*/
_Unload() {
this.keyboardView = null;
this.bannerView = null;
this._Box = null;
}
protected setBoxStyling() {
const s = this._Box.style;
s.zIndex = '9999';
s.display = 'none';
s.width = '100%';
s.position = 'fixed';
}
/**
* @override
*/
public refreshLayout(pending?: boolean): void {
// This function is generally triggered whenever the OSK's dimensions change, among other
// things.
if(this.isResizing) {
return;
}
try {
this.isResizing = true;
// This resizes the OSK to what is appropriate for the device's current orientation,
// which will often trigger a resize event... which in turn triggers a layout refresh.
//
// So, we mark and unmark the `isResizing` flag to prevent triggering a circular
// call-stack chain from this call.
this.doResize();
} finally {
this.isResizing = false;
}
super.refreshLayout(pending);
}
protected doResize() {
if(this.vkbd && this.device.touchable) {
let targetOSKHeight = this.getDefaultKeyboardHeight();
this.setSize(this.getDefaultWidth(), targetOSKHeight + this.banner.height);
}
}
protected postKeyboardLoad() {
// Initializes the size of a touch keyboard.
this.doResize();
this._Visible = false; // I3363 (Build 301)
this._Box.onmouseover = this._VKbdMouseOver;
this._Box.onmouseout = this._VKbdMouseOut;
if(this.displayIfActive) {
this.present();
}
}
/**
* Function restorePosition
* Scope Public
* @param {boolean?} keepDefaultPosition If true, does not reset the default x,y set by `setRect`.
* If false or omitted, resets the default x,y as well.
* Description Move OSK back to default position, floating under active input element
*/
['restorePosition']: (keepDefaultPosition?: boolean) => void = function(this: AnchoredOSKView, keepDefaultPosition?: boolean) {
return;
}.bind(this);
/**
* Function _VKbdMouseOver
* Scope Private
* @param {Object} e event
* Description Activate the KMW UI on mouse over
*/
private _VKbdMouseOver = function(this: AnchoredOSKView, e) {
com.keyman.singleton.uiManager.setActivatingUI(true);
}.bind(this);
/**
* Function _VKbdMouseOut
* Scope Private
* @param {Object} e event
* Description Cancel activation of KMW UI on mouse out
*/
private _VKbdMouseOut = function(this: AnchoredOSKView, e) {
com.keyman.singleton.uiManager.setActivatingUI(false);
}.bind(this);
/**
* Get the wanted height of the OSK for touch devices (does not include banner height)
* @return {number} height in pixels
**/
getDefaultKeyboardHeight(): number {
let keymanweb = com.keyman.singleton;
let device = keymanweb.util.device;
// KeymanTouch - get OSK height from device
if(typeof(keymanweb['getOskHeight']) == 'function') {
return keymanweb['getOskHeight']();
}
/*
* We've noticed some fairly inconsistent behavior in the past when attempting to base
* this logic on window.innerWidth/Height, as there can be very unexpected behavior
* on mobile devices during and after rotation.
*
* Online forums (such as https://stackoverflow.com/a/54812656) seem to indicate that
* document.documentElement.clientWidth/Height seem to be the most stable analogues
* to a window's size in the situations where it matters for Keyman Engine for Web.
*
* That said, an important note: this gets the dimensions of the _document element_,
* not the screen or even the window.
*/
let baseWidth = document?.documentElement?.clientWidth;
let baseHeight = document?.documentElement?.clientHeight;
if(typeof baseWidth == 'undefined') {
/*
* Fallback logic. We _shouldn't_ need this, but it's best to have _something_
* for the sake of robustness.
*/
baseWidth = Math.min(screen.height, screen.width);
baseHeight = Math.max(screen.height, screen.width);
if(!keymanweb.util.portraitView()) {
let temp = baseWidth;
baseWidth = baseHeight;
baseHeight = temp;
}
}
var oskHeightLandscapeView=Math.floor(Math.min(baseHeight, baseWidth)/2),
height=oskHeightLandscapeView;
if(device.formFactor == 'phone') {
/**
* Assuming the first-pass detection of width and height work correctly, note
* that these calculations are based on the document's size, not the device's
* resolution. This _particularly_ matters for height.
*
* - Is the mobile-device browser showing a URL bar? That's not included.
* - The standard signal-strength, battery-strength, etc device status bar?
* Also not included.
*/
if(keymanweb.util.portraitView())
height=Math.floor(baseHeight/2.4);
else
height=Math.floor(baseHeight/1.6); //adjust for aspect ratio, increase slightly for iPhone 5
}
// Correct for viewport scaling (iOS - Android 4.2 does not want this, at least on Galaxy Tab 3))
if(device.OS == 'iOS') {
height=height/keymanweb.util.getViewportScale();
}
return height;
}
/**
* Get the wanted width of the OSK for touch devices
*
* @return {number} height in pixels
**/
getDefaultWidth(): number {
let keymanweb = com.keyman.singleton;
let device = keymanweb.util.device;
// KeymanTouch - get OSK height from device
if(typeof(keymanweb['getOskWidth']) == 'function') {
return keymanweb['getOskWidth']();
}
var width: number;
width = document?.documentElement?.clientWidth;
if(typeof width == 'undefined') {
if(device.OS == 'iOS') {
width = window.innerWidth;
} else if(device.OS == 'Android') {
width=screen.availWidth;
} else {
width=screen.width;
}
}
return width;
}
/**
* Allow the UI or page to set the position and size of the OSK
* and (optionally) override user repositioning or sizing
*
* @param {Object.<string,number>} p Array object with position and size of OSK container
**/
['setRect'](p: OSKRect) {
return;
}
/**
* Get position of OSK window
*
* @return {Object.<string,number>} Array object with OSK window position
**/
getPos(): OSKPos {
var Lkbd=this._Box, p={
left: this._Visible ? Lkbd.offsetLeft : this.x,
top: this._Visible ? Lkbd.offsetTop : this.y
};
return p;
}
/**
* Function setPos
* Scope Private
* @param {Object.<string,number>} p Array object with OSK left, top
* Description Set position of OSK window, but limit to screen, and ignore if a touch input device
*/
['setPos'](p: OSKPos) {
return; // I3363 (Build 301)
}
protected setDisplayPositioning() {
let Ls = this._Box.style;
// The following code will always be executed except for externally created OSK such as EuroLatin
if(this.vkbd) {
Ls.position='fixed';
Ls.left=Ls.bottom='0px';
Ls.border='none';
Ls.borderTop='1px solid gray';
}
}
/**
* Allow UI to respond to OSK being shown (passing position and properties)
*
* @param {Object=} p object with coordinates and userdefined flag
* @return {boolean}
*
*/
doShow(p) {
return com.keyman.singleton.util.callEvent('osk.show',p);
}
/**
* Allow UI to update respond to OSK being hidden
*
* @param {Object=} p object with coordinates and userdefined flag
* @return {boolean}
*
*/
doHide(p) {
return com.keyman.singleton.util.callEvent('osk.hide',p);
}
}
}

View file

@ -1,736 +0,0 @@
///<reference path="visualKeyboard.ts" />
///<reference path="uiTouchHandlerBase.ts" />
namespace com.keyman.osk {
// Base class for a banner above the keyboard in the OSK
export abstract class Banner {
private _height: number; // pixels
private div: HTMLDivElement;
public static DEFAULT_HEIGHT: number = 37; // pixels; embedded apps can modify
public static readonly BANNER_CLASS: string = 'kmw-banner-bar';
public static readonly BANNER_ID: string = 'kmw-banner-bar';
/**
* Function height
* Scope Public
* @returns {number} height in pixels
* Description Returns the height of the banner in pixels
*/
public get height(): number {
return this._height;
}
/**
* Function height
* Scope Public
* @param {number} height the height in pixels
* Description Sets the height of the banner in pixels. If a negative
* height is given, set height to 0 pixels.
* Also updates the banner styling.
*/
public set height(height: number) {
this._height = (height > 0) ? height : 0;
this.update();
}
/**
* Function update
* @return {boolean} true if the banner styling changed
* Description Update the height and display styling of the banner
*/
private update() : boolean {
let ds = this.div.style;
let currentHeightStyle = ds.height;
let currentDisplayStyle = ds.display;
if (this._height > 0) {
ds.height = this._height + 'px';
ds.display = 'block';
} else {
ds.height = '0px';
ds.display = 'none';
}
return (!(currentHeightStyle === ds.height) ||
!(currentDisplayStyle === ds.display));
}
public constructor(height?: number) {
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
let d = util._CreateElement('div');
d.id = Banner.BANNER_ID;
d.className = Banner.BANNER_CLASS;
this.div = d;
this.height = height;
this.update();
}
public appendStyleSheet() {
let keymanweb = com.keyman.singleton;
let util = keymanweb.util;
// TODO: add stylesheets
}
/**
* Function getDiv
* Scope Public
* @returns {HTMLElement} Base element of the banner
* Description Returns the HTMLElelemnt of the banner
*/
public getDiv(): HTMLElement {
return this.div;
}
/**
* Function activate
* Scope Public
* Description Adds any relevant event listeners needed by this banner type.
*/
public activate() {
// Default implementation - no listeners.
}
/**
* Function activate
* Scope Public
* Description Removes any relevant event listeners previously added by this banner.
*/
public deactivate() {
// Default implementation - no listeners.
}
}
/**
* Function BlankBanner
* Description A banner of height 0 that should not be shown
*/
export class BlankBanner extends Banner {
constructor() {
super(0);
}
}
/**
* Function ImageBanner
* @param {string} imagePath Path of image to display in the banner
* @param {number} height If provided, the height of the banner in pixels
* Description Display an image in the banner
*/
export class ImageBanner extends Banner {
private img: HTMLElement;
constructor(imagePath: string, height?: number) {
if (imagePath.length > 0) {
super();
if (height) {
this.height = height;
}
} else {
super(0);
}
if(imagePath.indexOf('base64') >=0) {
console.log("Loading img from base64 data");
} else {
console.log("Loading img with src '" + imagePath + "'");
}
this.img = document.createElement('img');
this.img.setAttribute('src', imagePath);
let ds = this.img.style;
ds.width = '100%';
ds.height = '100%';
this.getDiv().appendChild(this.img);
console.log("Image loaded.");
}
/**
* Function setImagePath
* Scope Public
* @param {string} imagePath Path of image to display in the banner
* Description Update the image in the banner
*/
public setImagePath(imagePath: string) {
if (this.img) {
this.img.setAttribute('src', imagePath);
}
}
}
export class BannerSuggestion {
div: HTMLDivElement;
private display: HTMLSpanElement;
private fontFamily?: string;
private _suggestion: Suggestion;
private index: number;
static readonly BASE_ID = 'kmw-suggestion-';
constructor(index: number) {
let keyman = com.keyman.singleton;
this.index = index;
this.constructRoot();
// Provides an empty, base SPAN for text display. We'll swap these out regularly;
// `Suggestion`s will have varying length and may need different styling.
let display = this.display = keyman.util._CreateElement('span');
this.div.appendChild(display);
}
private constructRoot() {
let keyman = com.keyman.singleton;
// Add OSK suggestion labels
let div = this.div = keyman.util._CreateElement('div'), ds=div.style;
div.className = "kmw-suggest-option";
div.id = BannerSuggestion.BASE_ID + this.index;
let kbdDetails = keyman.keyboardManager.activeStub;
if(kbdDetails) {
if (kbdDetails['KLC']) {
div.lang = kbdDetails['KLC'];
}
// Establish base font settings
let font = kbdDetails['KFont'];
if(font && font.family && font.family != '') {
ds.fontFamily = this.fontFamily = font.family;
}
}
// Ensures that a reasonable width % is set.
let usableWidth = 100 - SuggestionBanner.MARGIN * (SuggestionBanner.SUGGESTION_LIMIT - 1);
let widthpc = usableWidth / SuggestionBanner.SUGGESTION_LIMIT;
ds.width = widthpc + '%';
this.div['suggestion'] = this;
}
get suggestion(): Suggestion {
return this._suggestion;
}
/**
* Function update
* @param {string} id Element ID for the suggestion span
* @param {Suggestion} suggestion Suggestion from the lexical model
* Description Update the ID and text of the BannerSuggestionSpec
*/
public update(suggestion: Suggestion) {
this._suggestion = suggestion;
this.updateText();
}
private updateText() {
let display = this.generateSuggestionText();
this.div.replaceChild(display, this.display);
this.display = display;
}
/**
* Function apply
* @param target (Optional) The OutputTarget to which the `Suggestion` ought be applied.
* Description Applies the predictive `Suggestion` represented by this `BannerSuggestion`.
*/
public apply(target?: text.OutputTarget): Promise<Reversion> {
let keyman = com.keyman.singleton;
if(this.isEmpty()) {
return null;
}
if(!target) {
/* Assume it's the currently-active `OutputTarget`. We should probably invalidate
* everything if/when the active `OutputTarget` changes, though we haven't gotten that
* far in implementation yet.
*/
target = dom.Utils.getOutputTarget();
}
if(this._suggestion.tag == 'revert') {
keyman.core.languageProcessor.applyReversion(this._suggestion as Reversion, target);
return null;
} else {
return keyman.core.languageProcessor.applySuggestion(this.suggestion, target, () => keyman.core.keyboardProcessor.layerId);
}
}
public isEmpty(): boolean {
return !this._suggestion;
}
/**
* Function generateSuggestionText
* @return {HTMLSpanElement} Span element of the suggestion
* Description Produces a HTMLSpanElement with the key's actual text.
*/
//
public generateSuggestionText(): HTMLSpanElement {
let keyman = com.keyman.singleton;
let util = keyman.util;
let suggestion = this._suggestion;
var suggestionText: string;
var s=util._CreateElement('span');
s.className = 'kmw-suggestion-text';
if(suggestion == null) {
return s;
}
if(suggestion.displayAs == null || suggestion.displayAs == '') {
suggestionText = '\xa0'; // default: nbsp.
} else {
// Default the LTR ordering to match that of the active keyboard.
let activeKeyboard = keyman.core.activeKeyboard;
let rtl = activeKeyboard && activeKeyboard.isRTL;
let orderCode = rtl ? 0x202e /* RTL */ : 0x202d /* LTR */;
suggestionText = String.fromCharCode(orderCode) + suggestion.displayAs;
}
// TODO: Dynamic suggestion text resizing. (Refer to OSKKey.getTextWidth in visualKeyboard.ts.)
// Finalize the suggestion text
s.innerHTML = suggestionText;
return s;
}
}
/**
* Function SuggestionBanner
* Scope Public
* @param {number} height - If provided, the height of the banner in pixels
* Description Display lexical model suggestions in the banner
*/
export class SuggestionBanner extends Banner {
public static readonly SUGGESTION_LIMIT: number = 3;
public static readonly MARGIN = 1;
private options : BannerSuggestion[];
private hostDevice: utils.DeviceSpec;
private manager: SuggestionManager;
static readonly TOUCHED_CLASS: string = 'kmw-suggest-touched';
static readonly BANNER_CLASS: string = 'kmw-suggest-banner';
constructor(hostDevice: utils.DeviceSpec, height?: number) {
super(height || Banner.DEFAULT_HEIGHT);
this.hostDevice = hostDevice;
this.getDiv().className = this.getDiv().className + ' ' + SuggestionBanner.BANNER_CLASS;
this.options = new Array();
for (var i=0; i<SuggestionBanner.SUGGESTION_LIMIT; i++) {
let d = new BannerSuggestion(i);
this.options[i] = d;
}
/* LTR behavior: the default (index 0) suggestion should be at the left
* RTL behavior: the default (index 0) suggestion should be at the right
*
* The cleanest way to make it work - simply invert the order in which
* the elements are inserted for RTL. This allows the banner to be RTL
* for visuals/UI while still being internally LTR.
*/
let activeKeyboard = com.keyman.singleton.core.activeKeyboard;
let rtl = activeKeyboard && activeKeyboard.isRTL;
for (var i=0; i<SuggestionBanner.SUGGESTION_LIMIT; i++) {
let indexToInsert = rtl ? SuggestionBanner.SUGGESTION_LIMIT - i -1 : i;
this.getDiv().appendChild(this.options[indexToInsert].div);
if(i != SuggestionBanner.SUGGESTION_LIMIT) {
// Adds a 'separator' div element for UI purposes.
let separatorDiv = com.keyman.singleton.util._CreateElement('div');
separatorDiv.className = 'kmw-banner-separator';
let ds = separatorDiv.style;
ds.marginLeft = (SuggestionBanner.MARGIN / 2) + '%';
ds.marginRight = (SuggestionBanner.MARGIN / 2) + '%';
this.getDiv().appendChild(separatorDiv);
}
}
this.manager = new SuggestionManager(this.getDiv(), this.options);
this.setupInputHandling();
}
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 = TouchEventEngine.forPredictiveBanner(this, this.manager);
} else {
inputEngine = MouseEventEngine.forPredictiveBanner(this, this.manager);
}
inputEngine.registerEventHandlers();
}
activate() {
let keyman = com.keyman.singleton;
let manager = this.manager;
keyman.core.languageProcessor.addListener('invalidatesuggestions', manager.invalidateSuggestions);
keyman.core.languageProcessor.addListener('suggestionsready', manager.updateSuggestions);
keyman.core.languageProcessor.addListener('tryaccept', manager.tryAccept);
keyman.core.languageProcessor.addListener('tryrevert', manager.tryRevert);
keyman.core.languageProcessor.addListener('suggestionapplied', this.suggestionApplied);
}
/**
* Handler for post-processing once a suggestion has been applied: calls
* into the active keyboard's `begin postKeystroke` entry point.
* @param outputTarget
* @returns true
*/
suggestionApplied: (outputTarget: text.OutputTarget) => boolean = function(this: SuggestionBanner, outputTarget: text.OutputTarget) {
const keyman = com.keyman.singleton;
// Tell the keyboard that the current layer has not changed
keyman.core.keyboardProcessor.newLayerStore.set('');
keyman.core.keyboardProcessor.oldLayerStore.set('');
// Call the keyboard's entry point.
keyman.core.keyboardProcessor.processPostKeystroke(this.hostDevice, outputTarget)
// If we have a RuleBehavior as a result, run it on the target. This should
// only change system store and variable store values.
?.finalize(keyman.core.keyboardProcessor, outputTarget, true);
return true;
}.bind(this);
postConfigure() {
let keyman = com.keyman.singleton;
// Trigger a null-based initial prediction to kick things off.
keyman.core.languageProcessor.predictFromTarget(dom.Utils.getOutputTarget(), keyman.core.keyboardProcessor.layerId);
}
deactivate() {
let keyman = com.keyman.singleton;
let manager = this.manager;
keyman.core.languageProcessor.removeListener('invalidatesuggestions', manager.invalidateSuggestions);
keyman.core.languageProcessor.removeListener('suggestionsready', manager.updateSuggestions);
keyman.core.languageProcessor.removeListener('tryaccept', manager.tryAccept);
keyman.core.languageProcessor.removeListener('tryrevert', manager.tryRevert);
keyman.core.languageProcessor.removeListener('suggestionapplied', this.suggestionApplied);
}
}
export class SuggestionManager extends UITouchHandlerBase<HTMLDivElement> {
private selected: BannerSuggestion;
platformHold: (suggestion: BannerSuggestion, isCustom: boolean) => void;
//#region Touch handling implementation
findTargetFrom(e: HTMLElement): HTMLDivElement {
let keyman = com.keyman.singleton;
let util = keyman.util;
try {
if(e) {
if(util.hasClass(e,'kmw-suggest-option')) {
return e as HTMLDivElement;
}
if(e.parentNode && util.hasClass(<HTMLElement> e.parentNode,'kmw-suggest-option')) {
return e.parentNode 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 classes = t.className;
let cs = ' ' + SuggestionBanner.TOUCHED_CLASS;
if(t.id.indexOf(BannerSuggestion.BASE_ID) == -1) {
console.warn("Cannot find BannerSuggestion object for element to highlight!");
} else {
// Never highlight an empty suggestion button.
let suggestion = this.selected = t['suggestion'] as BannerSuggestion;
if(suggestion.isEmpty()) {
on = false;
this.selected = null;
}
}
if(on && classes.indexOf(cs) < 0) {
t.className=classes+cs;
} else {
t.className=classes.replace(cs,'');
}
}
protected select(t: HTMLDivElement): void {
this.doAccept(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;
if(this.platformHold) {
// Implemented separately for native + embedded mode branches.
// Embedded mode should pass any info needed to show a submenu IMMEDIATELY.
this.platformHold(suggestionObj, isCustom); // No implementation yet for native.
}
}
protected clearHolds(): void {
// Temp, pending implementation of suggestion longpress submenus
// - nothing to clear without them -
// only really used in native-KMW
}
protected hasModalPopup(): boolean {
// Utilized by the mobile apps; allows them to 'take over' touch handling,
// blocking it within KMW when the apps are already managing an ongoing touch-hold.
let keyman = com.keyman.singleton;
return keyman['osk'].vkbd.subkeyGesture && keyman.isEmbedded;
}
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
private options: BannerSuggestion[];
private initNewContext: boolean = true;
private currentSuggestions: Suggestion[] = [];
private keepSuggestion: Keep;
private revertSuggestion: Reversion;
private recentAccept: boolean = false;
private revertAcceptancePromise: Promise<Reversion>;
private swallowPrediction: boolean = false;
private doRevert: boolean = false;
private recentRevert: boolean = false;
constructor(div: HTMLElement, options: BannerSuggestion[]) {
// TODO: Determine appropriate CSS styling names, etc.
super(div, Banner.BANNER_CLASS, SuggestionBanner.TOUCHED_CLASS);
this.options = options;
}
private doAccept(suggestion: BannerSuggestion) {
let _this = this;
// Selecting a suggestion or a reversion should both clear selection
// and clear the reversion-displaying state of the banner.
this.selected = null;
this.doRevert = false;
this.revertAcceptancePromise = suggestion.apply();
if(!this.revertAcceptancePromise) {
// We get here either if suggestion acceptance fails or if it was a reversion.
if(suggestion.suggestion && suggestion.suggestion.tag == 'revert') {
// Reversion state management
this.recentAccept = false;
this.recentRevert = true;
this.doUpdate();
}
return;
}
this.revertAcceptancePromise.then(function(suggestion) {
// Always null-check!
if(suggestion) {
_this.revertSuggestion = suggestion;
}
});
this.recentAccept = true;
this.recentRevert = false;
this.swallowPrediction = true;
this.doUpdate();
}
private showRevert() {
// Construct a 'revert suggestion' to facilitate a reversion UI component.
this.doRevert = true;
this.doUpdate();
}
/**
* Receives messages from the keyboard that the 'accept' keystroke has been entered.
* Should return 'false' if the current state allows accepting a suggestion and act accordingly.
* Otherwise, return true.
*/
tryAccept: (source: string) => boolean = function(this: SuggestionManager, source: string, returnObj: {shouldSwallow: boolean}) {
let keyman = com.keyman.singleton;
if(!this.recentAccept && this.selected) {
this.doAccept(this.selected);
returnObj.shouldSwallow = true;
} else if(this.recentAccept && source == 'space') {
this.recentAccept = false;
// If the model doesn't insert wordbreaks, don't swallow the space. If it does,
// we consider that insertion to be the results of the first post-accept space.
returnObj.shouldSwallow = !!keyman.core.languageProcessor.wordbreaksAfterSuggestions;
} else {
returnObj.shouldSwallow = false;
}
}.bind(this);
/**
* Receives messages from the keyboard that the 'revert' keystroke has been entered.
* Should return 'false' if the current state allows reverting a recently-applied suggestion and act accordingly.
* Otherwise, return true.
*/
tryRevert: () => boolean = function(this: SuggestionManager, returnObj: {shouldSwallow: boolean}) {
// Has the revert keystroke (BKSP) already been sent once since the last accept?
if(this.doRevert) {
// If so, clear the 'revert' option and start doing normal predictions again.
this.doRevert = false;
this.recentAccept = false;
// Otherwise, did we just accept something before the revert signal was received?
} else if(this.recentAccept) {
this.showRevert();
this.swallowPrediction = true;
}
// We don't yet actually do key-based reversions.
returnObj.shouldSwallow = false;
return;
}.bind(this);
/**
* Function invalidateSuggestions
* Scope Public
* Description Clears the suggestions in the suggestion banner
*/
public invalidateSuggestions: (this: SuggestionManager, source: text.prediction.InvalidateSourceEnum) => boolean =
function(this: SuggestionManager, source: string) {
// By default, we assume that the context is the same until we notice otherwise.
this.initNewContext = false;
if(!this.swallowPrediction || source == 'context') {
this.recentAccept = false;
this.doRevert = false;
this.recentRevert = false;
if(source == 'context') {
this.swallowPrediction = false;
this.initNewContext = true;
}
}
this.options.forEach((option: BannerSuggestion) => {
option.update(null);
});
}.bind(this);
public activateKeep(): boolean {
return !this.recentAccept && !this.recentRevert && !this.initNewContext;
}
private doUpdate() {
let suggestions = [];
// Insert 'current text' if/when valid as the leading option.
// Since we don't yet do auto-corrections, we only show 'keep' whenever it's
// a valid word (according to the model).
if(this.activateKeep() && this.keepSuggestion && this.keepSuggestion.matchesModel) {
suggestions.push(this.keepSuggestion);
} else if(this.doRevert) {
suggestions.push(this.revertSuggestion);
}
suggestions = suggestions.concat(this.currentSuggestions);
this.options.forEach((option: BannerSuggestion, i: number) => {
if(i < suggestions.length) {
option.update(suggestions[i]);
} else {
option.update(null);
}
});
}
/**
* Function updateSuggestions
* Scope Public
* @param {Suggestion[]} suggestions Array of suggestions from the lexical model.
* Description Update the displayed suggestions in the SuggestionBanner
*/
public updateSuggestions: (this: SuggestionManager, prediction: text.prediction.ReadySuggestions) => boolean =
function(this: SuggestionManager, prediction: text.prediction.ReadySuggestions) {
let suggestions = prediction.suggestions;
this.currentSuggestions = suggestions;
// Do we have a keep suggestion? If so, remove it from the list so that we can control its display position
// and prevent it from being hidden after reversion operations.
this.keepSuggestion = null;
for(let s of suggestions) {
if(s.tag == 'keep') {
this.keepSuggestion = s as Keep;
}
}
if(this.keepSuggestion) {
this.currentSuggestions.splice(this.currentSuggestions.indexOf(this.keepSuggestion), 1);
}
// If we've gotten an update request like this, it's almost always user-triggered and means the context has shifted.
if(!this.swallowPrediction) {
this.recentAccept = false;
this.doRevert = false;
this.recentRevert = false;
} else { // This prediction was triggered by a recent 'accept.' Now that it's fulfilled, we clear the flag.
this.swallowPrediction = false;
}
// The rest is the same, whether from input or from "self-updating" after a reversion to provide new suggestions.
this.doUpdate();
}.bind(this);
}
}

View file

@ -1,292 +0,0 @@
/// <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() {};
}
}

View file

@ -1,146 +0,0 @@
namespace com.keyman.osk.browser {
export class KeyTip implements com.keyman.osk.KeyTip {
public readonly element: HTMLDivElement;
public key: KeyElement;
public state: boolean = false;
// -----
// | | <-- tip
// | x | <-- label
// |_ _|
// | |
// | | <-- cap
// |___|
private readonly cap: HTMLDivElement;
private readonly tip: HTMLDivElement;
private readonly label: HTMLSpanElement;
private readonly constrain: boolean;
/**
*
* @param constrain keep the keytip within the bounds of the overall OSK.
* Will probably be handled via function in a later pass.
*/
constructor(constrain: boolean) {
let tipElement = this.element=document.createElement('div');
tipElement.className='kmw-keytip';
tipElement.id = 'kmw-keytip';
// The following style is critical, so do not rely on external CSS
tipElement.style.pointerEvents='none';
tipElement.style.display='none';
tipElement.appendChild(this.tip = document.createElement('div'));
tipElement.appendChild(this.cap = document.createElement('div'));
this.tip.appendChild(this.label = document.createElement('span'));
this.tip.className = 'kmw-keytip-tip';
this.cap.className = 'kmw-keytip-cap';
this.label.className = 'kmw-keytip-label';
this.constrain = constrain;
}
show(key: KeyElement, on: boolean, vkbd: VisualKeyboard) {
let keyman = com.keyman.singleton;
let util = keyman.util;
// Create and display the preview
// If !key.offsetParent, the OSK is probably hidden. Either way, it's a half-
// decent null-guard check.
if(on && key.offsetParent) {
// The key element is positioned relative to its key-square, which is,
// in turn, relative to its row. Rows take 100% width, so this is sufficient.
//
let rowElement = (key.key as OSKBaseKey).row.element;
// May need adjustment for borders if ever enabled for the desktop form-factor target.
let rkey = key.getClientRects()[0], rrow = rowElement.getClientRects()[0];
let xLeft = rkey.left - rrow.left,
xWidth = rkey.width,
xHeight = rkey.height,
kc = key.key.label,
previewFontScale = 1.8;
let kts = this.element.style;
// Roughly matches how the subkey positioning is set.
const _Box = vkbd.element.parentNode as HTMLDivElement;
const _BoxRect = _Box.getBoundingClientRect();
const keyRect = key.getBoundingClientRect();
let y = (keyRect.bottom - _BoxRect.top + 1);
let ySubPixelPadding = y - Math.floor(y);
// Canvas dimensions must be set explicitly to prevent clipping
// This gives us exactly the same number of pixels on left and right
let canvasWidth = xWidth + Math.ceil(xWidth * 0.3) * 2;
let canvasHeight = Math.ceil(2.3 * xHeight) + (ySubPixelPadding); //
kts.top = 'auto';
kts.bottom = Math.floor(keyman.osk.computedHeight - y) + 'px';
kts.textAlign = 'center';
kts.overflow = 'visible';
kts.fontFamily = util.getStyleValue(kc,'font-family');
kts.width = canvasWidth+'px';
kts.height = canvasHeight+'px';
var px=util.getStyleInt(kc, 'font-size');
if(px != 0) {
let popupFS = previewFontScale * px;
let scaleStyle = {
fontFamily: kts.fontFamily,
fontSize: popupFS + 'px',
height: 1.6 * xHeight + 'px' // as opposed to the canvas height of 2.3 * xHeight.
};
kts.fontSize = key.key.getIdealFontSize(vkbd, key.key.keyText, scaleStyle, true);
}
this.label.textContent = kc.textContent;
// Adjust shape if at edges
var xOverflow = (canvasWidth - xWidth) / 2;
if(xLeft < xOverflow) {
this.cap.style.left = '1px';
xLeft += xOverflow - 1;
} else if(xLeft > window.innerWidth - xWidth - xOverflow) {
this.cap.style.left = (canvasWidth - xWidth - 1) + 'px';
xLeft -= xOverflow - 1;
} else {
this.cap.style.left = xOverflow + 'px';
}
kts.left=(xLeft - xOverflow) + 'px';
let cs = getComputedStyle(this.element);
let oskHeight = keyman.osk.computedHeight;
let bottomY = parseFloat(cs.bottom);
let tipHeight = parseFloat(cs.height);
let halfHeight = Math.ceil(canvasHeight / 2);
this.cap.style.width = xWidth + 'px';
this.tip.style.height = halfHeight + 'px';
this.cap.style.top = (halfHeight - 3) + 'px';
this.cap.style.height = (keyRect.bottom - _BoxRect.top - Math.floor(y - canvasHeight) - (halfHeight)) + 'px'; //(halfHeight + 3 + ySubPixelPadding) + 'px';
if(this.constrain && tipHeight + bottomY > oskHeight) {
const delta = tipHeight + bottomY - oskHeight;
kts.height = (canvasHeight-delta) + 'px';
const hx = Math.max(0, (canvasHeight-delta)-(canvasHeight/2) + 2);
this.cap.style.height = hx + 'px';
}
kts.display = 'block';
} else { // Hide the key preview
this.element.style.display = 'none';
}
// Save the key preview state
this.key = key;
this.state = on;
}
}
}

View file

@ -1,68 +0,0 @@
/// <reference path="../oskKey.ts" />
namespace com.keyman.osk.browser {
export class OSKSubKey extends OSKKey {
constructor(spec: OSKKeySpec, layer: string) {
if(typeof(layer) != 'string' || layer == '') {
throw "The 'layer' parameter for subkey construction must be properly defined.";
}
super(spec, layer);
}
getId(): string {
// Create (temporarily) unique ID by prefixing 'popup-' to actual key ID
return 'popup-'+this.layer+'-'+this.spec['id'];
}
construct(osk: VisualKeyboard, baseKey: KeyElement, topMargin: boolean): HTMLDivElement {
let spec = this.spec;
let kDiv=document.createElement('div');
let tKey = osk.getDefaultKeyObject();
let ks=kDiv.style;
for(var tp in tKey) {
if(typeof spec[tp] != 'string') {
spec[tp]=tKey[tp];
}
}
kDiv.className='kmw-key-square-ex';
if(topMargin) {
ks.marginTop='5px';
}
if(typeof spec['width'] != 'undefined') {
ks.width=(spec['width']*baseKey.offsetWidth/100)+'px';
} else {
ks.width=baseKey.offsetWidth+'px';
}
ks.height=baseKey.offsetHeight+'px';
let btnEle=document.createElement('div');
let btn = this.btn = link(btnEle, new KeyData(this, spec['id']));
this.setButtonClass();
btn.id = this.getId();
// Must set button size (in px) dynamically, not from CSS
let bs=btn.style;
bs.height=ks.height;
bs.lineHeight=baseKey.style.lineHeight;
bs.width=ks.width;
// Must set position explicitly, at least for Android
bs.position='absolute';
btn.appendChild(this.label = this.generateKeyText(osk));
kDiv.appendChild(btn);
return this.square = kDiv;
}
public allowsKeyTip(): boolean {
return false;
}
}
}

View file

@ -1,73 +0,0 @@
/// <reference path="subkeyPopup.ts" />
/// <reference path="../pendingGesture.interface.ts" />
namespace com.keyman.osk.browser {
/**
* (Conceptually) represents a finite-state-machine that determines
* whether or not a series of touch events corresponds to a longpress
* touch input. The `resolve` method may be used to trigger the
* subkey menu early, as with the upward quick-display shortcut.
*
* This is the default implementation of longpress behavior for KMW.
* Alterate implementations are modeled through the `embedded`
* namespace's equivalent, which is designed to facilitate custom
* modeling for such gestures.
*
* Once the conditions to recognize a longpress gesture have been
* fulfilled, this class's `promise` will resolve with a `SubkeyPopup`
* matching the gesture's 'base' key, which itself provides a
* `promise` field that will resolve to a `KeyEvent` once the touch
* sequence is completed.
*/
export class PendingLongpress implements PendingGesture {
public readonly baseKey: KeyElement;
public readonly promise: Promise<SubkeyPopup>;
public readonly subkeyUI: SubkeyPopup;
private readonly vkbd: VisualKeyboard;
private resolver: (subkeyPopup: SubkeyPopup) => void;
private timerId: number;
private popupDelay: number = 500;
constructor(vkbd: VisualKeyboard, baseKey: KeyElement) {
this.vkbd = vkbd;
this.baseKey = baseKey;
let _this = this;
this.promise = new Promise<SubkeyPopup>(function(resolve, reject) {
_this.resolver = resolve;
// After the timeout, it's no longer deferred; it's being fulfilled.
// Even if the actual subkey itself is still async.
_this.timerId = window.setTimeout(_this.resolve.bind(_this), _this.popupDelay);
});
}
public cancel() {
if(this.timerId) {
window.clearTimeout(this.timerId);
this.timerId = null;
}
if(this.resolver) {
this.resolver(null);
this.resolver = null;
}
}
public resolve() {
// User has flicked up to get to the longpress, before
// the timeout has expired. We need to cancel the timeout.
// See #5950
if(this.timerId) {
window.clearTimeout(this.timerId);
this.timerId = null;
}
if(this.resolver) {
this.resolver(new SubkeyPopup(this.vkbd, this.baseKey));
}
}
}
}

View file

@ -1,128 +0,0 @@
/// <reference path="../pendingGesture.interface.ts" />
/// <reference path="../visualKeyboard.ts" />
namespace com.keyman.osk {
export enum PendingMultiTapState { Waiting, Realized, Cancelled };
/**
* Implements the multi-tap gesture, which is a series of taps on a single key
* (based on key id substring in the case of the shift key), within a
* specified timeout period.
*/
export class PendingMultiTap implements PendingGesture {
public readonly vkbd: VisualKeyboard;
public readonly baseKey: KeyElement;
public readonly count: number;
private timerId;
private _touches = 1; // we start the multitap with a single touch
private _state: PendingMultiTapState = PendingMultiTapState.Waiting;
private _timeout: Promise<void>;
private cancelDelayFactor = 125; // 125msec * count
private _destinationLayerId;
public get timeout() {
return this._timeout;
}
public get realized() {
return this._state == PendingMultiTapState.Realized;
}
public get cancelled() {
return this._state == PendingMultiTapState.Cancelled;
}
/**
* Construct a record of a potential multitap gesture
* @param vkbd
* @param baseKey key which is being tapped
* @param count number of taps required to finalize this gesture
*/
constructor(vkbd: VisualKeyboard, baseKey: KeyElement, count: number) {
this.vkbd = vkbd;
this.count = count;
this.baseKey = baseKey;
this._destinationLayerId = 'caps';
let multitap = baseKey?.key?.spec?.['multitap'];
if(multitap?.length && multitap[0]?.['nextlayer']) {
this._destinationLayerId = multitap[0]['nextlayer'];
}
const _this = this;
this._timeout = new Promise<void>(function(resolve) {
// If multiple taps do not occur within the timeout window,
// then we will abandon the gesture
_this.timerId = window.setTimeout(() => {
_this.cancel();
resolve();
}, _this.cancelDelayFactor * _this.count);
});
}
public static isValidTarget(vkbd: VisualKeyboard, baseKey: KeyElement) {
// Could use String.includes, but Chrome for Android must be version 41+.
// We support down to version 37.
return (
baseKey['keyId'].indexOf('K_SHIFT') >= 0 &&
vkbd.layerGroup.layers['caps'] &&
!baseKey['subKeys'] &&
vkbd.touchCount == 1
);
}
private cleanup(): void {
if(this.timerId) {
window.clearTimeout(this.timerId);
}
this.timerId = null;
}
/**
* Cancel a pending multitap gesture
*/
public cancel(): void {
this._state = PendingMultiTapState.Cancelled;
this.cleanup();
}
/**
* Increments the touch counter for the gesture, and
* if the touch count is reached, realize the gesture
* @returns new state of the gesture
*/
public incrementTouch(newKey: KeyElement): PendingMultiTapState {
// TODO: support for any key
if(this._state == PendingMultiTapState.Waiting) {
if(!newKey?.['keyId']?.includes('K_SHIFT')) {
this.cancel();
}
else if(++this._touches == this.count) {
this.realize();
}
}
return this._state;
}
/**
* Realize the gesture. In Keyman 15, this supports only
* the Caps double-tap gesture on the Shift key.
*/
public realize(): void {
if(this._state != PendingMultiTapState.Waiting) {
return;
}
this._state = PendingMultiTapState.Realized;
this.cleanup();
// In Keyman 15, only the K_SHIFT key supports multi-tap, so we can hack
// in the switch to the caps layer.
//
// TODO: generalize this with double-tap key properties in touch layout
// description.
let e = text.KeyEvent.constructNullKeyEvent(this.vkbd.device);
e.kNextLayer = this._destinationLayerId;
e.Lstates = text.Codes.stateBitmasks.CAPS;
e.LmodifierChange = true;
PreProcessor.raiseKeyEvent(e);
}
}
}

View file

@ -1,295 +0,0 @@
/// <reference path="oskSubKey.ts" />
/// <reference path="../realizedGesture.interface.ts" />
namespace com.keyman.osk.browser {
/**
* Represents a 'realized' longpress gesture's default implementation
* within KeymanWeb. Once a touch sequence has been confirmed to
* correspond to a longpress gesture, implementations of this class
* provide the following:
* * The UI needed to present a subkey menu
* * The state management needed to present feedback about the
* currently-selected subkey to the user
* * A `Promise` that will resolve to the user's selected subkey
* once the longpress operation is complete.
*
* As selection of the subkey occurs after the subkey popup is
* displayed, selection of the subkey is inherently asynchronous.
* The `Promise` may also resolve to `null` if the user indicates
* the desire to cancel subkey selection.
*/
export class SubkeyPopup implements RealizedGesture {
public readonly element: HTMLDivElement;
public readonly shim: HTMLDivElement;
private vkbd: VisualKeyboard;
private currentSelection: KeyElement;
private callout: HTMLDivElement;
public readonly baseKey: KeyElement;
public readonly promise: Promise<text.KeyEvent>;
// Resolves the promise that generated this SubkeyPopup.
private resolver: (keyEvent: text.KeyEvent) => void;
constructor(vkbd: VisualKeyboard, e: KeyElement) {
let keyman = com.keyman.singleton;
let _this = this;
this.promise = new Promise<text.KeyEvent>(function(resolve) {
_this.resolver = resolve;
})
this.vkbd = vkbd;
this.baseKey = e;
// If the user doesn't move their finger and releases, we'll output the base key
// by default.
this.currentSelection = e;
e.key.highlight(true);
// A tag we directly set on a key element during its construction.
let subKeySpec: OSKKeySpec[] = e['subKeys'];
// The holder is position:fixed, but the keys do not need to be, as no scrolling
// is possible while the array is visible. So it is simplest to let the keys have
// position:static and display:inline-block
var subKeys = this.element = document.createElement('div');
var i;
subKeys.id='kmw-popup-keys';
// #3718: No longer prepend base key to popup array
// Must set position dynamically, not in CSS
var ss=subKeys.style;
// Set key font according to layout, or defaulting to OSK font
// (copied, not inherited, since OSK is not a parent of popup keys)
ss.fontFamily=vkbd.fontFamily;
// Copy the font size from the parent key, allowing for style inheritance
ss.fontSize=keyman.util.getStyleValue(e,'font-size');
ss.visibility='hidden';
var nKeys=subKeySpec.length,nRows,nCols;
nRows=Math.min(Math.ceil(nKeys/9),2);
nCols=Math.ceil(nKeys/nRows);
ss.width=(nCols*e.offsetWidth+nCols*5)+'px';
// Add nested button elements for each sub-key
for(i=0; i<nKeys; i++) {
var needsTopMargin = false;
let nRow=Math.floor(i/nCols);
if(nRows > 1 && nRow > 0) {
needsTopMargin = true;
}
let layer = e['key'].layer;
if(typeof(layer) != 'string' || layer == '') {
// Use the currently-active layer.
layer = vkbd.layerId;
}
let keyGenerator = new OSKSubKey(subKeySpec[i], layer);
let kDiv = keyGenerator.construct(vkbd, <KeyElement> e, needsTopMargin);
subKeys.appendChild(kDiv);
}
// And add a filter to fade main keyboard
this.shim = document.createElement('div');
this.shim.id = 'kmw-popup-shim';
// Highlight the duplicated base key or ideal subkey (if a phone)
if(vkbd.device.formFactor == utils.FormFactor.Phone) {
this.selectDefaultSubkey(vkbd, e, subKeys /* == this.element */);
}
}
finalize(input: InputEventCoordinate) {
if(this.resolver) {
let keyEvent: text.KeyEvent = null;
if(this.currentSelection) {
keyEvent = this.vkbd.initKeyEvent(this.currentSelection, input);
this.currentSelection.key.highlight(false);
}
this.resolver(keyEvent);
}
this.resolver = null;
}
reposition(vkbd: VisualKeyboard) {
let keyman = com.keyman.singleton;
let subKeys = this.element;
let e = this.baseKey;
// And correct its position with respect to that element
const _Box = vkbd.element.offsetParent as HTMLDivElement;
let rowElement = (e.key as OSKBaseKey).row.element;
let ss=subKeys.style;
var x = e.offsetLeft + (<HTMLElement>e.offsetParent).offsetLeft + 0.5*(e.offsetWidth-subKeys.offsetWidth);
var xMax = keyman.osk.computedWidth - subKeys.offsetWidth;
if(x > xMax) {
x=xMax;
}
if(x < 0) {
x=0;
}
ss.left=x+'px';
let _BoxRect = _Box.getBoundingClientRect();
let rowElementRect = rowElement.getBoundingClientRect();
ss.top = (rowElementRect.top - _BoxRect.top - subKeys.offsetHeight - 3) + 'px';
// Make the popup keys visible
ss.visibility='visible';
// For now, should only be true (in production) when keyman.isEmbedded == true.
let constrainPopup = keyman.isEmbedded;
let cs = getComputedStyle(subKeys);
let topY = parseFloat(cs.top);
// Adjust the vertical position of the popup to keep it within the
// bounds of the keyboard rectangle, when on iPhone (system keyboard)
const topOffset = 0; // Set this when testing constrainPopup, e.g. to -80px
let delta = 0;
if(topY < topOffset && constrainPopup) {
delta = topOffset - topY;
ss.top = topOffset + 'px';
}
// Add the callout
if(vkbd.device.formFactor == utils.FormFactor.Phone && vkbd.device.OS == utils.OperatingSystem.iOS) {
this.callout = this.addCallout(e, delta);
}
}
/**
* Add a callout for popup keys (if KeymanWeb on a phone device)
*
* @param {Object} key HTML key element
* @return {Object} callout object
*/
addCallout(key: KeyElement, delta?: number): HTMLDivElement {
let keyman = com.keyman.singleton;
delta = delta || 0;
let calloutHeight = key.offsetHeight - delta + 6;
if(calloutHeight > 0) {
var cc = document.createElement('div'), ccs = cc.style;
cc.id = 'kmw-popup-callout';
keyman.osk._Box.appendChild(cc);
// Create the callout
let keyRect = key.getBoundingClientRect();
let _BoxRect = keyman.osk._Box.getBoundingClientRect();
// Set position and style
// We're going to adjust the top of the box to ensure it stays
// pixel aligned, otherwise we can get antialiasing artifacts
// that look ugly
let top = Math.floor(keyRect.top - _BoxRect.top - 9 + delta);
ccs.top = top + 'px';
ccs.left = (keyRect.left - _BoxRect.left) + 'px';
ccs.width = keyRect.width + 'px';
ccs.height = (keyRect.bottom - _BoxRect.top - top - 1) + 'px'; //(height - 1) + 'px';
// Return callout element, to allow removal later
return cc;
} else {
return null;
}
}
selectDefaultSubkey(vkbd: VisualKeyboard, baseKey: KeyElement, popupBase: HTMLElement) {
var bk: KeyElement;
let subkeys = baseKey['subKeys'];
for(let i=0; i < subkeys.length; i++) {
let skSpec = subkeys[i];
let skElement = <KeyElement> popupBase.childNodes[i].firstChild;
// Preference order:
// #1: if a default subkey has been specified, select it. (pending, for 15.0+)
// #2: if no default subkey is specified, default to a subkey with the same
// key ID and layer / modifier spec.
//if(skSpec.isDefault) { TODO for 15.0
// bk = skElement;
// break;
//} else
if(!baseKey.key || !baseKey.key.spec) {
continue;
}
if(skSpec.elementID == baseKey.key.spec.elementID) {
bk = skElement;
break; // Best possible match has been found. (Disable 'break' once above block is implemented.)
}
}
if(bk) {
vkbd.keyPending = bk;
// Subkeys never get key previews, so we can directly highlight the subkey.
bk.key.highlight(true);
}
}
isVisible(): boolean {
return this.element.style.visibility == 'visible';
}
clear() {
// Discard the reference to the Promise's resolve method, allowing
// GC to clean it up. The corresponding Promise's contract allows
// passive cancellation.
this.resolver = null;
// Remove the displayed subkey array, if any
if(this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
if(this.shim.parentNode) {
this.shim.parentNode.removeChild(this.shim);
}
if(this.callout && this.callout.parentNode) {
this.callout.parentNode.removeChild(this.callout);
}
}
updateTouch(input: InputEventCoordinate) {
this.currentSelection = null;
this.baseKey.key.highlight(false);
for(let i=0; i < this.baseKey['subKeys'].length; i++) {
try {
let sk = this.element.childNodes[i].firstChild as KeyElement;
let onKey = sk.key.isUnderTouch(input);
if(onKey) {
this.currentSelection = sk;
}
sk.key.highlight(onKey);
} catch(ex) {
if(ex.message) {
console.error("Unexpected error when attempting to update selected subkey:" + ex.message);
} else {
console.error("Unexpected error (and error type) when attempting to update selected subkey.");
}
}
}
// Use the popup duplicate of the base key if a phone with a visible popup array
if(!this.currentSelection && this.baseKey.key.isUnderTouch(input)) {
this.baseKey.key.highlight(true);
this.currentSelection = this.baseKey;
}
}
}
}

View file

@ -1,23 +0,0 @@
/// <reference path="keyboardView.interface.ts" />
namespace com.keyman.osk {
export class EmptyView implements KeyboardView {
readonly element: HTMLDivElement;
constructor() {
let Ldiv = this.element = document.createElement('div');
Ldiv.style.userSelect = 'none';
Ldiv.className='kmw-osk-none';
}
// No operations needed; this is a stand-in for the desktop OSK when no keyboard is active.
public postInsert() { }
public updateState() { }
public refreshLayout() { }
public get layoutHeight(): ParsedLengthStyle {
return ParsedLengthStyle.inPixels(0);
}
}
}

View file

@ -1,573 +0,0 @@
// Includes KMW-added property declaration extensions for HTML elements.
/// <reference path="../kmwexthtml.ts" />
// Includes the touch-mode language picker UI.
/// <reference path="languageMenu.ts" />
/// <reference path="lengthStyle.ts" />
// Defines desktop-centric OSK positioning + sizing behavior
/// <reference path="layouts/targetedFloatLayout.ts" />
/// <reference path="oskView.ts" />
/***
KeymanWeb 10.0
Copyright 2017 SIL International
***/
namespace com.keyman.osk {
type OSKRect = {'left'?: number, 'top'?: number, 'width'?: number, 'height'?: number,
'nosize'?: boolean, 'nomove'?: boolean};
type OSKPos = {'left'?: number, 'top'?: number};
export class FloatingOSKView extends OSKView {
readonly desktopLayout: layouts.TargetedFloatLayout;
// OSK positioning fields
userPositioned: boolean = false;
specifiedPosition: boolean = false;
x: number;
y: number;
noDrag: boolean = false;
dfltX: string;
dfltY: string;
// Key code definition aliases for legacy keyboards (They expect window['keyman']['osk'].___)
modifierCodes = text.Codes.modifierCodes;
modifierBitmasks = text.Codes.modifierBitmasks;
stateBitmasks = text.Codes.stateBitmasks;
keyCodes = text.Codes.keyCodes;
public constructor(modeledDevice: utils.DeviceSpec) {
super(modeledDevice);
document.body.appendChild(this._Box);
this.loadCookie();
// Add header element to OSK only for desktop browsers
const layout = this.desktopLayout = new layouts.TargetedFloatLayout();
this.headerView = layout.titleBar;
layout.titleBar.attachHandlers(this);
}
/**
* Function _Unload
* Scope Private
* Description Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)
*/
_Unload() {
this.keyboardView = null;
this.bannerView = null;
this._Box = null;
}
protected setBoxStyling() {
const s = this._Box.style;
s.zIndex = '9999';
s.display = 'none';
s.width = 'auto';
s.position = 'absolute';
}
protected postKeyboardLoad() {
this._Visible = false; // I3363 (Build 301)
this._Box.onmouseover = this._VKbdMouseOver;
this._Box.onmouseout = this._VKbdMouseOut;
// Add header element to OSK only for desktop browsers
const layout = this.desktopLayout;
layout.attachToView(this);
if(this.activeKeyboard) {
this.desktopLayout.titleBar.setTitleFromKeyboard(this.activeKeyboard);
}
if(this.vkbd) {
this.footerView = layout.resizeBar;
this._Box.appendChild(this.footerView.element);
} else {
if(this.footerView) {
this._Box.removeChild(this.footerView.element);
}
this.footerView = null;
}
this.loadCookie();
this.setNeedsLayout();
if(this.displayIfActive) {
this.present();
}
}
/**
* Function restorePosition
* Scope Public
* @param {boolean?} keepDefaultPosition If true, does not reset the default x,y set by `setRect`.
* If false or omitted, resets the default x,y as well.
* Description Move OSK back to default position, floating under active input element
*/
['restorePosition']: (keepDefaultPosition?: boolean) => void = function(this: FloatingOSKView, keepDefaultPosition?: boolean) {
let isVisible = this._Visible;
if(isVisible && this.activeTarget instanceof dom.targets.OutputTarget) {
this.activeTarget?.focus(); // I2036 - OSK does not unpin to correct location
}
this.loadCookie();
this.userPositioned=false;
if(!keepDefaultPosition) {
delete this.dfltX;
delete this.dfltY;
}
this.saveCookie();
if(isVisible) {
this.present();
}
this.doResizeMove(); //allow the UI to respond to OSK movements
this.desktopLayout.titleBar.showPin(false);
}.bind(this);
/**
* Function enabled
* Scope Public
* @return {boolean|number} True if KMW OSK enabled
* Description Test if KMW OSK is enabled
*/
['isEnabled'](): boolean {
return this.displayIfActive;
}
/**
* Function isVisible
* Scope Public
* @return {boolean|number} True if KMW OSK visible
* Description Test if KMW OSK is actually visible
* Note that this will usually return false after any UI event that results in (temporary) loss of input focus
*/
['isVisible'](): boolean {
return this._Visible;
}
/**
* Function _VKbdMouseOver
* Scope Private
* @param {Object} e event
* Description Activate the KMW UI on mouse over
*/
private _VKbdMouseOver = function(this: AnchoredOSKView, e) {
com.keyman.singleton.uiManager.setActivatingUI(true);
}.bind(this);
/**
* Function _VKbdMouseOut
* Scope Private
* @param {Object} e event
* Description Cancel activation of KMW UI on mouse out
*/
private _VKbdMouseOut = function(this: AnchoredOSKView, e) {
com.keyman.singleton.uiManager.setActivatingUI(false);
}.bind(this);
/**
* Save size, position, font size and visibility of OSK
*/
saveCookie() {
let util = com.keyman.singleton.util;
var c = util.loadCookie('KeymanWeb_OnScreenKeyboard');
var p = this.getPos();
c['visible'] = this.displayIfActive ? 1 : 0;
c['userSet'] = this.userPositioned ? 1 : 0;
c['left'] = p.left;
c['top'] = p.top;
c['_version'] = utils.Version.CURRENT.toString();
if(this.vkbd) {
c['width'] = this.width.val;
c['height'] = this.height.val;
}
util.saveCookie('KeymanWeb_OnScreenKeyboard',c);
}
/**
* Restore size, position, font size and visibility of desktop OSK
*
* @return {boolean}
*/
loadCookie(): void {
let util = com.keyman.singleton.util;
var c = util.loadCookie('KeymanWeb_OnScreenKeyboard');
this.displayIfActive = util.toNumber(c['visible'], 1) == 1;
this.userPositioned = util.toNumber(c['userSet'], 0) == 1;
this.x = util.toNumber(c['left'],-1);
this.y = util.toNumber(c['top'],-1);
let cookieVersionString = c['_version'];
// Restore OSK size - font size now fixed in relation to OSK height, unless overridden (in em) by keyboard
let dfltWidth=0.3*screen.width;
let dfltHeight=0.15*screen.height;
//if(util.toNumber(c['width'],0) == 0) dfltWidth=0.5*screen.width;
let newWidth = parseInt(c['width'], 10);
let newHeight = parseInt(c['height'], 10);
let isNewCookie = isNaN(newHeight);
newWidth = isNaN(newWidth) ? dfltWidth : newWidth;
newHeight = isNaN(newHeight) ? dfltHeight : newHeight;
// Limit the OSK dimensions to reasonable values
if(newWidth < 0.2*screen.width) {
newWidth = 0.2*screen.width;
}
if(newHeight < 0.1*screen.height) {
newHeight = 0.1*screen.height;
}
if(newWidth > 0.9*screen.width) {
newWidth=0.9*screen.width;
}
if(newHeight > 0.5*screen.height) {
newHeight=0.5*screen.height;
}
// if(!cookieVersionString) - this component was not tracked until 15.0.
// Before that point, the OSK's title bar and resize bar heights were not included
// in the OSK's cookie-persisted height.
if(isNewCookie || !cookieVersionString) {
// Adds some space to account for the OSK's header and footer, should they exist.
if(this.headerView && this.headerView.layoutHeight.absolute) {
newHeight += this.headerView.layoutHeight.val;
}
if(this.footerView && this.footerView.layoutHeight.absolute) {
newHeight += this.footerView.layoutHeight.val;
}
}
this.setSize(newWidth, newHeight);
// and OSK position if user located
if(this.x == -1 || this.y == -1 || (!this._Box)) {
this.userPositioned = false;
}
if(this.x < window.pageXOffset-0.8*newWidth) {
this.x=window.pageXOffset-0.8*newWidth;
}
if(this.y < 0) {
this.x=-1;
this.y=-1;
this.userPositioned=false;
}
if(this.userPositioned && this._Box) {
this.setPos({'left': this.x, 'top': this.y});
}
}
/**
* Get the wanted height of the OSK for touch devices (does not include banner height)
* @return {number} height in pixels
**/
getDefaultKeyboardHeight(): number {
let keymanweb = com.keyman.singleton;
let device = keymanweb.util.device;
// KeymanTouch - get OSK height from device
if(typeof(keymanweb['getOskHeight']) == 'function') {
return keymanweb['getOskHeight']();
}
var oskHeightLandscapeView=Math.floor(Math.min(screen.availHeight,screen.availWidth)/2),
height=oskHeightLandscapeView;
if(device.formFactor == 'phone') {
var sx=Math.min(screen.height,screen.width),
sy=Math.max(screen.height,screen.width);
if(keymanweb.util.portraitView())
height=Math.floor(Math.max(screen.availHeight,screen.availWidth)/3);
else
height=height*(sy/sx)/1.6; //adjust for aspect ratio, increase slightly for iPhone 5
}
// Correct for viewport scaling (iOS - Android 4.2 does not want this, at least on Galaxy Tab 3))
if(device.OS == 'iOS') {
height=height/keymanweb.util.getViewportScale();
}
return height;
}
/**
* Get the wanted width of the OSK for touch devices
*
* @return {number} height in pixels
**/
getDefaultWidth(): number {
let keymanweb = com.keyman.singleton;
let device = keymanweb.util.device;
// KeymanTouch - get OSK height from device
if(typeof(keymanweb['getOskWidth']) == 'function') {
return keymanweb['getOskWidth']();
}
var width: number;
if(device.OS == 'iOS') {
// iOS does not interchange these values when the orientation changes!
//width = util.portraitView() ? screen.width : screen.height;
width = window.innerWidth;
} else if(device.OS == 'Android') {
try {
width=document.documentElement.clientWidth;
} catch(ex) {
width=screen.availWidth;
}
} else {
width=screen.width;
}
return width;
}
/**
* Allow UI to update OSK position and properties
*
* @param {Object=} p object with coordinates and userdefined flag
*
*/
doResizeMove(p?) {
return com.keyman.singleton.util.callEvent('osk.resizemove',p);
}
/**
* Allow the UI or page to set the position and size of the OSK
* and (optionally) override user repositioning or sizing
*
* @param {Object.<string,number>} p Array object with position and size of OSK container
**/
['setRect'](p: OSKRect) {
let util = com.keyman.singleton.util;
if(this._Box == null || util.device.formFactor != 'desktop') {
return;
}
var b = this._Box, bs = b.style;
if('left' in p) {
this.x = p['left'] - dom.Utils.getAbsoluteX(b) + b.offsetLeft;
bs.left= this.x + 'px';
this.dfltX=bs.left;
}
if('top' in p) {
this.y = p['top'] - dom.Utils.getAbsoluteY(b) + b.offsetTop;
bs.top = this.y + 'px';
this.dfltY=bs.top;
}
//Do not allow user resizing for non-standard keyboards (e.g. EuroLatin)
if(this.vkbd != null) {
var d=this.vkbd.kbdDiv, ds=d.style;
// Set width, but limit to reasonable value
if('width' in p) {
var w=(p['width']-(b.offsetWidth-d.offsetWidth));
if(w < 0.2*screen.width) {
w=0.2*screen.width;
}
if(w > 0.9*screen.width) {
w=0.9*screen.width;
}
ds.width=w+'px';
// Use of the `computed` variant is here temporary.
// Shouldn't use `setSize` for this in the long-term.
this.setSize(w, this.computedHeight, true);
}
// Set height, but limit to reasonable value
// This sets the default font size for the OSK in px, but that
// can be modified at the key text level by setting
// the font size in em in the kmw-key-text class
if('height' in p) {
var h=(p['height']-(b.offsetHeight-d.offsetHeight));
if(h < 0.1*screen.height) {
h=0.1*screen.height;
}
if(h > 0.5*screen.height) {
h=0.5*screen.height;
}
ds.height=h+'px'; ds.fontSize=(h/8)+'px';
// Use of the `computed` variant is here temporary.
// Shouldn't use `setSize` for this in the long-term.
this.setSize(this.computedWidth, h, true);
}
// Fix or release user resizing
if('nosize' in p) {
this.desktopLayout.resizingEnabled = !p['nosize'];
}
}
// Fix or release user dragging
if('nomove' in p) {
this.noDrag=p['nomove'];
this.desktopLayout.movementEnabled = !this.noDrag;
}
// Save the user-defined OSK size
this.saveCookie();
}
/**
* Get position of OSK window
*
* @return {Object.<string,number>} Array object with OSK window position
**/
getPos(): OSKPos {
var Lkbd=this._Box, p={
left: this._Visible ? Lkbd.offsetLeft : this.x,
top: this._Visible ? Lkbd.offsetTop : this.y
};
return p;
}
/**
* Function setPos
* Scope Private
* @param {Object.<string,number>} p Array object with OSK left, top
* Description Set position of OSK window, but limit to screen, and ignore if a touch input device
*/
['setPos'](p: OSKPos) {
if(typeof(this._Box) == 'undefined') {
return; // I3363 (Build 301)
}
if(this.userPositioned) {
var Px=p['left'], Py=p['top'];
if(typeof(Px) != 'undefined') {
if(Px < -0.8*this._Box.offsetWidth) {
Px = -0.8*this._Box.offsetWidth;
}
if(this.userPositioned) {
this._Box.style.left=Px+'px';
this.x = Px;
}
}
// May not be needed - vertical positioning is handled differently and defaults to input field if off screen
if(typeof(Py) != 'undefined') {
if(Py < 0) {
Py = 0;
}
if(this.userPositioned) {
this._Box.style.top=Py+'px';
this.y = Py;
}
}
}
if(this.desktopLayout) {
this.desktopLayout.titleBar.showPin(this.userPositioned);
}
}
public setDisplayPositioning() {
var Ls = this._Box.style;
Ls.position='absolute'; Ls.display='block'; //Ls.visibility='visible';
Ls.left='0px';
if(this.specifiedPosition || this.userPositioned) {
Ls.left = this.x+'px';
Ls.top = this.y+'px';
} else {
let el: HTMLElement = null;
if(this.activeTarget instanceof dom.targets.OutputTarget) {
el = this.activeTarget?.getElement();
}
if(this.dfltX) {
Ls.left=this.dfltX;
} else if(typeof el != 'undefined' && el != null) {
Ls.left=dom.Utils.getAbsoluteX(el) + 'px';
}
if(this.dfltY) {
Ls.top=this.dfltY;
} else if(typeof el != 'undefined' && el != null) {
Ls.top=(dom.Utils.getAbsoluteY(el) + el.offsetHeight)+'px';
}
}
// Unset the flag, keeping 'specified position' specific to single
// presentAtPosition calls.
this.specifiedPosition = false;
}
/**
* Display KMW OSK at specified position (returns nothing)
*
* @param {number=} Px x-coordinate for OSK rectangle
* @param {number=} Py y-coordinate for OSK rectangle
*/
presentAtPosition(Px?: number, Py?: number) {
if(!this.mayShow()) {
return;
}
this.specifiedPosition = Px >= 0 || Py >= 0; //probably never happens, legacy support only
if(this.specifiedPosition) {
this.x = Px;
this.y = Py;
}
// Combines the two paths with set positioning.
this.specifiedPosition = this.specifiedPosition || this.userPositioned;
this.present();
}
present() {
if(!this.mayShow()) {
return;
}
this.desktopLayout.titleBar.showPin(this.userPositioned);
super.present();
// Allow desktop UI to execute code when showing the OSK
var Lpos={};
Lpos['x']=this._Box.offsetLeft;
Lpos['y']=this._Box.offsetTop;
Lpos['userLocated']=this.userPositioned;
this.doShow(Lpos);
}
public startHide(hiddenByUser: boolean) {
super.startHide(hiddenByUser);
if(hiddenByUser) {
this.saveCookie(); // Save current OSK state, size and position (desktop only)
}
}
['show'](bShow: boolean) {
super['show'](bShow);
this.saveCookie();
}
/**
* Function userPositioned
* Scope Public
* @return {(boolean|number)} true if user located
* Description Test if OSK window has been repositioned by user
*/
['userLocated']() {
return this.userPositioned;
}
}
}

View file

@ -1,10 +0,0 @@
namespace com.keyman.osk {
export interface GlobeHint {
text: string;
state: boolean;
element?: HTMLDivElement;
show(key: KeyElement, onDismiss?: () => void);
hide(key: KeyElement);
}
}

View file

@ -1,39 +0,0 @@
/// <reference path="keyboardView.interface.ts" />
namespace com.keyman.osk {
export class HelpPageView implements KeyboardView {
private readonly kbd: keyboards.Keyboard;
public readonly element: HTMLDivElement;
private static readonly ID = 'kmw-osk-help-page';
constructor(keyboard: keyboards.Keyboard) {
this.kbd = keyboard;
var Ldiv = this.element = document.createElement('div');
Ldiv.style.userSelect = "none";
Ldiv.className = 'kmw-osk-static';
Ldiv.id = HelpPageView.ID;
Ldiv.innerHTML = keyboard.helpText;
}
public postInsert() {
if(!this.element.parentElement || !document.getElementById(HelpPageView.ID)) {
throw new Error("The HelpPage root element has not yet been inserted into the DOM.");
}
if(this.kbd.hasScript) {
// .parentElement: ensure this matches the _Box element from OSKManager / OSKView
// Not a hard requirement for any known keyboards, but is asserted by legacy docs.
this.kbd.embedScript(this.element.parentElement);
}
}
public updateState() { }
public refreshLayout() { }
public get layoutHeight(): ParsedLengthStyle {
return ParsedLengthStyle.inPercent(100);
}
}
}

View file

@ -1,186 +0,0 @@
// Includes KMW-added property declaration extensions for HTML elements.
/// <reference path="../kmwexthtml.ts" />
// Includes the touch-mode language picker UI.
/// <reference path="languageMenu.ts" />
/// <reference path="lengthStyle.ts" />
// Defines desktop-centric OSK positioning + sizing behavior
/// <reference path="layouts/targetedFloatLayout.ts" />
/// <reference path="oskView.ts" />
/*
* Keyman is copyright (c) SIL International. MIT License.
*/
namespace com.keyman.osk {
type OSKPos = {'left'?: number, 'top'?: number};
/**
* Defines a version of the OSK that produces an element designed for site-controlled
* insertion into the DOM. Rather than "floating" over the page, this version is inlined
* as part of the host page's layout.
*/
export class InlinedOSKView extends OSKView {
// Key code definition aliases for legacy keyboards (They expect window['keyman']['osk'].___)
modifierCodes = text.Codes.modifierCodes;
modifierBitmasks = text.Codes.modifierBitmasks;
stateBitmasks = text.Codes.stateBitmasks;
keyCodes = text.Codes.keyCodes;
public constructor(modeledDevice: utils.DeviceSpec, hostDevice?: utils.DeviceSpec) {
super(modeledDevice, hostDevice);
this.activationMode = ActivationMode.manual;
}
public get element(): HTMLDivElement {
return this._Box;
}
/**
* Clears OSK variables prior to exit (JMD 1.9.1 - relocation of local variables 3/9/10)
*
* This should probably be merged or incorporated into the `shutdown` method at some point.
*/
_Unload() {
this.keyboardView = null;
this.bannerView = null;
this._Box = null;
}
protected setBoxStyling() {
const s = this._Box.style;
s.display = 'none';
// Positioned with no relative offset from its default position.
// This allows _Box to still serve as an offsetParent for keytip & subkey menu positioning.
s.position = 'relative';
}
protected postKeyboardLoad() {
this._Visible = false; // I3363 (Build 301)
this._Box.onmouseover = this._VKbdMouseOver;
this._Box.onmouseout = this._VKbdMouseOut;
if(this.displayIfActive) {
this.present();
}
}
/**
* Moves the OSK back to default position, floating under active input element
*
* Is a long-published API intended solely for use with the FloatingOSKView use pattern.
* @param keepDefaultPosition If true, does not reset the default x,y set by `setRect`.
* If false or omitted, resets the default x,y as well.
*/
['restorePosition']: (keepDefaultPosition?: boolean) => void = function(this: AnchoredOSKView, keepDefaultPosition?: boolean) {
return;
}.bind(this);
/**
* Activates the KMW UI on mouse over, allowing DOMManager to preserve the
* active element's (conceptual) focus during OSK interactions.
*/
private _VKbdMouseOver = function(this: AnchoredOSKView, e) {
com.keyman.singleton.uiManager.setActivatingUI(true);
}.bind(this);
/**
* Cancels activation of the KMW UI on mouse out, which is used to disable
* DOMManager's focus-preservation mode.
*
* @see _VKbdMouseOver
*/
private _VKbdMouseOut = function(this: AnchoredOSKView, e) {
com.keyman.singleton.uiManager.setActivatingUI(false);
}.bind(this);
/**
* Get the default height for the OSK
* @return height in pixels
**/
getDefaultKeyboardHeight(): number {
if(this.keyboardView instanceof VisualKeyboard) {
return this.keyboardView.height;
} else {
// Should probably refine, but it's a decent stopgap.
return this.computedHeight;
}
}
/**
* Get the default width for the OSK
* @return width in pixels
**/
getDefaultWidth(): number {
return this.computedWidth;
}
/**
* Allow the UI or page to set the position and size of the OSK
* and (optionally) override user repositioning or sizing
*
* Designed solely for use with the FloatingOSKView use pattern, but is a
* long-standing API endpoint that needs preservation.
*
* @param p Array object with position and size of OSK container
**/
['setRect'](p: OSKRect) {
return;
}
/**
* Get position of OSK window
*
* @return Array object with OSK window position
**/
getPos(): OSKPos {
var Lkbd=this._Box, p={
left: this._Visible ? Lkbd.offsetLeft : undefined,
top: this._Visible ? Lkbd.offsetTop : undefined
};
return p;
}
/**
* Set position of OSK window, but limited to the screen.
*
* Designed solely for use with the FloatingOSKView use pattern, but is a
* long-standing API endpoint that needs preservation.
* @param p Array object with OSK left, top
*/
['setPos'](p: OSKPos) {
return; // I3363 (Build 301)
}
protected setDisplayPositioning() {
// no-op; an inlined OSK cannot control its own positioning.
}
/**
* Allow UI to respond to OSK being shown (passing position and properties)
*
* @param p object with coordinates and userdefined flag
* @return
*
*/
doShow(p) {
return com.keyman.singleton.util.callEvent('osk.show',p);
}
/**
* Allows UI modules to update state when the OSK is being hidden
*
* @param p object with coordinates and userdefined flag
* @return
*/
doHide(p) {
return com.keyman.singleton.util.callEvent('osk.hide',p);
}
protected allowsDeviceChange(newSpec: com.keyman.utils.DeviceSpec): boolean {
return true;
}
}
}

View file

@ -1,74 +0,0 @@
namespace com.keyman.osk {
/**
* 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 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;
}
}
}

View file

@ -1,59 +0,0 @@
/// <reference path="inputEventCoordinate.ts" />
namespace com.keyman.osk {
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 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);
}
}
}
}

View file

@ -1,22 +0,0 @@
namespace com.keyman.osk {
/**
* An abstract representation for visualizations of the active keyboard within an
* OSKManager / OSKView. Most keyboards will default to use of a VisualKeyboard,
* though some will use HelpPage for certain form factors.
*/
export interface KeyboardView extends OSKViewComponent {
readonly element: HTMLDivElement;
/**
* Evaluates code that must be run _after_ the KeyboardView has been inserted into
* the DOM hierarchy.
*/
postInsert(): void;
/**
* Code that updates the state of the KeyboardView whenever the OSK itself needs to be
* refreshed or updated with new state information.
*/
updateState(): void;
}
}

View file

@ -1,9 +0,0 @@
namespace com.keyman.osk {
export interface KeyTip {
key: KeyElement;
state: boolean;
element?: HTMLDivElement;
show(key: KeyElement, on: boolean, vkbd: VisualKeyboard);
}
}

View file

@ -1,171 +0,0 @@
/// <reference path="../inputEventCoordinate.ts" />
namespace com.keyman.osk.layouts {
type MouseHandler = (this: GlobalEventHandlers, ev: MouseEvent) => any;
/**
* Used to store the page's original mouse handlers and properties
* when temporarily overridden by OSK moving or resizing handlers due
* to user interaction.
*/
class MouseStartSnapshot {
private readonly _VPreviousMouseMove: MouseHandler;
private readonly _VPreviousMouseUp: MouseHandler;
private readonly _VPreviousCursor: string;
private readonly _VPreviousMouseButton: number;
constructor(e: MouseEvent) {
this._VPreviousMouseMove = document.onmousemove;
this._VPreviousMouseUp = document.onmouseup;
this._VPreviousCursor = document.body.style.cursor;
this._VPreviousMouseButton = (typeof(e.which)=='undefined' ? e.button : e.which);
}
restore() {
document.onmousemove = this._VPreviousMouseMove;
document.onmouseup = this._VPreviousMouseUp;
if(document.body.style.cursor) {
document.body.style.cursor = this._VPreviousCursor;
}
}
matchesCausingClick(e: MouseEvent): boolean {
return this._VPreviousMouseButton == (typeof(e.which)=='undefined' ? e.button : e.which);
}
}
export abstract class MouseDragOperation {
private _enabled: boolean;
private _startCoord: InputEventCoordinate;
private _mouseStartSnapshot: MouseStartSnapshot;
private startHandler: (e: MouseEvent) => void;
private cursorType: string;
public constructor(cursorType?: string) {
this.startHandler = this._VMoveMouseDown.bind(this);
this.cursorType = cursorType;
}
/**
* Denotes whether or not this object should handle incoming events.
*/
public get enabled(): boolean {
return this._enabled;
}
public set enabled(flag: boolean) {
this._enabled = flag;
}
/**
* Denotes whether or not this object is currently handling an ongoing drag event.
*/
public get isActive(): boolean {
return !!this._mouseStartSnapshot;
}
public get mouseDownHandler(): (e: MouseEvent) => void {
return this.startHandler;
}
/**
* Function _VMoveMouseDown
* Scope Private
* @param {Object} e event
* Description Process mouse down on OSK
*/
private _VMoveMouseDown(e: MouseEvent) {
if(!e) {
return true;
}
if(!this._enabled) {
return true;
}
if(!this._mouseStartSnapshot) { // I1472 - Dragging off edge of browser window causes muckup
this._mouseStartSnapshot = new MouseStartSnapshot(e);
}
this._startCoord = InputEventCoordinate.fromEvent(e);
document.onmousemove = this._VMoveMouseMove.bind(this);
document.onmouseup = this._VMoveMouseUp.bind(this);
if(document.body.style.cursor) {
document.body.style.cursor = this.cursorType;
}
e.preventDefault();
e.cancelBubble = true;
this.onDragStart();
return false;
}
protected abstract onDragStart();
/**
* Process mouse drag on OSK
*
* @param {Object} e event
*/
private _VMoveMouseMove(e: MouseEvent) {
if(!e) {
return true;
}
if(!this.enabled) {
return true;
}
e.preventDefault();
e.cancelBubble = true;
if(!this._mouseStartSnapshot.matchesCausingClick(e)) { // I1472 - Dragging off edge of browser window causes muckup
return this._VMoveMouseUp(e);
} else {
const coord = InputEventCoordinate.fromEvent(e);
const deltaX = coord.x - this._startCoord.x;
const deltaY = coord.y - this._startCoord.y;
this.onDragMove(deltaX, deltaY);
return false;
}
}
/**
*
* @param deltaX The total horizontal distance moved, in pixels, since the start of the drag
* @param deltaY The total vertical distance moved, in pixels, since the start of the drag
*/
protected abstract onDragMove(deltaX: number, deltaY: number);
/**
* Function _VMoveMouseUp
* Scope Private
* @param {Object} e event
* Description Process mouse up during movement of KMW OSK UI
*/
private _VMoveMouseUp(e: MouseEvent) {
if(!e) {
return true;
}
this._mouseStartSnapshot.restore();
this._mouseStartSnapshot = null;
e.preventDefault();
e.cancelBubble = true;
this.onDragRelease();
return false;
}
protected abstract onDragRelease();
}
}

View file

@ -1,89 +0,0 @@
/// <reference path="../oskViewComponent.ts" />
namespace com.keyman.osk.layouts {
export class ResizeBar implements OSKViewComponent {
private _element: HTMLDivElement;
private _resizeHandle: HTMLDivElement;
private static readonly DISPLAY_HEIGHT = ParsedLengthStyle.inPixels(16); // As set in kmwosk.css
private mouseCancellingHandler: (ev: MouseEvent) => boolean = function(ev: MouseEvent) {
ev.preventDefault();
ev.cancelBubble = true;
return false;
};
public constructor(dragHandler?: MouseDragOperation) {
this._element = this.buildResizeBar();
if(dragHandler) {
this._resizeHandle.onmousedown = dragHandler.mouseDownHandler;
}
}
public get layoutHeight(): ParsedLengthStyle {
return ResizeBar.DISPLAY_HEIGHT;
}
public get element(): HTMLDivElement {
return this._element;
}
public get handle(): HTMLDivElement {
return this._resizeHandle;
}
public allowResizing(flag: boolean) {
this._resizeHandle.style.display = flag ? 'block' : 'none';
}
private markUnselectable(e: HTMLElement) {
e.style.MozUserSelect="none";
e.style.KhtmlUserSelect="none";
e.style.UserSelect="none";
e.style.WebkitUserSelect="none";
}
/**
* Create a bottom bar with a resizing icon for the desktop OSK
*/
buildResizeBar(): HTMLDivElement {
let util = com.keyman.singleton.util;
let osk = com.keyman.singleton.osk;
var bar = document.createElement('div');
this.markUnselectable(bar);
bar.className='kmw-footer';
bar.onmousedown = this.mouseCancellingHandler;
// Add caption
var Ltitle=document.createElement('div');
this.markUnselectable(Ltitle);
Ltitle.className='kmw-footer-caption';
Ltitle.innerHTML='<a href="https://keyman.com/developer/keymanweb/">KeymanWeb</a>';
Ltitle.id='keymanweb-osk-footer-caption';
// Display build number on shift+double click
util.attachDOMEvent(Ltitle,'dblclick', function(e) {
if(e && e.shiftKey) {
osk.showBuild();
}
return false;
}.bind(this),false);
bar.appendChild(Ltitle);
var Limg = document.createElement('div');
this.markUnselectable(Limg);
Limg.className='kmw-footer-resize';
bar.appendChild(Limg);
this._resizeHandle=Limg;
return bar;
}
public refreshLayout() {
// The title bar is adaptable as it is and needs no adjustments.
}
}
}

View file

@ -1,212 +0,0 @@
/// <reference path="resizeBar.ts" />
/// <reference path="titleBar.ts" />
/// <reference path="mouseDragOperation.ts" />
namespace com.keyman.osk.layouts {
export class TargetedFloatLayout {
titleBar: layouts.TitleBar;
resizeBar: layouts.ResizeBar;
private oskView: FloatingOSKView;
// Encapsulations of the drag behaviors for OSK movement & resizing
private _moveHandler: MouseDragOperation;
private _resizeHandler: MouseDragOperation;
public constructor() {
this.titleBar = new layouts.TitleBar(this.titleDragHandler);
this.resizeBar = new layouts.ResizeBar(this.resizeDragHandler);
}
public get movementEnabled(): boolean {
return this.titleDragHandler.enabled;
}
public set movementEnabled(flag: boolean) {
this.titleDragHandler.enabled = flag;
this.titleBar.showPin(flag && this.oskView.userPositioned);
}
public get resizingEnabled(): boolean {
return this.resizeDragHandler.enabled;
}
public set resizingEnabled(flag: boolean) {
this.resizeDragHandler.enabled = flag;
this.resizeBar.allowResizing(flag);
}
public get isBeingMoved(): boolean {
return this.titleDragHandler.isActive;
}
public get isBeingResized(): boolean {
return this.resizeDragHandler.isActive;
}
attachToView(view: FloatingOSKView) {
this.oskView = view;
this.titleBar.attachHandlers(view);
this.titleDragHandler.enabled = !view.noDrag;
this.resizeDragHandler.enabled = true; // by default.
}
private get titleDragHandler(): MouseDragOperation {
const layout = this;
if(this._moveHandler) {
return this._moveHandler;
}
this._moveHandler = new class extends MouseDragOperation {
startX: number;
startY: number;
constructor() {
super('move'); // The type of cursor to use while 'active'.
}
onDragStart() {
if(!layout.oskView) {
return;
}
this.startX = layout.oskView._Box.offsetLeft;
this.startY = layout.oskView._Box.offsetTop;
let keymanweb = com.keyman.singleton;
if(keymanweb.isCJK()) {
layout.titleBar.setPinCJKOffset();
}
keymanweb.uiManager.justActivated = true;
}
// Note: _this.oskView may not be initialized yet.
onDragMove(cumulativeX: number, cumulativeY: number) {
if(!layout.oskView) {
return;
}
layout.titleBar.showPin(true);
layout.oskView.userPositioned = true;
layout.oskView._Box.style.left = (this.startX + cumulativeX) + 'px';
layout.oskView._Box.style.top = (this.startY + cumulativeY) + 'px';
var r=layout.oskView.getRect();
layout.oskView.setSize(r.width, r.height, true);
layout.oskView.x = r.left;
layout.oskView.y = r.top;
}
onDragRelease() {
if(!layout.oskView) {
return;
}
let keymanweb = com.keyman.singleton;
keymanweb.domManager.focusLastActiveElement();
keymanweb.uiManager.justActivated = false;
keymanweb.uiManager.setActivatingUI(false);
if(layout.oskView.vkbd) {
layout.oskView.vkbd.currentKey=null;
}
layout.oskView.userPositioned = true;
layout.oskView.doResizeMove();
layout.oskView.saveCookie();
}
}
return this._moveHandler;
}
private get resizeDragHandler(): MouseDragOperation {
const layout = this;
if(this._resizeHandler) {
return this._resizeHandler;
}
this._resizeHandler = new class extends MouseDragOperation {
startWidth: number;
startHeight: number;
constructor() {
super('se-resize'); // The type of cursor to use while 'active'.
}
onDragStart() {
if(!layout.oskView) {
return;
}
this.startWidth = layout.oskView.computedWidth;
this.startHeight = layout.oskView.computedHeight;
let keymanweb = com.keyman.singleton;
keymanweb.uiManager.justActivated = true;
}
// Note: _this.oskView may not be initialized yet.
onDragMove(cumulativeX: number, cumulativeY: number) {
if(!layout.oskView) {
return;
}
let newWidth = this.startWidth + cumulativeX;
let newHeight = this.startHeight + cumulativeY;
// Set the smallest and largest OSK size
if(newWidth < 0.2*screen.width) {
newWidth = 0.2*screen.width;
}
if(newHeight < 0.1*screen.height) {
newHeight = 0.1*screen.height;
}
if(newWidth > 0.9*screen.width) {
newWidth = 0.9*screen.width;
}
if(newHeight > 0.5*screen.height) {
newHeight = 0.5*screen.height;
}
// Explicitly set OSK width, height, and font size - cannot safely rely on scaling from font
layout.oskView.setSize(newWidth, newHeight, true);
}
onDragRelease() {
if(!layout.oskView) {
return;
}
let keymanweb = com.keyman.singleton;
keymanweb.domManager.focusLastActiveElement();
keymanweb.uiManager.justActivated = false;
keymanweb.uiManager.setActivatingUI(false);
if(layout.oskView.vkbd) {
layout.oskView.vkbd.currentKey=null;
}
if(layout.oskView.vkbd) {
this.startWidth = layout.oskView.computedWidth;
this.startHeight = layout.oskView.computedHeight;
}
layout.oskView.refreshLayout(); // Finalize the resize.
layout.oskView.doResizeMove();
layout.oskView.saveCookie();
}
}
return this._resizeHandler;
}
}
}

View file

@ -1,205 +0,0 @@
/// <reference path="mouseDragOperation.ts" />
/// <reference path="../oskViewComponent.ts" />
namespace com.keyman.osk.layouts {
export class TitleBar implements OSKViewComponent {
private _element: HTMLDivElement;
private _unpinButton: HTMLDivElement;
private _closeButton: HTMLDivElement;
private _helpButton: HTMLDivElement;
private _configButton: HTMLDivElement;
private _caption: HTMLSpanElement;
private _helpEnabled: boolean;
private _configEnabled: boolean;
public get helpEnabled(): boolean {
return this._helpEnabled;
}
public set helpEnabled(val) {
this._helpEnabled = val;
this._helpButton.style.display = val ? 'inline' : 'none';
}
public get configEnabled(): boolean {
return this._configEnabled;
}
public set configEnabled(val) {
this._configEnabled = val;
this._configButton.style.display = val ? 'inline' : 'none';
}
private static readonly DISPLAY_HEIGHT = ParsedLengthStyle.inPixels(20); // As set in kmwosk.css
public constructor(dragHandler?: MouseDragOperation) {
this._element = this.buildTitleBar();
this.helpEnabled = false;
this.configEnabled = false;
if(dragHandler) {
this.element.onmousedown = dragHandler.mouseDownHandler;
}
}
public get layoutHeight(): ParsedLengthStyle {
return TitleBar.DISPLAY_HEIGHT;
}
private mouseCancellingHandler: (ev: MouseEvent) => boolean = function(ev: MouseEvent) {
ev.preventDefault();
ev.cancelBubble = true;
return false;
};
public get element(): HTMLDivElement {
return this._element;
}
public setPinCJKOffset() {
this._unpinButton.style.left = '15px';
}
public showPin(visible: boolean) {
this._unpinButton.style.display = visible ? 'block' : 'none';
}
public setTitle(str: string) {
this._caption.innerHTML = str;
}
public setTitleFromKeyboard(keyboard: keyboards.Keyboard) {
let title = "<span style='font-weight:bold'>" + keyboard?.name + '</span>'; // I1972 // I2186
this._caption.innerHTML = title;
}
public attachHandlers(osk: OSKView) {
let util = com.keyman.singleton.util;
this._helpButton.onclick = function() {
var p={};
util.callEvent('osk.helpclick',p);
if(window.event) {
window.event.returnValue=false;
}
return false;
}
this._configButton.onclick = function() {
var p={};
util.callEvent('osk.configclick',p);
if(window.event) {
window.event.returnValue=false;
}
return false;
}
this._closeButton.onclick = function () {
osk.startHide(true);
return false;
};
if(osk instanceof FloatingOSKView) {
const _osk = osk as FloatingOSKView;
this._unpinButton.onclick = function () {
_osk.restorePosition(true);
return false;
}
}
}
/**
* Create a control bar with title and buttons for the desktop OSK
*/
buildTitleBar(): HTMLDivElement {
let bar = document.createElement('div');
this.markUnselectable(bar);
bar.id='keymanweb_title_bar';
bar.className='kmw-title-bar';
var Ltitle = this._caption = document.createElement('span');
this.markUnselectable(Ltitle);
Ltitle.className='kmw-title-bar-caption';
Ltitle.style.color='#fff';
bar.appendChild(Ltitle);
var Limg = this._closeButton = this.buildCloseButton();
bar.appendChild(Limg);
Limg = this._helpButton = this.buildHelpButton()
bar.appendChild(Limg);
Limg = this._configButton = this.buildConfigButton();
bar.appendChild(Limg);
Limg = this._unpinButton = this.buildUnpinButton();
bar.appendChild(Limg);
return bar;
}
private markUnselectable(e: HTMLElement) {
e.style.MozUserSelect="none";
e.style.KhtmlUserSelect="none";
e.style.UserSelect="none";
e.style.WebkitUserSelect="none";
}
private buildCloseButton(): HTMLDivElement {
var Limg = document.createElement('div');
this.markUnselectable(Limg);
Limg.id='kmw-close-button';
Limg.className='kmw-title-bar-image';
Limg.onmousedown = this.mouseCancellingHandler;
return Limg;
}
private buildHelpButton(): HTMLDivElement {
let Limg = document.createElement('div');
this.markUnselectable(Limg);
Limg.id='kmw-help-image';
Limg.className='kmw-title-bar-image';
Limg.title='KeymanWeb Help';
Limg.onmousedown = this.mouseCancellingHandler;
return Limg;
}
private buildConfigButton(): HTMLDivElement {
let Limg = document.createElement('div');
this.markUnselectable(Limg);
Limg.id='kmw-config-image';
Limg.className='kmw-title-bar-image';
Limg.title='KeymanWeb Configuration Options';
Limg.onmousedown = this.mouseCancellingHandler;
return Limg;
}
/**
* Builds an 'unpin' button for restoring OSK to default location, handle mousedown and click events
*/
private buildUnpinButton(): HTMLDivElement {
let Limg = document.createElement('div'); //I2186
this.markUnselectable(Limg);
Limg.id = 'kmw-pin-image';
Limg.className = 'kmw-title-bar-image';
Limg.title='Pin the On Screen Keyboard to its default location on the active text box';
Limg.onmousedown = this.mouseCancellingHandler;
return Limg;
}
public refreshLayout() {
// The title bar is adaptable as it is and needs no adjustments.
}
}
}

View file

@ -1,87 +0,0 @@
namespace com.keyman.osk {
export interface LengthStyle {
val: number,
absolute: boolean,
special?: 'em' | 'rem';
};
export class ParsedLengthStyle implements LengthStyle {
public readonly val: number;
public readonly absolute: boolean;
public readonly special: 'em' | 'rem';
public constructor(style: LengthStyle | string) {
let parsed: LengthStyle = (typeof style == 'string') ? ParsedLengthStyle.parseLengthStyle(style) : style;
// While Object.assign would be nice (and previously, was used), it will break
// on old but still supported versions of Android if their Chrome isn't updated.
// Requires mobile Chrome 45+, but API 21 (5.0) launches with an older browser.
// Object.assign(this, parsed);
this.val = parsed.val;
this.absolute = parsed.absolute;
if(parsed.special) {
this.special = parsed.special;
}
}
public get styleString(): string {
if(this.absolute) {
return this.val + 'px';
} else if(this.special) {
// Only 'em' and 'rem' are allowed, and both may be treated similarly.
// Both relate to font sizes, though the path to the reference element
// differs between them.
return this.val + this.special;
} else {
return (this.val * 100) + '%';
}
}
public scaledBy(scalar: number): ParsedLengthStyle {
return new ParsedLengthStyle({
val: scalar * this.val,
absolute: this.absolute
});
}
public static inPixels(val: number): ParsedLengthStyle {
return new ParsedLengthStyle({val: val, absolute: true});
}
public static inPercent(val: number): ParsedLengthStyle {
return new ParsedLengthStyle({val: val/100, absolute: false});
}
public static forScalar(val: number): ParsedLengthStyle {
return new ParsedLengthStyle({val: val, absolute: false});
}
public static special(val: number, suffix: 'em' | 'rem'): ParsedLengthStyle {
return new ParsedLengthStyle({val: val, absolute: false, special: suffix});
}
private static parseLengthStyle(spec: string): LengthStyle {
const val = parseFloat(spec);
if(isNaN(val)) {
// Cannot parse.
console.error("Could not properly parse specified length style info: '" + spec + "'.");
return null;
}
return spec.indexOf('px') != -1 ? {val: val, absolute: true} :
// 16 px ~= 12 pt.
// Reference: https://kyleschaeffer.com/css-font-size-em-vs-px-vs-pt-vs-percent
spec.indexOf('pt') != -1 ? {val: (4 * val / 3), absolute: true} :
spec.indexOf('%') != -1 ? {val: val/100, absolute: false} :
spec.indexOf('rem') != -1 ? {val: val, absolute: false, special: 'rem'} :
spec.indexOf('em') != -1 ? {val: val, absolute: false, special: 'em'} :
// At this point, assuming either Number or number in a string without units
// Note: this one is NOT natively handled by browsers!
// We'll treat it as if it were 'pt', since that's likely the user's
// most familiar font size unit.
{val: (4 * val / 3), absolute: true};
}
}
}

View file

@ -1,127 +0,0 @@
/// <reference path="inputEventEngine.ts" />
namespace com.keyman.osk {
export 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);
}
public static forVisualKeyboard(vkbd: VisualKeyboard) {
const config: InputEventEngineConfig = {
targetRoot: vkbd.element,
// 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: vkbd.touch.bind(vkbd),
inputMoveHandler: vkbd.moveOver.bind(vkbd),
inputMoveCancelHandler: vkbd.moveCancel.bind(vkbd),
inputEndHandler: vkbd.release.bind(vkbd),
coordConstrainedWithinInteractiveBounds: vkbd.detectWithinInteractiveBounds.bind(vkbd)
};
return new MouseEventEngine(config);
}
public static forPredictiveBanner(banner: SuggestionBanner, handlerRoot: SuggestionManager) {
const config: InputEventEngineConfig = {
targetRoot: banner.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: handlerRoot.touchStart.bind(handlerRoot),
inputMoveHandler: handlerRoot.touchMove.bind(handlerRoot),
inputEndHandler: handlerRoot.touchEnd.bind(handlerRoot),
coordConstrainedWithinInteractiveBounds: function() { return true; }
};
return new MouseEventEngine(config);
}
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));
}
}
}

View file

@ -1,164 +0,0 @@
/// <reference path="oskKey.ts" />
namespace com.keyman.osk {
let Codes = com.keyman.text.Codes;
export class OSKBaseKey extends OSKKey {
private capLabel: HTMLDivElement;
public readonly row: OSKRow;
constructor(spec: OSKKeySpec, layer: string, row: OSKRow) {
super(spec, layer);
this.row = row;
}
getId(): string {
// Define each key element id by layer id and key id (duplicate possible for SHIFT - does it matter?)
return this.spec.elementID;
}
getCoreId(): string {
return this.spec.coreID;
}
getBaseId(): string {
return this.spec.baseKeyID;
}
// Produces a small reference label for the corresponding physical key on a US keyboard.
private generateKeyCapLabel(): HTMLDivElement {
// Create the default key cap labels (letter keys, etc.)
var x = Codes.keyCodes[this.spec.baseKeyID];
switch(x) {
// Converts the keyman key id code for common symbol keys into its representative ASCII code.
// K_COLON -> K_BKQUOTE
case 186: x=59; break;
case 187: x=61; break;
case 188: x=44; break;
case 189: x=45; break;
case 190: x=46; break;
case 191: x=47; break;
case 192: x=96; break;
// K_LBRKT -> K_QUOTE
case 219: x=91; break;
case 220: x=92; break;
case 221: x=93; break;
case 222: x=39; break;
default:
// No other symbol character represents a base key on the standard QWERTY English layout.
if(x < 48 || x > 90) {
x=0;
}
}
let q = document.createElement('div');
q.className='kmw-key-label';
if(x > 0) {
q.innerText=String.fromCharCode(x);
} else {
// Keyman-only virtual keys have no corresponding physical key.
// So, no text for the key-cap.
}
return q;
}
private processSubkeys(btn: KeyElement, vkbd: VisualKeyboard) {
// Add reference to subkey array if defined
var bsn: number, bsk=btn['subKeys'] = this.spec['sk'];
// Transform any special keys into their PUA representations.
for(bsn=0; bsn<bsk.length; bsn++) {
if(bsk[bsn]['sp'] == 1 || bsk[bsn]['sp'] == 2) {
var oldText=bsk[bsn]['text'];
bsk[bsn]['text']=this.renameSpecialKey(oldText, vkbd);
}
// If a subkey doesn't have a defined layer property, copy it from the base key's layer by default.
if(!bsk[bsn].layer) {
bsk[bsn].layer = btn.key.layer
}
}
// If a subkey array is defined, add an icon
var skIcon = document.createElement('div');
skIcon.className='kmw-key-popup-icon';
//kDiv.appendChild(skIcon);
btn.appendChild(skIcon);
}
construct(vkbd: VisualKeyboard): HTMLDivElement {
let spec = this.spec;
let kDiv = document.createElement('div');
kDiv.className='kmw-key-square';
let btnEle = document.createElement('div');
let btn = this.btn = link(btnEle, new KeyData(this, spec['id']));
// Set button class
this.setButtonClass();
// Add the (US English) keycap label for layouts requesting display of underlying keys
let keyCap = this.capLabel = this.generateKeyCapLabel();
btn.appendChild(keyCap);
// Define each key element id by layer id and key id (duplicate possible for SHIFT - does it matter?)
btn.id=this.getId();
// Make sure the key text is the element's first child - processSubkeys()
// will add an extra element if subkeys exist, which can interfere with
// keyboard/language name display on the space bar!
btn.appendChild(this.label = this.generateKeyText(vkbd));
// Handle subkey-related tasks.
if(typeof(spec['sk']) != 'undefined' && spec['sk'] != null) {
this.processSubkeys(btn, vkbd);
} else {
btn['subKeys']=null;
}
// Add text to button and button to placeholder div
kDiv.appendChild(btn);
// The 'return value' of this process.
return this.square = kDiv;
}
public refreshLayout(vkbd: VisualKeyboard) {
let key = this.spec as keyboards.ActiveKey;
this.square.style.width = vkbd.layoutWidth.scaledBy(key.proportionalWidth).styleString;
this.square.style.marginLeft = vkbd.layoutWidth.scaledBy(key.proportionalPad).styleString;
this.btn.style.width = vkbd.usesFixedWidthScaling ? this.square.style.width : '100%';
if(vkbd.usesFixedHeightScaling) {
// Matches its row's height.
this.square.style.height = vkbd.internalHeight.scaledBy(this.row.heightFraction).styleString;
} else {
this.square.style.height = '100%'; // use the full row height
}
super.refreshLayout(vkbd);
let util = com.keyman.singleton.util;
const device = vkbd.device;
const resizeLabels = (device.OS == utils.OperatingSystem.iOS &&
device.formFactor == utils.FormFactor.Phone
&& util.landscapeView());
// Rescale keycap labels on iPhone (iOS 7)
if(resizeLabels && this.capLabel) {
this.capLabel.style.fontSize = '6px';
}
}
public get displaysKeyCap(): boolean {
return this.capLabel && this.capLabel.style.display == 'block';
}
public set displaysKeyCap(flag: boolean) {
if(!this.capLabel) {
throw new Error("Key element not yet constructed; cannot display key cap");
}
this.capLabel.style.display = flag ? 'block' : 'none';
}
}
}

View file

@ -1,519 +0,0 @@
namespace com.keyman.osk {
export class KeyData {
['key']: OSKKey;
['keyId']: string;
['subKeys']?: OSKKeySpec[];
constructor(keyData: OSKKey, keyId: string) {
this['key'] = keyData;
this['keyId'] = keyId;
}
}
export type KeyElement = HTMLDivElement & KeyData;
// Many thanks to https://www.typescriptlang.org/docs/handbook/advanced-types.html for this.
export function link(elem: HTMLDivElement, data: KeyData): KeyElement {
let e = <KeyElement> elem;
// Merges all properties and methods of KeyData onto the underlying HTMLDivElement, creating a merged class.
for(let id in data) {
if(!e.hasOwnProperty(id)) {
(<any>e)[id] = (<any>data)[id];
}
}
return e;
}
export function isKey(elem: Node): boolean {
return elem && ('key' in elem) && ((<any> elem['key']) instanceof OSKKey);
}
export function getKeyFrom(elem: Node): KeyElement {
if(isKey(elem)) {
return <KeyElement> elem;
} else {
return null;
}
}
export class OSKKeySpec implements keyboards.LayoutKey {
id: string;
// Only set (within @keymanapp/keyboard-processor) for keys actually specified in a loaded layout
baseKeyID?: string;
coreID?: string;
elementID?: string;
text?: string;
sp?: keyboards.ButtonClass;
width: number;
layer?: string; // The key will derive its base modifiers from this property - may not equal the layer on which it is displayed.
nextlayer?: string;
pad?: number;
sk?: OSKKeySpec[];
constructor(id: string, text?: string, width?: number, sp?: keyboards.ButtonClass, nextlayer?: string, pad?: number) {
this.id = id;
this.text = text;
this.width = width ? width : 50;
this.sp = sp;
this.nextlayer = nextlayer;
this.pad = pad;
}
}
export abstract class OSKKey {
// Defines the PUA code mapping for the various 'special' modifier/control keys on keyboards.
// `specialCharacters` must be kept in sync with the same variable in builder.js. See also CompileKeymanWeb.pas: CSpecialText10
static readonly specialCharacters = {
'*Shift*': 8,
'*Enter*': 5,
'*Tab*': 6,
'*BkSp*': 4,
'*Menu*': 11,
'*Hide*': 10,
'*Alt*': 25,
'*Ctrl*': 1,
'*Caps*': 3,
'*ABC*': 16,
'*abc*': 17,
'*123*': 19,
'*Symbol*': 21,
'*Currency*': 20,
'*Shifted*': 9,
'*AltGr*': 2,
'*TabLeft*': 7,
'*LAlt*': 0x56,
'*RAlt*': 0x57,
'*LCtrl*': 0x58,
'*RCtrl*': 0x59,
'*LAltCtrl*': 0x60,
'*RAltCtrl*': 0x61,
'*LAltCtrlShift*': 0x62,
'*RAltCtrlShift*': 0x63,
'*AltShift*': 0x64,
'*CtrlShift*': 0x65,
'*AltCtrlShift*': 0x66,
'*LAltShift*': 0x67,
'*RAltShift*': 0x68,
'*LCtrlShift*': 0x69,
'*RCtrlShift*': 0x70,
// Added in Keyman 14.0.
'*LTREnter*': 0x05, // Default alias of '*Enter*'.
'*LTRBkSp*': 0x04, // Default alias of '*BkSp*'.
'*RTLEnter*': 0x71,
'*RTLBkSp*': 0x72,
'*ShiftLock*': 0x73,
'*ShiftedLock*': 0x74,
'*ZWNJ*': 0x75, // If this one is specified, auto-detection will kick in.
'*ZWNJiOS*': 0x75, // The iOS version will be used by default, but the
'*ZWNJAndroid*': 0x76, // Android platform has its own default glyph.
};
static readonly BUTTON_CLASSES = [
'default',
'shift',
'shift-on',
'special',
'special-on',
'', // Key classes 5 through 7 are reserved for future use.
'',
'',
'deadkey',
'blank',
'hidden'
];
static readonly HIGHLIGHT_CLASS = 'kmw-key-touched';
readonly spec: OSKKeySpec;
btn: KeyElement;
label: HTMLSpanElement;
square: HTMLDivElement;
/**
* The layer of the OSK on which the key is displayed.
*/
readonly layer: string;
constructor(spec: OSKKeySpec, layer: string) {
this.spec = spec;
this.layer = layer;
}
abstract getId(): string;
/**
* Attach appropriate class to each key button, according to the layout
*
* @param {Object=} layout source layout description (optional, sometimes)
*/
public setButtonClass() {
let key = this.spec;
let btn = this.btn;
var n=0;
if(typeof key['dk'] == 'string' && key['dk'] == '1') {
n=8;
}
n = key['sp'] ?? n;
if(n < 0 || n > 10) {
n=0;
}
btn.className='kmw-key kmw-key-'+OSKKey.BUTTON_CLASSES[n];
}
/**
* For keys with button classes that support toggle states, this method
* may be used to toggle which state the key's button class is in.
* - shift <=> shift-on
* - special <=> special-on
* @param {boolean=} flag The new toggle state
*/
public setToggleState(flag?: boolean) {
let btnClassId: number;
btnClassId = this.spec['sp'];
// 1 + 2: shift + shift-on
// 3 + 4: special + special-on
switch(OSKKey.BUTTON_CLASSES[btnClassId]) {
case 'shift':
case 'shift-on':
if(flag === undefined) {
flag = OSKKey.BUTTON_CLASSES[btnClassId] == 'shift';
}
this.spec['sp'] = 1 + (flag ? 1 : 0) as keyboards.ButtonClass;
break;
// Added in 15.0: special key highlight toggling.
// Was _intended_ in earlier versions, but not actually implemented.
case 'special':
case 'special-on':
if(flag === undefined) {
flag = OSKKey.BUTTON_CLASSES[btnClassId] == 'special';
}
this.spec['sp'] = 3 + (flag ? 1 : 0) as keyboards.ButtonClass;
break;
default:
return;
}
this.setButtonClass();
}
// "Frame key" - generally refers to non-linguistic keys on the keyboard
public isFrameKey(): boolean {
let classIndex = this.spec['sp'] || 0;
switch(OSKKey.BUTTON_CLASSES[classIndex]) {
case 'default':
case 'deadkey':
// Note: will (generally) include the spacebar.
return false;
default:
return true;
}
}
public allowsKeyTip(): boolean {
if(this.isFrameKey()) {
return false;
} else {
return !this.btn.classList.contains('kmw-spacebar');
}
}
public highlight(on: boolean) {
var classes=this.btn.classList;
if(on) {
if(!classes.contains(OSKKey.HIGHLIGHT_CLASS)) {
classes.add(OSKKey.HIGHLIGHT_CLASS);
}
} else {
classes.remove(OSKKey.HIGHLIGHT_CLASS);
}
}
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} style The CSSStyleDeclaration for an element to measure against, without modification.
*
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
* This version has been substantially modified to work for this particular application.
*/
static getTextMetrics(text: string, emScale: number, style: {fontFamily?: string, fontSize: string}): TextMetrics {
// Since we may mutate the incoming style, let's make sure to copy it first.
// Only the relevant properties, though.
style = {
fontFamily: style.fontFamily,
fontSize: style.fontSize
};
// A final fallback - having the right font selected makes a world of difference.
if(!style.fontFamily) {
style.fontFamily = getComputedStyle(document.body).fontFamily;
}
if(!style.fontSize || style.fontSize == "") {
style.fontSize = '1em';
}
let fontFamily = style.fontFamily;
let fontSpec = getFontSizeStyle(style.fontSize);
var fontSize: string;
if(fontSpec.absolute) {
// We've already got an exact size - use it!
fontSize = fontSpec.val + 'px';
} else {
fontSize = fontSpec.val * emScale + 'px';
}
// re-use canvas object for better performance
var canvas: HTMLCanvasElement = OSKKey.getTextMetrics['canvas'] ||
(OSKKey.getTextMetrics['canvas'] = document.createElement("canvas"));
var context = canvas.getContext("2d");
context.font = fontSize + " " + fontFamily;
var metrics = context.measureText(text);
return metrics;
}
/**
* Calculate the font size required for a key cap, scaling to fit longer text
* @param vkbd
* @param style specification for the desired base font size
* @param override if true, don't use the font spec from the button, just use the passed in spec
* @returns font size as a style string
*/
getIdealFontSize(vkbd: VisualKeyboard, text: string, style: {height?: string, fontFamily?: string, fontSize: string}, override?: boolean): string {
let buttonStyle = getComputedStyle(this.btn);
let keyWidth = parseFloat(buttonStyle.width);
let emScale = 1;
const originalSize = getFontSizeStyle(style.fontSize || '1em');
// Not yet available; it'll be handled in a later layout pass.
if(!buttonStyle.fontSize) {
// NOTE: preserves old behavior for use in documentation keyboards, for now.
// Once we no longer need to maintain this code block, we can drop all current
// method parameters safely.
//
// Recompute the new width for use in autoscaling calculations below, just in case.
emScale = vkbd.getKeyEmFontSize();
keyWidth = this.getKeyWidth(vkbd);
} else if(!override) {
// When available, just use computedStyle instead.
style = buttonStyle;
}
let fontSpec = getFontSizeStyle(style.fontSize || '1em');
let metrics = OSKKey.getTextMetrics(text, emScale, style);
const MAX_X_PROPORTION = 0.90;
const MAX_Y_PROPORTION = 0.90;
const X_PADDING = 2;
const Y_PADDING = 2;
var fontHeight: number, keyHeight: number;
if(metrics.fontBoundingBoxAscent) {
fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
}
let textHeight = fontHeight ? fontHeight + Y_PADDING : 0;
if(style.height && style.height.indexOf('px') != -1) {
keyHeight = Number.parseFloat(style.height.substring(0, style.height.indexOf('px')));
}
let xProportion = (keyWidth * MAX_X_PROPORTION) / (metrics.width + X_PADDING); // How much of the key does the text want to take?
let yProportion = textHeight && keyHeight ? (keyHeight * MAX_Y_PROPORTION) / textHeight : undefined;
var proportion: number = xProportion;
if(yProportion && yProportion < xProportion) {
proportion = yProportion;
}
// Never upscale keys past the default - only downscale them.
// Proportion < 1: ratio of key width to (padded [loosely speaking]) text width
// maxProportion determines the 'padding' involved.
//
if(proportion < 1) {
if(originalSize.absolute) {
return proportion * fontSpec.val + 'px';
} else {
return proportion * originalSize.val + 'em';
}
} else {
if(originalSize.absolute) {
return fontSpec.val + 'px';
} else {
return originalSize.val + 'em';
}
}
}
getKeyWidth(vkbd: VisualKeyboard): number {
let key = this.spec as keyboards.ActiveKey;
return key.proportionalWidth * vkbd.width;
}
/**
* Replace default key names by special font codes for modifier keys
*
* @param {string} oldText
* @return {string}
**/
protected renameSpecialKey(oldText: string, vkbd: VisualKeyboard): string {
// If a 'special key' mapping exists for the text, replace it with its corresponding special OSK character.
switch(oldText) {
case '*ZWNJ*':
// Default ZWNJ symbol comes from iOS. We'd rather match the system defaults where
// possible / available though, and there's a different standard symbol on Android.
oldText = vkbd.device.OS == com.keyman.utils.OperatingSystem.Android ?
'*ZWNJAndroid*' :
'*ZWNJiOS*';
break;
case '*Enter*':
oldText = vkbd.isRTL ? '*RTLEnter*' : '*LTREnter*';
break;
case '*BkSp*':
oldText = vkbd.isRTL ? '*RTLBkSp*' : '*LTRBkSp*';
break;
default:
// do nothing.
}
let specialCodePUA = 0XE000 + VisualKeyboard.specialCharacters[oldText];
return VisualKeyboard.specialCharacters[oldText] ?
String.fromCharCode(specialCodePUA) :
oldText;
}
public get keyText(): string {
const spec = this.spec;
const DEFAULT_BLANK = '\xa0';
// Add OSK key labels
let keyText = null;
if(spec['text'] == null || spec['text'] == '') {
if(typeof spec['id'] == 'string') {
// If the ID's Unicode-based, just use that code.
keyText = keyboards.ActiveKey.unicodeIDToText(spec['id']);
}
keyText = keyText || DEFAULT_BLANK;
} else {
keyText=spec['text'];
// Unique layer-based transformation: SHIFT-TAB uses a different glyph.
if(keyText == '*Tab*' && this.layer == 'shift') {
keyText = '*TabLeft*';
}
}
return keyText;
}
// Produces a HTMLSpanElement with the key's actual text.
protected generateKeyText(vkbd: VisualKeyboard): HTMLSpanElement {
const spec = this.spec;
let t = document.createElement('span'), ts=t.style;
t.className='kmw-key-text';
// Add OSK key labels
let keyText = this.keyText;
let specialText = this.renameSpecialKey(keyText, vkbd);
if(specialText != keyText) {
// The keyboard wants to use the code for a special glyph defined by the SpecialOSK font.
keyText = specialText;
spec['font'] = "SpecialOSK";
}
//Override font spec if set for this key in the layout
if(typeof spec['font'] == 'string' && spec['font'] != '') {
ts.fontFamily=spec['font'];
}
if(typeof spec['fontsize'] == 'string' && spec['fontsize'] != '') {
ts.fontSize=spec['fontsize'];
}
// For some reason, fonts will sometimes 'bug out' for the embedded iOS page if we
// instead assign fontFamily to the existing style 'ts'. (Occurs in iOS 12.)
let styleSpec: {fontFamily?: string, fontSize: string} = {fontSize: ts.fontSize};
if(ts.fontFamily) {
styleSpec.fontFamily = ts.fontFamily;
} else {
styleSpec.fontFamily = vkbd.fontFamily; // Helps with style sheet calculations.
}
// Check the key's display width - does the key visualize well?
let emScale = vkbd.getKeyEmFontSize();
var width: number = OSKKey.getTextMetrics(keyText, emScale, styleSpec).width;
if(width == 0 && keyText != '' && keyText != '\xa0') {
// Add the Unicode 'empty circle' as a base support for needy diacritics.
// Disabled by mcdurdin 2020-10-19; dotted circle display is inconsistent on iOS/Safari
// at least and doesn't combine with diacritic marks. For consistent display, it may be
// necessary to build a custom font that does not depend on renderer choices for base
// mark display -- e.g. create marks with custom base included, potentially even on PUA
// code points and use those in rendering the OSK. See #3039 for more details.
// keyText = '\u25cc' + keyText;
if(vkbd.isRTL) {
// Add the RTL marker to ensure it displays properly.
keyText = '\u200f' + keyText;
}
}
ts.fontSize = this.getIdealFontSize(vkbd, keyText, styleSpec);
// Finalize the key's text.
t.innerText = keyText;
return t;
}
public isUnderTouch(input: InputEventCoordinate): boolean {
let x = input.x;
let y = input.y;
let btn = this.btn;
let x0 = dom.Utils.getAbsoluteX(btn);
let y0 = dom.Utils.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) {
if(!this.label.classList.contains('kmw-spacebar-caption')) {
this.label.style.fontSize = this.getIdealFontSize(vkbd, this.keyText, this.btn.style);
} else {
// Remove any custom setting placed on it before recomputing its inherited style info.
this.label.style.fontSize = '';
const fontSize = this.getIdealFontSize(vkbd, this.label.textContent, getComputedStyle(this.label), true);
// Since the kmw-spacebar-caption version uses !important, we must specify
// it directly on the element too; otherwise, scaling gets ignored.
this.label.style.setProperty("font-size", fontSize, "important");
}
}
}
}
}

View file

@ -1,122 +0,0 @@
/// <reference path="oskRow.ts" />
namespace com.keyman.osk {
export class OSKLayer {
public readonly element: HTMLDivElement;
public readonly rows: OSKRow[];
public readonly spec: keyboards.ActiveLayer;
public readonly nextlayer: string;
public readonly globeKey: OSKBaseKey;
public readonly spaceBarKey: OSKBaseKey;
public readonly hideKey: OSKBaseKey;
public readonly capsKey: OSKBaseKey;
public readonly numKey: OSKBaseKey;
public readonly scrollKey: OSKBaseKey;
private _rowHeight: number;
public get rowHeight(): number {
return this._rowHeight;
}
public get id(): string {
return this.spec.id;
}
public constructor(vkbd: VisualKeyboard,
layout: keyboards.ActiveLayout,
layer: keyboards.ActiveLayer) {
this.spec = layer;
const gDiv = this.element = document.createElement('div');
const gs=gDiv.style;
gDiv.className='kmw-key-layer';
var nRows=layer['row'].length;
if(nRows > 4 && vkbd.device.formFactor == 'phone') {
gDiv.className = gDiv.className + ' kmw-5rows';
}
// Set font for layer if defined in layout
gs.fontFamily = 'font' in layout ? layout['font'] : '';
this.nextlayer = gDiv['layer'] = layer['id'];
if(typeof layer['nextlayer'] == 'string') {
// The gDiv['nextLayer'] is no longer referenced in KMW 15.0+, but is
// maintained for partial back-compat in case any site devs actually
// relied on its value from prior versions.
//
// We won't pay attention to any mutations to the gDiv copy, though.
gDiv['nextLayer'] = this.nextlayer = layer['nextlayer'];
}
// Create a DIV for each row of the group
let rows=layer['row'];
this.rows = [];
for(let i=0; i<rows.length; i++) {
let rowObj = new OSKRow(vkbd, layer, rows[i]);
rowObj.displaysKeyCaps = layout["displayUnderlying"];
gDiv.appendChild(rowObj.element);
this.rows.push(rowObj);
}
// Identify and save references to the language key, hide keyboard key, and space bar
if(vkbd.device.touchable) {
this.globeKey = this.findKey('K_LOPT');
this.hideKey = this.findKey('K_ROPT');
}
// Define for both desktop and touchable OSK
this.spaceBarKey = this.findKey('K_SPACE');
this.capsKey = this.findKey('K_CAPS');
this.numKey = this.findKey('K_NUMLOCK');
this.scrollKey = this.findKey('K_SCROLL');
}
/**
* Find the OSKBaseKey representing the specified
* key ID for the currently visible OSK layer
*
* @param {string} keyId key identifier
* @return {Object} Reference to key
*/
private findKey(keyId: string): OSKBaseKey {
for(const row of this.rows) {
for(const key of row.keys) {
if(key.getBaseId() == keyId) {
return key;
}
}
}
return null;
}
public refreshLayout(vkbd: VisualKeyboard, layerHeight: number) {
// Check the heights of each row, in case different layers have different row counts.
const nRows = this.rows.length;
const rowHeight = this._rowHeight = Math.floor(layerHeight/(nRows == 0 ? 1 : nRows));
if(vkbd.usesFixedHeightScaling) {
this.element.style.height=(layerHeight)+'px';
}
for(let nRow=0; nRow<nRows; nRow++) {
const oskRow = this.rows[nRow];
const bottom = (nRows-nRow-1)*rowHeight+1;
if(vkbd.usesFixedHeightScaling) {
// Calculate the exact vertical coordinate of the row's center.
this.spec.row[nRow].proportionalY = ((layerHeight - bottom) - rowHeight/2) / layerHeight;
if(nRow == nRows-1) {
oskRow.element.style.bottom = '1px';
}
}
oskRow.refreshLayout(vkbd);
}
}
}
}

View file

@ -1,53 +0,0 @@
/// <reference path="oskLayer.ts" />
namespace com.keyman.osk {
export class OSKLayerGroup {
public readonly element: HTMLDivElement;
public readonly layers: {[layerID: string]: OSKLayer} = {};
public constructor(vkbd: VisualKeyboard, keyboard: keyboards.Keyboard, formFactor: utils.FormFactor) {
let layout = keyboard.layout(formFactor);
const lDiv = this.element = document.createElement('div');
const ls=lDiv.style;
// Set OSK box default style
lDiv.className='kmw-key-layer-group';
// Return empty DIV if no layout defined
if(layout == null) {
return;
}
// Set default OSK font size (Build 344, KMEW-90)
let layoutFS = layout['fontsize'];
if(typeof layoutFS == 'undefined' || layoutFS == null || layoutFS == '') {
ls.fontSize='1em';
} else {
ls.fontSize=layout['fontsize'];
}
// Create a separate OSK div for each OSK layer, only one of which will ever be visible
var n: number, i: number, j: number;
var layers: keyboards.LayoutLayer[];
layers=layout['layer'];
// Set key default attributes (must use exportable names!)
var tKey=vkbd.getDefaultKeyObject();
tKey['fontsize']=ls.fontSize;
for(n=0; n<layers.length; n++) {
let layer=layers[n] as keyboards.ActiveLayer;
const layerObj = new OSKLayer(vkbd, layout, layer);
this.layers[layer.id] = layerObj;
// Always make the first layer visible
layerObj.element.style.display = (n==0 ? 'block' : 'none');
// Add layer to group
lDiv.appendChild(layerObj.element);
}
}
}
}

View file

@ -1,86 +0,0 @@
/// <reference path="oskBaseKey.ts" />
namespace com.keyman.osk {
/**
* Models one row of one layer of the OSK (`VisualKeyboard`) for a keyboard.
*/
export class OSKRow {
public readonly element: HTMLDivElement;
public readonly keys: OSKBaseKey[];
public readonly heightFraction: number;
public constructor(vkbd: VisualKeyboard,
layerSpec: keyboards.ActiveLayer,
rowSpec: keyboards.ActiveRow) {
const rDiv = this.element = document.createElement('div');
rDiv.className='kmw-key-row';
// Calculate default row height
this.heightFraction = 1 / layerSpec.row.length;
// Apply defaults, setting the width and other undefined properties for each key
const keys=rowSpec.key;
this.keys = [];
// Calculate actual key widths by multiplying by the OSK's width and rounding appropriately,
// adjusting the width of the last key to make the total exactly 100%.
// Overwrite the previously-computed percent.
// NB: the 'percent' suffix is historical, units are percent on desktop devices, but pixels on touch devices
// All key widths and paddings are rounded for uniformity
for(let j=0; j<keys.length; j++) {
const key = keys[j];
var keyObj = new OSKBaseKey(key as OSKKeySpec, layerSpec.id, this);
var element = keyObj.construct(vkbd);
this.keys.push(keyObj);
rDiv.appendChild(element);
}
}
public get displaysKeyCaps(): boolean {
if(this.keys.length > 0) {
return this.keys[0].displaysKeyCap;
} else {
return undefined;
}
}
public set displaysKeyCaps(flag: boolean) {
for(const key of this.keys) {
key.displaysKeyCap = flag;
}
}
public refreshLayout(vkbd: VisualKeyboard) {
const rs = this.element.style;
const rowHeight = vkbd.internalHeight.scaledBy(this.heightFraction);
rs.maxHeight=rs.lineHeight=rs.height=rowHeight.styleString;
// Only used for fixed-height scales at present.
const padRatio = 0.15;
const keyHeightBase = vkbd.usesFixedHeightScaling ? rowHeight : ParsedLengthStyle.forScalar(1);
const padTop = keyHeightBase.scaledBy(padRatio / 2);
const keyHeight = keyHeightBase.scaledBy(1 - padRatio);
for(const key of this.keys) {
const keySquare = key.btn.parentElement;
const keyElement = key.btn;
// Set the kmw-key-square position
const kss = keySquare.style;
kss.height=kss.minHeight=keyHeightBase.styleString;
const kes = keyElement.style;
kes.top = padTop.styleString;
kes.height=kes.lineHeight=kes.minHeight=keyHeight.styleString;
if(keyElement.key) {
keyElement.key.refreshLayout(vkbd);
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,7 +0,0 @@
namespace com.keyman.osk {
export interface OSKViewComponent {
readonly element: HTMLElement;
readonly layoutHeight: ParsedLengthStyle;
refreshLayout(): void;
}
}

View file

@ -1,45 +0,0 @@
namespace com.keyman.osk {
/**
* 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 interface PendingGesture {
readonly baseKey: KeyElement;
readonly promise?: Promise<RealizedGesture>;
cancel(): void;
resolve?(): void;
}
}

View file

@ -1,71 +0,0 @@
namespace com.keyman.osk {
export class PreProcessor {
/**
* Simulate a keystroke according to the touched keyboard button element
*
* Note that the test-case oriented 'recorder' stubs this method to facilitate OSK-based input
* recording for use in test cases. If changing this function, please ensure the recorder is
* not affected.
*
* @param {Object} e element touched (or clicked)
*/
static clickKey(e: osk.KeyElement, input?: InputEventCoordinate) {
let keyman = com.keyman.singleton;
let Lkc = keyman['osk'].vkbd.initKeyEvent(e, input);
if(!Lkc) {
return true;
}
return this.raiseKeyEvent(Lkc);
}
static raiseKeyEvent(keyEvent: text.KeyEvent) {
let keyman = com.keyman.singleton;
var Lelem = keyman.domManager.lastActiveElement;
if(Lelem != null) {
// Handle any DOM state management related to click inputs.
let outputTarget = dom.Utils.getOutputTarget(Lelem);
keyman.domManager.initActiveElement(Lelem);
// Clear any cached codepoint data; we can rebuild it if it's unchanged.
outputTarget.invalidateSelection();
// Deadkey matching continues to be troublesome.
// Deleting matched deadkeys here seems to correct some of the issues. (JD 6/6/14)
outputTarget.deadkeys().deleteMatched(); // Delete any matched deadkeys before continuing
if(!keyman.isEmbedded) {
keyman.uiManager.setActivatingUI(true);
com.keyman.dom.DOMEventHandlers.states._IgnoreNextSelChange = 100;
keyman.domManager.focusLastActiveElement();
com.keyman.dom.DOMEventHandlers.states._IgnoreNextSelChange = 0;
}
let retVal = PreProcessor.handleClick(keyEvent, outputTarget, null);
// Now that processing is done, we can do a bit of post-processing, too.
keyman.uiManager.setActivatingUI(false); // I2498 - KeymanWeb OSK does not accept clicks in FF when using automatic UI
return retVal;
} else {
return true;
}
}
// Serves to hold DOM-dependent code that affects both 'native' and 'embedded' mode OSK use
// after the KeyEvent object has been properly instantiated. This should help catch any
// mutual last-minute DOM-side interactions before passing control to the processor... such as
// the UI-control command keys as seen below.
static handleClick(Lkc: text.KeyEvent, outputTarget: text.OutputTarget, e: osk.KeyElement) {
let keyman = com.keyman.singleton;
// Exclude menu and OSK hide keys from normal click processing
if(Lkc.kName == 'K_LOPT' || Lkc.kName == 'K_ROPT') {
keyman['osk'].vkbd.optionKey(e, Lkc.kName, true);
return true;
}
let retVal = !!keyman.core.processKeyEvent(Lkc, outputTarget);
return retVal;
}
}
}

View file

@ -1,34 +0,0 @@
namespace com.keyman.osk {
/**
* 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 interface RealizedGesture {
readonly baseKey: KeyElement;
readonly promise: Promise<text.KeyEvent>;
clear(): void;
isVisible(): boolean;
updateTouch(input: InputEventCoordinate): void;
}
}

View file

@ -1,90 +0,0 @@
/// <reference path="inputEventEngine.ts" />
namespace com.keyman.osk {
export 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);
}
public static forVisualKeyboard(vkbd: VisualKeyboard) {
let config: InputEventEngineConfig = {
targetRoot: vkbd.element,
eventRoot: vkbd.element,
inputStartHandler: vkbd.touch.bind(vkbd),
inputMoveHandler: vkbd.moveOver.bind(vkbd),
inputMoveCancelHandler: vkbd.moveCancel.bind(vkbd),
inputEndHandler: vkbd.release.bind(vkbd),
coordConstrainedWithinInteractiveBounds: vkbd.detectWithinInteractiveBounds.bind(vkbd)
};
return new TouchEventEngine(config);
}
public static forPredictiveBanner(banner: SuggestionBanner, handlerRoot: SuggestionManager) {
const config: InputEventEngineConfig = {
targetRoot: banner.getDiv(),
// document.body is the event root b/c we need to track the mouse if it leaves
// the VisualKeyboard's hierarchy.
eventRoot: banner.getDiv(),
inputStartHandler: handlerRoot.touchStart.bind(handlerRoot),
inputMoveHandler: handlerRoot.touchMove.bind(handlerRoot),
inputEndHandler: handlerRoot.touchEnd.bind(handlerRoot),
coordConstrainedWithinInteractiveBounds: function() { return true; }
};
return new TouchEventEngine(config);
}
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));
}
}
}

View file

@ -1,428 +0,0 @@
namespace com.keyman.osk {
/**
* 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 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;
let util = keyman.util;
// 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);
// 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 = dom.Utils.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);
}
}
}
}

View file

@ -1,18 +0,0 @@
/// <reference path="lengthStyle.ts" />
namespace com.keyman.osk {
export function getFontSizeStyle(e: HTMLElement|string): {val: number, absolute: boolean} {
var fs: string;
if(typeof e == 'string') {
fs = e;
} else {
fs = e.style.fontSize;
if(!fs) {
fs = getComputedStyle(e).fontSize;
}
}
return new ParsedLengthStyle(fs);
}
}

File diff suppressed because it is too large Load diff