change(web): floating tablet key-previews, hosted by layer-independent element

This commit is contained in:
Joshua A. Horton 2023-11-30 13:01:10 +07:00
parent 977ad9cfbf
commit 4f19b4cd1e
5 changed files with 162 additions and 7 deletions

View file

@ -5,7 +5,7 @@ import VisualKeyboard from '../../../visualKeyboard.js';
import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';
const CSS_PREFIX = 'kmw-';
const DEFAULT_TIP_ORIENTATION = 'top';
const DEFAULT_TIP_ORIENTATION: PhoneKeyTipOrientation = 'top';
export type PhoneKeyTipOrientation = 'top' | 'bottom';
@ -127,8 +127,13 @@ export default class KeyTip implements KeyTipInterface {
kts.width = canvasWidth+'px';
kts.height = canvasHeight+'px';
// Some keyboards (such as `balochi_scientific`) do not _package_ a font but
// specify an extremely common one, such as Arial. In such cases, .kmw-key-text
// custom styling doesn't exist, relying on the layer object to simply specify
// the font-family.
const layerFontFamily = this.vkbd.currentLayer.element.style.fontFamily;
const ckts = getComputedStyle(vkbd.element);
kts.fontFamily = ckts.fontFamily;
kts.fontFamily = key.key.spec.font || layerFontFamily || ckts.fontFamily;
var px=parseInt(ckts.fontSize,10);
if(px == Number.NaN) {

View file

@ -0,0 +1,114 @@
import { KeyElement } from '../../../keyElement.js';
import KeyTipInterface from '../../../keytip.interface.js';
import VisualKeyboard from '../../../visualKeyboard.js';
import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';
const BASE_CLASS = 'kmw-keypreview';
const OVERLAY_CLASS = 'kmw-preview-overlay';
const BASE_ID = 'kmw-keytip';
export class TabletKeyTip implements KeyTipInterface {
public readonly element: HTMLDivElement;
public key: KeyElement;
public state: boolean = false;
private previewHost: GesturePreviewHost;
private preview: HTMLDivElement;
private readonly vkbd: VisualKeyboard;
/**
*
* @param constrain keep the keytip within the bounds of the overall OSK.
* Will probably be handled via function in a later pass.
*/
constructor(vkbd: VisualKeyboard) {
this.vkbd = vkbd;
const base = this.element=document.createElement('div');
base.className=BASE_CLASS;
base.id = 'kmw-keytip';
// The following style is critical, so do not rely on external CSS
base.style.pointerEvents='none';
base.style.display='none';
this.preview = document.createElement('div');
base.appendChild(this.preview);
}
show(key: KeyElement, on: boolean, previewHost: GesturePreviewHost) {
const vkbd = this.vkbd;
const keyLayer = key?.key.spec.displayLayer;
// During quick input sequences - especially during a multitap-modipress - it's possible
// for a user to request a preview for a key from a layer that is currently active, but
// currently not visible due to need previously-requested layout calcs for a different layer.
if(on) {
// Necessary for `key.offsetParent` and client-rect methods referenced below.
// Will not unnecessarily force reflow if the layer is already in proper document flow,
// but otherwise restores it.
vkbd.layerGroup.blinkLayer(keyLayer);
}
// 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) {
// May need adjustment for borders if ever enabled for the desktop form-factor target.
// const _Box = this.vkbd.topContainer;
const hostRect = this.vkbd.element.getBoundingClientRect();
const keyRect = key.getBoundingClientRect();
// Used to apply box-shadow overlay styling when the preview is for a key on a layer not
// currently active. This is done in case the layers don't have perfect alignment for
// all keys.
const conditionalOverlayStyle = (keyLayer != vkbd.layerId) ? OVERLAY_CLASS : '';
this.element.className = `${BASE_CLASS} ${key.className} ${conditionalOverlayStyle}`;
// Some keyboards use custom CSS styling based on partial-matching the key ID
// (like sil_cameroon_azerty); this lets us map the custom styles onto the tablet
// preview, too.
this.element.id = `${BASE_ID}-${key.id}`;
const kts = this.element.style;
// Some keyboards (such as `balochi_scientific`) do not _package_ a font but
// specify an extremely common one, such as Arial. In such cases, .kmw-key-text
// custom styling doesn't exist, relying on the layer object to simply specify
// the font-family.
const fontFamily = this.vkbd.currentLayer.element.style.fontFamily;
kts.fontFamily = key.key.spec.font || fontFamily;
kts.left = (keyRect.left - hostRect.left) + 'px';
kts.top = (keyRect.top - hostRect.top) + 'px';
kts.width = keyRect.width + 'px';
kts.height = keyRect.height + 'px';
this.element.style.display = 'block';
if(this.previewHost == previewHost) {
return;
}
const oldHost = this.preview;
this.previewHost = previewHost;
if(previewHost) {
this.preview = this.previewHost.element;
this.element.replaceChild(this.preview, oldHost);
previewHost.setCancellationHandler(() => this.show(null, false, null));
}
} else { // Hide the key preview
this.element.style.display = 'none';
this.element.className = `${BASE_CLASS}`;
this.previewHost = null;
const oldPreview = this.preview;
this.preview = document.createElement('div');
this.element.replaceChild(this.preview, oldPreview);
}
// Save the key preview state
this.key = key;
this.state = on;
}
}

View file

@ -19,6 +19,7 @@ interface EventMap {
export class GesturePreviewHost extends EventEmitter<EventMap> {
private readonly div: HTMLDivElement;
private readonly label: HTMLSpanElement;
private readonly hintLabel: HTMLSpanElement;
private readonly previewImgContainer: HTMLDivElement;
private flickPreviews = new Map<string, HTMLDivElement>;
@ -98,6 +99,24 @@ export class GesturePreviewHost extends EventEmitter<EventMap> {
previewImgContainer.appendChild(flickPreview);
});
}
const hintLabel = this.hintLabel = document.createElement('div');
hintLabel.className='kmw-key-popup-icon';
if(!isPhone) {
hintLabel.textContent = keySpec == keySpec.hintSrc ? keySpec.hint : keySpec.hintSrc?.text;
hintLabel.style.fontWeight= hintLabel.textContent == '\u2022' ? 'bold' : '';
}
// Default positioning puts it far too close to the flick-preview bit.
let yAdjustment = 0;
hintLabel.style.marginTop = `-${yAdjustment}px`;
// b/c multitap's border forces position shifting
let xAdjustment = 0;
hintLabel.style.marginRight = `-${xAdjustment}px`;
base.appendChild(hintLabel);
}
public refreshLayout() {
@ -119,6 +138,8 @@ export class GesturePreviewHost extends EventEmitter<EventMap> {
}
public scrollFlickPreview(x: number, y: number) {
this.clearHint();
const scrollStyle = this.previewImgContainer.style;
const edge = this.flickEdgeLength * FLICK_OVERFLOW_OFFSET;
@ -140,6 +161,10 @@ export class GesturePreviewHost extends EventEmitter<EventMap> {
this.previewImgContainer.classList.add('flick-clear');
}
public clearHint() {
this.hintLabel.classList.add('hint-clear');
}
public clearAll() {
this.clearFlick();
}

View file

@ -44,7 +44,8 @@ import OSKLayer from './keyboard-layout/oskLayer.js';
import OSKLayerGroup from './keyboard-layout/oskLayerGroup.js';
import { LengthStyle, ParsedLengthStyle } from './lengthStyle.js';
import { defaultFontSize, getFontSizeStyle } from './fontSizeUtils.js';
import InternalKeyTip from './input/gestures/browser/keytip.js';
import PhoneKeyTip from './input/gestures/browser/keytip.js';
import { TabletKeyTip } from './input/gestures/browser/tabletPreview.js';
import CommonConfiguration from './config/commonConfiguration.js';
import { DEFAULT_GESTURE_PARAMS, GestureParams, gestureSetForLayout } from './input/gestures/specsForLayout.js';
@ -1533,7 +1534,7 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
const keyCS = getComputedStyle(key);
const parsedHeight = Number.parseInt(keyCS.height, 10);
const parsedWidth = Number.parseInt(keyCS.width, 10);
const previewHost = new GesturePreviewHost(key, !!tip, parsedWidth, parsedHeight);
const previewHost = new GesturePreviewHost(key, this.device.formFactor == 'phone', parsedWidth, parsedHeight);
if (tip == null) {
const baseKey = key.key as OSKBaseKey;
@ -1551,11 +1552,13 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
* Create a key preview element for phone devices
*/
createKeyTip() {
if(this.device.formFactor == 'phone') {
if (this.keytip == null) {
if (this.keytip == null) {
if(this.device.formFactor == 'phone') {
// For now, should only be true (in production) when keyman.isEmbedded == true.
let constrainPopup = this.isEmbedded;
this.keytip = new InternalKeyTip(this, constrainPopup);
this.keytip = new PhoneKeyTip(this, constrainPopup);
} else {
this.keytip = new TabletKeyTip(this);
}
}

View file

@ -201,6 +201,7 @@
}
.tablet.ios .kmw-key-layer-group {background-color: #cfd3d9}
.tablet.ios .kmw-key.kmw-keypreview.kmw-preview-overlay { box-shadow: 0 0 5px 3px #cfd3d9 }
.tablet.ios .kmw-key {border: none; border-bottom: solid 1px #8a8d90; box-shadow:none; border-radius: 5px;}
.tablet.ios .kmw-key.kmw-key-default {color:#000;background-color:#fdfdfe;}
.tablet.ios .kmw-key.kmw-key-shift,
@ -214,6 +215,7 @@
/* Probably best to make this its own CSS that can be optionally included? */
@media (prefers-color-scheme: dark) {
.tablet.ios .kmw-key-layer-group {background-color: #0f1319}
.tablet.ios .kmw-key.kmw-keypreview.kmw-preview-overlay { box-shadow: 0 0 5px 3px #0f1319 }
.tablet.ios .kmw-key.kmw-key-default {color:#fff;background-color:#3d3d3e}
.tablet.ios .kmw-key.kmw-key-shift,
.tablet.ios .kmw-key.kmw-key-special {color:#fff;background-color:#595c62;}
@ -226,6 +228,7 @@
.tablet .kmw-key-row {-webkit-touch-callout:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:rgba(0,0,0,0);}
.tablet.android .kmw-key-layer-group {background-color: #333;}
.tablet.android .kmw-key.kmw-keypreview.kmw-preview-overlay { box-shadow: 0 0 5px 3px #333 }
.tablet.android .kmw-key {border: none; border-bottom: solid 1px #8a8d90; box-shadow:none; border-radius: 5px;}
.tablet.android .kmw-key.kmw-key-default {color:#fff;background-color:#777;}
.tablet.android .kmw-key.kmw-key-shift,
@ -346,6 +349,11 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f
z-index: 10002;
}
.kmw-keypreview {
z-index: 10002;
position: absolute;
}
#kmw-gesture-preview {
position: absolute;
display: block;