// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo // Bootstrap tooltip and help-popover initializer (help-init.js; design D23 // "Help is disclosed by click-opened popovers"; acceptance-fixes, // 2026-09-02, replacing tooltip-init.js). // // Two Bootstrap primitives, one initializer: // // [data-bs-toggle="tooltip"] — a hover/focus tooltip. Bootstrap's data // API auto-wires dropdowns, offcanvas, collapse, and modals, but NOT // tooltips or popovers: each needs an explicit instance, or the browser // shows only the native `title` on hover and nothing on keyboard focus. // The disabled-control part (ui_disabled_control.html; design-system §3 // "Disabled controls") relies on the tooltip appearing on focus so a // keyboard user reads the reason a control is unavailable — this is a // reason on a control, the one case hover tooltips remain for. // // [data-bs-toggle="popover"] — the helpIcon part's click-opened help // popover (ui_help_icon.html; design D23). Titled with the field or // column it explains (its data-bs-title), with a close button in the // header built as a real DOM element — never string concatenation of // the label or the close button's markup, which would run whatever a // label might carry through innerHTML. Escape and a click outside any // open popover or its own trigger also close it; Bootstrap's // click-trigger popovers do not dismiss on an outside click on their // own. // // Re-runs after every htmx swap via htmx.onLoad; getOrCreateInstance is // idempotent, so re-processing an already-wired node is a no-op. CSP-safe: // Bootstrap and Popper position tooltips/popovers through CSSOM // (element.style), never inline style attributes, so the strict style-src // holds; the popover title is assembled with document.createElement, never // innerHTML, so the strict script-src holds too. (function () { "use strict"; function initTooltips(root) { if (!window.bootstrap || !window.bootstrap.Tooltip) { return; } var nodes = (root || document).querySelectorAll('[data-bs-toggle="tooltip"]'); Array.prototype.forEach.call(nodes, function (el) { window.bootstrap.Tooltip.getOrCreateInstance(el); }); } // popoverTitle returns the title-builder Bootstrap calls each time it // renders this trigger's popover: a span holding the label text and a // close button that hides this same trigger's popover instance. Built // as DOM nodes (never a string), so the label's text can never be read // back as markup. function popoverTitle(el, label) { return function () { var wrap = document.createElement("span"); wrap.className = "d-flex align-items-center justify-content-between gap-2"; var text = document.createElement("span"); text.textContent = label; wrap.appendChild(text); var close = document.createElement("button"); close.type = "button"; close.className = "btn-close"; close.setAttribute("aria-label", "Close"); close.addEventListener("click", function () { var instance = window.bootstrap.Popover.getInstance(el); if (instance) { instance.hide(); } }); wrap.appendChild(close); return wrap; }; } function initPopovers(root) { if (!window.bootstrap || !window.bootstrap.Popover) { return; } var nodes = (root || document).querySelectorAll('[data-bs-toggle="popover"]'); Array.prototype.forEach.call(nodes, function (el) { var label = el.getAttribute("data-bs-title") || ""; window.bootstrap.Popover.getOrCreateInstance(el, { html: true, title: popoverTitle(el, label), }); }); } function hideAllPopovers() { if (!window.bootstrap || !window.bootstrap.Popover) { return; } var triggers = document.querySelectorAll('[data-bs-toggle="popover"]'); Array.prototype.forEach.call(triggers, function (el) { var instance = window.bootstrap.Popover.getInstance(el); if (instance) { instance.hide(); } }); } // Escape closes every open popover, wherever focus is. document.addEventListener("keydown", function (event) { if (event.key === "Escape") { hideAllPopovers(); } }); // A click outside any popover and outside any popover trigger closes // every open popover. A click on a trigger (this one's or another's) is // left to Bootstrap's own click-toggle handling, and a click inside an // open popover (e.g. selecting its text) must not close it. document.addEventListener("click", function (event) { var target = event.target; if (target && target.closest && (target.closest('[data-bs-toggle="popover"]') || target.closest(".popover"))) { return; } hideAllPopovers(); }); function init(root) { initTooltips(root); initPopovers(root); } if (window.htmx && typeof window.htmx.onLoad === "function") { window.htmx.onLoad(function (content) { init(content); }); } else if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { init(document); }); } else { init(document); } })();