mnfst 0.5.260 → 0.5.262
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.data.js +1 -1
- package/lib/manifest.colorpicker.js +23 -12
- package/lib/manifest.combobox.js +17 -5
- package/lib/manifest.data.js +1 -1
- package/lib/manifest.datepicker.js +32 -12
- package/lib/manifest.defer.js +58 -5
- package/lib/manifest.dropdowns.js +209 -149
- package/lib/manifest.integrity.json +9 -9
- package/lib/manifest.sw.js +1 -1
- package/lib/manifest.tooltips.js +21 -7
- package/package.json +1 -1
|
@@ -1068,6 +1068,8 @@ function initializeColorpickerPlugin() {
|
|
|
1068
1068
|
|
|
1069
1069
|
const state = {
|
|
1070
1070
|
rootEl,
|
|
1071
|
+
_effects: [],
|
|
1072
|
+
_observers: [],
|
|
1071
1073
|
pickerUid,
|
|
1072
1074
|
|
|
1073
1075
|
// Data
|
|
@@ -1615,9 +1617,9 @@ function initializeColorpickerPlugin() {
|
|
|
1615
1617
|
if (this.libraryRootValue && window.Alpine?.effect && window.Alpine?.evaluateLater) {
|
|
1616
1618
|
// Explicit expression — bypass discovery, re-render on dep change.
|
|
1617
1619
|
const evalFn = Alpine.evaluateLater(evalHost, this.libraryRootValue);
|
|
1618
|
-
Alpine.effect(() => {
|
|
1620
|
+
this._effects.push(Alpine.effect(() => {
|
|
1619
1621
|
evalFn(v => { this._libraryResolvedData = v; this._doRenderLibrary(); });
|
|
1620
|
-
});
|
|
1622
|
+
}));
|
|
1621
1623
|
} else if (window.Alpine?.effect) {
|
|
1622
1624
|
// Zero-config — scan $x.manifest.data for `colorpicker:`-flagged
|
|
1623
1625
|
// entries, collect their $x.<name> values, merge in declaration
|
|
@@ -1688,7 +1690,7 @@ function initializeColorpickerPlugin() {
|
|
|
1688
1690
|
};
|
|
1689
1691
|
|
|
1690
1692
|
// Reactive deps trigger re-runs on locale switch / manifest load.
|
|
1691
|
-
Alpine.effect(() => {
|
|
1693
|
+
this._effects.push(Alpine.effect(() => {
|
|
1692
1694
|
try { Alpine.evaluate(evalCtx, 'typeof $locale !== "undefined" && $locale.current'); } catch {}
|
|
1693
1695
|
try { Alpine.evaluate(evalCtx, '$x && $x.manifest && $x.manifest._loadedFrom'); } catch {}
|
|
1694
1696
|
runDiscovery();
|
|
@@ -1705,7 +1707,7 @@ function initializeColorpickerPlugin() {
|
|
|
1705
1707
|
}
|
|
1706
1708
|
}, 150);
|
|
1707
1709
|
}
|
|
1708
|
-
});
|
|
1710
|
+
}));
|
|
1709
1711
|
} else {
|
|
1710
1712
|
this._doRenderLibrary();
|
|
1711
1713
|
}
|
|
@@ -2022,6 +2024,7 @@ function initializeColorpickerPlugin() {
|
|
|
2022
2024
|
if (r.width > 0 && r.height > 0) { self.drawCanvas(); self.updateCanvasMarker(); }
|
|
2023
2025
|
});
|
|
2024
2026
|
ro.observe(refs.canvas);
|
|
2027
|
+
self._observers.push(ro);
|
|
2025
2028
|
}
|
|
2026
2029
|
|
|
2027
2030
|
if (refs.hueSlider) {
|
|
@@ -2453,10 +2456,17 @@ function initializeColorpickerPlugin() {
|
|
|
2453
2456
|
|
|
2454
2457
|
// Defer mount so child directives register first. setTimeout (not rAF)
|
|
2455
2458
|
// so it still fires when backgrounded.
|
|
2456
|
-
setTimeout(() => state.mount(), 0);
|
|
2459
|
+
setTimeout(() => { if (!state._destroyed) state.mount(); }, 0);
|
|
2457
2460
|
|
|
2458
2461
|
cleanup(() => {
|
|
2459
2462
|
if (el.id) delete _pickerRegistry[el.id];
|
|
2463
|
+
state._destroyed = true;
|
|
2464
|
+
state._effects.forEach((fx) => Alpine.release(fx));
|
|
2465
|
+
state._effects.length = 0;
|
|
2466
|
+
state._observers.forEach((ro) => ro.disconnect());
|
|
2467
|
+
state._observers.length = 0;
|
|
2468
|
+
clearInterval(state._libraryPollTimer);
|
|
2469
|
+
state._libraryPollTimer = null;
|
|
2460
2470
|
delete el._colorpickerState;
|
|
2461
2471
|
});
|
|
2462
2472
|
return;
|
|
@@ -2468,18 +2478,12 @@ function initializeColorpickerPlugin() {
|
|
|
2468
2478
|
// Swatches trigger OUTSIDE any picker — handle before the ancestor check.
|
|
2469
2479
|
// We assign IDs + the popover tag; x-dropdown owns popover mechanics.
|
|
2470
2480
|
if (role === 'swatch') {
|
|
2471
|
-
if (el._cpSwatchWired) return; // guard against re-firing via initTree
|
|
2472
|
-
el._cpSwatchWired = true;
|
|
2473
|
-
|
|
2474
|
-
// ---- Optional x-model binding ----
|
|
2475
|
-
// x-model is the swatch's source of truth: reactive background preview,
|
|
2476
|
-
// plus read (open) / write (close) accessors the picker uses.
|
|
2477
2481
|
const modelExpr = el.getAttribute('x-model');
|
|
2478
2482
|
if (modelExpr && window.Alpine?.evaluateLater && window.Alpine?.effect) {
|
|
2479
2483
|
try {
|
|
2480
2484
|
const readFn = Alpine.evaluateLater(el, modelExpr);
|
|
2481
2485
|
// Reactive background preview
|
|
2482
|
-
Alpine.effect(() => {
|
|
2486
|
+
const modelEffect = Alpine.effect(() => {
|
|
2483
2487
|
readFn(v => {
|
|
2484
2488
|
if (typeof v === 'string' && v.length) {
|
|
2485
2489
|
el.style.setProperty('--color-picker-swatch', v);
|
|
@@ -2487,6 +2491,7 @@ function initializeColorpickerPlugin() {
|
|
|
2487
2491
|
}
|
|
2488
2492
|
});
|
|
2489
2493
|
});
|
|
2494
|
+
cleanup(() => Alpine.release(modelEffect));
|
|
2490
2495
|
el._cpModelGetter = (cb) => readFn(cb);
|
|
2491
2496
|
// Writer: `<modelExpr> = <JSON value>` (color/gradient strings are JSON-safe).
|
|
2492
2497
|
el._cpModelSetter = (v) => {
|
|
@@ -2495,6 +2500,12 @@ function initializeColorpickerPlugin() {
|
|
|
2495
2500
|
} catch {}
|
|
2496
2501
|
}
|
|
2497
2502
|
|
|
2503
|
+
if (el._cpSwatchWired) return; // guard against re-firing via initTree
|
|
2504
|
+
el._cpSwatchWired = true;
|
|
2505
|
+
|
|
2506
|
+
// ---- Optional x-model binding ----
|
|
2507
|
+
// x-model is the swatch's source of truth: reactive background preview,
|
|
2508
|
+
// plus read (open) / write (close) accessors the picker uses.
|
|
2498
2509
|
// ---- Initial color via `value` attribute ----
|
|
2499
2510
|
// Mirrors native <input type=color>: seeds the picker on first open and
|
|
2500
2511
|
// the swatch CSS var so border-color paints before interaction.
|
package/lib/manifest.combobox.js
CHANGED
|
@@ -165,7 +165,9 @@ function initializeComboboxPlugin() {
|
|
|
165
165
|
Alpine.directive('combobox', (el, { modifiers, expression }, { cleanup }) => {
|
|
166
166
|
captureModel(el); // fallback for any path that bypasses the above
|
|
167
167
|
// Build after the current tick so sibling sources (datalist/menu) exist.
|
|
168
|
-
|
|
168
|
+
let removed = false;
|
|
169
|
+
cleanup(() => { removed = true; });
|
|
170
|
+
setTimeout(() => { if (!removed) build(el, modifiers, expression || '', cleanup); }, 0);
|
|
169
171
|
});
|
|
170
172
|
|
|
171
173
|
// Resolve the menu's owning top-layer context fresh, every time — never cache it.
|
|
@@ -201,6 +203,8 @@ function initializeComboboxPlugin() {
|
|
|
201
203
|
// it into wherever the owner lives now. Harmless no-op otherwise.
|
|
202
204
|
const m = el.__mnfstComboboxMenu;
|
|
203
205
|
if (m) reattach(el, m);
|
|
206
|
+
// A re-initialized field: its cleanup released these
|
|
207
|
+
el.__cbBindEffects?.(cleanup);
|
|
204
208
|
return;
|
|
205
209
|
}
|
|
206
210
|
el.__mnfstCombobox = true;
|
|
@@ -941,6 +945,8 @@ function initializeComboboxPlugin() {
|
|
|
941
945
|
disabledObserver.observe(el, { attributes: true, attributeFilter: ['disabled'] });
|
|
942
946
|
if (cleanup) cleanup(() => disabledObserver.disconnect());
|
|
943
947
|
|
|
948
|
+
const effectFactories = [];
|
|
949
|
+
|
|
944
950
|
// ----- Reactive model (x-model) -----
|
|
945
951
|
// Render chips from the bound value on init and external change; write back on
|
|
946
952
|
// add/remove. Read inside an Alpine effect so $x / nested state stay reactive.
|
|
@@ -951,7 +957,7 @@ function initializeComboboxPlugin() {
|
|
|
951
957
|
: (modelArrayShape === false ? vals.join(',') : vals.slice());
|
|
952
958
|
try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(out)}`); } catch (_) { }
|
|
953
959
|
};
|
|
954
|
-
Alpine.effect(() => {
|
|
960
|
+
effectFactories.push(() => Alpine.effect(() => {
|
|
955
961
|
read(raw => {
|
|
956
962
|
if (modelArrayShape === null && raw != null && raw !== '') modelArrayShape = Array.isArray(raw);
|
|
957
963
|
const incoming = modelToValues(raw);
|
|
@@ -960,20 +966,26 @@ function initializeComboboxPlugin() {
|
|
|
960
966
|
if (!multiple && selected.length > 1) selected = selected.slice(-1);
|
|
961
967
|
render();
|
|
962
968
|
});
|
|
963
|
-
});
|
|
969
|
+
}));
|
|
964
970
|
}
|
|
965
971
|
|
|
966
972
|
// ----- Reactive locked list (config-object form, so `locked` can gate on state) -----
|
|
967
973
|
if (expr.startsWith('{')) {
|
|
968
|
-
Alpine.effect(() => {
|
|
974
|
+
effectFactories.push(() => Alpine.effect(() => {
|
|
969
975
|
let c; try { c = Alpine.evaluate(el, expr); } catch (_) { return; }
|
|
970
976
|
const next = computeLocked(c && typeof c === 'object' && !Array.isArray(c) ? c : null);
|
|
971
977
|
if (next.size === lockedSet.size && [...next].every(v => lockedSet.has(v))) return;
|
|
972
978
|
lockedSet = next;
|
|
973
979
|
render();
|
|
974
|
-
});
|
|
980
|
+
}));
|
|
975
981
|
}
|
|
976
982
|
|
|
983
|
+
el.__cbBindEffects = (onCleanup) => {
|
|
984
|
+
const effects = effectFactories.map((make) => make());
|
|
985
|
+
if (onCleanup) onCleanup(() => effects.forEach((fx) => Alpine.release(fx)));
|
|
986
|
+
};
|
|
987
|
+
el.__cbBindEffects(cleanup);
|
|
988
|
+
|
|
977
989
|
// ----- Dynamic options: re-read when x-for / $x fills the source after build,
|
|
978
990
|
// keeping the menu, chip labels, and data-locked flags current. -----
|
|
979
991
|
if (src) {
|
package/lib/manifest.data.js
CHANGED
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
|
|
109
109
|
// Reactive registry id -> api, so $date('id') resolves across mount order.
|
|
110
110
|
const _registry = Alpine.reactive ? Alpine.reactive({}) : {};
|
|
111
|
+
const _registryOwners = new Map();
|
|
111
112
|
let _uid = 0;
|
|
112
113
|
|
|
113
114
|
// Default English UI chrome; overridable via a data source's `_ui.date`.
|
|
@@ -173,6 +174,7 @@
|
|
|
173
174
|
const now = new Date();
|
|
174
175
|
const state = {
|
|
175
176
|
rootEl,
|
|
177
|
+
_effects: [],
|
|
176
178
|
id: rootEl.id || ('mnfst-dp-' + (++_uid)),
|
|
177
179
|
selectionMode: 'single', // 'single' | 'range' | 'multiple'
|
|
178
180
|
selected: null, // Date | null (single)
|
|
@@ -975,7 +977,7 @@
|
|
|
975
977
|
}
|
|
976
978
|
function bindConfigValue(state, el, expression) {
|
|
977
979
|
if (!expression) return;
|
|
978
|
-
Alpine.effect(() => {
|
|
980
|
+
state._effects.push(Alpine.effect(() => {
|
|
979
981
|
let cfg;
|
|
980
982
|
try { cfg = Alpine.evaluate(el, expression); } catch (_) { return; }
|
|
981
983
|
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return;
|
|
@@ -990,14 +992,14 @@
|
|
|
990
992
|
if (Array.isArray(cfg.presets)) { state.presets = cfg.presets; dirty = true; }
|
|
991
993
|
if (cfg.format !== undefined) { state.format = cfg.format || null; dirty = true; }
|
|
992
994
|
if (dirty) { state.bump(); if (state._mounted) state.render(); }
|
|
993
|
-
});
|
|
995
|
+
}));
|
|
994
996
|
}
|
|
995
997
|
/* ---- Reactive chrome — re-render when locale, direction, or resolved `_ui`
|
|
996
998
|
* text changes. Output comparison skips no-op _dataVersion bumps. */
|
|
997
999
|
function bindReactiveRender(state) {
|
|
998
1000
|
if (state._uiEffectBound) return;
|
|
999
1001
|
state._uiEffectBound = true;
|
|
1000
|
-
Alpine.effect(() => {
|
|
1002
|
+
state._effects.push(Alpine.effect(() => {
|
|
1001
1003
|
let locale = '', dir = '';
|
|
1002
1004
|
try { const ls = Alpine.store('locale'); locale = ls?.current || document.documentElement.lang || ''; dir = ls?.direction || ''; } catch (_) { }
|
|
1003
1005
|
try { const ds = Alpine.store('data'); if (ds) void ds._dataVersion; } catch (_) { }
|
|
@@ -1008,7 +1010,7 @@
|
|
|
1008
1010
|
state._uiKey = key;
|
|
1009
1011
|
if (state._destroyed || first) return; // initial render happens at mount
|
|
1010
1012
|
if (state._mounted) { state.render(); state.syncOut(); }
|
|
1011
|
-
});
|
|
1013
|
+
}));
|
|
1012
1014
|
}
|
|
1013
1015
|
|
|
1014
1016
|
function bindModel(state, el, modelExpr) {
|
|
@@ -1016,7 +1018,23 @@
|
|
|
1016
1018
|
state.modelGet = (cb) => { let out; read(v => { out = v; if (cb) cb(v); }); return out; };
|
|
1017
1019
|
state.modelSet = (v) => { try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(v)}`); } catch (_) { } };
|
|
1018
1020
|
// React to external model changes; syncOut writes the same value back (no loop).
|
|
1019
|
-
Alpine.effect(() => { read(v => { const iso = (v instanceof Date) ? D.toISO(v) : v; const cur = state._primaryValue(); if (typeof iso === 'string' && iso !== cur) { const tm = iso.match(/T(\d{2}:\d{2})/); if (tm) state.time = tm[1]; const d = D.fromISO(iso); state.selected = d; if (d) state.view = { y: d.getFullYear(), m: d.getMonth() + 1 }; state.bump(); if (state._mounted) { state.render(); state.syncOut(); } } }); });
|
|
1021
|
+
state._effects.push(Alpine.effect(() => { read(v => { const iso = (v instanceof Date) ? D.toISO(v) : v; const cur = state._primaryValue(); if (typeof iso === 'string' && iso !== cur) { const tm = iso.match(/T(\d{2}:\d{2})/); if (tm) state.time = tm[1]; const d = D.fromISO(iso); state.selected = d; if (d) state.view = { y: d.getFullYear(), m: d.getMonth() + 1 }; state.bump(); if (state._mounted) { state.render(); state.syncOut(); } } }); }));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Global effects stay subscribed to the stores (and pin the calendar) until released.
|
|
1025
|
+
function register(id, state) {
|
|
1026
|
+
_registryOwners.set(id, state);
|
|
1027
|
+
_registry[id] = state.api;
|
|
1028
|
+
}
|
|
1029
|
+
function unregister(id, state) {
|
|
1030
|
+
if (!id || _registryOwners.get(id) !== state) return;
|
|
1031
|
+
_registryOwners.delete(id);
|
|
1032
|
+
delete _registry[id];
|
|
1033
|
+
}
|
|
1034
|
+
function disposeState(state) {
|
|
1035
|
+
state._destroyed = true;
|
|
1036
|
+
state._effects.forEach((fx) => Alpine.release(fx));
|
|
1037
|
+
state._effects.length = 0;
|
|
1020
1038
|
}
|
|
1021
1039
|
|
|
1022
1040
|
function createDefaultMenu(fieldEl) {
|
|
@@ -1075,9 +1093,9 @@
|
|
|
1075
1093
|
if (modelExpr) bindModel(state, fieldEl, modelExpr);
|
|
1076
1094
|
|
|
1077
1095
|
if (!state._mounted) { state.seed(); state.syncOut(); state.render(); state._mounted = true; }
|
|
1078
|
-
if (calendar.id)
|
|
1096
|
+
if (calendar.id) register(calendar.id, state);
|
|
1079
1097
|
// Also resolve $date(fieldId) when the calendar is an auto-generated menu.
|
|
1080
|
-
if (fieldEl.id)
|
|
1098
|
+
if (fieldEl.id) register(fieldEl.id, state);
|
|
1081
1099
|
|
|
1082
1100
|
// CSS anchor positioning (as in the dropdowns plugin); showPopover() on click
|
|
1083
1101
|
// since a text <input> can't carry popovertarget.
|
|
@@ -1098,7 +1116,9 @@
|
|
|
1098
1116
|
if (fieldEl._dpSynthesizedHidden && fieldEl._dpSynthesizedHidden.isConnected) fieldEl._dpSynthesizedHidden.remove();
|
|
1099
1117
|
if (state._timeMenuEl && state._timeMenuEl.isConnected) state._timeMenuEl.remove();
|
|
1100
1118
|
if (calendar && calendar.isConnected) calendar.remove(); // the generated dropdown
|
|
1101
|
-
|
|
1119
|
+
unregister(fieldEl.id, state);
|
|
1120
|
+
unregister(calendar.id, state);
|
|
1121
|
+
disposeState(state);
|
|
1102
1122
|
});
|
|
1103
1123
|
}
|
|
1104
1124
|
|
|
@@ -1127,18 +1147,18 @@
|
|
|
1127
1147
|
|
|
1128
1148
|
// Defer mount so linked fields can register first.
|
|
1129
1149
|
setTimeout(() => {
|
|
1130
|
-
if (state._mounted) return;
|
|
1150
|
+
if (state._mounted || state._destroyed) return;
|
|
1131
1151
|
state.seed();
|
|
1132
1152
|
state.render();
|
|
1133
1153
|
state._mounted = true;
|
|
1134
|
-
if (el.id)
|
|
1154
|
+
if (el.id) register(el.id, state);
|
|
1135
1155
|
bindReactiveRender(state);
|
|
1136
1156
|
}, 0);
|
|
1137
1157
|
|
|
1138
1158
|
cleanup(() => {
|
|
1139
|
-
|
|
1159
|
+
unregister(el.id, state);
|
|
1140
1160
|
if (state._timeMenuEl && state._timeMenuEl.isConnected) state._timeMenuEl.remove();
|
|
1141
|
-
state
|
|
1161
|
+
disposeState(state);
|
|
1142
1162
|
delete el._dateState;
|
|
1143
1163
|
});
|
|
1144
1164
|
});
|
package/lib/manifest.defer.js
CHANGED
|
@@ -134,18 +134,41 @@ function register(el, rule, opts) {
|
|
|
134
134
|
cleanup: []
|
|
135
135
|
};
|
|
136
136
|
wire(rec);
|
|
137
|
+
Alpine.onElRemoved?.(el, () => forget(rec));
|
|
137
138
|
if (rule === 'route') routes.add(rec);
|
|
138
|
-
else if (!rec.lazy) { addPending(rec); if (!el.closest('[x-route][hidden]')) track(rec); schedule(); }
|
|
139
|
+
else if (!rec.lazy) { addPending(rec); if (!el.closest('[x-route][hidden]')) track(rec); if (rec.urgent || warm.size < PREWARM_CAP) schedule(); }
|
|
139
140
|
return rec;
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// A copy that renders like the original: a baked x-if clone is adopted through expandos
|
|
144
|
+
// cloneNode drops, so the copy leaves it out and its x-if renders fresh.
|
|
145
|
+
function pristineCopy(fragment) {
|
|
146
|
+
const copy = fragment.cloneNode(true);
|
|
147
|
+
const copies = copy.querySelectorAll('template[x-if]');
|
|
148
|
+
fragment.querySelectorAll('template[x-if]').forEach((t, i) => {
|
|
149
|
+
if (t._x_currentIfEl && t._x_currentIfEl === t.nextElementSibling) copies[i].nextElementSibling?.remove();
|
|
150
|
+
});
|
|
151
|
+
return copy;
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
function render(rec, viaPrewarm) {
|
|
143
|
-
if (rec.rendered) {
|
|
155
|
+
if (rec.rendered) {
|
|
156
|
+
if (!viaPrewarm && warm.delete(rec)) {
|
|
157
|
+
rec.pristine = null; // opened: never evicted
|
|
158
|
+
schedule();
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
144
162
|
rec.rendered = true;
|
|
145
163
|
pending.delete(rec);
|
|
146
164
|
if (viaPrewarm) warm.add(rec); else { warm.delete(rec); untrack(rec); }
|
|
147
165
|
const { el, tpl } = rec;
|
|
148
|
-
|
|
166
|
+
// A prewarm can be evicted, and eviction must restore unused markup: re-initializing
|
|
167
|
+
// used nodes stacks listeners on any directive without cleanup and re-runs plugins that
|
|
168
|
+
// rewrite their markup. So it keeps a copy, and the originals (with whatever adoption
|
|
169
|
+
// set on them) still render.
|
|
170
|
+
if (viaPrewarm && !rec.discard) rec.pristine = pristineCopy(tpl.content);
|
|
171
|
+
const content = rec.discard ? pristineCopy(tpl.content) : tpl.content;
|
|
149
172
|
Alpine.mutateDom(() => {
|
|
150
173
|
tpl.remove();
|
|
151
174
|
el.appendChild(content);
|
|
@@ -165,6 +188,30 @@ function teardown(rec) {
|
|
|
165
188
|
});
|
|
166
189
|
}
|
|
167
190
|
|
|
191
|
+
// Records hold the element and its anchor: a removed one must not pin its page.
|
|
192
|
+
function forget(rec) {
|
|
193
|
+
rec.forgotten = true;
|
|
194
|
+
rec.pristine = null;
|
|
195
|
+
pending.delete(rec);
|
|
196
|
+
routes.delete(rec);
|
|
197
|
+
pool = null;
|
|
198
|
+
untrack(rec);
|
|
199
|
+
rec.invoker = null;
|
|
200
|
+
if (warm.delete(rec)) schedule();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// A forgotten container initialized again (re-attached, or destroyed and re-walked).
|
|
204
|
+
// destroyTree also dropped its attribute watchers.
|
|
205
|
+
function revive(rec) {
|
|
206
|
+
if (!rec.forgotten) return;
|
|
207
|
+
rec.forgotten = false;
|
|
208
|
+
Alpine.onElRemoved?.(rec.el, () => forget(rec));
|
|
209
|
+
if (rec.rendered) return;
|
|
210
|
+
rec.rearm?.();
|
|
211
|
+
if (rec.rule === 'route') routes.add(rec);
|
|
212
|
+
else if (!rec.lazy) { addPending(rec); track(rec); schedule(); }
|
|
213
|
+
}
|
|
214
|
+
|
|
168
215
|
// Evict a warm, still-closed, never-opened container back to its stash (non-urgent, least reachable first)
|
|
169
216
|
function restash(rec) {
|
|
170
217
|
if (!rec.rendered) return;
|
|
@@ -174,7 +221,8 @@ function restash(rec) {
|
|
|
174
221
|
const { el, tpl } = rec;
|
|
175
222
|
Alpine.mutateDom(() => {
|
|
176
223
|
Array.from(el.children).forEach((child) => Alpine.destroyTree(child));
|
|
177
|
-
while (el.firstChild)
|
|
224
|
+
while (el.firstChild) el.firstChild.remove();
|
|
225
|
+
if (rec.pristine) { tpl.content.appendChild(rec.pristine); rec.pristine = null; }
|
|
178
226
|
el.appendChild(tpl);
|
|
179
227
|
});
|
|
180
228
|
warm.delete(rec);
|
|
@@ -226,6 +274,7 @@ function wire(rec) {
|
|
|
226
274
|
render(rec);
|
|
227
275
|
if (rec.discard) arm();
|
|
228
276
|
});
|
|
277
|
+
rec.rearm = arm;
|
|
229
278
|
arm();
|
|
230
279
|
if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
|
|
231
280
|
} else if (rule === 'route') {
|
|
@@ -235,6 +284,7 @@ function wire(rec) {
|
|
|
235
284
|
if (!el.hasAttribute('hidden')) render(rec);
|
|
236
285
|
if (!rec.rendered || rec.discard) arm();
|
|
237
286
|
});
|
|
287
|
+
rec.rearm = arm;
|
|
238
288
|
arm();
|
|
239
289
|
if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
|
|
240
290
|
} else if (rule === 'panel') {
|
|
@@ -300,6 +350,7 @@ function reachablePending() {
|
|
|
300
350
|
if (pool && poolEpoch === reachEpoch && pool.length < pending.size * 2 + 64) return pool;
|
|
301
351
|
poolEpoch = reachEpoch;
|
|
302
352
|
pool = [];
|
|
353
|
+
for (const set of [warm, routes]) for (const rec of set) if (!rec.el.isConnected) { set.delete(rec); untrack(rec); }
|
|
303
354
|
for (const rec of pending) {
|
|
304
355
|
if (!rec.el.isConnected) { pending.delete(rec); untrack(rec); continue; }
|
|
305
356
|
if (reachable(rec)) { track(rec); pool.push(rec); } else untrack(rec);
|
|
@@ -416,12 +467,14 @@ const inViewport = (r) => !!r && r.bottom > 0 && r.top < window.innerHeight && r
|
|
|
416
467
|
// On-screen rect for ranking, or null when off-screen / boxless
|
|
417
468
|
function nearRect(rec) {
|
|
418
469
|
if (rec.box !== undefined) return rec.ioNear ? rec.box : null;
|
|
470
|
+
if (rec.anchor) return null; // IO reports within a frame; a read here forces layout per record
|
|
419
471
|
const r = anchorRect(rec);
|
|
420
472
|
rec.near = inViewport(r);
|
|
421
473
|
return rec.near ? r : null;
|
|
422
474
|
}
|
|
423
475
|
function isNear(rec) {
|
|
424
476
|
if (rec.box !== undefined) return !!rec.ioNear;
|
|
477
|
+
if (rec.anchor) return false;
|
|
425
478
|
if (rec.near === undefined) rec.near = inViewport(anchorRect(rec));
|
|
426
479
|
return rec.near;
|
|
427
480
|
}
|
|
@@ -498,7 +551,7 @@ function defer(el) {
|
|
|
498
551
|
}
|
|
499
552
|
|
|
500
553
|
function intercept(el, skip) {
|
|
501
|
-
if (el.__mnfstDefer) return;
|
|
554
|
+
if (el.__mnfstDefer) { revive(el.__mnfstDefer); return; }
|
|
502
555
|
if (defer(el)) skip();
|
|
503
556
|
}
|
|
504
557
|
|
|
@@ -11,7 +11,7 @@ let lastInputModality = 'mouse';
|
|
|
11
11
|
// navigating item came from.
|
|
12
12
|
let lastPointerTarget = null;
|
|
13
13
|
document.addEventListener('keydown', () => { lastInputModality = 'keyboard'; }, true);
|
|
14
|
-
document.addEventListener('pointerdown', (e) => { lastInputModality = 'mouse'; lastPointerTarget = e.target; }, true);
|
|
14
|
+
document.addEventListener('pointerdown', (e) => { lastInputModality = 'mouse'; lastPointerTarget = new WeakRef(e.target); }, true);
|
|
15
15
|
|
|
16
16
|
// Context-menu dismiss (click outside / Escape). Module scope on purpose: a closure
|
|
17
17
|
// made inside the directive shares its V8 context with every sibling closure, so it
|
|
@@ -38,6 +38,114 @@ function ensureContextDismiss(menu, owner) {
|
|
|
38
38
|
bindContextDismiss(menu, ctxAbort.signal);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// A removed trigger stops being the menu's context host and dismiss owner. A menu it
|
|
42
|
+
// opened closes with it; one opened by another trigger keeps its dismiss listeners.
|
|
43
|
+
function releaseContextTrigger(menu, el) {
|
|
44
|
+
if (menu._triggerHost === el) {
|
|
45
|
+
menu._triggerHost = null;
|
|
46
|
+
menu._triggerEl = null;
|
|
47
|
+
hideIfOpen(menu);
|
|
48
|
+
}
|
|
49
|
+
if (menu.__mnfstCtxOwner === el) {
|
|
50
|
+
menu.__mnfstContextAbort?.abort();
|
|
51
|
+
menu.__mnfstContextDismissBound = false;
|
|
52
|
+
menu.__mnfstContextAbort = null;
|
|
53
|
+
menu.__mnfstCtxOwner = null;
|
|
54
|
+
const host = menu._triggerHost;
|
|
55
|
+
if (host && host.isConnected && menu.matches(':popover-open')) ensureContextDismiss(menu, host);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hideIfOpen(menu) {
|
|
60
|
+
try { if (menu.matches(':popover-open')) menu.hidePopover(); } catch (_) { }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Menus the plugin cloned into <body> (template, component) belong to the triggers
|
|
64
|
+
// using them, and leave with the last one. Removal waits a tick: a menu item's own
|
|
65
|
+
// click may be what removed the trigger, and its dispatch must finish in the DOM.
|
|
66
|
+
function retainManagedMenu(menu, el) {
|
|
67
|
+
(menu.__mnfstManagedBy ||= new Set()).add(el);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function releaseManagedMenu(menu, el) {
|
|
71
|
+
const users = menu.__mnfstManagedBy;
|
|
72
|
+
if (!users || !users.delete(el) || users.size) return;
|
|
73
|
+
hideIfOpen(menu);
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
if (users.size) return;
|
|
76
|
+
menu.__mnfstManagedBy = null;
|
|
77
|
+
window.Alpine?.destroyTree?.(menu);
|
|
78
|
+
menu.remove();
|
|
79
|
+
}, 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Per-menu handlers (bound once, outliving any one trigger). Module scope for the same
|
|
83
|
+
// reason as bindContextDismiss: they must hold the menu and nothing else.
|
|
84
|
+
function bindMenuRoles(menu) {
|
|
85
|
+
const roleItems = () => menu.querySelectorAll('li').forEach((li) => {
|
|
86
|
+
if (!li.hasAttribute('role')) li.setAttribute('role', 'menuitem');
|
|
87
|
+
});
|
|
88
|
+
roleItems();
|
|
89
|
+
if (menu.__mnfstRoleOnRender) return;
|
|
90
|
+
menu.__mnfstRoleOnRender = true;
|
|
91
|
+
menu.addEventListener('manifest:defer-render', roleItems);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function bindMenuAriaToggle(menu) {
|
|
95
|
+
if (menu.__mnfstAriaToggleBound) return;
|
|
96
|
+
menu.__mnfstAriaToggleBound = true;
|
|
97
|
+
menu.addEventListener('toggle', (e) => {
|
|
98
|
+
const expanded = e.newState === 'open' ? 'true' : 'false';
|
|
99
|
+
document.querySelectorAll(`[aria-controls="${menu.id}"]`).forEach(t => t.setAttribute('aria-expanded', expanded));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const MENU_ROW = 'li, a, button, label, [role=menuitem], [role=option]';
|
|
104
|
+
const MENU_CONTROL = 'input, select, textarea, [contenteditable]:not([contenteditable=false]), [role=switch], .combobox, [x-dropdown], [popovertarget], [keep-open]';
|
|
105
|
+
|
|
106
|
+
function bindMenuClose(menu) {
|
|
107
|
+
if (menu.__mnfstCloseBound) return;
|
|
108
|
+
menu.__mnfstCloseBound = true;
|
|
109
|
+
|
|
110
|
+
// Close the item's menu plus any menus it was nested in. The owned
|
|
111
|
+
// popover isn't always a <menu> (e.g. .dropdown-menu on a div), so
|
|
112
|
+
// fall back to it rather than relying on the ancestor lookup.
|
|
113
|
+
const closeFrom = (node) => {
|
|
114
|
+
let m = node.closest('menu[popover]') || menu;
|
|
115
|
+
while (m) {
|
|
116
|
+
if (m.matches(':popover-open')) m.hidePopover();
|
|
117
|
+
m = m.parentElement?.closest('menu[popover]');
|
|
118
|
+
}
|
|
119
|
+
if (lastInputModality === 'keyboard') {
|
|
120
|
+
const trigger = menu.__mnfstTrigger || menu._triggerHost || document.querySelector(`[aria-controls="${menu.id}"]`);
|
|
121
|
+
trigger?.focus?.();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
menu.addEventListener('click', (e) => {
|
|
126
|
+
// Explicit opt-in wins over every exclusion below
|
|
127
|
+
const opted = e.target.closest('[close]');
|
|
128
|
+
if (opted && menu.contains(opted)) return closeFrom(opted);
|
|
129
|
+
if (!menu.__mnfstAutoClose) return;
|
|
130
|
+
|
|
131
|
+
// Outermost row between the click target and the menu
|
|
132
|
+
let row = null;
|
|
133
|
+
for (let n = e.target; n && n !== menu; n = n.parentElement) {
|
|
134
|
+
if (n.matches?.(MENU_ROW)) row = n;
|
|
135
|
+
}
|
|
136
|
+
if (!row) return;
|
|
137
|
+
|
|
138
|
+
// A control that *is* the row is a menu item; one nested
|
|
139
|
+
// inside it is embedded UI, so leave the menu open
|
|
140
|
+
const hit = e.target.closest(MENU_CONTROL);
|
|
141
|
+
if (hit && hit !== row && row.contains(hit)) return;
|
|
142
|
+
if (row.querySelector(MENU_CONTROL)) return;
|
|
143
|
+
if (row.matches('[role=switch], [x-dropdown], [popovertarget], [keep-open]')) return;
|
|
144
|
+
|
|
145
|
+
closeFrom(row);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
41
149
|
function initializeDropdownPlugin() {
|
|
42
150
|
// Ensure an Alpine context exists. Keep the scope empty — seeding properties
|
|
43
151
|
// collides with author state and the tabs plugin's `tab` property.
|
|
@@ -79,6 +187,11 @@ function initializeDropdownPlugin() {
|
|
|
79
187
|
registerDirective('dropdown', (el, { modifiers, expression }, { effect, evaluateLater, cleanup }) => {
|
|
80
188
|
let menu;
|
|
81
189
|
|
|
190
|
+
// Shared hover state for all dropdown types
|
|
191
|
+
let hoverTimeout;
|
|
192
|
+
let autoCloseTimeout;
|
|
193
|
+
let startAutoCloseTimer;
|
|
194
|
+
|
|
82
195
|
// Listeners this trigger adds to things that outlive it (its menu, document)
|
|
83
196
|
// are bound with this run's signal and aborted when Alpine tears the trigger
|
|
84
197
|
// down, or when the effect re-runs setup. Without it every re-rendered trigger
|
|
@@ -88,23 +201,17 @@ function initializeDropdownPlugin() {
|
|
|
88
201
|
cleanup(() => {
|
|
89
202
|
if (runAbort) runAbort.abort();
|
|
90
203
|
runAbort = null;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (menu && menu.__mnfstTrigger === el) menu.__mnfstTrigger = null;
|
|
98
|
-
if (menu && menu._triggerHost === el) { menu._triggerHost = null; menu._triggerEl = null; }
|
|
204
|
+
clearTimeout(hoverTimeout);
|
|
205
|
+
clearTimeout(autoCloseTimeout);
|
|
206
|
+
if (!menu) return;
|
|
207
|
+
releaseContextTrigger(menu, el);
|
|
208
|
+
if (menu.__mnfstTrigger === el) menu.__mnfstTrigger = null;
|
|
209
|
+
releaseManagedMenu(menu, el);
|
|
99
210
|
});
|
|
100
211
|
|
|
101
|
-
// Shared hover state for all dropdown types
|
|
102
|
-
let hoverTimeout;
|
|
103
|
-
let autoCloseTimeout;
|
|
104
|
-
let startAutoCloseTimer;
|
|
105
|
-
|
|
106
212
|
effect(() => {
|
|
107
213
|
if (runAbort) runAbort.abort();
|
|
214
|
+
if (menu) releaseManagedMenu(menu, el);
|
|
108
215
|
const runCtl = new AbortController();
|
|
109
216
|
runAbort = runCtl;
|
|
110
217
|
const signal = runCtl.signal;
|
|
@@ -146,6 +253,7 @@ function initializeDropdownPlugin() {
|
|
|
146
253
|
const uniqueDropdownId = `dropdown-${anchorCode}`;
|
|
147
254
|
menu.setAttribute('id', uniqueDropdownId);
|
|
148
255
|
document.body.appendChild(menu);
|
|
256
|
+
retainManagedMenu(menu, el);
|
|
149
257
|
// Appending to <body> loses the fact that the trigger sits inside a
|
|
150
258
|
// dialog, which is how manifest.dialog.css tells a dialog's own menus
|
|
151
259
|
// from the page's. An authored menu keeps its place and needs no mark.
|
|
@@ -171,6 +279,16 @@ function initializeDropdownPlugin() {
|
|
|
171
279
|
const waitForComponent = async () => {
|
|
172
280
|
const loader = window.ManifestComponentsLoader;
|
|
173
281
|
const content = await loader.loadComponent(componentName);
|
|
282
|
+
if (signal.aborted) return;
|
|
283
|
+
// Another trigger of this menu may have cloned it during the await
|
|
284
|
+
const loaded = document.getElementById(dropdownId);
|
|
285
|
+
if (loaded) {
|
|
286
|
+
menu = loaded;
|
|
287
|
+
if (menu.__mnfstManagedBy) retainManagedMenu(menu, el);
|
|
288
|
+
if (!modifiers.includes('context')) el.setAttribute('popovertarget', dropdownId);
|
|
289
|
+
setupDropdown();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
174
292
|
if (content) {
|
|
175
293
|
// Create a temporary container to parse the component
|
|
176
294
|
const tempDiv = document.createElement('div');
|
|
@@ -182,6 +300,7 @@ function initializeDropdownPlugin() {
|
|
|
182
300
|
menu = menuElement.cloneNode(true);
|
|
183
301
|
menu.setAttribute('id', dropdownId);
|
|
184
302
|
document.body.appendChild(menu);
|
|
303
|
+
retainManagedMenu(menu, el);
|
|
185
304
|
if (el.closest('[popover]')) menu.setAttribute('data-popover-nested', '');
|
|
186
305
|
if (!modifiers.includes('context')) el.setAttribute('popovertarget', dropdownId);
|
|
187
306
|
|
|
@@ -216,6 +335,7 @@ function initializeDropdownPlugin() {
|
|
|
216
335
|
console.warn(`[Manifest] Dropdown menu with id "${dropdownId}" not found`);
|
|
217
336
|
return;
|
|
218
337
|
}
|
|
338
|
+
if (menu.__mnfstManagedBy) retainManagedMenu(menu, el);
|
|
219
339
|
if (!modifiers.includes('context')) el.setAttribute('popovertarget', dropdownId);
|
|
220
340
|
}
|
|
221
341
|
|
|
@@ -241,10 +361,9 @@ function initializeDropdownPlugin() {
|
|
|
241
361
|
const a = el.style.getPropertyValue('--trigger-anchor');
|
|
242
362
|
if (a) menu.style.setProperty('position-anchor', a);
|
|
243
363
|
};
|
|
244
|
-
if (!modifiers.includes('context')
|
|
245
|
-
el.
|
|
246
|
-
el.addEventListener('
|
|
247
|
-
el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') pointMenuHere(); });
|
|
364
|
+
if (!modifiers.includes('context')) {
|
|
365
|
+
el.addEventListener('pointerdown', pointMenuHere, { signal });
|
|
366
|
+
el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') pointMenuHere(); }, { signal });
|
|
248
367
|
}
|
|
249
368
|
|
|
250
369
|
// A11y wiring (WAI-ARIA Menu Button): trigger gets aria-haspopup/
|
|
@@ -258,22 +377,9 @@ function initializeDropdownPlugin() {
|
|
|
258
377
|
if (!menu.hasAttribute('role')) menu.setAttribute('role', 'menu');
|
|
259
378
|
// Deferred menus get their items later — role them off the gesture,
|
|
260
379
|
// never in the toggle task (243 role writes on a visible list ≈ 70ms)
|
|
261
|
-
|
|
262
|
-
if (!li.hasAttribute('role')) li.setAttribute('role', 'menuitem');
|
|
263
|
-
});
|
|
264
|
-
roleItems();
|
|
265
|
-
if (!menu.__mnfstRoleOnRender) {
|
|
266
|
-
menu.__mnfstRoleOnRender = true;
|
|
267
|
-
menu.addEventListener('manifest:defer-render', roleItems);
|
|
268
|
-
}
|
|
380
|
+
bindMenuRoles(menu);
|
|
269
381
|
// Keep aria-expanded in sync across every trigger of this menu.
|
|
270
|
-
|
|
271
|
-
menu.__mnfstAriaToggleBound = true;
|
|
272
|
-
menu.addEventListener('toggle', (e) => {
|
|
273
|
-
const expanded = e.newState === 'open' ? 'true' : 'false';
|
|
274
|
-
document.querySelectorAll(`[aria-controls="${menu.id}"]`).forEach(t => t.setAttribute('aria-expanded', expanded));
|
|
275
|
-
});
|
|
276
|
-
}
|
|
382
|
+
bindMenuAriaToggle(menu);
|
|
277
383
|
}
|
|
278
384
|
|
|
279
385
|
// Set up hover functionality after menu is ready
|
|
@@ -312,8 +418,8 @@ function initializeDropdownPlugin() {
|
|
|
312
418
|
}, 300); // Small delay to prevent accidental closes
|
|
313
419
|
};
|
|
314
420
|
|
|
315
|
-
el.addEventListener('mouseenter', handleShowPopover);
|
|
316
|
-
el.addEventListener('mouseleave', startAutoCloseTimer);
|
|
421
|
+
el.addEventListener('mouseenter', handleShowPopover, { signal });
|
|
422
|
+
el.addEventListener('mouseleave', startAutoCloseTimer, { signal });
|
|
317
423
|
}
|
|
318
424
|
|
|
319
425
|
// Set up context menu (right-click) functionality
|
|
@@ -359,84 +465,78 @@ function initializeDropdownPlugin() {
|
|
|
359
465
|
});
|
|
360
466
|
};
|
|
361
467
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
el.addEventListener('contextmenu', (e) => {
|
|
366
|
-
e.preventDefault();
|
|
367
|
-
e.stopPropagation();
|
|
468
|
+
el.addEventListener('contextmenu', (e) => {
|
|
469
|
+
e.preventDefault();
|
|
470
|
+
e.stopPropagation();
|
|
368
471
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
472
|
+
// Android fires a real contextmenu after a touch long-press too —
|
|
473
|
+
// our pointer path already opened the menu, so ignore the dupe.
|
|
474
|
+
if (el.__mnfstLongPressOpenedAt && Date.now() - el.__mnfstLongPressOpenedAt < LONG_PRESS_DUPE_WINDOW_MS) return;
|
|
372
475
|
|
|
373
|
-
|
|
374
|
-
|
|
476
|
+
openContextMenuAt(e.target, e.clientX, e.clientY);
|
|
477
|
+
}, { signal });
|
|
375
478
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
479
|
+
// iOS Safari never fires `contextmenu` for a touch long-press (Android
|
|
480
|
+
// does). Drive the same open from pointer events instead — touch/pen
|
|
481
|
+
// only, mouse keeps native contextmenu semantics.
|
|
482
|
+
const LONG_PRESS_MS = 500;
|
|
483
|
+
const MOVE_SLOP_PX = 10;
|
|
484
|
+
let pressTimer = null;
|
|
485
|
+
let startX = 0, startY = 0;
|
|
486
|
+
let pressTarget = null;
|
|
487
|
+
|
|
488
|
+
const suppressCallout = () => {
|
|
489
|
+
el.style.setProperty('-webkit-touch-callout', 'none');
|
|
490
|
+
el.style.setProperty('-webkit-user-select', 'none');
|
|
491
|
+
el.style.setProperty('user-select', 'none');
|
|
492
|
+
};
|
|
493
|
+
const restoreCallout = () => {
|
|
494
|
+
el.style.removeProperty('-webkit-touch-callout');
|
|
495
|
+
el.style.removeProperty('-webkit-user-select');
|
|
496
|
+
el.style.removeProperty('user-select');
|
|
497
|
+
};
|
|
498
|
+
const cancelPress = () => {
|
|
499
|
+
if (pressTimer) clearTimeout(pressTimer);
|
|
500
|
+
pressTimer = null;
|
|
501
|
+
restoreCallout();
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
el.addEventListener('pointerdown', (e) => {
|
|
505
|
+
if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return;
|
|
506
|
+
cancelPress();
|
|
507
|
+
startX = e.clientX;
|
|
508
|
+
startY = e.clientY;
|
|
509
|
+
pressTarget = e.target;
|
|
510
|
+
suppressCallout();
|
|
511
|
+
pressTimer = setTimeout(() => {
|
|
397
512
|
pressTimer = null;
|
|
513
|
+
if (signal.aborted) return;
|
|
398
514
|
restoreCallout();
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
el.addEventListener('pointerup', cancelPress);
|
|
422
|
-
el.addEventListener('pointercancel', cancelPress);
|
|
423
|
-
el.addEventListener('scroll', cancelPress, true);
|
|
424
|
-
|
|
425
|
-
// Suppress the synthetic click that follows the touch sequence which
|
|
426
|
-
// opened the menu, so it doesn't also activate/navigate the trigger.
|
|
427
|
-
el.addEventListener('click', (e) => {
|
|
428
|
-
if (el.__mnfstLongPressOpenedAt && Date.now() - el.__mnfstLongPressOpenedAt < LONG_PRESS_DUPE_WINDOW_MS) {
|
|
429
|
-
e.preventDefault();
|
|
430
|
-
e.stopPropagation();
|
|
431
|
-
}
|
|
432
|
-
}, true);
|
|
433
|
-
}
|
|
515
|
+
el.__mnfstLongPressOpenedAt = Date.now();
|
|
516
|
+
// No contextmenu event on iOS: announce the press so composing plugins (x-edit) can track the target.
|
|
517
|
+
pressTarget.dispatchEvent(new CustomEvent('dropdown:context', { bubbles: true, detail: { x: startX, y: startY } }));
|
|
518
|
+
openContextMenuAt(pressTarget, startX, startY);
|
|
519
|
+
}, LONG_PRESS_MS);
|
|
520
|
+
}, { signal });
|
|
521
|
+
el.addEventListener('pointermove', (e) => {
|
|
522
|
+
if (!pressTimer) return;
|
|
523
|
+
if (Math.abs(e.clientX - startX) > MOVE_SLOP_PX || Math.abs(e.clientY - startY) > MOVE_SLOP_PX) cancelPress();
|
|
524
|
+
}, { signal });
|
|
525
|
+
el.addEventListener('pointerup', cancelPress, { signal });
|
|
526
|
+
el.addEventListener('pointercancel', cancelPress, { signal });
|
|
527
|
+
el.addEventListener('scroll', cancelPress, { capture: true, signal });
|
|
528
|
+
|
|
529
|
+
// Suppress the synthetic click that follows the touch sequence which
|
|
530
|
+
// opened the menu, so it doesn't also activate/navigate the trigger.
|
|
531
|
+
el.addEventListener('click', (e) => {
|
|
532
|
+
if (el.__mnfstLongPressOpenedAt && Date.now() - el.__mnfstLongPressOpenedAt < LONG_PRESS_DUPE_WINDOW_MS) {
|
|
533
|
+
e.preventDefault();
|
|
534
|
+
e.stopPropagation();
|
|
535
|
+
}
|
|
536
|
+
}, { capture: true, signal });
|
|
434
537
|
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
// Bound once per menu, owned by one trigger and released in that
|
|
438
|
-
// trigger's cleanup (see top). Any other trigger of the same menu
|
|
439
|
-
// re-binds on its next open, so a shared menu never loses dismiss.
|
|
538
|
+
// Owned by one trigger, released in its cleanup. Any other trigger of
|
|
539
|
+
// the same menu re-binds on its next open.
|
|
440
540
|
ensureContextDismiss(menu, el);
|
|
441
541
|
}
|
|
442
542
|
|
|
@@ -445,47 +545,7 @@ function initializeDropdownPlugin() {
|
|
|
445
545
|
// Context menus auto-close by nature, so .close is implied.
|
|
446
546
|
if (modifiers.includes('close') || modifiers.includes('context')) menu.__mnfstAutoClose = true;
|
|
447
547
|
|
|
448
|
-
|
|
449
|
-
menu.__mnfstCloseBound = true;
|
|
450
|
-
|
|
451
|
-
const ROW = 'li, a, button, label, [role=menuitem], [role=option]';
|
|
452
|
-
const CONTROL = 'input, select, textarea, [contenteditable]:not([contenteditable=false]), [role=switch], .combobox, [x-dropdown], [popovertarget], [keep-open]';
|
|
453
|
-
|
|
454
|
-
// Close the item's menu plus any menus it was nested in. The owned
|
|
455
|
-
// popover isn't always a <menu> (e.g. .dropdown-menu on a div), so
|
|
456
|
-
// fall back to it rather than relying on the ancestor lookup.
|
|
457
|
-
const closeFrom = (node) => {
|
|
458
|
-
let m = node.closest('menu[popover]') || menu;
|
|
459
|
-
while (m) {
|
|
460
|
-
if (m.matches(':popover-open')) m.hidePopover();
|
|
461
|
-
m = m.parentElement?.closest('menu[popover]');
|
|
462
|
-
}
|
|
463
|
-
if (lastInputModality === 'keyboard') (menu.__mnfstTrigger || el).focus?.();
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
menu.addEventListener('click', (e) => {
|
|
467
|
-
// Explicit opt-in wins over every exclusion below
|
|
468
|
-
const opted = e.target.closest('[close]');
|
|
469
|
-
if (opted && menu.contains(opted)) return closeFrom(opted);
|
|
470
|
-
if (!menu.__mnfstAutoClose) return;
|
|
471
|
-
|
|
472
|
-
// Outermost row between the click target and the menu
|
|
473
|
-
let row = null;
|
|
474
|
-
for (let n = e.target; n && n !== menu; n = n.parentElement) {
|
|
475
|
-
if (n.matches?.(ROW)) row = n;
|
|
476
|
-
}
|
|
477
|
-
if (!row) return;
|
|
478
|
-
|
|
479
|
-
// A control that *is* the row is a menu item; one nested
|
|
480
|
-
// inside it is embedded UI, so leave the menu open
|
|
481
|
-
const hit = e.target.closest(CONTROL);
|
|
482
|
-
if (hit && hit !== row && row.contains(hit)) return;
|
|
483
|
-
if (row.querySelector(CONTROL)) return;
|
|
484
|
-
if (row.matches('[role=switch], [x-dropdown], [popovertarget], [keep-open]')) return;
|
|
485
|
-
|
|
486
|
-
closeFrom(row);
|
|
487
|
-
});
|
|
488
|
-
}
|
|
548
|
+
bindMenuClose(menu);
|
|
489
549
|
|
|
490
550
|
// Add keyboard navigation handling
|
|
491
551
|
menu.addEventListener('keydown', (e) => {
|
|
@@ -695,7 +755,7 @@ document.addEventListener('click', (event) => {
|
|
|
695
755
|
// the route change it dispatches instead. Hash links aren't intercepted and still
|
|
696
756
|
// close via the normal path.
|
|
697
757
|
window.addEventListener('manifest:route-change', () => {
|
|
698
|
-
const pressed = lastPointerTarget;
|
|
758
|
+
const pressed = lastPointerTarget?.deref();
|
|
699
759
|
document.querySelectorAll('[popover]:popover-open').forEach((menu) => {
|
|
700
760
|
if (!menu.__mnfstCloseBound) return;
|
|
701
761
|
const row = pressed && menu.contains(pressed) ? pressed : null;
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest.appwrite.auth.js": "sha384-9WNhIeSEzxxJm+yP7cV41kV2xyzyXgFCNpThqiREcNbb6tnIW6O7VRZPL2jwy9Nk",
|
|
3
|
-
"manifest.appwrite.data.js": "sha384-
|
|
3
|
+
"manifest.appwrite.data.js": "sha384-lNtw0QrcI1Hnshur38Dqw+iA9nTQ4GSnkeYUgKYdUWkGyqQ47XFiVAFr1LKGV/ZP",
|
|
4
4
|
"manifest.appwrite.presences.js": "sha384-/CagPsgWhO1sWEevCNGJmZ/ezRK9YCjLjvF7hxNSM4gVlJmEVtfSLfF7wnsRq441",
|
|
5
5
|
"manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
|
|
6
6
|
"manifest.charts.js": "sha384-lzcoovnPCKhcwigrwYvc5rPAZuZNHwWmNNuu7hxPsfJuKAu8ejla5F7agHZ2Xft9",
|
|
7
7
|
"manifest.chat.js": "sha384-88DfMfJLoi2oxclVD2iIMDkIGAExMXq5k47mekUredlicLaP3oZY5bwtwqs+TqDF",
|
|
8
8
|
"manifest.code.js": "sha384-WGPlZE5Sp9GygUzJSmCdpLyYQTgvmEKc9BQS9Hmq1VTQwkqYJzPSWibHvPekcres",
|
|
9
9
|
"manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
|
|
10
|
-
"manifest.colorpicker.js": "sha384-
|
|
11
|
-
"manifest.combobox.js": "sha384-
|
|
10
|
+
"manifest.colorpicker.js": "sha384-H6u6FHBbqIViQAYM9DTcFSVScghuvSEZqT1ZIKg7vv7zQx/5GiebDlJvyXdBxuM/",
|
|
11
|
+
"manifest.combobox.js": "sha384-eY5w3XHiFa1s6mXfmFbn/ff2GNHvlGUDAPVedlc0rvH4XhfBn3mWGLjj539q5IwS",
|
|
12
12
|
"manifest.components.js": "sha384-BUFA8h+G5Plr82sV7bKzO42pPoiRZwRQcKj1lt2ROFiKjjlkayU/vmkAFqEsdBA+",
|
|
13
13
|
"manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
|
|
14
|
-
"manifest.data.js": "sha384-
|
|
15
|
-
"manifest.datepicker.js": "sha384-
|
|
16
|
-
"manifest.defer.js": "sha384-
|
|
14
|
+
"manifest.data.js": "sha384-kwLHIxnThzjCDfjqJwq96lSb2dPcz4ntutRZ25jWmADaR9U8+T9ISU7GankNch4Z",
|
|
15
|
+
"manifest.datepicker.js": "sha384-tA4Dh8xXfyRw+3C19XXXDE0O6O6V6mGMdWv/lJA3Jcw/gcLG3aFTTEhvQEkJNkvb",
|
|
16
|
+
"manifest.defer.js": "sha384-5cHzFTbx+cZmGpk9FuiUtW3ciC9NvvHwJeHP7xL5yiZVp9PhMWVdQ7L9y6ju2yoo",
|
|
17
17
|
"manifest.device.js": "sha384-YeQaS56L8IILqrQNBSeAOLwIh5DuAOrE60cYLK1+46qJtUrXlAvqGe2j5mbV4Vq7",
|
|
18
|
-
"manifest.dropdowns.js": "sha384-
|
|
18
|
+
"manifest.dropdowns.js": "sha384-TlFrYVWTXvpUmGnYaAk2til4lu0p6w5YcmWWEzZAokzd2qlmoyRLLXXVuYAK/AlR",
|
|
19
19
|
"manifest.edit.js": "sha384-MOgQNdNLeHijhR0u2W6ntoOulKFglsATCFDqkinesOU15tHoILk/JNzHKkGi5dUG",
|
|
20
20
|
"manifest.export.js": "sha384-Rx6XvqBZc4apzP/FwcDr6yIln4/zNa08Q4ZqBZcEqUz1aNZQfl4h0+Ae7mqCQyuG",
|
|
21
21
|
"manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
|
|
29
29
|
"manifest.status.js": "sha384-Zf07q/5tRS2F0o128sINYrce8xP035FTV0Ot7V7/hh+cwFhVZg+EWkHZN5b42aBn",
|
|
30
30
|
"manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
|
|
31
|
-
"manifest.sw.js": "sha384-
|
|
31
|
+
"manifest.sw.js": "sha384-my5z7X0SU63k0XDo1MuISuHz/R0iD8TOoJhwBs1hLSxAej366Skfo4QDtMl87EqJ",
|
|
32
32
|
"manifest.tabs.js": "sha384-otU09H7jnawVKQdLAYpMCd3jx00tgyPVr42D2hLIBzN5dG4se505YesavdnMqEi1",
|
|
33
33
|
"manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
|
|
34
34
|
"manifest.toasts.js": "sha384-3zkWxzw693pxg3QDFsoYCA3CqYaLKmBaMArULXODD66Utolpewl0OAeTB92aWUVv",
|
|
35
|
-
"manifest.tooltips.js": "sha384-
|
|
35
|
+
"manifest.tooltips.js": "sha384-hXGp5j0s6p2kGZY/hqLPEbaUv68ca63kFCjYiZqK3DcC12mpHTBy1vz3kuApyf4p",
|
|
36
36
|
"manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
|
|
37
37
|
"manifest.utilities.js": "sha384-h17EquaRtNcEW0yfhk7R+a+2+CzTlBxPuFryO6i9NV/OiIRMihREoyzQ77QawoQ7",
|
|
38
38
|
"manifest.virtual.js": "sha384-ZDNoF9QVhVcVpkjIK6NH15S6I5c6OBshiJpXSOYjNsIB+298I0a7g3aFO+jyq4Vb",
|
package/lib/manifest.sw.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
// Build stamps the published version here; unstamped (dev) falls back to the
|
|
18
18
|
// registration URL's `v`, then 'dev'.
|
|
19
|
-
const BUILD_VERSION = '0.5.
|
|
19
|
+
const BUILD_VERSION = '0.5.262';
|
|
20
20
|
const CACHE_PREFIX = 'mnfst-sw:';
|
|
21
21
|
const META_CACHE = CACHE_PREFIX + 'meta';
|
|
22
22
|
// Sized to hold a full precache.json warm-up (PRECACHE_MAX_ENTRIES server-side)
|
package/lib/manifest.tooltips.js
CHANGED
|
@@ -171,6 +171,7 @@ function initializeTooltipPlugin() {
|
|
|
171
171
|
s.el.style.setProperty('position-anchor', anchorName);
|
|
172
172
|
|
|
173
173
|
s.activeTrigger = trigger;
|
|
174
|
+
trigger._tooltipSingleton = s;
|
|
174
175
|
s.currentAnchorName = anchorName;
|
|
175
176
|
|
|
176
177
|
// A11y: aria-describedby links trigger → tooltip for screen readers.
|
|
@@ -212,7 +213,20 @@ function initializeTooltipPlugin() {
|
|
|
212
213
|
|
|
213
214
|
// ---- Directive ----
|
|
214
215
|
|
|
215
|
-
Alpine.directive('tooltip', (el, { modifiers, expression }, { effect, evaluateLater }) => {
|
|
216
|
+
Alpine.directive('tooltip', (el, { modifiers, expression }, { effect, evaluateLater, cleanup }) => {
|
|
217
|
+
const listeners = new AbortController();
|
|
218
|
+
const { signal } = listeners;
|
|
219
|
+
cleanup(() => {
|
|
220
|
+
listeners.abort();
|
|
221
|
+
if (_pendingTrigger === el) cancelPendingShow();
|
|
222
|
+
// A removal root is already detached here, so its host can't be looked up
|
|
223
|
+
const s = el._tooltipSingleton;
|
|
224
|
+
el._tooltipSingleton = null;
|
|
225
|
+
if (s && s.activeTrigger === el) {
|
|
226
|
+
if (s.el.matches(':popover-open')) s.el.hidePopover();
|
|
227
|
+
s.activeTrigger = null;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
216
230
|
|
|
217
231
|
// --- Content ---
|
|
218
232
|
// Escape by default; author literal HTML or .html/.safe opts in.
|
|
@@ -288,15 +302,15 @@ function initializeTooltipPlugin() {
|
|
|
288
302
|
};
|
|
289
303
|
|
|
290
304
|
// Mouse interactions
|
|
291
|
-
el.addEventListener('mouseenter', requestShow);
|
|
292
|
-
el.addEventListener('mouseleave', requestHide);
|
|
305
|
+
el.addEventListener('mouseenter', requestShow, { signal });
|
|
306
|
+
el.addEventListener('mouseleave', requestHide, { signal });
|
|
293
307
|
|
|
294
308
|
// Keyboard focus (WCAG 2.1 SC 1.4.13). Gated on :focus-visible so mouse-click
|
|
295
309
|
// focus doesn't flash the tooltip.
|
|
296
310
|
el.addEventListener('focus', () => {
|
|
297
311
|
if (el.matches(':focus-visible')) requestShow();
|
|
298
|
-
});
|
|
299
|
-
el.addEventListener('blur', requestHide);
|
|
312
|
+
}, { signal });
|
|
313
|
+
el.addEventListener('blur', requestHide, { signal });
|
|
300
314
|
|
|
301
315
|
// Mousedown/click hides now and clears the chain window, so a synthetic
|
|
302
316
|
// re-hover (content shifting under the cursor) waits the full delay.
|
|
@@ -305,8 +319,8 @@ function initializeTooltipPlugin() {
|
|
|
305
319
|
hideAnySingleton();
|
|
306
320
|
clearChainWindow();
|
|
307
321
|
};
|
|
308
|
-
el.addEventListener('mousedown', hideOnInteraction);
|
|
309
|
-
el.addEventListener('click', hideOnInteraction);
|
|
322
|
+
el.addEventListener('mousedown', hideOnInteraction, { signal });
|
|
323
|
+
el.addEventListener('click', hideOnInteraction, { signal });
|
|
310
324
|
});
|
|
311
325
|
|
|
312
326
|
// Global: when ANY other popover opens, close the singleton(s). Dropdowns and
|