mnfst 0.5.163 → 0.5.164
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.appwrite.auth.js +107 -199
- package/lib/manifest.appwrite.data.js +1 -3
- package/lib/manifest.charts.js +34 -79
- package/lib/manifest.chat.js +515 -0
- package/lib/manifest.code.js +110 -340
- package/lib/manifest.colorpicker.js +252 -551
- package/lib/manifest.combobox.js +73 -91
- package/lib/manifest.components.js +34 -95
- package/lib/manifest.css +0 -132
- package/lib/manifest.data.js +189 -562
- package/lib/manifest.datepicker.js +43 -128
- package/lib/manifest.dropdowns.js +18 -39
- package/lib/manifest.export.js +29 -147
- package/lib/manifest.icons.js +9 -24
- package/lib/manifest.integrity.json +23 -23
- package/lib/manifest.js +62 -126
- package/lib/manifest.localization.js +67 -200
- package/lib/manifest.markdown.js +65 -174
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.payments.js +40 -139
- package/lib/manifest.router.js +24 -73
- package/lib/manifest.status.js +20 -56
- package/lib/manifest.svg.js +12 -30
- package/lib/manifest.tooltips.js +28 -65
- package/lib/manifest.utilities.js +40 -107
- package/lib/manifest.virtual.js +15 -53
- package/package.json +1 -1
- package/lib/manifest.appwrite.presence.js +0 -1650
package/lib/manifest.status.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/* Manifest Status — config
|
|
2
2
|
/* By Andrew Matlock under MIT license
|
|
3
3
|
/* https://manifestx.dev
|
|
4
|
-
/*
|
|
5
|
-
/* Reads the top-level `status` block from manifest.json and normalizes each
|
|
6
|
-
/* named entry into { signals[], rollup, refresh, ... }. Pure signal layer —
|
|
7
|
-
/* no UI. Each entry resolves to $status.<name> (see manifest.status.main.js).
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
6
|
(function () {
|
|
11
7
|
'use strict';
|
|
12
8
|
|
|
13
|
-
// Infer
|
|
14
|
-
// data plugin's field-presence inference).
|
|
9
|
+
// Infer provider type from which field is present.
|
|
15
10
|
function normalizeSignal(sig) {
|
|
16
11
|
if (typeof sig === 'string') return { type: 'probe', url: sig, label: sig };
|
|
17
12
|
if (!sig || typeof sig !== 'object') return { type: 'unknown', label: 'unknown' };
|
|
@@ -50,7 +45,7 @@
|
|
|
50
45
|
if (def.confirmations) opts.confirmations = def.confirmations;
|
|
51
46
|
if (def.staleAfter) opts.staleAfter = def.staleAfter;
|
|
52
47
|
} else {
|
|
53
|
-
// Single-signal object
|
|
48
|
+
// Single-signal object.
|
|
54
49
|
signals = [normalizeSignal(def)];
|
|
55
50
|
if (def.refresh) opts.refresh = def.refresh;
|
|
56
51
|
}
|
|
@@ -61,9 +56,8 @@
|
|
|
61
56
|
return { name, signals, ...opts };
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// keys they don't consume, so fall through to a fresh fetch otherwise.
|
|
59
|
+
// Reuse a cached manifest only if it carries the `status` block; some plugins
|
|
60
|
+
// cache a normalized copy that omits keys they don't consume.
|
|
67
61
|
async function ensureStatusManifest() {
|
|
68
62
|
const cached = window.ManifestComponentsRegistry?.manifest || window.__manifestLoaded;
|
|
69
63
|
if (cached && cached.status) return cached;
|
|
@@ -93,16 +87,12 @@
|
|
|
93
87
|
/* Manifest Status — store
|
|
94
88
|
/* By Andrew Matlock under MIT license
|
|
95
89
|
/* https://manifestx.dev
|
|
96
|
-
/*
|
|
97
|
-
/* Internal reactive store backing $status. Mirrors store('data'):
|
|
98
|
-
/* named entries + a _version counter bumped on every update.
|
|
99
90
|
*/
|
|
100
91
|
|
|
101
92
|
(function () {
|
|
102
93
|
'use strict';
|
|
103
94
|
|
|
104
|
-
// Ordered severity
|
|
105
|
-
// worst-of rollup surfaces it over a fully-operational set. unknown = -1.
|
|
95
|
+
// Ordered severity; maintenance sits between operational and degraded.
|
|
106
96
|
const LEVELS = {
|
|
107
97
|
operational: 0,
|
|
108
98
|
maintenance: 0.5,
|
|
@@ -134,11 +124,6 @@
|
|
|
134
124
|
/* Manifest Status — signal resolvers + rollup
|
|
135
125
|
/* By Andrew Matlock under MIT license
|
|
136
126
|
/* https://manifestx.dev
|
|
137
|
-
/*
|
|
138
|
-
/* Each provider type resolves a normalized signal to { state, latencyMs }.
|
|
139
|
-
/* Generic providers (probe/feed/static/heartbeat) are fully implemented;
|
|
140
|
-
/* appwrite resolves via the public /health endpoint; mirror uses a small
|
|
141
|
-
/* registry of known upstream feeds; mcp is feed-based and pluggable.
|
|
142
127
|
*/
|
|
143
128
|
|
|
144
129
|
(function () {
|
|
@@ -183,8 +168,7 @@
|
|
|
183
168
|
return { state, latencyMs };
|
|
184
169
|
} catch (err) {
|
|
185
170
|
clearTimeout(t);
|
|
186
|
-
//
|
|
187
|
-
// so report unknown rather than a false outage.
|
|
171
|
+
// Can't distinguish down from CORS-blocked, so report unknown not outage.
|
|
188
172
|
return { state: 'unknown', latencyMs: null, error: err && err.name ? err.name : 'fetch failed' };
|
|
189
173
|
}
|
|
190
174
|
}
|
|
@@ -194,13 +178,11 @@
|
|
|
194
178
|
const res = await fetch(sig.url, { cache: 'no-store' });
|
|
195
179
|
if (!res.ok) return { state: 'unknown', latencyMs: null, error: 'feed ' + res.status };
|
|
196
180
|
const json = await res.json();
|
|
197
|
-
//
|
|
198
|
-
// both a state and a human update message.
|
|
181
|
+
// Feed node may be a bare state string or an object with state + message.
|
|
199
182
|
const node = sig.path ? dig(json, sig.path) : json;
|
|
200
183
|
const obj = (node && typeof node === 'object') ? node : null;
|
|
201
184
|
const raw = obj ? (obj.state != null ? obj.state : obj.status) : node;
|
|
202
185
|
const message = (obj && obj.message) ? obj.message : (json.message || null);
|
|
203
|
-
// Optional historical data a backend can hydrate.
|
|
204
186
|
const history = Array.isArray(obj && obj.history) ? obj.history : (Array.isArray(json.history) ? json.history : undefined);
|
|
205
187
|
const uptime = (obj && typeof obj.uptime === 'number') ? obj.uptime : (typeof json.uptime === 'number' ? json.uptime : undefined);
|
|
206
188
|
const incidents = Array.isArray(obj && obj.incidents) ? obj.incidents : (Array.isArray(json.incidents) ? json.incidents : undefined);
|
|
@@ -324,21 +306,14 @@
|
|
|
324
306
|
/* Manifest Status — main
|
|
325
307
|
/* By Andrew Matlock under MIT license
|
|
326
308
|
/* https://manifestx.dev
|
|
327
|
-
/*
|
|
328
|
-
/* Wires the store, polls each entry's signals, and registers the $status
|
|
329
|
-
/* magic. $status.<name> → rolled-up health object; $status.overall → worst
|
|
330
|
-
/* across all entries. The plugin renders nothing: the author drives their own
|
|
331
|
-
/* UI from these reactive values (:class, x-show, x-text, etc.).
|
|
332
309
|
*/
|
|
333
310
|
|
|
334
311
|
(function () {
|
|
335
312
|
'use strict';
|
|
336
313
|
|
|
337
|
-
/* Shared
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
baked-in labels be localized/overridden via any loaded data source's
|
|
341
|
-
`_ui` key, locale-reactive. Kept behaviourally identical across copies. */
|
|
314
|
+
/* Shared `_ui` text resolver — defined once across plugins, first load wins.
|
|
315
|
+
Localizes baked-in labels via any data source's `_ui` key. Kept identical
|
|
316
|
+
across copies. */
|
|
342
317
|
if (!window.ManifestUI) {
|
|
343
318
|
window.ManifestUI = {
|
|
344
319
|
_loadedSourceNames() {
|
|
@@ -388,9 +363,8 @@
|
|
|
388
363
|
|
|
389
364
|
function store() { return window.Alpine && window.Alpine.store('status'); }
|
|
390
365
|
|
|
391
|
-
// Debounce
|
|
392
|
-
//
|
|
393
|
-
// (default) commits immediately.
|
|
366
|
+
// Debounce: a new state must be observed `confirmations` times in a row
|
|
367
|
+
// before it replaces the committed one (default 1 = commit immediately).
|
|
394
368
|
function applyHysteresis(entry, observed) {
|
|
395
369
|
const need = entry.confirmations || 1;
|
|
396
370
|
const prev = committed[entry.name];
|
|
@@ -450,8 +424,7 @@
|
|
|
450
424
|
|
|
451
425
|
const rolled = Signals.rollup(entry, resolved);
|
|
452
426
|
|
|
453
|
-
// Manual override wins and bypasses hysteresis
|
|
454
|
-
// observed state through applyHysteresis.
|
|
427
|
+
// Manual override wins and bypasses hysteresis.
|
|
455
428
|
let state, message = null;
|
|
456
429
|
if (overrides[entry.name]) {
|
|
457
430
|
state = overrides[entry.name].state;
|
|
@@ -474,8 +447,7 @@
|
|
|
474
447
|
hist = rolled.history.slice(-entry.history);
|
|
475
448
|
while (hist.length < entry.history) hist.unshift('unknown');
|
|
476
449
|
} else {
|
|
477
|
-
// Pre-fill with `unknown` so the bar is
|
|
478
|
-
// from the right as checks accrue (StatusPage-style no-data segments).
|
|
450
|
+
// Pre-fill with `unknown` so the bar is full-width and fills from the right.
|
|
479
451
|
hist = histories[entry.name] ? histories[entry.name].slice() : new Array(entry.history).fill('unknown');
|
|
480
452
|
hist.push(state);
|
|
481
453
|
if (hist.length > entry.history) hist = hist.slice(hist.length - entry.history);
|
|
@@ -532,8 +504,7 @@
|
|
|
532
504
|
return known.reduce((a, b) => (level(b.state) > level(a.state) ? b : a)).state;
|
|
533
505
|
}
|
|
534
506
|
|
|
535
|
-
// Default English labels
|
|
536
|
-
// data source's `_ui.status.label` (same mechanism as datepicker/colorpicker).
|
|
507
|
+
// Default English labels; overridable per-locale via a data source's `_ui.status.label`.
|
|
537
508
|
const UI_FALLBACK = {
|
|
538
509
|
label: {
|
|
539
510
|
operational: 'Operational',
|
|
@@ -545,9 +516,7 @@
|
|
|
545
516
|
}
|
|
546
517
|
};
|
|
547
518
|
|
|
548
|
-
// Resolve `$x`/`$locale`/`${…}` reference strings in `_ui` values
|
|
549
|
-
// treatment as the colorpicker's _resolveRefString — kept semantically
|
|
550
|
-
// identical). Reading inside the caller's effect registers the deps.
|
|
519
|
+
// Resolve `$x`/`$locale`/`${…}` reference strings in `_ui` values.
|
|
551
520
|
function _resolveRefString(val) {
|
|
552
521
|
if (typeof val !== 'string' || val.length === 0) return val;
|
|
553
522
|
const trimmed = val.trim();
|
|
@@ -566,13 +535,9 @@
|
|
|
566
535
|
return val;
|
|
567
536
|
}
|
|
568
537
|
|
|
569
|
-
// Human label for a state
|
|
570
|
-
// Falls back to the English default, then a generic title-case for any
|
|
571
|
-
// custom/unrecognized state.
|
|
538
|
+
// Human label for a state, localized through `_ui`, else title-cased.
|
|
572
539
|
function formatStateLabel(state) {
|
|
573
|
-
// Dep on the data store version so labels re-resolve when a `_ui`
|
|
574
|
-
// source loads late or reloads on locale switch (same fix as the
|
|
575
|
-
// datepicker's ui effect).
|
|
540
|
+
// Dep on the data store version so labels re-resolve when a `_ui` source loads late.
|
|
576
541
|
try { void (window.Alpine && Alpine.store('data')?._dataVersion); } catch (_) { }
|
|
577
542
|
const ui = window.ManifestUI ? window.ManifestUI.resolve('status', UI_FALLBACK) : UI_FALLBACK;
|
|
578
543
|
const labels = (ui && ui.label) || UI_FALLBACK.label;
|
|
@@ -580,15 +545,14 @@
|
|
|
580
545
|
return String(state == null ? '' : state).replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
581
546
|
}
|
|
582
547
|
|
|
583
|
-
// Reserved accessor names — never treated as
|
|
584
|
-
// `x-for="(service, name) in $status"` iterates only real services.
|
|
548
|
+
// Reserved accessor names — never treated as entries or enumerated.
|
|
585
549
|
const RESERVED = new Set(['overall', 'all', 'ready', 'incidents', 'label', 'set', 'clear', 'refresh', 'beat']);
|
|
586
550
|
|
|
587
551
|
function setOverride(name, state, message) {
|
|
588
552
|
overrides[name] = { state, message: message || null };
|
|
589
553
|
committed[name] = state;
|
|
590
554
|
delete pending[name];
|
|
591
|
-
// An operator update is an incident: open/append, or resolve when
|
|
555
|
+
// An operator update is an incident: open/append, or resolve when back to operational.
|
|
592
556
|
if (state === 'operational') { if (message) logIncident(name, state, message); resolveIncident(name); }
|
|
593
557
|
else { logIncident(name, state, message); }
|
|
594
558
|
const e = config && config.entries[name];
|
package/lib/manifest.svg.js
CHANGED
|
@@ -2,19 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const svgCache = new Map();
|
|
4
4
|
|
|
5
|
-
// Shared DOMPurify loader
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// DOMPurify with the SVG profile so authors rendering user-uploaded SVGs
|
|
10
|
-
// (e.g. profile avatars from an Appwrite bucket) don't have to write
|
|
11
|
-
// per-source sanitization.
|
|
12
|
-
//
|
|
13
|
-
// Defined on window so manifest.markdown.js can share the same loader and
|
|
14
|
-
// in-flight promise — otherwise both plugins' top-level `let purifyPromise`
|
|
15
|
-
// declarations collide in the realm's global lexical env and the second
|
|
16
|
-
// script throws SyntaxError ("Identifier 'purifyPromise' has already been
|
|
17
|
-
// declared") at parse time, taking the whole plugin offline.
|
|
5
|
+
// Shared DOMPurify loader for the .safe modifier (user-supplied SVG can carry
|
|
6
|
+
// <script>, on* handlers, javascript: URLs, foreignObject HTML). On window so
|
|
7
|
+
// manifest.markdown.js shares one loader/promise — separate top-level `let`
|
|
8
|
+
// declarations would collide in the global lexical env and throw at parse time.
|
|
18
9
|
if (!window.ManifestDOMPurify) {
|
|
19
10
|
window.ManifestDOMPurify = {
|
|
20
11
|
_promise: null,
|
|
@@ -23,8 +14,7 @@ if (!window.ManifestDOMPurify) {
|
|
|
23
14
|
if (this._promise) return this._promise;
|
|
24
15
|
this._promise = new Promise((resolve, reject) => {
|
|
25
16
|
const script = document.createElement('script');
|
|
26
|
-
// Pinned +
|
|
27
|
-
// floating `@latest` or tampered CDN file can't inject JS here.
|
|
17
|
+
// Pinned + SRI so a tampered/floating CDN file can't inject JS.
|
|
28
18
|
script.src = 'https://cdn.jsdelivr.net/npm/dompurify@3.4.10/dist/purify.min.js';
|
|
29
19
|
script.integrity = 'sha384-eguRoJERj8ghOpzO//Rl7+ScQsQIR1cH+ajll7+fG+IpbNPlkZsQn9h8ccr+wPXx';
|
|
30
20
|
script.crossOrigin = 'anonymous';
|
|
@@ -47,10 +37,7 @@ if (!window.ManifestDOMPurify) {
|
|
|
47
37
|
};
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
//
|
|
51
|
-
// otherwise. Uses DOMPurify's SVG profile which strips <script>, on* event
|
|
52
|
-
// handlers, javascript: URLs, and dangerous foreignObject HTML — while
|
|
53
|
-
// keeping the visual SVG markup intact.
|
|
40
|
+
// Sanitize with DOMPurify's SVG profile when .safe is on; pass through otherwise.
|
|
54
41
|
async function maybeSanitizeSvg(svgText, safe) {
|
|
55
42
|
if (!safe) return svgText;
|
|
56
43
|
try {
|
|
@@ -190,10 +177,8 @@ async function initializeSvgPlugin() {
|
|
|
190
177
|
return;
|
|
191
178
|
}
|
|
192
179
|
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// used for animations, etc.). Use .safe when the SVG source can
|
|
196
|
-
// come from untrusted parties (uploaded avatars, third-party APIs).
|
|
180
|
+
// .safe opts into sanitization; default keeps full SVG fidelity (filters,
|
|
181
|
+
// animation scripts). Use it when the source is untrusted.
|
|
197
182
|
const safe = Array.isArray(modifiers) && modifiers.includes('safe');
|
|
198
183
|
|
|
199
184
|
const hasBakedContent =
|
|
@@ -268,8 +253,7 @@ async function initializeSvgPlugin() {
|
|
|
268
253
|
|
|
269
254
|
let svgPluginInitialized = false;
|
|
270
255
|
|
|
271
|
-
// True once Alpine
|
|
272
|
-
// module load so we never miss the event, whatever the script order.
|
|
256
|
+
// True once Alpine finished its initial walk. Bound at load so we never miss it.
|
|
273
257
|
let svgAlpineHasWalked = false;
|
|
274
258
|
document.addEventListener('alpine:initialized', () => { svgAlpineHasWalked = true; });
|
|
275
259
|
|
|
@@ -284,11 +268,9 @@ async function ensureSvgPluginInitialized() {
|
|
|
284
268
|
svgPluginInitialized = true;
|
|
285
269
|
await initializeSvgPlugin();
|
|
286
270
|
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// process every element with all sibling directives already registered —
|
|
291
|
-
// walking here during boot would drop nested plugin content.
|
|
271
|
+
// Walk existing [x-svg] subtrees only if Alpine already finished its boot walk
|
|
272
|
+
// (late load). During boot, let Alpine's own walk handle them with all sibling
|
|
273
|
+
// directives registered — walking here would drop nested plugin content.
|
|
292
274
|
if (svgAlpineHasWalked && typeof window.Alpine.initTree === 'function') {
|
|
293
275
|
const existing = document.querySelectorAll('[x-svg]');
|
|
294
276
|
existing.forEach((el) => {
|
package/lib/manifest.tooltips.js
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
/* Manifest Tooltips
|
|
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
|
-
*/
|
|
1
|
+
/* Manifest Tooltips */
|
|
13
2
|
|
|
14
3
|
// Hover delay from CSS var (with time-unit parsing). Defaults to 500ms.
|
|
15
4
|
function getTooltipHoverDelay(element) {
|
|
@@ -36,9 +25,8 @@ function getTooltipHostForTrigger(triggerEl) {
|
|
|
36
25
|
|
|
37
26
|
function initializeTooltipPlugin() {
|
|
38
27
|
|
|
39
|
-
// Chain mode:
|
|
40
|
-
//
|
|
41
|
-
// when gliding across many triggers — the singleton just re-anchors.
|
|
28
|
+
// Chain mode: a recently-dismissed tooltip lets the next show immediately (no
|
|
29
|
+
// delay), and gliding across triggers re-anchors the singleton without flicker.
|
|
42
30
|
const TOOLTIP_CHAIN_GRACE_MS = 250;
|
|
43
31
|
let _lastTooltipHideTime = 0;
|
|
44
32
|
const markTooltipHidden = () => { _lastTooltipHideTime = Date.now(); };
|
|
@@ -46,11 +34,9 @@ function initializeTooltipPlugin() {
|
|
|
46
34
|
const isInChainWindow = () => (Date.now() - _lastTooltipHideTime) < TOOLTIP_CHAIN_GRACE_MS;
|
|
47
35
|
|
|
48
36
|
// ---- Singletons per host ----
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// across the top-layer boundary. We create them lazily on first use and keep them
|
|
53
|
-
// (small, hidden <div>s) for the life of the host.
|
|
37
|
+
// One under document.body; open popovers (menus, dialogs) need their own because
|
|
38
|
+
// CSS anchor positioning can't resolve across the top-layer boundary. Created
|
|
39
|
+
// lazily, kept for the host's life.
|
|
54
40
|
const _singletons = new WeakMap();
|
|
55
41
|
|
|
56
42
|
function getSingleton(host) {
|
|
@@ -87,12 +73,8 @@ function initializeTooltipPlugin() {
|
|
|
87
73
|
}
|
|
88
74
|
|
|
89
75
|
// ---- Controller ----
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// show and the user moves to another trigger before it fires, the first timer is
|
|
93
|
-
// cancelled in favor of the new one. If the singleton is already visible, the new
|
|
94
|
-
// trigger updates it in place (chain mode) — no hide/show flicker.
|
|
95
|
-
|
|
76
|
+
// One shared pending-show timer: moving to another trigger cancels the first. If
|
|
77
|
+
// the singleton is already visible, the new trigger updates it in place (chain mode).
|
|
96
78
|
let _showTimer = null;
|
|
97
79
|
let _pendingTrigger = null;
|
|
98
80
|
// Hide is deferred briefly so an incoming show on a different trigger can take
|
|
@@ -109,9 +91,8 @@ function initializeTooltipPlugin() {
|
|
|
109
91
|
}
|
|
110
92
|
|
|
111
93
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// previous transform state is cleared so the tooltip sits squarely at its anchor.
|
|
94
|
+
// Point the singleton at a trigger (anchor, content, classes) and show it. Switches
|
|
95
|
+
// re-anchor with no animation; residual transform state is cleared.
|
|
115
96
|
function showSingletonFor(trigger, contentHtml, positions, allowHtml = true) {
|
|
116
97
|
const host = getTooltipHostForTrigger(trigger);
|
|
117
98
|
const s = getSingleton(host);
|
|
@@ -126,9 +107,8 @@ function initializeTooltipPlugin() {
|
|
|
126
107
|
if (positions.length) s.el.classList.add(positions.join('-'));
|
|
127
108
|
s.currentPositions = positions;
|
|
128
109
|
|
|
129
|
-
// Escape by default
|
|
130
|
-
//
|
|
131
|
-
// explicit `.html`/`.safe` opt-in render as markup (allowHtml=true).
|
|
110
|
+
// Escape by default (dynamic content can carry attacker markup); author HTML
|
|
111
|
+
// and the .html/.safe opt-in render as markup.
|
|
132
112
|
if (allowHtml) s.el.innerHTML = contentHtml || '';
|
|
133
113
|
else s.el.textContent = contentHtml || '';
|
|
134
114
|
|
|
@@ -139,9 +119,7 @@ function initializeTooltipPlugin() {
|
|
|
139
119
|
s.activeTrigger = trigger;
|
|
140
120
|
s.currentAnchorName = anchorName;
|
|
141
121
|
|
|
142
|
-
// A11y:
|
|
143
|
-
// tooltip text as a description when the trigger receives focus or hover.
|
|
144
|
-
// Per WAI-ARIA, aria-describedby is the standard for this relationship.
|
|
122
|
+
// A11y: aria-describedby links trigger → tooltip for screen readers.
|
|
145
123
|
if (!s.el.id) s.el.id = 'mnfst-tooltip-' + Math.random().toString(36).slice(2, 9);
|
|
146
124
|
s.el.setAttribute('role', 'tooltip');
|
|
147
125
|
// Preserve any author-provided aria-describedby so we don't stomp it.
|
|
@@ -160,11 +138,10 @@ function initializeTooltipPlugin() {
|
|
|
160
138
|
let wasOpen = false;
|
|
161
139
|
document.querySelectorAll('.tooltip[popover="hint"]:popover-open').forEach(el => {
|
|
162
140
|
wasOpen = true;
|
|
163
|
-
try { el.hidePopover(); } catch {}
|
|
141
|
+
try { el.hidePopover(); } catch { }
|
|
164
142
|
});
|
|
165
|
-
// Restore each
|
|
166
|
-
//
|
|
167
|
-
// the tooltipped triggers and remove our id from their describedby list.
|
|
143
|
+
// Restore each trigger's prior aria-describedby (can't reach it from the
|
|
144
|
+
// popover, so walk tooltipped triggers).
|
|
168
145
|
document.querySelectorAll('[aria-describedby]').forEach((el) => {
|
|
169
146
|
if (!el._tooltipPriorDescribedBy && el._tooltipPriorDescribedBy !== '') return;
|
|
170
147
|
const prior = el._tooltipPriorDescribedBy;
|
|
@@ -172,8 +149,8 @@ function initializeTooltipPlugin() {
|
|
|
172
149
|
else el.removeAttribute('aria-describedby');
|
|
173
150
|
el._tooltipPriorDescribedBy = undefined;
|
|
174
151
|
});
|
|
175
|
-
// Only arm the chain window when something was
|
|
176
|
-
//
|
|
152
|
+
// Only arm the chain window when something was open (else a plain click
|
|
153
|
+
// would fast-track the next focus show).
|
|
177
154
|
if (wasOpen) markTooltipHidden();
|
|
178
155
|
}
|
|
179
156
|
|
|
@@ -187,10 +164,7 @@ function initializeTooltipPlugin() {
|
|
|
187
164
|
expression.startsWith('$x.') ||
|
|
188
165
|
(expression.includes('+') || expression.includes('`') || expression.includes('${'));
|
|
189
166
|
|
|
190
|
-
//
|
|
191
|
-
// dynamic/runtime content can carry attacker markup. Author-authored
|
|
192
|
-
// literal HTML in the attribute, or an explicit `.html`/`.safe` modifier,
|
|
193
|
-
// opts into raw markup.
|
|
167
|
+
// Render as HTML? Default false (escape); author literal HTML or .html/.safe opts in.
|
|
194
168
|
let allowHtml = modifiers.includes('html') || modifiers.includes('safe');
|
|
195
169
|
|
|
196
170
|
if (expression.startsWith('$x.')) {
|
|
@@ -204,8 +178,7 @@ function initializeTooltipPlugin() {
|
|
|
204
178
|
}
|
|
205
179
|
});
|
|
206
180
|
} else if (expression.includes('<') && expression.includes('>')) {
|
|
207
|
-
// Literal HTML
|
|
208
|
-
// trusted as the surrounding template, so render it as markup.
|
|
181
|
+
// Literal HTML in the attribute — author-authored, trusted as the template.
|
|
209
182
|
allowHtml = true;
|
|
210
183
|
const escaped = expression.replace(/'/g, "\\'");
|
|
211
184
|
getContent = evaluateLater(`'${escaped}'`);
|
|
@@ -276,17 +249,15 @@ function initializeTooltipPlugin() {
|
|
|
276
249
|
el.addEventListener('mouseenter', requestShow);
|
|
277
250
|
el.addEventListener('mouseleave', requestHide);
|
|
278
251
|
|
|
279
|
-
// Keyboard
|
|
280
|
-
//
|
|
281
|
-
// Gated on :focus-visible so mouse-click focus doesn't flash the tooltip.
|
|
252
|
+
// Keyboard focus (WCAG 2.1 SC 1.4.13). Gated on :focus-visible so mouse-click
|
|
253
|
+
// focus doesn't flash the tooltip.
|
|
282
254
|
el.addEventListener('focus', () => {
|
|
283
255
|
if (el.matches(':focus-visible')) requestShow();
|
|
284
256
|
});
|
|
285
257
|
el.addEventListener('blur', requestHide);
|
|
286
258
|
|
|
287
|
-
// Mousedown/click hides
|
|
288
|
-
//
|
|
289
|
-
// navigation) waits the full delay instead of showing instantly.
|
|
259
|
+
// Mousedown/click hides now and clears the chain window, so a synthetic
|
|
260
|
+
// re-hover (content shifting under the cursor) waits the full delay.
|
|
290
261
|
const hideOnInteraction = () => {
|
|
291
262
|
cancelPendingShow();
|
|
292
263
|
hideAnySingleton();
|
|
@@ -306,18 +277,10 @@ function initializeTooltipPlugin() {
|
|
|
306
277
|
}, true);
|
|
307
278
|
|
|
308
279
|
// ---- Public programmatic-show API ----
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// reused, so this respects chain mode / focus behaviour just like a
|
|
314
|
-
// hover-shown tooltip would. Auto-hides after `durationMs`.
|
|
315
|
-
//
|
|
316
|
-
// `positions` accepts the same vocabulary as the x-tooltip directive's
|
|
317
|
-
// modifiers — array of any subset of ['top','bottom','start','end',
|
|
318
|
-
// 'center','corner']. Joined with '-' to form the position class
|
|
319
|
-
// (e.g. ['top','end'] → '.top-end'), matching what `x-tooltip.top.end`
|
|
320
|
-
// would emit.
|
|
280
|
+
// Flash a tooltip for an action (e.g. code plugin's copy confirmation) without an
|
|
281
|
+
// x-tooltip directive; the trigger is the anchor, the singleton is reused. Auto-
|
|
282
|
+
// hides after `durationMs`. `positions` is the directive modifier vocabulary
|
|
283
|
+
// (subset of ['top','bottom','start','end','center','corner'], joined with '-').
|
|
321
284
|
window.ManifestTooltips = window.ManifestTooltips || {};
|
|
322
285
|
window.ManifestTooltips.showTransient = function (triggerEl, contentHtml, durationMs, positions) {
|
|
323
286
|
if (!triggerEl) return;
|