mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-09 10:25:32 +00:00
chore(web): removes old namespaced version of osk source files
This commit is contained in:
parent
d987444683
commit
f6d79964d6
37 changed files with 0 additions and 8484 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
namespace com.keyman.osk {
|
||||
export interface KeyTip {
|
||||
key: KeyElement;
|
||||
state: boolean;
|
||||
element?: HTMLDivElement;
|
||||
|
||||
show(key: KeyElement, on: boolean, vkbd: VisualKeyboard);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||
namespace com.keyman.osk {
|
||||
export interface OSKViewComponent {
|
||||
readonly element: HTMLElement;
|
||||
readonly layoutHeight: ParsedLengthStyle;
|
||||
refreshLayout(): void;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue