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
|
@@ -1,37 +1,14 @@
|
|
|
1
|
-
/* Manifest Date Picker
|
|
1
|
+
/* Manifest Date Picker — modular date/time picker behind one `x-date` directive
|
|
2
2
|
/* By Andrew Matlock under MIT license
|
|
3
3
|
/* https://manifestx.dev
|
|
4
|
-
/*
|
|
5
|
-
/* A fully modular date (and time) picker behind one `x-date` directive.
|
|
6
|
-
/*
|
|
7
|
-
/* The element decides its role — no role modifiers needed:
|
|
8
|
-
/* <input x-date> field with auto-generated dropdown calendar
|
|
9
|
-
/* <input x-date type="date"> same, degrades to the native picker without JS
|
|
10
|
-
/* <input x-date type="time"> standalone time picker
|
|
11
|
-
/* <input x-date type="datetime-local"> date + time, degrades natively without JS
|
|
12
|
-
/* <button x-date="cal-id"> button trigger for an authored calendar
|
|
13
|
-
/* <div x-date> inline calendar
|
|
14
|
-
/* <menu popover x-date> / <dialog x-date> dropdown / modal calendar
|
|
15
|
-
/*
|
|
16
|
-
/* Selection modes are modifiers: x-date.range, x-date.multiple, x-date.time
|
|
17
|
-
/* (x-date.time on a date field appends a time-of-day control).
|
|
18
|
-
/*
|
|
19
|
-
/* The value is polymorphic, mirroring the color picker's swatch:
|
|
20
|
-
/* string → the id of an authored calendar element (fields only)
|
|
21
|
-
/* object → reactive config: { months, min, max, disabled, presets }
|
|
22
4
|
*/
|
|
23
5
|
|
|
24
6
|
(function () {
|
|
25
7
|
'use strict';
|
|
26
8
|
|
|
27
|
-
/*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* every add/subtract immune to DST shifts (a ±1h move never crosses a
|
|
31
|
-
* calendar day from noon). All formatting/localization is delegated to
|
|
32
|
-
* Intl, so we never parse locale-formatted strings — the one place a date
|
|
33
|
-
* library would earn its weight. Thin by design: swappable for Temporal.
|
|
34
|
-
* ------------------------------------------------------------------ */
|
|
9
|
+
/* Shared global: ManifestDates — dependency-free date math, noon-anchored to
|
|
10
|
+
* dodge DST shifts. Formatting/localization delegated to Intl (never parses
|
|
11
|
+
* locale strings). */
|
|
35
12
|
if (!window.ManifestDates) {
|
|
36
13
|
const pad = (n) => String(n).padStart(2, '0');
|
|
37
14
|
const D = {
|
|
@@ -78,24 +55,13 @@
|
|
|
78
55
|
window.ManifestDates = D;
|
|
79
56
|
}
|
|
80
57
|
|
|
81
|
-
/*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* `_ui` is a reserved, self-identifying key: any loaded data source may
|
|
86
|
-
* carry a top-level `_ui` object, namespaced per element, e.g.
|
|
87
|
-
* _ui: { date: { today: …, clear: … }, colorpicker: { … } }
|
|
88
|
-
* No manifest flag — overrides piggyback on the normal local-data/
|
|
89
|
-
* localization model and can be colocated with author content. Values may
|
|
90
|
-
* be plain strings or $x/$locale references (locale-reactive). resolve()
|
|
91
|
-
* deep-merges every loaded source's `_ui[component]` onto the plugin's
|
|
92
|
-
* English fallbacks. Defensive: any failure returns the fallbacks untouched.
|
|
93
|
-
* Kept byte-identical across the colorpicker / charts copies.
|
|
94
|
-
* ------------------------------------------------------------------ */
|
|
58
|
+
/* Shared global: ManifestUI — `_ui` text resolver. A loaded data source may carry
|
|
59
|
+
* a top-level `_ui` object namespaced per component (_ui.date, _ui.colorpicker, …);
|
|
60
|
+
* resolve() deep-merges it onto the plugin's English fallbacks. Locale-reactive via
|
|
61
|
+
* $x/$locale. Kept byte-identical across the colorpicker / charts copies. */
|
|
95
62
|
if (!window.ManifestUI) {
|
|
96
63
|
window.ManifestUI = {
|
|
97
|
-
/*
|
|
98
|
-
* sources only — never force-loads others just to scan them for `_ui`. */
|
|
64
|
+
/* Loaded data-source names only — never force-loads others to scan them. */
|
|
99
65
|
_loadedSourceNames() {
|
|
100
66
|
try {
|
|
101
67
|
const store = window.ManifestDataStore && window.ManifestDataStore.rawDataStore;
|
|
@@ -103,8 +69,7 @@
|
|
|
103
69
|
} catch (_) { }
|
|
104
70
|
return [];
|
|
105
71
|
},
|
|
106
|
-
/* Deep-merge every loaded source's `_ui[component]` onto `fallbacks`.
|
|
107
|
-
* Reads inside the caller's Alpine effect (if any) so $x/$locale make it reactive. */
|
|
72
|
+
/* Deep-merge every loaded source's `_ui[component]` onto `fallbacks`. */
|
|
108
73
|
resolve(component, fallbacks) {
|
|
109
74
|
const merged = JSON.parse(JSON.stringify(fallbacks || {}));
|
|
110
75
|
try {
|
|
@@ -141,8 +106,7 @@
|
|
|
141
106
|
const Alpine = window.Alpine;
|
|
142
107
|
const D = window.ManifestDates;
|
|
143
108
|
|
|
144
|
-
// Reactive registry
|
|
145
|
-
// calendar mounts after the reader binds.
|
|
109
|
+
// Reactive registry id -> api, so $date('id') resolves across mount order.
|
|
146
110
|
const _registry = Alpine.reactive ? Alpine.reactive({}) : {};
|
|
147
111
|
let _uid = 0;
|
|
148
112
|
|
|
@@ -188,9 +152,7 @@
|
|
|
188
152
|
return (!isNaN(n) && n >= 0 && n <= 6) ? n : null;
|
|
189
153
|
}
|
|
190
154
|
|
|
191
|
-
//
|
|
192
|
-
// labels should read like an author wrote them: capitalized, no
|
|
193
|
-
// abbreviation periods. aria-labels keep the untouched Intl output.
|
|
155
|
+
// Capitalize + strip trailing periods for chrome labels; aria-labels keep raw Intl output.
|
|
194
156
|
function tidy(s, locale) {
|
|
195
157
|
if (!s) return s;
|
|
196
158
|
let out = s.replace(/\.+$/, '');
|
|
@@ -302,9 +264,8 @@
|
|
|
302
264
|
return { start: s, end: e };
|
|
303
265
|
},
|
|
304
266
|
|
|
305
|
-
// (Re)apply selection/today
|
|
306
|
-
//
|
|
307
|
-
// range shape has no ARIA equivalent, so plain state classes.
|
|
267
|
+
// (Re)apply selection/today to a day button — ARIA for selection/today,
|
|
268
|
+
// plain classes for range shape (no ARIA equivalent).
|
|
308
269
|
applyDayState(btn, date) {
|
|
309
270
|
btn.classList.remove('range-start', 'range-end', 'in-range');
|
|
310
271
|
btn.removeAttribute('aria-selected');
|
|
@@ -331,10 +292,7 @@
|
|
|
331
292
|
|
|
332
293
|
select(date) {
|
|
333
294
|
if (!date || this.isDisabled(date)) return;
|
|
334
|
-
//
|
|
335
|
-
// visible (matters in multi-month: clicking a day in the 2nd
|
|
336
|
-
// month must not scroll it into the 1st). Only jump when the
|
|
337
|
-
// date falls outside the visible [view.m … view.m+count-1] window.
|
|
295
|
+
// Jump the view only when the picked date is outside the visible month window.
|
|
338
296
|
const count = Math.max(1, this.monthCount || 1);
|
|
339
297
|
const idx = (date.getFullYear() * 12 + date.getMonth()) - (this.view.y * 12 + (this.view.m - 1));
|
|
340
298
|
if (idx < 0 || idx >= count) this.view = { y: date.getFullYear(), m: date.getMonth() + 1 };
|
|
@@ -361,8 +319,7 @@
|
|
|
361
319
|
this.selected = date;
|
|
362
320
|
if (this.withTime) this.timeSet = true;
|
|
363
321
|
this.syncOut(); this.bump(); this.render();
|
|
364
|
-
// With a time control,
|
|
365
|
-
// the time after picking the day.
|
|
322
|
+
// With a time control, stay open so the user can set the time.
|
|
366
323
|
if (this.mode === 'menu' && !this.withTime) this.close();
|
|
367
324
|
},
|
|
368
325
|
|
|
@@ -418,17 +375,13 @@
|
|
|
418
375
|
const disp = this.displayValue();
|
|
419
376
|
if (this.field && this.field.tagName === 'INPUT' && this.field.type !== 'hidden') {
|
|
420
377
|
this.field.value = disp;
|
|
421
|
-
//
|
|
422
|
-
// into it during the same reactive flush — re-assert the
|
|
423
|
-
// human-readable display after that write settles.
|
|
378
|
+
// Re-assert the display after x-model writes the raw value back this flush.
|
|
424
379
|
if (this.field.hasAttribute('x-model')) {
|
|
425
380
|
const f = this.field;
|
|
426
381
|
setTimeout(() => { f.value = this.displayValue(); }, 0);
|
|
427
382
|
}
|
|
428
383
|
} else if (this.field && !this.field.firstElementChild) {
|
|
429
|
-
// Text-only triggers swap their text
|
|
430
|
-
// Triggers with element children (icons, custom layout) own
|
|
431
|
-
// their content — authors bind $date(id) for display.
|
|
384
|
+
// Text-only triggers swap their text; triggers with element children own theirs.
|
|
432
385
|
this.field.textContent = disp || this.fieldPlaceholder;
|
|
433
386
|
}
|
|
434
387
|
},
|
|
@@ -467,13 +420,8 @@
|
|
|
467
420
|
}
|
|
468
421
|
},
|
|
469
422
|
|
|
470
|
-
// Only the plugin-generated dropdown
|
|
471
|
-
//
|
|
472
|
-
// here. We still fire native showPopover() — so the menu rides the
|
|
473
|
-
// reset.css popover transitions, dropdown.css anchor positioning
|
|
474
|
-
// (anchor-name/position-anchor are wired in wireField), light-
|
|
475
|
-
// dismiss, and nested-popover cascade. Authored <div>/<menu>/
|
|
476
|
-
// <dialog> calendars are inline; native HTML owns their visibility.
|
|
423
|
+
// Only the plugin-generated dropdown opens here; native showPopover() rides
|
|
424
|
+
// the reset.css/dropdown.css popover wiring. Authored calendars are inline.
|
|
477
425
|
open() {
|
|
478
426
|
if (this.mode !== 'menu') return;
|
|
479
427
|
this.viewMode = 'days';
|
|
@@ -718,17 +666,10 @@
|
|
|
718
666
|
return wrap;
|
|
719
667
|
}
|
|
720
668
|
|
|
721
|
-
/* ---- Time of day
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* Clear (configurable) sit in the actions column.
|
|
726
|
-
* • Date+time: a ghost row of typed segments inside the calendar, with a
|
|
727
|
-
* columns picker opened by a popovertarget chevron. That menu is
|
|
728
|
-
* appended to <body> so the calendar's overflow can't clip it, yet the
|
|
729
|
-
* in-calendar invoker keeps it nested (it won't dismiss the calendar).
|
|
730
|
-
* setTime never triggers a full re-render (that would blur the segments).
|
|
731
|
-
* ---------------------------------------------------------------- */
|
|
669
|
+
/* ---- Time of day — no native time input (unstylable). Standalone: columns
|
|
670
|
+
* render inline in the dropdown. Date+time: typed segments + a body-appended
|
|
671
|
+
* columns menu (nested under an in-calendar invoker). setTime avoids a full
|
|
672
|
+
* re-render to keep segment focus. */
|
|
732
673
|
function renderTime(state, shell, locale, ui) {
|
|
733
674
|
const h12 = uses12h(locale);
|
|
734
675
|
const cur = () => { const [h, mi] = state.time.split(':').map(Number); return { h: h || 0, mi: mi || 0 }; };
|
|
@@ -823,9 +764,8 @@
|
|
|
823
764
|
if (h12) { apBtn = document.createElement('button'); apBtn.type = 'button'; apBtn.setAttribute('aria-label', 'AM or PM'); fieldBox.appendChild(apBtn); }
|
|
824
765
|
segs = { hIn, mIn, apBtn };
|
|
825
766
|
|
|
826
|
-
// Columns menu —
|
|
827
|
-
//
|
|
828
|
-
// calendar, so the menu nests under it (won't dismiss it on interaction).
|
|
767
|
+
// Columns menu — body-appended popover so the calendar's overflow can't clip it,
|
|
768
|
+
// yet nests under the in-calendar invoker (won't dismiss the calendar).
|
|
829
769
|
const menu = buildColumns(document.createElement('menu'), false);
|
|
830
770
|
menu.setAttribute('popover', '');
|
|
831
771
|
menu.id = 'mnfst-dp-time-' + (++_uid);
|
|
@@ -839,8 +779,7 @@
|
|
|
839
779
|
trigger.setAttribute('popovertarget', menu.id);
|
|
840
780
|
trigger.setAttribute('aria-label', ui.time);
|
|
841
781
|
fieldBox.appendChild(trigger);
|
|
842
|
-
// The "Time" label is
|
|
843
|
-
// the click to its associated button, whose popovertarget opens the menu.
|
|
782
|
+
// The "Time" label is an invoker too (htmlFor forwards activation to the button).
|
|
844
783
|
lab.htmlFor = trigger.id;
|
|
845
784
|
|
|
846
785
|
const timeAnchor = '--datepicker-time-' + _uid;
|
|
@@ -961,11 +900,8 @@
|
|
|
961
900
|
}
|
|
962
901
|
|
|
963
902
|
/* ---- Config: the directive's value --------------------------- */
|
|
964
|
-
// An object value is reactive config: { months, min, max, disabled,
|
|
965
|
-
//
|
|
966
|
-
// (an input's own min/max are semantic). Modal/dropdown opening of
|
|
967
|
-
// authored calendars is native HTML's job (popovertarget), so the value
|
|
968
|
-
// is no longer used to link a field to a calendar by id.
|
|
903
|
+
// An object value is reactive config: { months, min, max, disabled, presets }.
|
|
904
|
+
// min/max also read the element's native attributes.
|
|
969
905
|
function applyAttrConfig(state, el) {
|
|
970
906
|
const min = el.getAttribute('min');
|
|
971
907
|
const max = el.getAttribute('max');
|
|
@@ -990,14 +926,8 @@
|
|
|
990
926
|
if (dirty) { state.bump(); if (state._mounted) state.render(); }
|
|
991
927
|
});
|
|
992
928
|
}
|
|
993
|
-
/* ---- Reactive chrome
|
|
994
|
-
*
|
|
995
|
-
* changes. An Alpine effect tracking $locale + the data store's version
|
|
996
|
-
* heartbeat covers async locale-source reloads settling after a switch,
|
|
997
|
-
* and `_ui` sources that finish loading post-mount. The resolved-output
|
|
998
|
-
* comparison keeps high-frequency _dataVersion bumps (e.g. realtime
|
|
999
|
-
* plugins) from re-rendering a calendar that didn't change.
|
|
1000
|
-
* ---------------------------------------------------------------- */
|
|
929
|
+
/* ---- Reactive chrome — re-render when locale, direction, or resolved `_ui`
|
|
930
|
+
* text changes. Output comparison skips no-op _dataVersion bumps. */
|
|
1001
931
|
function bindReactiveRender(state) {
|
|
1002
932
|
if (state._uiEffectBound) return;
|
|
1003
933
|
state._uiEffectBound = true;
|
|
@@ -1019,10 +949,7 @@
|
|
|
1019
949
|
const read = Alpine.evaluateLater(el, modelExpr);
|
|
1020
950
|
state.modelGet = (cb) => { let out; read(v => { out = v; if (cb) cb(v); }); return out; };
|
|
1021
951
|
state.modelSet = (v) => { try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(v)}`); } catch (_) { } };
|
|
1022
|
-
// React to external model changes
|
|
1023
|
-
// value (date+time when withTime) and re-sync the field display —
|
|
1024
|
-
// syncOut writes the same value back to the model, which Alpine
|
|
1025
|
-
// treats as a no-op, so this cannot loop.
|
|
952
|
+
// React to external model changes; syncOut writes the same value back (no loop).
|
|
1026
953
|
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(); } } }); });
|
|
1027
954
|
}
|
|
1028
955
|
|
|
@@ -1034,11 +961,9 @@
|
|
|
1034
961
|
return menu;
|
|
1035
962
|
}
|
|
1036
963
|
|
|
1037
|
-
/* ---- Wiring an <input>/<button> field to a generated dropdown
|
|
1038
|
-
*
|
|
1039
|
-
* <
|
|
1040
|
-
* modal — authored <dialog>/<menu> calendars are inline and opened
|
|
1041
|
-
* natively (popovertarget). */
|
|
964
|
+
/* ---- Wiring an <input>/<button> field to a generated dropdown — the plugin
|
|
965
|
+
* owns this menu's open/close (a text <input> can't be a native popover
|
|
966
|
+
* invoker). Authored <dialog>/<menu> calendars are inline. */
|
|
1042
967
|
function wireField(fieldEl, expression, modifiers, cleanup) {
|
|
1043
968
|
const calendar = createDefaultMenu(fieldEl);
|
|
1044
969
|
const state = (calendar._dateState = createState(calendar));
|
|
@@ -1048,8 +973,7 @@
|
|
|
1048
973
|
else if (modifiers.includes('multiple')) state.selectionMode = 'multiple';
|
|
1049
974
|
if (modifiers.includes('time')) state.withTime = true;
|
|
1050
975
|
|
|
1051
|
-
// Piggyback the input's authored type
|
|
1052
|
-
// the no-JS fallback (native picker). We capture it, then take over
|
|
976
|
+
// Piggyback the input's authored type (format + no-JS fallback), then take over
|
|
1053
977
|
// with text+readonly so the styled calendar owns the UX.
|
|
1054
978
|
if (fieldEl.tagName === 'INPUT') {
|
|
1055
979
|
const authored = (fieldEl.getAttribute('type') || '').toLowerCase();
|
|
@@ -1060,19 +984,15 @@
|
|
|
1060
984
|
fieldEl.readOnly = true; fieldEl.autocomplete = 'off';
|
|
1061
985
|
} else {
|
|
1062
986
|
applyAttrConfig(state, fieldEl);
|
|
1063
|
-
// Text-only triggers:
|
|
1064
|
-
// selection swaps in and out of. Triggers with element children
|
|
1065
|
-
// are left untouched (syncOut skips them).
|
|
987
|
+
// Text-only triggers: authored text is the placeholder the selection swaps.
|
|
1066
988
|
state.fieldPlaceholder = fieldEl.firstElementChild ? '' : fieldEl.textContent.trim();
|
|
1067
989
|
}
|
|
1068
990
|
|
|
1069
991
|
// Object config in the value.
|
|
1070
992
|
bindConfigValue(state, fieldEl, expression);
|
|
1071
993
|
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
// visible field would submit its display text under the same key.
|
|
1075
|
-
// No-JS fallback is unaffected: this only runs once the plugin has.
|
|
994
|
+
// Move the name onto a synthesized hidden (ISO) input so the form submits the
|
|
995
|
+
// value, not the display text.
|
|
1076
996
|
const nameAttr = fieldEl.getAttribute('name');
|
|
1077
997
|
if (nameAttr && !state.hiddenInput) {
|
|
1078
998
|
const hidden = document.createElement('input');
|
|
@@ -1092,10 +1012,8 @@
|
|
|
1092
1012
|
// Also resolve $date(fieldId) when the calendar is an auto-generated menu.
|
|
1093
1013
|
if (fieldEl.id) _registry[fieldEl.id] = state.api;
|
|
1094
1014
|
|
|
1095
|
-
// CSS anchor positioning (
|
|
1096
|
-
//
|
|
1097
|
-
// with auto-flip fallbacks. We only fire showPopover() on click since
|
|
1098
|
-
// a text <input> can't carry popovertarget.
|
|
1015
|
+
// CSS anchor positioning (as in the dropdowns plugin); showPopover() on click
|
|
1016
|
+
// since a text <input> can't carry popovertarget.
|
|
1099
1017
|
const anchorName = '--datepicker-' + (++_uid);
|
|
1100
1018
|
fieldEl.style.setProperty('anchor-name', `${anchorName}, var(--co-anchor, --no-anchor)`);
|
|
1101
1019
|
fieldEl.style.setProperty('--trigger-anchor', anchorName);
|
|
@@ -1106,8 +1024,7 @@
|
|
|
1106
1024
|
calendar.addEventListener('toggle', (e) => fieldEl.setAttribute('aria-expanded', e.newState === 'open' ? 'true' : 'false'));
|
|
1107
1025
|
fieldEl.addEventListener('click', () => state.open());
|
|
1108
1026
|
|
|
1109
|
-
// Reactive chrome re-render (locale, direction, _ui).
|
|
1110
|
-
// menus never run the calendar-element path, so wire it here too.
|
|
1027
|
+
// Reactive chrome re-render (locale, direction, _ui).
|
|
1111
1028
|
bindReactiveRender(state);
|
|
1112
1029
|
|
|
1113
1030
|
if (cleanup) cleanup(() => {
|
|
@@ -1128,9 +1045,7 @@
|
|
|
1128
1045
|
return;
|
|
1129
1046
|
}
|
|
1130
1047
|
|
|
1131
|
-
// Authored calendar
|
|
1132
|
-
// inline: the plugin renders into it; native HTML owns any
|
|
1133
|
-
// open/close (popovertarget, light-dismiss).
|
|
1048
|
+
// Authored calendar (<div>/<menu>/<dialog>) — always inline; native HTML owns open/close.
|
|
1134
1049
|
const state = el._dateState || (el._dateState = createState(el));
|
|
1135
1050
|
state.mode = 'inline';
|
|
1136
1051
|
if (modifiers.includes('range')) state.selectionMode = 'range';
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
/* Manifest Dropdowns */
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// see the event before any handler can stop it. This mirrors the heuristic the
|
|
6
|
-
// browser uses for :focus-visible, but lets us decide whether to focus at all.
|
|
3
|
+
// Most recent input modality, so we only move focus into a menu for keyboard opens.
|
|
4
|
+
// Capture-phase so no handler can stop it first.
|
|
7
5
|
let lastInputModality = 'mouse';
|
|
8
6
|
document.addEventListener('keydown', () => { lastInputModality = 'keyboard'; }, true);
|
|
9
7
|
document.addEventListener('pointerdown', () => { lastInputModality = 'mouse'; }, true);
|
|
10
8
|
|
|
11
|
-
// Initialize plugin when either DOM is ready or Alpine is ready
|
|
12
9
|
function initializeDropdownPlugin() {
|
|
13
|
-
// Ensure Alpine
|
|
14
|
-
//
|
|
15
|
-
// tabs plugin's page-level `tab` property.
|
|
10
|
+
// Ensure an Alpine context exists. Keep the scope empty — seeding properties
|
|
11
|
+
// collides with author state and the tabs plugin's `tab` property.
|
|
16
12
|
function ensureAlpineContext() {
|
|
17
13
|
const body = document.body;
|
|
18
14
|
if (!body.hasAttribute('x-data')) {
|
|
@@ -186,14 +182,9 @@ function initializeDropdownPlugin() {
|
|
|
186
182
|
el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') pointMenuHere(); });
|
|
187
183
|
}
|
|
188
184
|
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// The menu element gets `role="menu"` and each list item gets
|
|
193
|
-
// `role="menuitem"` so screen readers announce the relationship.
|
|
194
|
-
//
|
|
195
|
-
// We don't apply these for `.context` dropdowns invoked by right-
|
|
196
|
-
// click — they're not button-triggered popups in the APG sense.
|
|
185
|
+
// A11y wiring (WAI-ARIA Menu Button): trigger gets aria-haspopup/
|
|
186
|
+
// controls/expanded, menu gets role=menu, items role=menuitem. Skipped
|
|
187
|
+
// for .context (right-click) — not button-triggered popups per APG.
|
|
197
188
|
if (!modifiers.includes('context')) {
|
|
198
189
|
if (!menu.id) menu.id = 'mnfst-dropdown-' + Math.random().toString(36).slice(2, 9);
|
|
199
190
|
el.setAttribute('aria-haspopup', 'menu');
|
|
@@ -257,9 +248,7 @@ function initializeDropdownPlugin() {
|
|
|
257
248
|
if (menu.matches(':popover-open')) menu.hidePopover();
|
|
258
249
|
};
|
|
259
250
|
|
|
260
|
-
// Bind
|
|
261
|
-
// the guard, reprocessing (router re-walks, re-init) would
|
|
262
|
-
// stack duplicate handlers.
|
|
251
|
+
// Bind once per element (reprocessing would stack duplicates).
|
|
263
252
|
if (!el.__mnfstContextTriggerBound) {
|
|
264
253
|
el.__mnfstContextTriggerBound = true;
|
|
265
254
|
el.addEventListener('contextmenu', (e) => {
|
|
@@ -291,10 +280,8 @@ function initializeDropdownPlugin() {
|
|
|
291
280
|
});
|
|
292
281
|
}
|
|
293
282
|
|
|
294
|
-
// Document-level dismiss listeners are
|
|
295
|
-
//
|
|
296
|
-
// Bind once per menu, tied to an AbortController so they
|
|
297
|
-
// share one removable signal.
|
|
283
|
+
// Document-level dismiss listeners are global — bind once per
|
|
284
|
+
// menu via an AbortController so reprocessing can't leak them.
|
|
298
285
|
if (!menu.__mnfstContextDismissBound) {
|
|
299
286
|
menu.__mnfstContextDismissBound = true;
|
|
300
287
|
const ctxAbort = new AbortController();
|
|
@@ -394,14 +381,11 @@ function initializeDropdownPlugin() {
|
|
|
394
381
|
if (firstLi && !menu.querySelector('button, [href], input, select, textarea, [tabindex="0"]')) {
|
|
395
382
|
firstLi.setAttribute('tabindex', '0');
|
|
396
383
|
if (lastInputModality === 'keyboard') {
|
|
397
|
-
// Keyboard open
|
|
398
|
-
// :focus-visible correctly paints the ring.
|
|
384
|
+
// Keyboard open: focus the first item (rings correctly).
|
|
399
385
|
firstLi.focus();
|
|
400
386
|
} else {
|
|
401
|
-
// Pointer open: park focus on the menu
|
|
402
|
-
//
|
|
403
|
-
// focus-visible ring on an item the user didn't reach
|
|
404
|
-
// by keyboard.
|
|
387
|
+
// Pointer open: park focus on the menu so arrow keys enter
|
|
388
|
+
// the list without ringing an item the user didn't reach.
|
|
405
389
|
if (!menu.hasAttribute('tabindex')) menu.setAttribute('tabindex', '-1');
|
|
406
390
|
menu.focus();
|
|
407
391
|
}
|
|
@@ -446,11 +430,9 @@ function initializeDropdownPlugin() {
|
|
|
446
430
|
});
|
|
447
431
|
}
|
|
448
432
|
|
|
449
|
-
// Track initialization to prevent duplicates
|
|
450
433
|
let dropdownPluginInitialized = false;
|
|
451
434
|
|
|
452
|
-
// True once Alpine
|
|
453
|
-
// module load so we never miss the event, whatever the script order.
|
|
435
|
+
// True once Alpine finished its initial walk. Bound at load so we never miss it.
|
|
454
436
|
let alpineHasWalked = false;
|
|
455
437
|
document.addEventListener('alpine:initialized', () => { alpineHasWalked = true; });
|
|
456
438
|
|
|
@@ -465,13 +447,10 @@ function ensureDropdownPluginInitialized() {
|
|
|
465
447
|
dropdownPluginInitialized = true;
|
|
466
448
|
initializeDropdownPlugin();
|
|
467
449
|
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
// (x-icon, x-tooltip, …) already registered. Walking here during boot would
|
|
473
|
-
// re-init those subtrees before the other directives exist, permanently
|
|
474
|
-
// dropping nested plugin content (e.g. a trailing x-icon in a menu button).
|
|
450
|
+
// Walk existing [x-dropdown] subtrees only if Alpine already finished its boot
|
|
451
|
+
// walk (late load). During boot, let Alpine's own walk handle them with all
|
|
452
|
+
// sibling directives registered — walking here would drop nested plugin content
|
|
453
|
+
// (e.g. a trailing x-icon in a menu button).
|
|
475
454
|
if (alpineHasWalked && typeof window.Alpine.initTree === 'function') {
|
|
476
455
|
document.querySelectorAll('[x-dropdown]').forEach(el => {
|
|
477
456
|
if (!el.__x) {
|