export class Overlay extends HTMLElement {} /** @type Overlays */ export let overlays = null; export class Overlays extends HTMLElement { overlays = new Set(); connectedCallback() { overlays = this; } disconnectedCallback() { overlays = null; } open(overlay) { this.appendChild(overlay); this.overlays.add(overlay); return overlay; } close(overlay) { this.removeChild(overlay); this.overlays.delete(overlay); } } customElements.define("th-overlays", Overlays); export class Tooltip extends Overlay { constructor(element, side) { super(); this.element = element; this.side = side; } connectedCallback() { this.role = "tooltip"; this.setAttribute("th-side", this.side); let bb = this.element.getBoundingClientRect(); switch (this.side) { // NOTE: The elements are positioned directly at (width / 2) or (height / 2), because // they are transformed to the centre over on the CSS side. case "bottom": this.style.left = `${bb.left + bb.width / 2}px`; this.style.top = `${bb.bottom}px`; break; case "left": this.style.left = `${bb.left}px`; this.style.top = `${bb.top + bb.height / 2}px`; break; default: console.error(`th-tooltip: unknown attachment side ${this.side}`); break; } this.addEventListener("transitionend", (event) => { if (event.propertyName == "opacity") { let style = getComputedStyle(this); if (style.opacity < 0.01) { this.dispatchEvent(new Event(".close")); } } }); // Timeout is zero because we just want to execute this later, to be definitely sure // the transition plays out. setTimeout(() => this.classList.add("transitioned-in"), 0); } close() { this.classList.remove("transitioned-in"); // NOTE: In case there is no transition, we may need to trigger the close event immediately. let style = getComputedStyle(this); if (style.opacity < 0.01) { this.dispatchEvent(new Event(".close")); } } } customElements.define("th-tooltip", Tooltip); export function attachTooltip(element, makeTooltip) { let show = () => { let tooltip = overlays.open(makeTooltip(element)); let abortController = new AbortController(); tooltip.addEventListener(".close", () => { overlays.close(tooltip); abortController.abort(); console.log("closing tooltip"); }); window.addEventListener("wheel", () => tooltip.close(), { signal: abortController.signal, passive: true, }); element.addEventListener("mouseleave", () => tooltip.close(), { signal: abortController.signal, }); }; return { show, showOnHover() { element.addEventListener("mouseenter", show); return this; }, }; }