main
js 127 lines 3.62 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2
3 let bootstrapTooltipObserver = null;
4
5 function ensureBootstrapTooltip(element) {
6 if (!element || !(element instanceof Element)) return;
7
8 const bs = globalThis.bootstrap;
9 if (!bs?.Tooltip) return;
10
11 const existing = bs.Tooltip.getInstance(element);
12 const title = element.getAttribute("title") || element.getAttribute("data-bs-original-title");
13
14 if (!title) return;
15
16 if (existing) {
17 if (element.getAttribute("title")) {
18 element.setAttribute("data-bs-original-title", title);
19 element.removeAttribute("title");
20 }
21 existing.setContent({ ".tooltip-inner": title });
22 return;
23 }
24
25 if (element.getAttribute("title")) {
26 element.setAttribute("data-bs-original-title", title);
27 element.removeAttribute("title");
28 }
29
30 element.setAttribute("data-bs-toggle", "tooltip");
31 element.setAttribute("data-bs-trigger", "hover");
32 element.setAttribute("data-bs-tooltip-initialized", "true");
33 new bs.Tooltip(element, {
34 delay: { show: 0, hide: 0 },
35 trigger: "hover",
36 });
37 }
38
39 function initBootstrapTooltips(root = document) {
40 if (!globalThis.bootstrap?.Tooltip) return;
41 const selector = "[title], [data-bs-original-title]";
42 const rootIsElement = root instanceof Element;
43 const tooltipTargets = [
44 ...(rootIsElement && root.matches(selector) ? [root] : []),
45 ...Array.from(root.querySelectorAll(selector)),
46 ];
47 tooltipTargets.forEach((element) => ensureBootstrapTooltip(element));
48 }
49
50 function disposeBootstrapTooltip(element) {
51 const instance = globalThis.bootstrap?.Tooltip?.getInstance(element);
52 if (!instance) return;
53 try {
54 instance.dispose();
55 } catch {
56 // Bootstrap 5 can throw while disposing an already-torn-down tooltip node.
57 }
58 }
59
60 function observeBootstrapTooltips() {
61 if (!globalThis.bootstrap?.Tooltip) return;
62
63 // Prevent multiple observers
64 if (bootstrapTooltipObserver) return;
65
66 bootstrapTooltipObserver = new MutationObserver((mutations) => {
67 mutations.forEach((mutation) => {
68 if (
69 mutation.type === "attributes" &&
70 (mutation.attributeName === "title" ||
71 mutation.attributeName === "data-bs-original-title")
72 ) {
73 ensureBootstrapTooltip(mutation.target);
74 return;
75 }
76
77 if (mutation.type === "childList") {
78 // Check removed nodes for tooltip cleanup
79 mutation.removedNodes.forEach((node) => {
80 if (!(node instanceof Element)) return;
81 const tooltipElements = node.matches?.("[data-bs-tooltip-initialized]")
82 ? [node]
83 : Array.from(
84 node.querySelectorAll?.("[data-bs-tooltip-initialized]") || []
85 );
86 tooltipElements.forEach((el) => {
87 if (el.isConnected) return;
88 disposeBootstrapTooltip(el);
89 });
90 });
91
92 mutation.addedNodes.forEach((node) => {
93 if (!(node instanceof Element)) return;
94 if (
95 node.matches("[title], [data-bs-original-title]") ||
96 node.querySelector("[title], [data-bs-original-title]")
97 ) {
98 initBootstrapTooltips(node);
99 }
100 });
101 }
102 });
103 });
104
105 bootstrapTooltipObserver.observe(document.body, {
106 childList: true,
107 subtree: true,
108 attributes: true,
109 attributeFilter: ["title", "data-bs-original-title"],
110 });
111 }
112
113 function cleanupTooltipObserver() {
114 if (bootstrapTooltipObserver) {
115 bootstrapTooltipObserver.disconnect();
116 bootstrapTooltipObserver = null;
117 }
118 }
119
120 export const store = createStore("tooltips", {
121 init() {
122 initBootstrapTooltips();
123 observeBootstrapTooltips();
124 },
125
126 cleanup: cleanupTooltipObserver,
127 });