From d1e5c93a08e956b26281518a6630f45c7fb0e567 Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 14 May 2025 12:38:13 +0200 Subject: [PATCH] Improve tooltips by using mutation observers --- core/static/bundled/core/tooltips-index.ts | 72 +++++++++++++++++----- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/core/static/bundled/core/tooltips-index.ts b/core/static/bundled/core/tooltips-index.ts index f9721ca4..eb0a201a 100644 --- a/core/static/bundled/core/tooltips-index.ts +++ b/core/static/bundled/core/tooltips-index.ts @@ -18,7 +18,7 @@ import { * Note: placement are suggestions and the position could change if the popup gets * outside of the screen. * - * You can customize your tooltip by passing additionnal classes or ids to it + * You can customize your tooltip by passing additional classes or ids to it * You can use `tooltip-class` and `tooltip-id` to add additional elements to the * `class` and `id` attribute of the generated tooltip * @@ -32,7 +32,7 @@ import { type Status = "open" | "close"; -const tooltips = new Map(); +const tooltips: Map = new Map(); function getPosition(element: HTMLElement): Placement | "auto" { const position = element.getAttribute("tooltip-position"); @@ -93,12 +93,10 @@ function getTooltip(element: HTMLElement) { return tooltip; } -addEventListener("mouseover", (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!target.hasAttribute("tooltip")) { - return; - } - +function tooltipMouseover(event: MouseEvent) { + // We get the closest tooltip to have a consistent behavior + // when hovering over a child element of a tooltip marked element + const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement; const tooltip = getTooltip(target); updateTooltip(target, tooltip, "open"); @@ -113,13 +111,57 @@ addEventListener("mouseover", (event: MouseEvent) => { }); document.body.append(tooltip); -}); - -addEventListener("mouseout", (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!target.hasAttribute("tooltip")) { - return; - } +} +function tooltipMouseout(event: MouseEvent) { + // We get the closest tooltip to have a consistent behavior + // when hovering over a child element of a tooltip marked element + const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement; updateTooltip(target, getTooltip(target), "close"); +} + +window.addEventListener("DOMContentLoaded", () => { + for (const el of document.querySelectorAll("[tooltip]")) { + el.addEventListener("mouseover", tooltipMouseover); + el.addEventListener("mouseout", tooltipMouseout); + } +}); + +// Add / remove callback when tooltip attribute is added / removed +new MutationObserver((mutations: MutationRecord[]) => { + for (const mutation of mutations) { + const target = mutation.target as HTMLElement; + target.removeEventListener("mouseover", tooltipMouseover); + target.removeEventListener("mouseout", tooltipMouseout); + if (target.hasAttribute("tooltip")) { + target.addEventListener("mouseover", tooltipMouseover); + target.addEventListener("mouseout", tooltipMouseout); + } + } +}).observe(document.body, { + attributes: true, + attributeFilter: ["tooltip"], + subtree: true, +}); + +// Remove orphan tooltips +new MutationObserver((mutations: MutationRecord[]) => { + for (const mutation of mutations) { + for (const node of mutation.removedNodes) { + if (node.nodeType !== node.ELEMENT_NODE) { + continue; + } + const target = node as HTMLElement; + if (!target.hasAttribute("tooltip")) { + continue; + } + if (tooltips.has(target)) { + tooltips.get(target).remove(); + tooltips.delete(target); + } + } + } +}).observe(document.body, { + subtree: true, + childList: true, });