mnfst 0.5.157 → 0.5.158
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.combobox.css +6 -0
- package/lib/manifest.combobox.js +148 -16
- package/lib/manifest.css +6 -0
- package/lib/manifest.integrity.json +2 -2
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.virtual.js +423 -190
- package/package.json +1 -1
|
@@ -117,6 +117,12 @@
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/* Locked — selected but not removable; no × button, so restore the right padding */
|
|
121
|
+
&[data-locked] {
|
|
122
|
+
padding-inline-end: calc(var(--spacing, 0.25rem) * 2);
|
|
123
|
+
cursor: default
|
|
124
|
+
}
|
|
125
|
+
|
|
120
126
|
/* Failed validation */
|
|
121
127
|
&[aria-invalid="true"] {
|
|
122
128
|
color: var(--color-negative-inverse, oklch(44.4% 0.177 26.899));
|
package/lib/manifest.combobox.js
CHANGED
|
@@ -81,6 +81,7 @@ function initializeComboboxPlugin() {
|
|
|
81
81
|
value: li.dataset.value != null ? li.dataset.value : li.textContent.trim(),
|
|
82
82
|
label: li.dataset.label || li.textContent.trim(),
|
|
83
83
|
pattern: li.dataset.pattern || null,
|
|
84
|
+
locked: li.hasAttribute('data-locked'),
|
|
84
85
|
html: li.innerHTML
|
|
85
86
|
}));
|
|
86
87
|
}
|
|
@@ -88,11 +89,31 @@ function initializeComboboxPlugin() {
|
|
|
88
89
|
value: o.value || o.textContent.trim(),
|
|
89
90
|
label: o.textContent.trim() || o.value,
|
|
90
91
|
pattern: o.getAttribute('data-pattern') || null,
|
|
92
|
+
locked: o.hasAttribute('data-locked'),
|
|
91
93
|
html: null
|
|
92
94
|
}));
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// Take ownership of x-model: capture the expression and strip the attribute so
|
|
98
|
+
// Alpine's own model directive never binds the editor. In chips mode the editor is
|
|
99
|
+
// a transient typing buffer — Alpine's x-model would dump the bound array into it
|
|
100
|
+
// and write partial typing back to the model. We read/write the model ourselves.
|
|
101
|
+
function captureModel(el) {
|
|
102
|
+
if (el.__cbModelExpr !== undefined) return;
|
|
103
|
+
let attr = null;
|
|
104
|
+
for (const a of Array.from(el.attributes)) {
|
|
105
|
+
if (a.name === 'x-model' || a.name.indexOf('x-model.') === 0) { attr = a.name; break; }
|
|
106
|
+
}
|
|
107
|
+
el.__cbModelExpr = attr ? el.getAttribute(attr) : null;
|
|
108
|
+
if (attr) el.removeAttribute(attr);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pre-walk pass: this runs on alpine:init / DOMContentLoaded, before Alpine walks
|
|
112
|
+
// the tree, so x-model is gone by the time Alpine's model directive looks for it.
|
|
113
|
+
document.querySelectorAll('[x-combobox]').forEach(captureModel);
|
|
114
|
+
|
|
95
115
|
Alpine.directive('combobox', (el, { modifiers, expression }, { cleanup }) => {
|
|
116
|
+
captureModel(el); // fallback for elements added after the pre-walk pass
|
|
96
117
|
// Build after the current tick so sibling sources (datalist/menu) exist.
|
|
97
118
|
setTimeout(() => build(el, modifiers, expression || '', cleanup), 0);
|
|
98
119
|
});
|
|
@@ -198,6 +219,41 @@ function initializeComboboxPlugin() {
|
|
|
198
219
|
let selected = adopt && seedSelected ? seedSelected : [];
|
|
199
220
|
const isSelected = (v) => selected.some(s => String(s.value).toLowerCase() === String(v).toLowerCase());
|
|
200
221
|
const atCap = () => multiple && selected.length >= max;
|
|
222
|
+
const labelFor = (v) => {
|
|
223
|
+
const o = options.find(o => String(o.value).toLowerCase() === String(v).toLowerCase());
|
|
224
|
+
return o ? o.label : String(v);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// ----- Locked values (non-removable chips) -----
|
|
228
|
+
// From a `locked: [...]` config list (re-evaluated reactively below) and/or a
|
|
229
|
+
// `data-locked` flag on individual options. Locked chips keep their × hidden and
|
|
230
|
+
// refuse removal while staying selected.
|
|
231
|
+
const lockedFromOptions = options.filter(o => o.locked).map(o => String(o.value).toLowerCase());
|
|
232
|
+
const computeLocked = (cfgObj) => {
|
|
233
|
+
const set = new Set(lockedFromOptions);
|
|
234
|
+
const raw = cfgObj && cfgObj.locked != null ? cfgObj.locked : cfg.locked;
|
|
235
|
+
const list = Array.isArray(raw) ? raw : (raw != null ? String(raw).split(',') : []);
|
|
236
|
+
list.forEach(v => set.add(String(v).trim().toLowerCase()));
|
|
237
|
+
return set;
|
|
238
|
+
};
|
|
239
|
+
let lockedSet = computeLocked();
|
|
240
|
+
const isLocked = (v) => lockedSet.has(String(v).toLowerCase());
|
|
241
|
+
|
|
242
|
+
// ----- Reactive model (x-model) — captured + stripped pre-walk, owned here -----
|
|
243
|
+
// The bound value may be a token array or a CSV string; we preserve whichever the
|
|
244
|
+
// author used (default array for .multiple). Chips render labels; the model carries
|
|
245
|
+
// values/tokens. Read/effect/set are wired near mount, once render() exists.
|
|
246
|
+
const modelExpr = el.__cbModelExpr || null;
|
|
247
|
+
let modelArrayShape = null; // null until first read; true = array, false = CSV
|
|
248
|
+
let modelSet = null;
|
|
249
|
+
const modelToValues = (v) => {
|
|
250
|
+
if (v == null || v === '') return [];
|
|
251
|
+
if (Array.isArray(v)) return v.map(x => (x && typeof x === 'object' && x.value != null) ? x.value : x).map(String);
|
|
252
|
+
if (typeof v === 'string') return v.split(/[,\n;]+/).map(s => s.trim()).filter(Boolean);
|
|
253
|
+
return [String(v)];
|
|
254
|
+
};
|
|
255
|
+
const sameList = (a, b) => a.length === b.length && a.every((x, i) => String(x).toLowerCase() === String(b[i]).toLowerCase());
|
|
256
|
+
function syncOut() { if (modelSet) modelSet(selected.map(s => s.value)); }
|
|
201
257
|
|
|
202
258
|
// ----- Menu -----
|
|
203
259
|
let menu = null, generatedMenu = null, optionEls = [], createEl = null, emptyEl = null, activeIndex = -1;
|
|
@@ -421,18 +477,21 @@ function initializeComboboxPlugin() {
|
|
|
421
477
|
function refocus() { suppressOpen = true; el.focus(); setTimeout(() => { suppressOpen = false; }, 0); }
|
|
422
478
|
|
|
423
479
|
function addValue(value, label) {
|
|
424
|
-
if (!multiple) { selected = [{ value, label }]; render(); announce(label + ' selected'); return; }
|
|
480
|
+
if (!multiple) { selected = [{ value, label }]; render(); syncOut(); announce(label + ' selected'); return; }
|
|
425
481
|
if (isSelected(value)) return;
|
|
426
482
|
if (selected.length >= max) { announce('Maximum of ' + max + ' reached'); return; }
|
|
427
483
|
selected.push({ value, label });
|
|
428
484
|
render();
|
|
485
|
+
syncOut();
|
|
429
486
|
announce(label + ' added');
|
|
430
487
|
}
|
|
431
488
|
function removeValue(value) {
|
|
489
|
+
if (isLocked(value)) return; // locked chips stay put
|
|
432
490
|
const i = selected.findIndex(s => String(s.value).toLowerCase() === String(value).toLowerCase());
|
|
433
491
|
if (i < 0) return;
|
|
434
492
|
const [g] = selected.splice(i, 1);
|
|
435
493
|
render();
|
|
494
|
+
syncOut();
|
|
436
495
|
announce(g.label + ' removed');
|
|
437
496
|
if (menu && !isAsync) filter();
|
|
438
497
|
refocus();
|
|
@@ -444,13 +503,24 @@ function initializeComboboxPlugin() {
|
|
|
444
503
|
chip.dataset.value = s.value;
|
|
445
504
|
const label = document.createElement('span');
|
|
446
505
|
label.textContent = s.label;
|
|
506
|
+
chip.appendChild(label);
|
|
507
|
+
applyLock(chip, s.value, s.label);
|
|
508
|
+
return chip;
|
|
509
|
+
}
|
|
510
|
+
// Add or drop the × to match the value's locked state. Re-run from render() so a
|
|
511
|
+
// reactive `locked` change toggles the affordance on existing chips too.
|
|
512
|
+
function applyLock(chip, value, label) {
|
|
513
|
+
const locked = isLocked(value);
|
|
514
|
+
chip.toggleAttribute('data-locked', locked);
|
|
515
|
+
const btn = chip.querySelector(':scope > button');
|
|
516
|
+
if (locked) { if (btn) btn.remove(); return; }
|
|
517
|
+
if (btn) return;
|
|
447
518
|
const x = document.createElement('button');
|
|
448
519
|
x.type = 'button';
|
|
449
|
-
x.setAttribute('aria-label', 'Remove ' +
|
|
520
|
+
x.setAttribute('aria-label', 'Remove ' + (label || (chip.querySelector(':scope > span') || {}).textContent || value));
|
|
450
521
|
x.textContent = '×';
|
|
451
|
-
x.addEventListener('click', () => removeValue(
|
|
452
|
-
chip.
|
|
453
|
-
return chip;
|
|
522
|
+
x.addEventListener('click', () => removeValue(value));
|
|
523
|
+
chip.appendChild(x);
|
|
454
524
|
}
|
|
455
525
|
function hidden(value) {
|
|
456
526
|
const h = document.createElement('input');
|
|
@@ -463,17 +533,27 @@ function initializeComboboxPlugin() {
|
|
|
463
533
|
|
|
464
534
|
function render() {
|
|
465
535
|
if (chips) {
|
|
466
|
-
// Incremental: keep existing chip nodes, only add new
|
|
467
|
-
// Recreating every chip re-inserts them next to the
|
|
468
|
-
// collapses them to an ellipsis in WebKit; untouched
|
|
536
|
+
// Incremental: keep existing chip nodes, only add new / drop removed /
|
|
537
|
+
// refresh locked state. Recreating every chip re-inserts them next to the
|
|
538
|
+
// focused editor, which collapses them to an ellipsis in WebKit; untouched
|
|
539
|
+
// nodes keep their width.
|
|
540
|
+
const norm = v => String(v).toLowerCase();
|
|
469
541
|
const have = new Map();
|
|
470
|
-
wrap.querySelectorAll('.combobox-chip').forEach(c => have.set(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
});
|
|
542
|
+
wrap.querySelectorAll('.combobox-chip').forEach(c => have.set(norm(c.dataset.value), c));
|
|
543
|
+
const want = selected.map(s => norm(s.value));
|
|
544
|
+
const wantSet = new Set(want);
|
|
545
|
+
have.forEach((node, key) => { if (!wantSet.has(key)) { node.remove(); have.delete(key); } });
|
|
474
546
|
selected.forEach(s => {
|
|
475
|
-
|
|
547
|
+
const key = norm(s.value);
|
|
548
|
+
const node = have.get(key);
|
|
549
|
+
if (!node) { const n = makeChip(s); have.set(key, n); wrap.insertBefore(n, el); }
|
|
550
|
+
else applyLock(node, s.value, s.label);
|
|
476
551
|
});
|
|
552
|
+
// Reorder to match selection only when it actually differs — needless
|
|
553
|
+
// re-insertion collapses chips in WebKit while the editor is focused, so the
|
|
554
|
+
// common append path (order already matches) skips this.
|
|
555
|
+
const cur = Array.from(wrap.querySelectorAll('.combobox-chip')).map(c => norm(c.dataset.value));
|
|
556
|
+
if (!sameList(cur, want)) selected.forEach(s => wrap.insertBefore(have.get(norm(s.value)), el));
|
|
477
557
|
// Hidden inputs are type=hidden (no layout), so a clean rebuild is harmless.
|
|
478
558
|
wrap.querySelectorAll('input[data-cb]').forEach(n => n.remove());
|
|
479
559
|
if (name) selected.forEach(s => wrap.appendChild(hidden(s.value)));
|
|
@@ -597,9 +677,61 @@ function initializeComboboxPlugin() {
|
|
|
597
677
|
// a stale, sometimes-open duplicate. Authored/adopted menus go with their container.
|
|
598
678
|
if (cleanup && generatedMenu) cleanup(() => { if (generatedMenu.isConnected) generatedMenu.remove(); });
|
|
599
679
|
|
|
600
|
-
// -----
|
|
601
|
-
//
|
|
602
|
-
|
|
680
|
+
// ----- Reactive model (x-model) -----
|
|
681
|
+
// Renders chips from the bound value on init and whenever it changes externally
|
|
682
|
+
// (e.g. switching which record is being edited); writes back on add/remove. The
|
|
683
|
+
// read runs inside an Alpine effect so $x / nested state stay reactive.
|
|
684
|
+
if (modelExpr) {
|
|
685
|
+
const read = Alpine.evaluateLater(el, modelExpr);
|
|
686
|
+
modelSet = (vals) => {
|
|
687
|
+
const out = !multiple ? (vals.length ? vals[0] : '')
|
|
688
|
+
: (modelArrayShape === false ? vals.join(',') : vals.slice());
|
|
689
|
+
try { Alpine.evaluate(el, `${modelExpr} = ${JSON.stringify(out)}`); } catch (_) { }
|
|
690
|
+
};
|
|
691
|
+
Alpine.effect(() => {
|
|
692
|
+
read(raw => {
|
|
693
|
+
if (modelArrayShape === null && raw != null && raw !== '') modelArrayShape = Array.isArray(raw);
|
|
694
|
+
const incoming = modelToValues(raw);
|
|
695
|
+
if (sameList(incoming, selected.map(s => s.value))) return; // unchanged / our own write-back
|
|
696
|
+
selected = incoming.map(v => ({ value: v, label: labelFor(v) }));
|
|
697
|
+
if (!multiple && selected.length > 1) selected = selected.slice(-1);
|
|
698
|
+
render();
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ----- Reactive locked list (config-object form re-evaluates, so authors can
|
|
704
|
+
// gate it on state, e.g. last-owner: locked: owners.length <= 1 ? ['owner'] : []) -----
|
|
705
|
+
if (expr.startsWith('{')) {
|
|
706
|
+
Alpine.effect(() => {
|
|
707
|
+
let c; try { c = Alpine.evaluate(el, expr); } catch (_) { return; }
|
|
708
|
+
const next = computeLocked(c && typeof c === 'object' && !Array.isArray(c) ? c : null);
|
|
709
|
+
if (next.size === lockedSet.size && [...next].every(v => lockedSet.has(v))) return;
|
|
710
|
+
lockedSet = next;
|
|
711
|
+
render();
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ----- Dynamic options: re-read when x-for / $x fills the source list after
|
|
716
|
+
// build, so the menu, chip labels, and data-locked flags stay current. -----
|
|
717
|
+
if (src) {
|
|
718
|
+
const reread = () => {
|
|
719
|
+
options = readOptions(src);
|
|
720
|
+
lockedFromOptions.length = 0;
|
|
721
|
+
options.filter(o => o.locked).forEach(o => lockedFromOptions.push(String(o.value).toLowerCase()));
|
|
722
|
+
lockedSet = computeLocked();
|
|
723
|
+
selected = selected.map(s => ({ value: s.value, label: labelFor(s.value) }));
|
|
724
|
+
if (generatedMenu) setOptions(options);
|
|
725
|
+
render();
|
|
726
|
+
};
|
|
727
|
+
const mo = new MutationObserver(reread);
|
|
728
|
+
mo.observe(src, { childList: true, subtree: true, characterData: true });
|
|
729
|
+
if (cleanup) cleanup(() => mo.disconnect());
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ----- Seed initial chips from value="a, b" — only when there's no x-model
|
|
733
|
+
// (x-model wins) and no adopted wrapper (which already seeded). -----
|
|
734
|
+
if (!modelExpr && !adopt && !editorNone && chips && el.value) {
|
|
603
735
|
const seeds = el.value.split(/[,\n;]+/).map(s => s.trim()).filter(Boolean);
|
|
604
736
|
el.value = '';
|
|
605
737
|
seeds.forEach(s => commitText(s));
|
package/lib/manifest.css
CHANGED
|
@@ -1575,6 +1575,12 @@
|
|
|
1575
1575
|
}
|
|
1576
1576
|
}
|
|
1577
1577
|
|
|
1578
|
+
/* Locked — selected but not removable; no × button, so restore the right padding */
|
|
1579
|
+
&[data-locked] {
|
|
1580
|
+
padding-inline-end: calc(var(--spacing, 0.25rem) * 2);
|
|
1581
|
+
cursor: default
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1578
1584
|
/* Failed validation */
|
|
1579
1585
|
&[aria-invalid="true"] {
|
|
1580
1586
|
color: var(--color-negative-inverse, oklch(44.4% 0.177 26.899));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"manifest.code.js": "sha384-BJSRJ7txA6fY660qX+SfEEBrEB0dOChHdHI4/9iNGz3rGqohEZdzs7qA3LuR1GJW",
|
|
7
7
|
"manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
|
|
8
8
|
"manifest.colorpicker.js": "sha384-Wqz0ZIbeIi7KarqqqSLsQk+7E/fMaKhb32hrq5/eWzX1yjqMrpPZKH8y+jZ3mfg+",
|
|
9
|
-
"manifest.combobox.js": "sha384-
|
|
9
|
+
"manifest.combobox.js": "sha384-3rP3iWhh5lJsoQcqY6pBJUMzfSqQUqwHRxkSCOJiRo0HbmIXu5YXzy3UFOYnfbrh",
|
|
10
10
|
"manifest.components.js": "sha384-mzPFoM0vqL9dnTVLMN3OrmO+KCgSqGknM1fd7bM1xzYeCco5OaZi56IMR5RS5oad",
|
|
11
11
|
"manifest.data.js": "sha384-bCYTYyAYNVkg5pSwGcoe07Dgf5B7JDN7GtOIQdS+BtrBStQwvjZtskiQ38Bncvrf",
|
|
12
12
|
"manifest.datepicker.js": "sha384-NEb/H4vuR3CFtRcodHsm3jJjrcYW2JMpDlQKlgwTrzpMMTcDkFKYXzAYJD0gZ7Ov",
|
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
"manifest.tooltips.js": "sha384-ADzAx9D0HWq2b46mvNG05iOwPmEWdiFZNpEOXONSbBxs4xj1B/bzNL7S3x2R9cS1",
|
|
28
28
|
"manifest.url.parameters.js": "sha384-FIufiClqDx1rJpU/QUc9z/D43qClQ6Qm8rBahipbJl9BDHUvhrOsUDegmTWW7Tuf",
|
|
29
29
|
"manifest.utilities.js": "sha384-HWyVkjQoDRlWFKDBQw4RQOYODkBcU72NHW6l1p4bhQv1RtN0/XtnjwIb+lQK6+zv",
|
|
30
|
-
"manifest.virtual.js": "sha384-
|
|
30
|
+
"manifest.virtual.js": "sha384-0W3Hg1Gu4YcHibh0KB0G56/a2v9n7tO5Fw2Pz2rJmHbitvJWZ69f6nvYRVkQB2JE",
|
|
31
31
|
"manifest.js": "sha384-zPXrym9jwpYVdg4TJ1XPQVxQhz7uezdDubaO/MZDvLeiTufor+6lNJsTG75cYPXm"
|
|
32
32
|
}
|