77 lines
2.3 KiB
JavaScript
77 lines
2.3 KiB
JavaScript
// @ts-check
|
|||
|
|
/**
|
||
|
|
* Tooltips: a full explanation of a button, shown below it after the
|
||
|
|
* pointer (or a pen hovering above the screen) has rested on it for a
|
||
|
|
* moment. The buttons themselves carry terse labels; this is where the
|
||
|
|
* explanation lives.
|
||
|
|
*
|
||
|
|
* Any element with a `data-help` attribute gets one. Elements added later
|
||
|
|
* are covered too, since the listeners are on the document.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const DELAY_MS = 1000;
|
||
|
|
const GAP_PX = 6;
|
||
|
|
|
||
|
|
/** @type {HTMLElement | null} */
|
||
|
|
let tooltip = null;
|
||
|
|
/** @type {number | null} */
|
||
|
|
let timer = null;
|
||
|
|
/** @type {Element | null} */
|
||
|
|
let shownFor = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {EventTarget | null} target
|
||
|
|
* @returns {HTMLElement | null} The element with help under the pointer.
|
||
|
|
*/
|
||
|
|
function helpElement(target) {
|
||
|
|
return target instanceof Element ? target.closest('[data-help]') : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @param {HTMLElement} element */
|
||
|
|
function show(element) {
|
||
|
|
if (!tooltip) {
|
||
|
|
tooltip = document.createElement('div');
|
||
|
|
tooltip.className = 'tooltip';
|
||
|
|
tooltip.setAttribute('role', 'tooltip');
|
||
|
|
document.body.append(tooltip);
|
||
|
|
}
|
||
|
|
tooltip.textContent = element.getAttribute('data-help');
|
||
|
|
tooltip.hidden = false;
|
||
|
|
shownFor = element;
|
||
|
|
|
||
|
|
// Below the element, aligned with its left edge, kept within the window.
|
||
|
|
const box = element.getBoundingClientRect();
|
||
|
|
const width = tooltip.offsetWidth;
|
||
|
|
const left = Math.max(GAP_PX, Math.min(box.left, window.innerWidth - width - GAP_PX));
|
||
|
|
tooltip.style.left = `${left}px`;
|
||
|
|
tooltip.style.top = `${box.bottom + GAP_PX}px`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function hide() {
|
||
|
|
if (timer !== null) clearTimeout(timer);
|
||
|
|
timer = null;
|
||
|
|
shownFor = null;
|
||
|
|
if (tooltip) tooltip.hidden = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function enableTooltips() {
|
||
|
|
document.addEventListener('pointerover', (event) => {
|
||
|
|
const element = helpElement(event.target);
|
||
|
|
if (!element) {
|
||
|
|
hide();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (element === shownFor) return;
|
||
|
|
hide();
|
||
|
|
timer = setTimeout(() => { timer = null; show(element); }, DELAY_MS);
|
||
|
|
});
|
||
|
|
document.addEventListener('pointerleave', hide);
|
||
|
|
document.addEventListener('pointerdown', hide);
|
||
|
|
document.addEventListener('focusin', (event) => {
|
||
|
|
const element = helpElement(event.target);
|
||
|
|
if (element) show(element);
|
||
|
|
});
|
||
|
|
document.addEventListener('focusout', hide);
|
||
|
|
// Typing means the user's attention is on the keys, not the tooltip.
|
||
|
|
document.addEventListener('keydown', hide);
|
||
|
|
}
|