mnfst 0.5.145 → 0.5.147

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.
@@ -0,0 +1,570 @@
1
+ /* Manifest Combobox */
2
+
3
+ (function () {
4
+
5
+ // A select-like field with four orthogonal axes:
6
+ // trigger: input (default) | textarea | button (editor:none, click only)
7
+ // options: none (free entry) | datalist/select (generated menu) | menu (authored) | async
8
+ // value: single (default) | multiple (+ max cap)
9
+ // display: text (default) | chips
10
+ //
11
+ // Config is the directive value: a bare id string (the options source, like
12
+ // x-dropdown) or a { source, max, filter, separators, min, debounce } object.
13
+ // Modes are modifiers. The dropdown reuses Manifest's menu[popover] styles.
14
+
15
+ /* ------------------------------------------------------------------ *
16
+ * Shared localized-UI resolver (byte-identical to the datepicker /
17
+ * colorpicker copies; first plugin to load defines it).
18
+ * ------------------------------------------------------------------ */
19
+ if (!window.ManifestUI) {
20
+ window.ManifestUI = {
21
+ _loadedSourceNames() {
22
+ try {
23
+ const store = window.ManifestDataStore && window.ManifestDataStore.rawDataStore;
24
+ if (store && typeof store.keys === 'function') return [...store.keys()];
25
+ } catch (_) { }
26
+ return [];
27
+ },
28
+ resolve(component, fallbacks) {
29
+ const merged = JSON.parse(JSON.stringify(fallbacks || {}));
30
+ try {
31
+ if (!window.Alpine || typeof Alpine.evaluate !== 'function') return merged;
32
+ try { Alpine.evaluate(document.body, '$locale && $locale.current'); } catch (_) { }
33
+ for (const name of this._loadedSourceNames()) {
34
+ let ui;
35
+ try { ui = Alpine.evaluate(document.body, `$x['${name}'] && $x['${name}']._ui && $x['${name}']._ui['${component}']`); } catch (_) { ui = null; }
36
+ if (ui && typeof ui === 'object' && !Array.isArray(ui)) this._deepOverlay(merged, ui);
37
+ }
38
+ } catch (_) { }
39
+ return merged;
40
+ },
41
+ _deepOverlay(target, src) {
42
+ for (const k of Object.keys(src)) {
43
+ if (k.startsWith('$') || k === 'contentType' || k === 'valueOf' || k === 'toString') continue;
44
+ const v = src[k];
45
+ if (typeof v === 'function') continue;
46
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
47
+ if (!target[k] || typeof target[k] !== 'object') target[k] = {};
48
+ this._deepOverlay(target[k], v);
49
+ } else if (v !== undefined && v !== null && v !== '') {
50
+ target[k] = v;
51
+ }
52
+ }
53
+ }
54
+ };
55
+ }
56
+
57
+ // Default English UI chrome; overridable via a data source's `_ui.combobox`.
58
+ const UI_FALLBACK = {
59
+ empty: 'No matches',
60
+ add: 'Add “{value}”',
61
+ loading: 'Searching…',
62
+ prompt: 'Type to search'
63
+ };
64
+
65
+ function initializeComboboxPlugin() {
66
+
67
+ function ensureAlpineContext() {
68
+ if (!document.body.hasAttribute('x-data')) document.body.setAttribute('x-data', '{}');
69
+ }
70
+
71
+ ensureAlpineContext();
72
+
73
+ const rand = () => Math.random().toString(36).slice(2, 9);
74
+ const ui = () => window.ManifestUI ? window.ManifestUI.resolve('combobox', UI_FALLBACK) : UI_FALLBACK;
75
+
76
+ // Read options from a source element (datalist/select → option, menu → li)
77
+ function readOptions(src) {
78
+ if (!src) return [];
79
+ if (src.tagName === 'MENU') {
80
+ return Array.from(src.querySelectorAll('li')).map(li => ({
81
+ value: li.dataset.value != null ? li.dataset.value : li.textContent.trim(),
82
+ label: li.dataset.label || li.textContent.trim(),
83
+ pattern: li.dataset.pattern || null,
84
+ html: li.innerHTML
85
+ }));
86
+ }
87
+ return Array.from(src.querySelectorAll('option')).map(o => ({
88
+ value: o.value || o.textContent.trim(),
89
+ label: o.textContent.trim() || o.value,
90
+ pattern: o.getAttribute('data-pattern') || null,
91
+ html: null
92
+ }));
93
+ }
94
+
95
+ Alpine.directive('combobox', (el, { modifiers, expression }) => {
96
+ // Build after the current tick so sibling sources (datalist/menu) exist.
97
+ setTimeout(() => build(el, modifiers, expression || ''), 0);
98
+ });
99
+
100
+ function build(el, modifiers, expression) {
101
+ if (el.__mnfstCombobox) return;
102
+ el.__mnfstCombobox = true;
103
+
104
+ // ----- Config: bare id string, or a { } object -----
105
+ let cfg = {};
106
+ const expr = expression.trim();
107
+ if (expr.startsWith('{')) {
108
+ try { cfg = window.Alpine.evaluate(el, expr) || {}; } catch (_) { cfg = {}; }
109
+ } else if (expr) {
110
+ cfg.source = expr;
111
+ }
112
+
113
+ const editorNone = el.tagName === 'BUTTON';
114
+ const multiple = modifiers.includes('multiple');
115
+ const chips = modifiers.includes('chips');
116
+ const strict = modifiers.includes('strict');
117
+ const create = modifiers.includes('create');
118
+ const isAsync = modifiers.includes('async');
119
+ const max = parseInt(cfg.max, 10) || (multiple ? Infinity : 1);
120
+ const filterMode = cfg.filter || 'includes';
121
+ const minChars = parseInt(cfg.min, 10) || 0;
122
+ const debounceMs = parseInt(cfg.debounce, 10) || 200;
123
+ const separators = cfg.separators != null
124
+ ? String(cfg.separators).split('').filter(c => c.trim() || c === ' ')
125
+ : (multiple ? [','] : []);
126
+ const name = el.getAttribute('name');
127
+ const placeholder = el.getAttribute('placeholder') || (editorNone ? el.textContent.trim() : '');
128
+
129
+ // ----- Source / options -----
130
+ const sourceId = cfg.source ? String(cfg.source).replace(/^#/, '') : null;
131
+ const src = sourceId ? document.getElementById(sourceId) : null;
132
+ let options = readOptions(src);
133
+ const hasMenu = !!src || isAsync;
134
+ if (editorNone && !hasMenu) return; // a button trigger needs a list
135
+
136
+ // ----- Shell -----
137
+ const wrap = document.createElement('div');
138
+ wrap.className = 'combobox';
139
+ el.parentNode.insertBefore(wrap, el);
140
+ wrap.appendChild(el);
141
+ // The wrapper IS the field, so move the author's sizing (inline style) onto
142
+ // it. Otherwise the editor/button is constrained inside a full-width field.
143
+ const authorStyle = el.getAttribute('style');
144
+ if (authorStyle) { wrap.setAttribute('style', authorStyle); el.removeAttribute('style'); }
145
+ el.setAttribute('autocomplete', 'off');
146
+ if (!editorNone) el.removeAttribute('placeholder');
147
+ if (name) el.removeAttribute('name'); // hidden inputs carry the value(s)
148
+
149
+ // Live region for add/remove announcements
150
+ const live = document.createElement('span');
151
+ live.setAttribute('role', 'status');
152
+ wrap.appendChild(live);
153
+ const announce = (m) => { live.textContent = ''; live.textContent = m; };
154
+
155
+ // ----- Selection model -----
156
+ let selected = [];
157
+ const isSelected = (v) => selected.some(s => String(s.value).toLowerCase() === String(v).toLowerCase());
158
+ const atCap = () => multiple && selected.length >= max;
159
+
160
+ // ----- Menu -----
161
+ let menu = null, optionEls = [], createEl = null, emptyEl = null, activeIndex = -1;
162
+
163
+ function makeOption(o, i) {
164
+ const li = document.createElement('li');
165
+ li.id = menu.id + '-opt-' + i;
166
+ li.dataset.value = o.value;
167
+ li.dataset.label = o.label;
168
+ if (o.pattern) li.dataset.pattern = o.pattern;
169
+ if (o.html) li.innerHTML = o.html; else li.textContent = o.label;
170
+ li.setAttribute('role', 'option');
171
+ li.setAttribute('aria-selected', isSelected(o.value) ? 'true' : 'false');
172
+ return li;
173
+ }
174
+ function setOptions(opts) {
175
+ optionEls.forEach(li => li.remove());
176
+ optionEls = opts.map((o, i) => {
177
+ const li = makeOption(o, i);
178
+ menu.insertBefore(li, createEl || emptyEl);
179
+ return li;
180
+ });
181
+ activeIndex = -1;
182
+ }
183
+
184
+ if (hasMenu) {
185
+ if (src && src.tagName === 'MENU') {
186
+ menu = src;
187
+ } else {
188
+ menu = document.createElement('menu');
189
+ document.body.appendChild(menu);
190
+ if (src) src.style.setProperty('display', 'none', 'important');
191
+ }
192
+ menu.setAttribute('popover', 'manual');
193
+ if (!menu.id) menu.id = 'combobox-menu-' + rand();
194
+ menu.setAttribute('role', 'listbox');
195
+ if (multiple) menu.setAttribute('aria-multiselectable', 'true');
196
+
197
+ Array.from(menu.querySelectorAll('li')).forEach(li => li.remove());
198
+
199
+ if (create) {
200
+ createEl = document.createElement('li');
201
+ createEl.className = 'combobox-create';
202
+ createEl.setAttribute('role', 'option');
203
+ createEl.hidden = true;
204
+ menu.appendChild(createEl);
205
+ }
206
+ emptyEl = document.createElement('div');
207
+ emptyEl.className = 'combobox-empty';
208
+ emptyEl.hidden = true;
209
+ menu.appendChild(emptyEl);
210
+
211
+ setOptions(options);
212
+
213
+ const anchorName = '--combobox-' + rand();
214
+ wrap.style.setProperty('anchor-name', anchorName);
215
+ menu.style.setProperty('position-anchor', anchorName);
216
+
217
+ el.setAttribute('aria-controls', menu.id);
218
+ el.setAttribute('aria-expanded', 'false');
219
+ if (editorNone) {
220
+ el.setAttribute('aria-haspopup', 'listbox');
221
+ } else {
222
+ el.setAttribute('role', 'combobox');
223
+ el.setAttribute('aria-autocomplete', 'list');
224
+ }
225
+
226
+ menu.addEventListener('mousedown', (e) => e.preventDefault());
227
+ menu.addEventListener('click', (e) => {
228
+ const li = e.target.closest('li');
229
+ if (li && !li.hidden) selectOption(li);
230
+ });
231
+ }
232
+
233
+ function openMenu() {
234
+ if (!menu || menu.matches(':popover-open') || atCap()) return;
235
+ menu.style.minWidth = wrap.offsetWidth + 'px';
236
+ menu.showPopover();
237
+ el.setAttribute('aria-expanded', 'true');
238
+ }
239
+ function closeMenu() {
240
+ if (!menu || !menu.matches(':popover-open')) return;
241
+ menu.hidePopover();
242
+ el.setAttribute('aria-expanded', 'false');
243
+ setActive(-1);
244
+ }
245
+ function showEmpty(text) {
246
+ if (emptyEl) { emptyEl.textContent = text; emptyEl.hidden = false; }
247
+ setActive(-1);
248
+ }
249
+
250
+ function setActive(i) {
251
+ optionEls.forEach(li => li.removeAttribute('aria-current'));
252
+ if (createEl) createEl.removeAttribute('aria-current');
253
+ activeIndex = i;
254
+ const li = liAt(i);
255
+ if (li) { li.setAttribute('aria-current', 'true'); el.setAttribute('aria-activedescendant', li.id); }
256
+ else el.removeAttribute('aria-activedescendant');
257
+ }
258
+ function liAt(i) {
259
+ if (i === -2) return createEl && !createEl.hidden ? createEl : null;
260
+ return optionEls[i] || null;
261
+ }
262
+ function visibleIndexes() {
263
+ const v = optionEls.map((li, i) => (!li.hidden ? i : -1)).filter(i => i >= 0);
264
+ if (createEl && !createEl.hidden) v.push(-2);
265
+ return v;
266
+ }
267
+ function moveActive(dir) {
268
+ const vis = visibleIndexes();
269
+ if (!vis.length) return;
270
+ let pos = vis.indexOf(activeIndex);
271
+ pos = (pos + dir + vis.length) % vis.length;
272
+ setActive(vis[pos]);
273
+ }
274
+
275
+ function testPattern(p, input) {
276
+ try { return new RegExp(p, 'i').test(input); } catch { return false; }
277
+ }
278
+
279
+ function filter() {
280
+ if (!menu) return;
281
+ const raw = (el.value || '').trim();
282
+ const q = raw.toLowerCase();
283
+ let first = -1, anyVisible = false, exact = false;
284
+ optionEls.forEach((li, i) => {
285
+ const t = String(li.dataset.label).toLowerCase();
286
+ let show =
287
+ isAsync || editorNone || !q || filterMode === 'none' ? true
288
+ : filterMode === 'startswith' ? t.startsWith(q)
289
+ : filterMode === 'pattern' ? (li.dataset.pattern ? testPattern(li.dataset.pattern, raw) : t.includes(q))
290
+ : t.includes(q);
291
+ if (show && multiple && isSelected(li.dataset.value)) show = false;
292
+ li.hidden = !show;
293
+ if (show) { anyVisible = true; if (first < 0) first = i; }
294
+ if (t === q) exact = true;
295
+ });
296
+ if (createEl) {
297
+ const show = create && q && !exact;
298
+ createEl.hidden = !show;
299
+ createEl.textContent = show ? ui().add.replace('{value}', raw) : '';
300
+ createEl.dataset.value = raw;
301
+ if (show) anyVisible = true;
302
+ }
303
+ if (emptyEl) { emptyEl.textContent = ui().empty; emptyEl.hidden = anyVisible; }
304
+ setActive(first >= 0 ? first : (createEl && !createEl.hidden ? -2 : -1));
305
+ }
306
+
307
+ // Show the whole list (single field re-opened on a committed value, so the
308
+ // choice can be swapped — its current value shouldn't narrow the list to itself).
309
+ function showAll() {
310
+ optionEls.forEach(li => { li.hidden = false; });
311
+ if (createEl) createEl.hidden = true;
312
+ if (emptyEl) emptyEl.hidden = optionEls.length > 0;
313
+ const sel = optionEls.findIndex(li => isSelected(li.dataset.value));
314
+ setActive(sel >= 0 ? sel : (optionEls.length ? 0 : -1));
315
+ }
316
+
317
+ // ----- Async option fetching (Open UI's beforefilter analog) -----
318
+ let asyncTimer, asyncSeq = 0;
319
+ function scheduleAsync() {
320
+ openMenu();
321
+ clearTimeout(asyncTimer);
322
+ setOptions([]); // drop stale results so nothing flashes
323
+ showEmpty(ui().loading);
324
+ asyncTimer = setTimeout(runAsync, debounceMs);
325
+ }
326
+ function runAsync() {
327
+ const seq = ++asyncSeq;
328
+ const value = (el.value || '').trim();
329
+ el.dispatchEvent(new CustomEvent('combobox-filter', {
330
+ bubbles: true,
331
+ detail: {
332
+ value,
333
+ setOptions: (opts) => {
334
+ if (seq !== asyncSeq) return; // ignore stale responses
335
+ options = (opts || []).map(o => typeof o === 'string' ? { value: o, label: o } : o);
336
+ setOptions(options);
337
+ filter();
338
+ }
339
+ }
340
+ }));
341
+ }
342
+ // Async: open and either fetch or prompt, depending on the typed length.
343
+ function asyncRefresh() {
344
+ openMenu();
345
+ if ((el.value || '').trim().length >= minChars) scheduleAsync();
346
+ else { setOptions([]); showEmpty(ui().prompt); }
347
+ }
348
+
349
+ function commitText(raw) {
350
+ raw = (raw || '').trim();
351
+ if (!raw) return false;
352
+ if (strict) {
353
+ const o = options.find(o => o.label.toLowerCase() === raw.toLowerCase());
354
+ if (!o) return false;
355
+ addValue(o.value, o.label);
356
+ } else {
357
+ addValue(raw, raw);
358
+ }
359
+ if (chips || multiple) el.value = '';
360
+ if (menu && !isAsync) filter();
361
+ return true;
362
+ }
363
+
364
+ function selectOption(li) {
365
+ if (li === createEl) { commitText(li.dataset.value); return refocus(); }
366
+ addValue(li.dataset.value, li.dataset.label);
367
+ if (!editorNone && (chips || multiple)) el.value = '';
368
+ if (!multiple) closeMenu(); else filter();
369
+ refocus();
370
+ }
371
+ // refocus keeps the caret in the field after a pick. Suppress the focus
372
+ // handler's auto-open for that one tick so a selection doesn't reopen the list.
373
+ let suppressOpen = false;
374
+ function refocus() { suppressOpen = true; el.focus(); setTimeout(() => { suppressOpen = false; }, 0); }
375
+
376
+ function addValue(value, label) {
377
+ if (!multiple) { selected = [{ value, label }]; render(); announce(label + ' selected'); return; }
378
+ if (isSelected(value)) return;
379
+ if (selected.length >= max) { announce('Maximum of ' + max + ' reached'); return; }
380
+ selected.push({ value, label });
381
+ render();
382
+ announce(label + ' added');
383
+ }
384
+ function removeValue(value) {
385
+ const i = selected.findIndex(s => String(s.value).toLowerCase() === String(value).toLowerCase());
386
+ if (i < 0) return;
387
+ const [g] = selected.splice(i, 1);
388
+ render();
389
+ announce(g.label + ' removed');
390
+ if (menu && !isAsync) filter();
391
+ refocus();
392
+ }
393
+
394
+ function makeChip(s) {
395
+ const chip = document.createElement('span');
396
+ chip.className = 'combobox-chip';
397
+ chip.dataset.value = s.value;
398
+ const label = document.createElement('span');
399
+ label.textContent = s.label;
400
+ const x = document.createElement('button');
401
+ x.type = 'button';
402
+ x.setAttribute('aria-label', 'Remove ' + s.label);
403
+ x.textContent = '×';
404
+ x.addEventListener('click', () => removeValue(s.value));
405
+ chip.append(label, x);
406
+ return chip;
407
+ }
408
+ function hidden(value) {
409
+ const h = document.createElement('input');
410
+ h.type = 'hidden';
411
+ h.name = name;
412
+ h.value = value;
413
+ h.setAttribute('data-cb', '');
414
+ return h;
415
+ }
416
+
417
+ function render() {
418
+ wrap.querySelectorAll('.combobox-chip, input[data-cb]').forEach(n => n.remove());
419
+ if (chips) {
420
+ selected.forEach(s => {
421
+ wrap.insertBefore(makeChip(s), el);
422
+ if (name) wrap.appendChild(hidden(s.value));
423
+ });
424
+ if (!editorNone) el.placeholder = selected.length ? '' : placeholder;
425
+ } else if (!multiple) {
426
+ if (editorNone) el.textContent = selected[0] ? selected[0].label : placeholder;
427
+ else { el.value = selected[0] ? selected[0].label : ''; el.placeholder = placeholder; }
428
+ if (name && selected[0]) wrap.appendChild(hidden(selected[0].value));
429
+ }
430
+ optionEls.forEach(li => li.setAttribute('aria-selected', isSelected(li.dataset.value) ? 'true' : 'false'));
431
+ // At cap: drop the input affordance entirely; a removed chip restores it.
432
+ el.hidden = atCap();
433
+ if (el.hidden) closeMenu();
434
+ }
435
+
436
+ // Pull complete tokens (those ended by a separator) out of the field into
437
+ // chips, leaving any trailing partial. Reading the value here rather than on
438
+ // keydown avoids the keydown/character-insert race, and covers paste too.
439
+ function extractTokens() {
440
+ if (!separators.length) return;
441
+ const val = el.value;
442
+ if (!val || ![...val].some(ch => separators.includes(ch))) return;
443
+ const parts = []; let buf = '';
444
+ for (const ch of val) {
445
+ if (separators.includes(ch)) { parts.push(buf); buf = ''; }
446
+ else buf += ch;
447
+ }
448
+ el.value = '';
449
+ parts.forEach(p => commitText(p));
450
+ el.value = buf;
451
+ }
452
+
453
+ // Open for fresh entry. A committed single value is shown alongside the full
454
+ // list so it can be swapped; selectText (keyboard focus) also selects it to type over.
455
+ function openForEntry(selectText) {
456
+ const committed = !multiple && selected.length && el.value === selected[0].label;
457
+ if (committed && selectText) el.select();
458
+ if (isAsync) {
459
+ if (committed) { openMenu(); setOptions([]); showEmpty(ui().prompt); }
460
+ else asyncRefresh();
461
+ } else {
462
+ openMenu();
463
+ if (committed) showAll(); else filter();
464
+ }
465
+ }
466
+
467
+ // ----- Events -----
468
+ if (editorNone) {
469
+ el.addEventListener('click', (e) => {
470
+ e.preventDefault();
471
+ if (menu.matches(':popover-open')) closeMenu();
472
+ else { openMenu(); filter(); }
473
+ });
474
+ el.addEventListener('keydown', (e) => {
475
+ const open = menu.matches(':popover-open');
476
+ if (!open && ['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
477
+ e.preventDefault(); openMenu(); filter(); return;
478
+ }
479
+ if (e.key === 'ArrowDown') { e.preventDefault(); moveActive(1); }
480
+ else if (e.key === 'ArrowUp') { e.preventDefault(); moveActive(-1); }
481
+ else if (e.key === 'Enter' || e.key === ' ') {
482
+ if (activeIndex >= 0 || activeIndex === -2) { e.preventDefault(); selectOption(liAt(activeIndex)); }
483
+ }
484
+ else if (e.key === 'Escape') { if (open) { e.preventDefault(); closeMenu(); } }
485
+ });
486
+ } else {
487
+ el.addEventListener('keydown', (e) => {
488
+ if (e.key === 'ArrowDown' && menu) { e.preventDefault(); openMenu(); moveActive(1); }
489
+ else if (e.key === 'ArrowUp' && menu) { e.preventDefault(); moveActive(-1); }
490
+ else if (e.key === 'Enter') {
491
+ if (menu && menu.matches(':popover-open') && (activeIndex >= 0 || activeIndex === -2)) {
492
+ e.preventDefault();
493
+ selectOption(liAt(activeIndex));
494
+ } else if (!strict || !menu) {
495
+ if (commitText(el.value)) e.preventDefault();
496
+ }
497
+ }
498
+ else if (e.key === 'Escape') { if (menu && menu.matches(':popover-open')) { e.preventDefault(); closeMenu(); } }
499
+ else if (e.key === 'Backspace' && el.value === '' && chips && selected.length) {
500
+ removeValue(selected[selected.length - 1].value);
501
+ }
502
+ });
503
+
504
+ // Separators (and paste) commit from here, the moment one lands in the value.
505
+ el.addEventListener('input', () => {
506
+ extractTokens();
507
+ if (isAsync) asyncRefresh(); else { openMenu(); filter(); }
508
+ });
509
+
510
+ el.addEventListener('focus', () => { if (!suppressOpen) openForEntry(true); });
511
+ // Reopen on click even when the field is already focused (focus won't re-fire).
512
+ el.addEventListener('mousedown', () => { if (menu && !menu.matches(':popover-open') && !suppressOpen) openForEntry(false); });
513
+ }
514
+
515
+ // Close when focus leaves the field entirely (Tab away).
516
+ el.addEventListener('blur', () => setTimeout(() => {
517
+ if (menu && !wrap.contains(document.activeElement) && !menu.contains(document.activeElement)) closeMenu();
518
+ }, 0));
519
+
520
+ // Click empty shell → focus the trigger
521
+ wrap.addEventListener('mousedown', (e) => {
522
+ if (e.target === wrap) { e.preventDefault(); refocus(); }
523
+ });
524
+
525
+ // Outside dismiss (manual popover)
526
+ if (menu && !menu.__mnfstCbDismiss) {
527
+ menu.__mnfstCbDismiss = true;
528
+ document.addEventListener('pointerdown', (e) => {
529
+ if (menu.matches(':popover-open') && !menu.contains(e.target) && !wrap.contains(e.target)) closeMenu();
530
+ });
531
+ }
532
+
533
+ // ----- Seed initial chips from value="a, b" (input triggers only) -----
534
+ if (!editorNone && chips && el.value) {
535
+ const seeds = el.value.split(/[,\n;]+/).map(s => s.trim()).filter(Boolean);
536
+ el.value = '';
537
+ seeds.forEach(s => commitText(s));
538
+ }
539
+ render();
540
+ }
541
+ }
542
+
543
+ // ----- Boot (mirrors the dropdowns plugin lifecycle) -----
544
+ let comboboxPluginInitialized = false;
545
+ let alpineHasWalked = false;
546
+ document.addEventListener('alpine:initialized', () => { alpineHasWalked = true; });
547
+
548
+ function ensureComboboxPluginInitialized() {
549
+ if (comboboxPluginInitialized) return;
550
+ if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
551
+ comboboxPluginInitialized = true;
552
+ initializeComboboxPlugin();
553
+ if (alpineHasWalked && typeof window.Alpine.initTree === 'function') {
554
+ document.querySelectorAll('[x-combobox]').forEach(el => { if (!el._x_dataStack) window.Alpine.initTree(el); });
555
+ }
556
+ }
557
+ window.ensureComboboxPluginInitialized = ensureComboboxPluginInitialized;
558
+
559
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ensureComboboxPluginInitialized);
560
+ document.addEventListener('alpine:init', ensureComboboxPluginInitialized);
561
+ if (window.Alpine && typeof window.Alpine.directive === 'function') {
562
+ setTimeout(ensureComboboxPluginInitialized, 0);
563
+ } else {
564
+ const t = setInterval(() => {
565
+ if (window.Alpine && typeof window.Alpine.directive === 'function') { clearInterval(t); ensureComboboxPluginInitialized(); }
566
+ }, 10);
567
+ setTimeout(() => clearInterval(t), 5000);
568
+ }
569
+
570
+ })();