mnfst 0.5.168 → 0.5.169

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.
@@ -497,13 +497,31 @@ function initializeComboboxPlugin() {
497
497
  let suppressOpen = false;
498
498
  function refocus() { suppressOpen = true; el.focus(); setTimeout(() => { suppressOpen = false; }, 0); }
499
499
 
500
+ // The model is written ONLY on explicit user selection; commits also fire a DOM
501
+ // change event so apps can save on events instead of watching the model.
502
+ // `dirty` = the user actually edited the text since focus — gates Enter's
503
+ // free-text commit so a stray Enter on an untouched field can't re-commit the
504
+ // display label (or, with the menu open, an auto-highlighted option) as a value.
505
+ let dirty = false;
506
+ let seeding = false; // init-time seeding is not a user commit — no change event
507
+ const fireChange = () => { if (!seeding) el.dispatchEvent(new Event('change', { bubbles: true })); };
508
+
500
509
  function addValue(value, label) {
501
- if (!multiple) { selected = [{ value, label }]; render(); syncOut(); announce(label + ' selected'); return; }
510
+ if (!multiple) {
511
+ const same = selected[0] && String(selected[0].value) === String(value);
512
+ selected = [{ value, label }];
513
+ render();
514
+ dirty = false;
515
+ if (!same) { syncOut(); fireChange(); announce(label + ' selected'); }
516
+ return;
517
+ }
502
518
  if (isSelected(value)) return;
503
519
  if (selected.length >= max) { announce('Maximum of ' + max + ' reached'); return; }
504
520
  selected.push({ value, label });
505
521
  render();
522
+ dirty = false;
506
523
  syncOut();
524
+ fireChange();
507
525
  announce(label + ' added');
508
526
  }
509
527
  function removeValue(value) {
@@ -513,6 +531,7 @@ function initializeComboboxPlugin() {
513
531
  const [g] = selected.splice(i, 1);
514
532
  render();
515
533
  syncOut();
534
+ fireChange();
516
535
  announce(g.label + ' removed');
517
536
  if (menu && !isAsync) filter();
518
537
  refocus();
@@ -634,7 +653,7 @@ function initializeComboboxPlugin() {
634
653
  if (e.key === 'ArrowDown') { e.preventDefault(); moveActive(1); }
635
654
  else if (e.key === 'ArrowUp') { e.preventDefault(); moveActive(-1); }
636
655
  else if (e.key === 'Enter' || e.key === ' ') {
637
- if (activeIndex >= 0 || activeIndex === -2) { e.preventDefault(); selectOption(liAt(activeIndex)); }
656
+ if (!e.repeat && menu.hasAttribute('data-kbd') && (activeIndex >= 0 || activeIndex === -2)) { e.preventDefault(); selectOption(liAt(activeIndex)); }
638
657
  }
639
658
  else if (e.key === 'Escape') { if (open) { e.preventDefault(); closeMenu(); } }
640
659
  });
@@ -643,10 +662,15 @@ function initializeComboboxPlugin() {
643
662
  if (e.key === 'ArrowDown' && menu) { e.preventDefault(); openMenu(); moveActive(1); }
644
663
  else if (e.key === 'ArrowUp' && menu) { e.preventDefault(); moveActive(-1); }
645
664
  else if (e.key === 'Enter') {
646
- if (menu && menu.matches(':popover-open') && (activeIndex >= 0 || activeIndex === -2)) {
665
+ // Commit only on deliberate action: an option needs a keyboard-driven
666
+ // highlight (data-kbd — arrows or typing), free text needs an actual
667
+ // edit since focus (dirty). A stray Enter on an untouched field — key
668
+ // repeat, form submit, saving the record — must never write the model.
669
+ if (e.repeat) return;
670
+ if (menu && menu.matches(':popover-open') && menu.hasAttribute('data-kbd') && (activeIndex >= 0 || activeIndex === -2)) {
647
671
  e.preventDefault();
648
672
  selectOption(liAt(activeIndex));
649
- } else if (!strict || !menu) {
673
+ } else if ((!strict || !menu) && dirty) {
650
674
  if (commitText(el.value)) e.preventDefault();
651
675
  }
652
676
  }
@@ -658,19 +682,27 @@ function initializeComboboxPlugin() {
658
682
 
659
683
  // Separators (and paste) commit from here, the moment one lands in the value.
660
684
  el.addEventListener('input', () => {
685
+ dirty = true;
661
686
  if (menu) menu.setAttribute('data-kbd', ''); // typing drives the highlight onto the first match
662
687
  extractTokens();
663
688
  if (isAsync) asyncRefresh(); else { openMenu(); filter(); }
664
689
  });
665
690
 
666
- el.addEventListener('focus', () => { if (!suppressOpen) openForEntry(true); });
691
+ el.addEventListener('focus', () => { dirty = false; if (!suppressOpen) openForEntry(true); });
667
692
  // Reopen on click even when the field is already focused (focus won't re-fire).
668
693
  el.addEventListener('mousedown', () => { if (menu && !menu.matches(':popover-open') && !suppressOpen) openForEntry(false); });
669
694
  }
670
695
 
671
- // Close when focus leaves the field entirely (Tab away).
696
+ // Close when focus leaves the field entirely (Tab away). A committed single
697
+ // field also snaps its display back to the selection's label — abandoned typing
698
+ // never commits, and the model is left untouched.
672
699
  el.addEventListener('blur', () => setTimeout(() => {
673
- if (menu && !wrap.contains(document.activeElement) && !menu.contains(document.activeElement)) closeMenu();
700
+ if (wrap.contains(document.activeElement) || (menu && menu.contains(document.activeElement))) return;
701
+ closeMenu();
702
+ if (!multiple && !chips && !editorNone && selected[0] && el.value !== selected[0].label) {
703
+ el.value = selected[0].label;
704
+ dirty = false;
705
+ }
674
706
  }, 0));
675
707
 
676
708
  // Click empty shell → focus the trigger
@@ -753,7 +785,9 @@ function initializeComboboxPlugin() {
753
785
  if (!modelExpr && !adopt && !editorNone && chips && el.value) {
754
786
  const seeds = el.value.split(/[,\n;]+/).map(s => s.trim()).filter(Boolean);
755
787
  el.value = '';
788
+ seeding = true;
756
789
  seeds.forEach(s => commitText(s));
790
+ seeding = false;
757
791
  }
758
792
  // In chips/multiple the editor is a typing buffer. Clear value AND remove the
759
793
  // attribute: mnfst-render serializes the attribute, so leaving it bakes seed text
@@ -7,7 +7,7 @@
7
7
  "manifest.code.js": "sha384-G3MNfvQRWWY+gm4SGGrSsgQbDeil9MP+ON2DTk6Rcq9Nsex8woHMDt5EJHrFH0Nj",
8
8
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
9
9
  "manifest.colorpicker.js": "sha384-N5ezEwgKrDodV4YdURxhdyZKmg1n/QhqvmAC4SEG03mw1pEAZAA8Gphp+NLAOxts",
10
- "manifest.combobox.js": "sha384-tK8cVTQy3v4RK8pDJKFU+UW7zdsS7ysMqExOsCoWpFBXD0tMfjLNrkMRCQd4+tsW",
10
+ "manifest.combobox.js": "sha384-ZPOkx4hKGlcrOqeb3MmWLyBfjZXlnkP47Yc9VLE7yHds6dXRqMeB5DPFeJl3NZS/",
11
11
  "manifest.components.js": "sha384-eZDib+RFdwd9mOUdi6o53eNS0LBlEEkZPDegxtkAcE+GrGIFxsuB4O0ymfixGNfM",
12
12
  "manifest.data.js": "sha384-3lLvGnqcQr4Ium80MdaQTr8RytjVbs5rnxd0q41BF3ppcoQccMFAcvgFSyZP/0sl",
13
13
  "manifest.datepicker.js": "sha384-e6rJI0BxuE+AyI96/u6XoeOstsxLuQTO6+wYSr6rm2aKhOP2APM2xJqTz//0v4Pw",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.168",
3
+ "version": "0.5.169",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",