mnfst 0.5.57 → 0.5.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/manifest.code.js +7 -0
- package/lib/manifest.colorpicker.css +503 -0
- package/lib/manifest.css +418 -298
- package/lib/manifest.dropdown.css +2 -2
- package/lib/manifest.dropdowns.js +57 -5
- package/lib/manifest.js +26 -39
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.tooltips.js +240 -289
- package/lib/manifest.utilities.css +15 -1
- package/package.json +1 -1
package/lib/manifest.tooltips.js
CHANGED
|
@@ -1,360 +1,311 @@
|
|
|
1
|
-
/* Manifest Tooltips
|
|
1
|
+
/* Manifest Tooltips — singleton architecture.
|
|
2
|
+
*
|
|
3
|
+
* Instead of creating one <div popover="hint"> per x-tooltip trigger, this plugin
|
|
4
|
+
* maintains ONE tooltip element per popover host (usually just document.body plus
|
|
5
|
+
* optionally one per open popover). Every trigger with x-tooltip becomes a
|
|
6
|
+
* lightweight content provider that asks the shared controller to show its text,
|
|
7
|
+
* anchored to that trigger.
|
|
8
|
+
*
|
|
9
|
+
* Why: N triggers × 1 tooltip each = N extra DOM nodes that are empty 99% of the
|
|
10
|
+
* time. For dense UIs like colorpicker libraries (~300+ swatches × 20 pickers),
|
|
11
|
+
* this is the difference between a usable page and a laggy one.
|
|
12
|
+
*/
|
|
2
13
|
|
|
3
|
-
//
|
|
14
|
+
// Hover delay from CSS var (with time-unit parsing). Defaults to 500ms.
|
|
4
15
|
function getTooltipHoverDelay(element) {
|
|
5
|
-
// Try to get the value from the element first, then from document root
|
|
6
16
|
let computedStyle = getComputedStyle(element);
|
|
7
17
|
let delayValue = computedStyle.getPropertyValue('--tooltip-hover-delay').trim();
|
|
8
|
-
|
|
9
18
|
if (!delayValue) {
|
|
10
|
-
// If not found on element, check document root
|
|
11
19
|
computedStyle = getComputedStyle(document.documentElement);
|
|
12
20
|
delayValue = computedStyle.getPropertyValue('--tooltip-hover-delay').trim();
|
|
13
21
|
}
|
|
14
|
-
|
|
15
|
-
if (!delayValue) {
|
|
16
|
-
return 500; // Default to 500ms if not set
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Parse CSS time value (supports various time units)
|
|
22
|
+
if (!delayValue) return 500;
|
|
20
23
|
const timeValue = parseFloat(delayValue);
|
|
21
|
-
|
|
22
|
-
if (delayValue.endsWith('s'))
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return timeValue * 60 * 1000; // Convert minutes to milliseconds
|
|
28
|
-
} else if (delayValue.endsWith('h')) {
|
|
29
|
-
return timeValue * 60 * 60 * 1000; // Convert hours to milliseconds
|
|
30
|
-
} else if (delayValue.endsWith('min')) {
|
|
31
|
-
return timeValue * 60 * 1000; // Convert minutes to milliseconds
|
|
32
|
-
} else if (delayValue.endsWith('sec')) {
|
|
33
|
-
return timeValue * 1000; // Convert seconds to milliseconds
|
|
34
|
-
} else if (delayValue.endsWith('second')) {
|
|
35
|
-
return timeValue * 1000; // Convert seconds to milliseconds
|
|
36
|
-
} else if (delayValue.endsWith('minute')) {
|
|
37
|
-
return timeValue * 60 * 1000; // Convert minutes to milliseconds
|
|
38
|
-
} else if (delayValue.endsWith('hour')) {
|
|
39
|
-
return timeValue * 60 * 60 * 1000; // Convert hours to milliseconds
|
|
40
|
-
} else {
|
|
41
|
-
// If no unit, assume milliseconds (backward compatibility)
|
|
42
|
-
return timeValue;
|
|
43
|
-
}
|
|
24
|
+
if (delayValue.endsWith('ms')) return timeValue;
|
|
25
|
+
if (delayValue.endsWith('s')) return timeValue * 1000;
|
|
26
|
+
if (delayValue.endsWith('min') || delayValue.endsWith('m') || delayValue.endsWith('minute')) return timeValue * 60 * 1000;
|
|
27
|
+
if (delayValue.endsWith('h') || delayValue.endsWith('hour')) return timeValue * 60 * 60 * 1000;
|
|
28
|
+
if (delayValue.endsWith('sec') || delayValue.endsWith('second')) return timeValue * 1000;
|
|
29
|
+
return timeValue; // unitless → ms
|
|
44
30
|
}
|
|
45
31
|
|
|
46
|
-
|
|
47
|
-
const ANCHOR_RESTORE_DELAY_MS = 2000;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* DOM parent for the tooltip popover. Must be the same popover subtree as the trigger when the trigger
|
|
51
|
-
* lives inside menu/dialog/etc.; otherwise CSS anchor positioning cannot resolve (tooltip in body + anchor
|
|
52
|
-
* inside top-layer popover → invalid position-anchor, jump to origin).
|
|
53
|
-
*/
|
|
32
|
+
// Popover host for anchor positioning: the closest top-layer popover ancestor, or body.
|
|
54
33
|
function getTooltipHostForTrigger(triggerEl) {
|
|
55
34
|
return triggerEl.closest('[popover]') || document.body;
|
|
56
35
|
}
|
|
57
36
|
|
|
58
37
|
function initializeTooltipPlugin() {
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
39
|
+
// Chain mode: if another tooltip was dismissed this recently, the next one
|
|
40
|
+
// shows immediately (no hover delay). Also used to skip the hide-show flicker
|
|
41
|
+
// when gliding across many triggers — the singleton just re-anchors.
|
|
42
|
+
const TOOLTIP_CHAIN_GRACE_MS = 250;
|
|
43
|
+
let _lastTooltipHideTime = 0;
|
|
44
|
+
const markTooltipHidden = () => { _lastTooltipHideTime = Date.now(); };
|
|
45
|
+
const isInChainWindow = () => (Date.now() - _lastTooltipHideTime) < TOOLTIP_CHAIN_GRACE_MS;
|
|
46
|
+
|
|
47
|
+
// ---- Singletons per host ----
|
|
48
|
+
//
|
|
49
|
+
// Most pages only need one singleton (under document.body). Open popovers (menus,
|
|
50
|
+
// dialogs) require their own singleton because CSS anchor positioning can't resolve
|
|
51
|
+
// across the top-layer boundary. We create them lazily on first use and keep them
|
|
52
|
+
// (small, hidden <div>s) for the life of the host.
|
|
53
|
+
const _singletons = new WeakMap();
|
|
54
|
+
|
|
55
|
+
function getSingleton(host) {
|
|
56
|
+
let s = _singletons.get(host);
|
|
57
|
+
if (s) return s;
|
|
58
|
+
const el = document.createElement('div');
|
|
59
|
+
el.setAttribute('popover', 'hint');
|
|
60
|
+
el.className = 'tooltip';
|
|
61
|
+
host.appendChild(el);
|
|
62
|
+
s = {
|
|
63
|
+
el,
|
|
64
|
+
host,
|
|
65
|
+
activeTrigger: null,
|
|
66
|
+
currentPositions: [],
|
|
67
|
+
currentAnchorName: null
|
|
68
|
+
};
|
|
69
|
+
_singletons.set(host, s);
|
|
70
|
+
return s;
|
|
71
|
+
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
getTooltipContent = evaluateLater(`'${escapedExpression}'`);
|
|
85
|
-
} else if (expression.includes('+') || expression.includes('`') || expression.includes('${')) {
|
|
86
|
-
// Try to evaluate as a dynamic expression
|
|
87
|
-
getTooltipContent = evaluateLater(expression);
|
|
73
|
+
// Restore a trigger's original anchor-name (captured before we overrode it).
|
|
74
|
+
// Scheduled with a long delay so the anchor stays valid through popover transitions.
|
|
75
|
+
const _pendingAnchorRestores = new WeakMap(); // trigger → timeoutId
|
|
76
|
+
const ANCHOR_RESTORE_DELAY_MS = 2000;
|
|
77
|
+
|
|
78
|
+
function scheduleAnchorRestore(trigger) {
|
|
79
|
+
const existing = _pendingAnchorRestores.get(trigger);
|
|
80
|
+
if (existing) clearTimeout(existing);
|
|
81
|
+
const id = setTimeout(() => {
|
|
82
|
+
_pendingAnchorRestores.delete(trigger);
|
|
83
|
+
if (trigger._tooltipOriginalAnchor) {
|
|
84
|
+
trigger.style.setProperty('anchor-name', trigger._tooltipOriginalAnchor);
|
|
88
85
|
} else {
|
|
89
|
-
|
|
90
|
-
getTooltipContent = evaluateLater(`'${expression}'`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
effect(() => {
|
|
95
|
-
// Generate a unique ID for the tooltip
|
|
96
|
-
const tooltipCode = Math.random().toString(36).substr(2, 9);
|
|
97
|
-
const tooltipId = `tooltip-${tooltipCode}`;
|
|
98
|
-
|
|
99
|
-
// Store the original popovertarget if it exists, or check for x-dropdown
|
|
100
|
-
let originalTarget = el.getAttribute('popovertarget');
|
|
101
|
-
|
|
102
|
-
// If no popovertarget but has x-dropdown, that will become the target
|
|
103
|
-
if (!originalTarget && el.hasAttribute('x-dropdown')) {
|
|
104
|
-
originalTarget = el.getAttribute('x-dropdown');
|
|
86
|
+
trigger.style.removeProperty('anchor-name');
|
|
105
87
|
}
|
|
88
|
+
}, ANCHOR_RESTORE_DELAY_MS);
|
|
89
|
+
_pendingAnchorRestores.set(trigger, id);
|
|
90
|
+
}
|
|
91
|
+
function cancelAnchorRestore(trigger) {
|
|
92
|
+
const id = _pendingAnchorRestores.get(trigger);
|
|
93
|
+
if (id) { clearTimeout(id); _pendingAnchorRestores.delete(trigger); }
|
|
94
|
+
}
|
|
106
95
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
96
|
+
// ---- Controller ----
|
|
97
|
+
//
|
|
98
|
+
// Single pending-show timer shared across the whole plugin. If a trigger arms a
|
|
99
|
+
// show and the user moves to another trigger before it fires, the first timer is
|
|
100
|
+
// cancelled in favor of the new one. If the singleton is already visible, the new
|
|
101
|
+
// trigger updates it in place (chain mode) — no hide/show flicker.
|
|
102
|
+
|
|
103
|
+
let _showTimer = null;
|
|
104
|
+
let _pendingTrigger = null;
|
|
105
|
+
// Hide is deferred briefly so an incoming show on a different trigger can take
|
|
106
|
+
// over (chain-mode glide) instead of producing a hide/show flicker.
|
|
107
|
+
let _hideTimer = null;
|
|
108
|
+
const HIDE_DEFER_MS = 60;
|
|
109
|
+
|
|
110
|
+
function cancelPendingShow() {
|
|
111
|
+
if (_showTimer) { clearTimeout(_showTimer); _showTimer = null; }
|
|
112
|
+
_pendingTrigger = null;
|
|
113
|
+
}
|
|
114
|
+
function cancelPendingHide() {
|
|
115
|
+
if (_hideTimer) { clearTimeout(_hideTimer); _hideTimer = null; }
|
|
116
|
+
}
|
|
112
117
|
|
|
113
|
-
// Store the original anchor name if it exists
|
|
114
|
-
const originalAnchorName = el.style.getPropertyValue('anchor-name');
|
|
115
|
-
const tooltipAnchor = `--tooltip-${tooltipCode}`;
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// Update the singleton to point at a trigger (anchor, content, classes) and show it.
|
|
120
|
+
// Switches between triggers happen by re-anchoring — no positional animation. Any
|
|
121
|
+
// previous transform state is cleared so the tooltip sits squarely at its anchor.
|
|
122
|
+
function showSingletonFor(trigger, contentHtml, positions) {
|
|
123
|
+
const host = getTooltipHostForTrigger(trigger);
|
|
124
|
+
const s = getSingleton(host);
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
// Clear any residual transform state (defensive — should already be clean)
|
|
127
|
+
s.el.style.transition = '';
|
|
128
|
+
s.el.style.translate = '';
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
// Capture the trigger's original anchor-name so we can restore it later.
|
|
131
|
+
if (!trigger._tooltipOriginalAnchorCaptured) {
|
|
132
|
+
trigger._tooltipOriginalAnchor = trigger.style.getPropertyValue('anchor-name') || '';
|
|
133
|
+
trigger._tooltipOriginalAnchorCaptured = true;
|
|
134
|
+
}
|
|
135
|
+
cancelAnchorRestore(trigger);
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let restoreAnchorTimeout = null;
|
|
138
|
-
let anchorRestoreGeneration = 0;
|
|
139
|
-
let isMouseDown = false;
|
|
140
|
-
let isDynamic = expression.includes('+') || expression.includes('`') || expression.includes('${') || expression.startsWith('$x.');
|
|
141
|
-
let isUpdatingContent = false;
|
|
142
|
-
|
|
143
|
-
function restoreOriginalAnchor() {
|
|
144
|
-
if (el._originalAnchorName) {
|
|
145
|
-
el.style.setProperty('anchor-name', el._originalAnchorName);
|
|
146
|
-
} else {
|
|
147
|
-
el.style.removeProperty('anchor-name');
|
|
148
|
-
}
|
|
149
|
-
}
|
|
137
|
+
// Update position classes on the singleton. These drive the CSS positioning
|
|
138
|
+
// variants (top, bottom-end, etc.) defined in manifest.tooltip.css.
|
|
139
|
+
if (s.currentPositions.length) s.el.classList.remove(s.currentPositions.join('-'));
|
|
140
|
+
if (positions.length) s.el.classList.add(positions.join('-'));
|
|
141
|
+
s.currentPositions = positions;
|
|
150
142
|
|
|
151
|
-
|
|
152
|
-
anchorRestoreGeneration += 1;
|
|
153
|
-
if (restoreAnchorTimeout !== null) {
|
|
154
|
-
clearTimeout(restoreAnchorTimeout);
|
|
155
|
-
restoreAnchorTimeout = null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
143
|
+
s.el.innerHTML = contentHtml || '';
|
|
158
144
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
145
|
+
// Anchor binding: give the trigger a unique anchor-name, point the singleton at it.
|
|
146
|
+
if (!trigger._tooltipAnchorName) {
|
|
147
|
+
const code = Math.random().toString(36).slice(2, 9);
|
|
148
|
+
trigger._tooltipAnchorName = `--tooltip-trigger-${code}`;
|
|
149
|
+
}
|
|
150
|
+
const anchorName = trigger._tooltipAnchorName;
|
|
151
|
+
trigger.style.setProperty('anchor-name', anchorName);
|
|
152
|
+
void trigger.offsetHeight; // reflow so anchor-name registers
|
|
153
|
+
s.el.style.setProperty('position-anchor', anchorName);
|
|
169
154
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
155
|
+
s.activeTrigger = trigger;
|
|
156
|
+
s.currentAnchorName = anchorName;
|
|
173
157
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (tooltip.parentNode !== host) {
|
|
177
|
-
host.appendChild(tooltip);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
158
|
+
if (!s.el.matches(':popover-open')) s.el.showPopover();
|
|
159
|
+
}
|
|
180
160
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
tooltip.innerHTML = content || '';
|
|
189
|
-
// Use requestAnimationFrame to ensure DOM update completes before allowing next update
|
|
190
|
-
requestAnimationFrame(() => {
|
|
191
|
-
isUpdatingContent = false;
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
};
|
|
161
|
+
// Hide the singleton that's currently showing (if any), regardless of host.
|
|
162
|
+
function hideAnySingleton() {
|
|
163
|
+
document.querySelectorAll('.tooltip[popover="hint"]:popover-open').forEach(el => {
|
|
164
|
+
try { el.hidePopover(); } catch {}
|
|
165
|
+
});
|
|
166
|
+
markTooltipHidden();
|
|
167
|
+
}
|
|
195
168
|
|
|
196
|
-
|
|
197
|
-
// For dynamic content, set it only when showing to prevent double updates from reactivity
|
|
198
|
-
if (!isDynamic) {
|
|
199
|
-
updateTooltipContent();
|
|
200
|
-
}
|
|
169
|
+
// ---- Directive ----
|
|
201
170
|
|
|
202
|
-
|
|
203
|
-
cancelScheduledAnchorRestore();
|
|
204
|
-
clearTimeout(showTimeout);
|
|
205
|
-
if (!isMouseDown) {
|
|
206
|
-
const hoverDelay = getTooltipHoverDelay(el);
|
|
207
|
-
showTimeout = setTimeout(() => {
|
|
208
|
-
// Check if user is actively interacting with other popovers
|
|
209
|
-
const hasOpenPopover = originalTarget && document.getElementById(originalTarget)?.matches(':popover-open');
|
|
210
|
-
|
|
211
|
-
if (!isMouseDown && !tooltip.matches(':popover-open') && !hasOpenPopover) {
|
|
212
|
-
// For dynamic content, update right before showing to ensure current value
|
|
213
|
-
// The isUpdatingContent flag prevents the reactive callback from causing double updates
|
|
214
|
-
if (isDynamic) {
|
|
215
|
-
updateTooltipContent();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
ensureTooltipHostMatchesTrigger();
|
|
219
|
-
|
|
220
|
-
// Only manage anchor-name if element has other popover functionality
|
|
221
|
-
if (originalTarget) {
|
|
222
|
-
// Store current anchor name (dropdown may have set it by now)
|
|
223
|
-
const currentAnchorName = el.style.getPropertyValue('anchor-name');
|
|
224
|
-
if (currentAnchorName && currentAnchorName !== tooltipAnchor) {
|
|
225
|
-
el._originalAnchorName = currentAnchorName;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Set anchor-name on element first
|
|
230
|
-
el.style.setProperty('anchor-name', tooltipAnchor);
|
|
231
|
-
|
|
232
|
-
// Force a reflow to ensure anchor is registered before setting position-anchor
|
|
233
|
-
void el.offsetHeight;
|
|
234
|
-
|
|
235
|
-
// Set position-anchor on tooltip
|
|
236
|
-
tooltip.style.setProperty('position-anchor', tooltipAnchor);
|
|
237
|
-
|
|
238
|
-
// Show tooltip without changing popovertarget
|
|
239
|
-
tooltip.showPopover();
|
|
240
|
-
}
|
|
241
|
-
}, hoverDelay);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
171
|
+
Alpine.directive('tooltip', (el, { modifiers, expression }, { effect, evaluateLater }) => {
|
|
244
172
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
});
|
|
173
|
+
// --- Content evaluator ---
|
|
174
|
+
let getContent;
|
|
175
|
+
const isDynamic =
|
|
176
|
+
expression.startsWith('$x.') ||
|
|
177
|
+
(expression.includes('+') || expression.includes('`') || expression.includes('${'));
|
|
252
178
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
179
|
+
if (expression.startsWith('$x.')) {
|
|
180
|
+
const path = expression.substring(3);
|
|
181
|
+
const [contentType] = path.split('.');
|
|
182
|
+
getContent = evaluateLater(expression);
|
|
183
|
+
effect(() => {
|
|
184
|
+
const store = Alpine.store('collections');
|
|
185
|
+
if (store && typeof store.loadCollection === 'function' && !store[contentType]) {
|
|
186
|
+
store.loadCollection(contentType);
|
|
258
187
|
}
|
|
259
|
-
scheduleRestoreAnchorAfterClose();
|
|
260
188
|
});
|
|
189
|
+
} else if (expression.includes('<') && expression.includes('>')) {
|
|
190
|
+
// Literal HTML string
|
|
191
|
+
const escaped = expression.replace(/'/g, "\\'");
|
|
192
|
+
getContent = evaluateLater(`'${escaped}'`);
|
|
193
|
+
} else if (expression.includes('+') || expression.includes('`') || expression.includes('${')) {
|
|
194
|
+
getContent = evaluateLater(expression);
|
|
195
|
+
} else {
|
|
196
|
+
// Static literal — wrap in quotes so evaluateLater returns it verbatim
|
|
197
|
+
getContent = evaluateLater(`'${expression}'`);
|
|
198
|
+
}
|
|
261
199
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
200
|
+
// --- Positioning modifiers ---
|
|
201
|
+
const validPositions = ['top', 'bottom', 'start', 'end', 'center', 'corner'];
|
|
202
|
+
const positions = modifiers.filter(m => validPositions.includes(m));
|
|
265
203
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
204
|
+
// For non-dynamic content, cache once to avoid re-evaluating every show.
|
|
205
|
+
let cachedContent = null;
|
|
206
|
+
if (!isDynamic) {
|
|
207
|
+
getContent(v => { cachedContent = v; });
|
|
208
|
+
}
|
|
269
209
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
210
|
+
// Resolves the content to show, calling the provided callback with the HTML string.
|
|
211
|
+
const resolveContent = (cb) => {
|
|
212
|
+
if (!isDynamic && cachedContent != null) { cb(cachedContent); return; }
|
|
213
|
+
getContent(v => cb(v));
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// --- Event handlers ---
|
|
217
|
+
const requestShow = () => {
|
|
218
|
+
cancelPendingShow();
|
|
219
|
+
cancelPendingHide(); // incoming show cancels the deferred hide — this is the glide takeover
|
|
220
|
+
_pendingTrigger = el;
|
|
221
|
+
// Chain mode: if the singleton is still open (hide was deferred, about to
|
|
222
|
+
// happen), or was just dismissed within the grace window, show now.
|
|
223
|
+
const anyOpen = document.querySelector('.tooltip[popover="hint"]:popover-open');
|
|
224
|
+
const delay = (anyOpen || isInChainWindow()) ? 0 : getTooltipHoverDelay(el);
|
|
225
|
+
_showTimer = setTimeout(() => {
|
|
226
|
+
_showTimer = null;
|
|
227
|
+
if (_pendingTrigger !== el) return;
|
|
228
|
+
const triggerTargetId = el.getAttribute('popovertarget') || el.getAttribute('x-dropdown');
|
|
229
|
+
if (triggerTargetId) {
|
|
230
|
+
const t = document.getElementById(triggerTargetId);
|
|
231
|
+
if (t && t.matches && t.matches(':popover-open')) return;
|
|
273
232
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
scheduleAnchorRestoreAfterTooltipDismissal(() => {
|
|
277
|
-
if (originalTarget) {
|
|
278
|
-
const targetPopover = document.getElementById(originalTarget);
|
|
279
|
-
const isPopoverOpen = targetPopover?.matches(':popover-open');
|
|
280
|
-
if (!targetPopover || !isPopoverOpen) {
|
|
281
|
-
restoreOriginalAnchor();
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
restoreOriginalAnchor();
|
|
285
|
-
}
|
|
233
|
+
resolveContent(html => {
|
|
234
|
+
showSingletonFor(el, html, positions);
|
|
286
235
|
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// Cleanup function for when element is removed
|
|
305
|
-
const cleanup = () => {
|
|
306
|
-
cancelScheduledAnchorRestore();
|
|
307
|
-
if (el._tooltipPopoverListener) {
|
|
308
|
-
document.removeEventListener('toggle', el._tooltipPopoverListener);
|
|
309
|
-
delete el._tooltipPopoverListener;
|
|
310
|
-
}
|
|
311
|
-
if (tooltip && tooltip.parentElement) {
|
|
312
|
-
tooltip.remove();
|
|
236
|
+
}, delay);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const requestHide = () => {
|
|
240
|
+
cancelPendingShow();
|
|
241
|
+
// Defer the actual hide briefly so an incoming show on a different trigger
|
|
242
|
+
// can take over (chain mode: immediate show) rather than flicker-close.
|
|
243
|
+
cancelPendingHide();
|
|
244
|
+
_hideTimer = setTimeout(() => {
|
|
245
|
+
_hideTimer = null;
|
|
246
|
+
const host = getTooltipHostForTrigger(el);
|
|
247
|
+
const s = _singletons.get(host);
|
|
248
|
+
if (s && s.activeTrigger === el && s.el.matches(':popover-open')) {
|
|
249
|
+
s.el.hidePopover();
|
|
250
|
+
s.activeTrigger = null;
|
|
251
|
+
markTooltipHidden();
|
|
252
|
+
scheduleAnchorRestore(el);
|
|
313
253
|
}
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
254
|
+
}, HIDE_DEFER_MS);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Mouse interactions
|
|
258
|
+
el.addEventListener('mouseenter', requestShow);
|
|
259
|
+
el.addEventListener('mouseleave', requestHide);
|
|
260
|
+
|
|
261
|
+
// Mousedown/click: always hide immediately; scheduleAnchorRestore so the
|
|
262
|
+
// trigger's anchor-name stays valid long enough for any dropdown popover
|
|
263
|
+
// it launches to position itself correctly.
|
|
264
|
+
const hideAndScheduleRestore = () => {
|
|
265
|
+
cancelPendingShow();
|
|
266
|
+
hideAnySingleton();
|
|
267
|
+
scheduleAnchorRestore(el);
|
|
268
|
+
};
|
|
269
|
+
el.addEventListener('mousedown', hideAndScheduleRestore);
|
|
270
|
+
el.addEventListener('click', hideAndScheduleRestore);
|
|
319
271
|
});
|
|
272
|
+
|
|
273
|
+
// Global: when ANY other popover opens, close the singleton(s). Dropdowns and
|
|
274
|
+
// dialogs take precedence over tooltips.
|
|
275
|
+
document.addEventListener('toggle', (event) => {
|
|
276
|
+
if (event.newState !== 'open') return;
|
|
277
|
+
const t = event.target;
|
|
278
|
+
if (t.classList && t.classList.contains('tooltip') && t.getAttribute('popover') === 'hint') return;
|
|
279
|
+
hideAnySingleton();
|
|
280
|
+
}, true);
|
|
320
281
|
}
|
|
321
282
|
|
|
322
|
-
//
|
|
283
|
+
// ---- Plugin init boilerplate ----
|
|
284
|
+
|
|
323
285
|
let tooltipPluginInitialized = false;
|
|
324
286
|
|
|
325
287
|
function ensureTooltipPluginInitialized() {
|
|
326
288
|
if (tooltipPluginInitialized) return;
|
|
327
289
|
if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
|
|
328
|
-
|
|
329
290
|
tooltipPluginInitialized = true;
|
|
330
291
|
initializeTooltipPlugin();
|
|
331
|
-
// Do not call Alpine.initTree() on [x-tooltip] elements here. That initializes
|
|
332
|
-
// them in isolation and breaks scope (e.g. "tab is not defined"). Alpine will
|
|
333
|
-
// process the full tree from the root, so [x-tooltip] elements get the correct
|
|
334
|
-
// scope. Dynamically loaded components are already initialized by the component
|
|
335
|
-
// processor with initTree on the swapped-in root.
|
|
336
292
|
}
|
|
337
293
|
|
|
338
|
-
// Expose on window for loader to call if needed
|
|
339
294
|
window.ensureTooltipPluginInitialized = ensureTooltipPluginInitialized;
|
|
340
295
|
|
|
341
|
-
// Handle both DOMContentLoaded and alpine:init
|
|
342
296
|
if (document.readyState === 'loading') {
|
|
343
297
|
document.addEventListener('DOMContentLoaded', ensureTooltipPluginInitialized);
|
|
344
298
|
}
|
|
345
|
-
|
|
346
299
|
document.addEventListener('alpine:init', ensureTooltipPluginInitialized);
|
|
347
300
|
|
|
348
|
-
// If Alpine is already initialized when this script loads, initialize immediately
|
|
349
301
|
if (window.Alpine && typeof window.Alpine.directive === 'function') {
|
|
350
302
|
setTimeout(ensureTooltipPluginInitialized, 0);
|
|
351
303
|
} else if (document.readyState === 'complete') {
|
|
352
|
-
// If document is already loaded but Alpine isn't ready yet, wait for it
|
|
353
304
|
const checkAlpine = setInterval(() => {
|
|
354
305
|
if (window.Alpine && typeof window.Alpine.directive === 'function') {
|
|
355
306
|
clearInterval(checkAlpine);
|
|
356
307
|
ensureTooltipPluginInitialized();
|
|
357
308
|
}
|
|
358
|
-
},
|
|
309
|
+
}, 50);
|
|
359
310
|
setTimeout(() => clearInterval(checkAlpine), 5000);
|
|
360
|
-
}
|
|
311
|
+
}
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
|
|
86
86
|
/* Selected */
|
|
87
87
|
:where(.selected) {
|
|
88
|
-
background-color:
|
|
88
|
+
background-color: var(--color-field-surface, oklch(91.79% 0.0029 264.26))
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/* Transparent */
|
|
@@ -173,6 +173,20 @@
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/* No spinners */
|
|
177
|
+
.no-spinner {
|
|
178
|
+
-moz-appearance: textfield !important;
|
|
179
|
+
appearance: textfield !important;
|
|
180
|
+
|
|
181
|
+
&::-webkit-inner-spin-button,
|
|
182
|
+
&::-webkit-outer-spin-button {
|
|
183
|
+
-webkit-appearance: none;
|
|
184
|
+
appearance: none;
|
|
185
|
+
display: none;
|
|
186
|
+
margin: 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
/* Banner overlays */
|
|
177
191
|
:where(.overlay-dark, .overlay-light) {
|
|
178
192
|
position: relative;
|