mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-13 17:37:31 +00:00
188 lines
No EOL
6.5 KiB
TypeScript
188 lines
No EOL
6.5 KiB
TypeScript
///<reference path="singleInputCapture.ts" />
|
|
|
|
namespace com.keyman.osk.embedded {
|
|
/**
|
|
* The GlobeHint is a specialized key tip that is presented above
|
|
* the globe key on first use of Keyman, to show the user how to
|
|
* switch input methods. The label is localizable text.
|
|
*/
|
|
export class GlobeHint implements osk.GlobeHint {
|
|
public readonly element: HTMLDivElement;
|
|
public state: boolean = false;
|
|
|
|
// -----
|
|
// | | <-- tip
|
|
// | x | <-- label
|
|
// |_ _|
|
|
// \ / <-- cap
|
|
// v
|
|
|
|
private readonly cap: HTMLDivElement;
|
|
private readonly tip: HTMLDivElement;
|
|
private readonly label: HTMLSpanElement;
|
|
|
|
private readonly vkbd: VisualKeyboard;
|
|
|
|
private docInputCatch?: SingleInputCapture;
|
|
private tipInputTrap?: SingleInputCapture;
|
|
|
|
private delayedDismissTimerHandle: number;
|
|
|
|
constructor(vkbd: VisualKeyboard) {
|
|
this.vkbd = vkbd;
|
|
|
|
let tipElement = this.element=document.createElement('div');
|
|
tipElement.className = 'kmw-globehint';
|
|
tipElement.id = 'kmw-globehint';
|
|
|
|
// 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-globehint-tip';
|
|
this.cap.className = 'kmw-globehint-cap';
|
|
this.label.className = 'kmw-globehint-label';
|
|
|
|
this.text = "Tap here to change keyboard.";
|
|
}
|
|
|
|
get text(): string {
|
|
return this.label.innerText;
|
|
}
|
|
|
|
set text(val: string) {
|
|
this.label.innerText = val;
|
|
}
|
|
|
|
private _show(key: KeyElement, on: boolean) {
|
|
let keyman = com.keyman.singleton;
|
|
|
|
// 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) {
|
|
this.element.style.display = 'none';
|
|
} else {
|
|
// 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;
|
|
|
|
let center = rkey.left + rkey.width/2;
|
|
|
|
let bubbleStyle = this.element.style;
|
|
|
|
// Roughly matches how the subkey positioning is set.
|
|
const _Box = this.vkbd.element.parentNode as HTMLDivElement;
|
|
const _BoxRect = _Box.getBoundingClientRect();
|
|
const keyRect = key.getBoundingClientRect();
|
|
let y = (keyRect.bottom - _BoxRect.top + 1);
|
|
|
|
// Width dimensions must be set explicitly to prevent clipping.
|
|
// We'll assume that the globe key is always positioned on the bottom row.
|
|
let oskBaseFontSize = new ParsedLengthStyle(keyman.osk.baseFontSize);
|
|
let bubbleWidth = Math.ceil(xWidth * 3);
|
|
|
|
bubbleStyle.bottom = Math.floor(keyman.osk.computedHeight - y) + 'px';
|
|
// CSS already defines transform: translateX(-50%) - this centers the element.
|
|
bubbleStyle.left = center + 'px';
|
|
|
|
this.tip.style.bottom = (rrow.height - 1) + 'px';
|
|
this.tip.style.width = bubbleWidth + 'px';
|
|
this.tip.style.pointerEvents = on ? 'auto' : 'none';
|
|
|
|
// Adjust shape if at edges
|
|
|
|
// how much width lies outside the range of the base key, per side
|
|
const xOverflow = (bubbleWidth - xWidth) / 2;
|
|
|
|
const capWidth = xWidth * 2 / 3;
|
|
|
|
const leftEdgeMargin = xLeft;
|
|
const rightEdgeMargin = window.innerWidth - (xLeft + xWidth);
|
|
|
|
if(leftEdgeMargin < xOverflow) {
|
|
// The overflow would be clipped by the left edge when centered,
|
|
// so we "offset" this part to keep it within screen bounds.
|
|
this.tip.style.transform = 'translateX(' + (xOverflow - leftEdgeMargin + 1) + 'px)';
|
|
} else if(rightEdgeMargin < xOverflow) {
|
|
// The overflow would be clipped by the right edge when centered,
|
|
// so we "offset" this part to keep it within screen bounds.
|
|
this.tip.style.transform = 'translateX(-' + (xOverflow - rightEdgeMargin + 1) + 'px)';
|
|
}
|
|
|
|
if(!oskBaseFontSize.absolute) {
|
|
this.element.style.fontSize = keyman.osk.baseFontSize;
|
|
}
|
|
|
|
let capHeight = xHeight / 3;
|
|
|
|
this.cap.style.bottom = (rrow.height - capHeight) + 'px';
|
|
this.cap.style.width = '0px';
|
|
this.cap.style.height = '0px';
|
|
this.cap.style.borderLeftWidth = (capWidth / 2) + 'px';
|
|
this.cap.style.borderRightWidth = (capWidth / 2) + 'px';
|
|
this.cap.style.borderTopWidth = capHeight + 'px';
|
|
|
|
bubbleStyle.display = 'block';
|
|
}
|
|
|
|
// Save the key preview state
|
|
this.state = on;
|
|
}
|
|
|
|
private clearCatchers() {
|
|
// disable input-capture system
|
|
if(this.docInputCatch) {
|
|
this.docInputCatch.cancel();
|
|
this.docInputCatch = null;
|
|
}
|
|
|
|
if(this.tipInputTrap) {
|
|
this.tipInputTrap.cancel();
|
|
this.tipInputTrap = null;
|
|
}
|
|
}
|
|
|
|
public show(key: KeyElement, onDismiss?: () => void) {
|
|
this._show(key, true);
|
|
|
|
const captureHandler = () => {
|
|
// Prevent duplicated dismissal calls that'd otherwise result when the tip itself receives a touch.
|
|
window.clearTimeout(this.delayedDismissTimerHandle);
|
|
this.hide(key);
|
|
onDismiss();
|
|
};
|
|
|
|
if(onDismiss) {
|
|
// enable input-capture system
|
|
this.tipInputTrap = new SingleInputCapture(this.tip, true, captureHandler);
|
|
this.docInputCatch = new SingleInputCapture(document.body, false, () => {
|
|
this.delayedDismissTimerHandle = window.setTimeout(() => {
|
|
captureHandler();
|
|
}, 1); // we need to delay processing so that events directly on the tip get priority first!
|
|
});
|
|
} else {
|
|
// disable any previously-set input-captures as the incoming
|
|
// (thus, most-recent) call didn't request one.
|
|
this.clearCatchers();
|
|
}
|
|
}
|
|
|
|
public hide(key: KeyElement) {
|
|
this._show(key, false);
|
|
|
|
this.clearCatchers();
|
|
}
|
|
}
|
|
} |