slim-select 4.0.2 → 4.0.4

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/README.md CHANGED
@@ -41,7 +41,7 @@ Full docs, live demos, and copy-paste examples: **[slimselectjs.com](https://sli
41
41
 
42
42
  **Built to ship**
43
43
 
44
- - Zero runtime dependencies
44
+ - Zero dependencies
45
45
  - ~66KB JS (~16KB gzip) · ~12KB CSS (~2KB gzip)
46
46
  - TypeScript types included
47
47
  - WCAG 2.1 Level AA accessibility (ARIA, keyboard, screen reader support)
@@ -227,7 +227,11 @@ new SlimSelect({
227
227
  afterChange: (newVal: Option[]) => void,
228
228
  beforeOpen: () => void,
229
229
  afterOpen: () => void,
230
- beforeClose: () => void,
230
+ // Return false to cancel close. info.source is why close was requested
231
+ // ('select' | 'deselect' | 'outside' | 'toggle' | 'escape' | 'tab' | 'modal' | 'api').
232
+ // info.selectionChanged is true when the selection is changing in this action.
233
+ // info.option is the clicked option when source is 'select' (undefined otherwise).
234
+ beforeClose: (info: CloseInfo) => boolean | void,
231
235
  afterClose: () => void,
232
236
  error: (err: Error) => void
233
237
  }
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { default as CssClasses } from './classes';
2
2
  import { default as Lifecycle } from './lifecycle';
3
- import { default as Render } from './render';
3
+ import { default as Render, CloseInfo } from './render';
4
4
  import { default as Select } from './select';
5
5
  import { default as Settings, MODAL_MOBILE_BREAKPOINT } from './settings';
6
6
  import { default as Store, Option, Optgroup } from './store';
7
7
  import { default as SyncCoordinator } from './sync';
8
8
  export { Settings, Option, Optgroup, MODAL_MOBILE_BREAKPOINT };
9
9
  export type { ModalSetting } from './settings';
10
- export type { Main, Content, Search } from './render';
10
+ export type { Main, Content, Search, CloseInfo, CloseSource } from './render';
11
11
  export interface Config {
12
12
  select: string | Element;
13
13
  data?: (Partial<Option> | Partial<Optgroup>)[];
@@ -23,7 +23,7 @@ export interface Events {
23
23
  afterChange?: (newVal: Option[]) => void;
24
24
  beforeOpen?: () => void;
25
25
  afterOpen?: () => void;
26
- beforeClose?: () => void;
26
+ beforeClose?: (info: CloseInfo) => boolean | void;
27
27
  afterClose?: () => void;
28
28
  error?: (err: Error) => void;
29
29
  }
@@ -49,7 +49,9 @@ export default class SlimSelect {
49
49
  setSelected(values: string | string[], runAfterChange?: boolean): void;
50
50
  addOption(option: Partial<Option>): void;
51
51
  open(): void;
52
- close(eventType?: string | null): void;
52
+ close(info?: CloseInfo): void;
53
+ /** Sync close — invoked by lifecycle after beforeClose approves. */
54
+ private applyClose;
53
55
  search(value: string): void;
54
56
  private clearSearch;
55
57
  private runLocalSearch;
@@ -1,21 +1,12 @@
1
- /**
2
- * Open/close state machine for SlimSelect.
3
- *
4
- * Replaces scattered openTimeout/closeTimeout in index.ts with a single flow:
5
- * closed → opening → open → closing → closed
6
- *
7
- * Maps to settings consumers expect:
8
- * - isOpen: true during opening + open
9
- * - isFullOpen: true only in open state (after animation / afterOpen)
10
- *
11
- * Lifecycle waits for render.waitForAnimation() before firing afterOpen/afterClose.
12
- * When no animation waiter is provided, falls back to settings.timeoutDelay.
13
- */
1
+ import { CloseInfo } from './render';
14
2
  export type LifecycleState = 'closed' | 'opening' | 'open' | 'closing';
15
3
  export interface LifecycleHandlers {
16
4
  beforeOpen?: () => void;
17
5
  afterOpen?: () => void;
18
- beforeClose?: () => void;
6
+ /** Return false to cancel close before any DOM changes. */
7
+ beforeClose?: (info: CloseInfo) => boolean | void;
8
+ /** Sync close work — render, focus, settings. Runs after beforeClose approves. */
9
+ onClose?: (info: CloseInfo) => void;
19
10
  afterClose?: () => void;
20
11
  /** Fires when fully open — used to attach document click-outside listener. */
21
12
  onOpenReady?: () => void;
@@ -41,7 +32,7 @@ export default class Lifecycle {
41
32
  get isOpen(): boolean;
42
33
  get isFullOpen(): boolean;
43
34
  requestOpen(): Promise<void>;
44
- requestClose(): Promise<void>;
35
+ requestClose(info?: CloseInfo): Promise<boolean>;
45
36
  /** Cancel timers and resolve waiting phases — called when open/close race each other. */
46
37
  cancelPending(): void;
47
38
  destroy(): void;
@@ -335,11 +335,14 @@ var N = class {
335
335
  }
336
336
  this.state = "open", this.handlers.afterOpen && this.handlers.afterOpen(), this.handlers.onOpenReady && this.handlers.onOpenReady();
337
337
  }
338
- async requestClose() {
339
- if (this.state === "closed" || this.state === "closing") return;
338
+ async requestClose(e = {
339
+ source: "api",
340
+ selectionChanged: !1
341
+ }) {
342
+ if (this.state === "closed" || this.state === "closing" || this.handlers.beforeClose && this.handlers.beforeClose(e) === !1) return !1;
340
343
  this.cancelPending();
341
- let e = ++this.generation;
342
- this.handlers.beforeClose && this.handlers.beforeClose(), this.state = "closing", await this.waitForPhase("close", e), e === this.generation && (this.state = "closed", this.handlers.afterClose && this.handlers.afterClose(), this.handlers.onCloseReady && this.handlers.onCloseReady());
344
+ let t = ++this.generation;
345
+ return this.handlers.onClose && this.handlers.onClose(e), this.state = "closing", await this.waitForPhase("close", t), t === this.generation ? (this.state = "closed", this.handlers.afterClose && this.handlers.afterClose(), this.handlers.onCloseReady && this.handlers.onCloseReady(), !0) : !1;
343
346
  }
344
347
  cancelPending() {
345
348
  this.generation++, this.animationAbort?.abort(), this.animationAbort = null, this.pendingTimer &&= (clearTimeout(this.pendingTimer), null);
@@ -598,6 +601,24 @@ var N = class {
598
601
  constructor(e, t, n, r) {
599
602
  this.store = n, this.settings = e, this.classes = t, this.callbacks = r, this.lastSelectedOption = null, this.lastRenderedOptions = [], this.main = this.mainDiv(), this.content = this.contentDiv(), this.updateClassStyles(), this.updateAriaAttributes(), this.settings.contentPosition !== "relative" && (this.content.main.style.top = "-9999px", this.content.main.style.left = "-9999px", this.content.main.style.margin = "0", this.content.main.style.width = "auto"), this.settings.contentLocation && this.settings.contentLocation.appendChild(this.content.main), this.settings.modal !== "off" && !this.settings.alwaysOpen && (this.modalElements = this.createModalElements(), document.body.appendChild(this.modalElements.overlay));
600
603
  }
604
+ requestClose(e, t) {
605
+ this.callbacks.close({
606
+ source: e,
607
+ option: t?.option,
608
+ selectionChanged: t?.selectionChanged ?? !1
609
+ });
610
+ }
611
+ selectionChanged(e, t) {
612
+ if (e.length !== t.length) return !0;
613
+ let n = e.map((e) => e.id).sort(), r = t.map((e) => e.id).sort();
614
+ return n.some((e, t) => e !== r[t]);
615
+ }
616
+ closeOnSingleSelectReclick(e) {
617
+ this.settings.closeOnSelect && this.requestClose("select", {
618
+ option: e,
619
+ selectionChanged: !1
620
+ });
621
+ }
601
622
  addClasses(e, t) {
602
623
  if (!t || t.trim() === "") return;
603
624
  let n = t.split(" ").filter((e) => e.trim() !== "");
@@ -643,13 +664,13 @@ var N = class {
643
664
  this.settings.modalTitle ? (n = document.createElement("div"), this.addClasses(n, this.classes.modalTitle), n.id = `${this.settings.id}-modal-title`, n.textContent = this.settings.modalTitle, t.setAttribute("aria-labelledby", n.id)) : t.setAttribute("aria-label", this.settings.ariaLabel);
644
665
  let r = document.createElement("button");
645
666
  r.type = "button", this.addClasses(r, this.classes.modalClose), r.setAttribute("aria-label", "Close"), r.onclick = (e) => {
646
- e.stopPropagation(), this.callbacks.close();
667
+ e.stopPropagation(), this.requestClose("modal");
647
668
  };
648
669
  let i = document.createElementNS("http://www.w3.org/2000/svg", "svg");
649
670
  i.setAttribute("viewBox", "0 0 100 100");
650
671
  let a = document.createElementNS("http://www.w3.org/2000/svg", "path");
651
672
  return a.setAttribute("d", this.classes.deselectPath), i.appendChild(a), r.appendChild(i), e.onclick = (t) => {
652
- t.target === e && this.callbacks.close();
673
+ t.target === e && this.requestClose("modal");
653
674
  }, n && t.appendChild(n), e.appendChild(t), {
654
675
  overlay: e,
655
676
  dialog: t,
@@ -672,7 +693,7 @@ var N = class {
672
693
  this.settings.contentLocation && this.settings.contentLocation.appendChild(this.content.main), this.settings.contentPosition !== "relative" && (this.content.main.style.top = "-9999px", this.content.main.style.left = "-9999px", this.content.main.style.margin = "0", this.content.main.style.width = "auto");
673
694
  }
674
695
  finalizeModalClose() {
675
- this.modalElements && this.content.main.parentElement === this.modalElements.dialog && (this.addClasses(this.modalElements.overlay, this.classes.hide), this.removeClasses(this.modalElements.overlay, this.classes.contentOpen), this.removeClasses(this.content.main, this.classes.modalContent), this.restoreContentOffscreen(), this.unlockBodyScroll());
696
+ this.modalElements && this.content.main.parentElement === this.modalElements.dialog && (this.addClasses(this.modalElements.overlay, this.classes.hide), this.removeClasses(this.modalElements.overlay, this.classes.contentOpen), this.removeClasses(this.content.main, this.classes.modalContent), this.modalElements.closeButton.remove(), this.restoreContentOffscreen(), this.unlockBodyScroll());
676
697
  }
677
698
  waitForAnimation(e, t) {
678
699
  let n = d(this.content.main, this.settings.timeoutDelay);
@@ -700,17 +721,17 @@ var N = class {
700
721
  switch (e.key) {
701
722
  case "ArrowUp":
702
723
  case "ArrowDown": return this.callbacks.open(), e.key === "ArrowDown" ? this.highlight("down") : this.highlight("up"), !1;
703
- case "Tab": return this.callbacks.close(), !0;
724
+ case "Tab": return this.requestClose("tab"), !0;
704
725
  case "Enter":
705
726
  case " ":
706
727
  this.callbacks.open();
707
728
  let t = this.content.list.querySelector("." + this.classes.getFirst("highlighted"));
708
729
  return t && t.click(), !1;
709
- case "Escape": return this.callbacks.close(), !1;
730
+ case "Escape": return this.requestClose("escape"), !1;
710
731
  }
711
732
  return e.key.length === 1 && this.callbacks.open(), !0;
712
733
  }, e.onclick = (e) => {
713
- this.settings.disabled || (this.settings.isOpen ? this.callbacks.close() : this.callbacks.open());
734
+ this.settings.disabled || (this.settings.isOpen ? this.requestClose("toggle") : this.callbacks.open());
714
735
  };
715
736
  let t = document.createElement("div");
716
737
  this.addClasses(t, this.classes.values), e.appendChild(t);
@@ -732,7 +753,7 @@ var N = class {
732
753
  let e = this.store.getFirstOption(), t = e ? e.id : "";
733
754
  this.callbacks.setSelected(t, !1);
734
755
  }
735
- this.settings.closeOnSelect && this.callbacks.close(), this.callbacks.afterChange && this.callbacks.afterChange(this.store.getSelectedOptions());
756
+ this.settings.closeOnSelect && this.requestClose("deselect", { selectionChanged: !0 }), this.callbacks.afterChange && this.callbacks.afterChange(this.store.getSelectedOptions());
736
757
  }
737
758
  }, n.onkeydown = (e) => {
738
759
  (e.key === "Enter" || e.key === " ") && (e.preventDefault(), n.click());
@@ -852,7 +873,7 @@ var N = class {
852
873
  if (t instanceof I) for (let n of t.options) n.id && e.push(n.id);
853
874
  t instanceof F && e.push(t.id);
854
875
  }
855
- this.callbacks.setSelected(e, !1), this.settings.closeOnSelect && this.callbacks.close(), this.callbacks.afterChange && this.callbacks.afterChange(i), this.updateDeselectAll(), this.updateMultipleValueDeleteVisibility();
876
+ this.callbacks.setSelected(e, !1), this.settings.closeOnSelect && this.requestClose("deselect", { selectionChanged: !0 }), this.callbacks.afterChange && this.callbacks.afterChange(i), this.updateDeselectAll(), this.updateMultipleValueDeleteVisibility();
856
877
  }
857
878
  };
858
879
  let r = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -930,8 +951,8 @@ var N = class {
930
951
  switch (e.key) {
931
952
  case "ArrowUp":
932
953
  case "ArrowDown": return e.key === "ArrowDown" ? this.highlight("down") : this.highlight("up"), !1;
933
- case "Tab": return this.callbacks.close(), !0;
934
- case "Escape": return this.callbacks.close(), !1;
954
+ case "Tab": return this.requestClose("tab"), !0;
955
+ case "Escape": return this.requestClose("escape"), !1;
935
956
  case "Enter":
936
957
  let t = this.content.list.querySelector("." + this.classes.getFirst("highlighted"));
937
958
  return t ? (t.click(), !1) : this.callbacks.addable ? (n.click(), !1) : !0;
@@ -958,7 +979,7 @@ var N = class {
958
979
  if (this.callbacks.search(""), this.settings.closeOnSelect) {
959
980
  let e = u(this.content.main);
960
981
  setTimeout(() => {
961
- this.callbacks.close();
982
+ this.requestClose("select", { selectionChanged: !0 });
962
983
  }, e);
963
984
  }
964
985
  }, r = this.callbacks.addable(t);
@@ -1241,23 +1262,35 @@ var N = class {
1241
1262
  t.classList.add(e);
1242
1263
  }), e.style && (t.style.cssText = e.style), this.setOptionElementContent(t, e, this.content.search.input.value), e.display || this.addClasses(t, this.classes.hide), e.disabled && this.addClasses(t, this.classes.disabled), e.selected && this.settings.hideSelected && this.addClasses(t, this.classes.hide), e.selected ? (this.addClasses(t, this.classes.selected), t.setAttribute("aria-selected", "true"), this.main.main.setAttribute("aria-activedescendant", t.id)) : (this.removeClasses(t, this.classes.selected), t.setAttribute("aria-selected", "false")), t.addEventListener("click", (t) => {
1243
1264
  t.preventDefault(), t.stopPropagation();
1244
- let n = this.store.getSelected(), r = t.currentTarget, i = String(r.dataset.id), a = t.ctrlKey || t.metaKey;
1245
- if (e.disabled || !this.settings.isMultiple && e.selected && !this.settings.allowDeselect || e.selected && e.mandatory || this.settings.isMultiple && this.settings.maxSelected <= n.length && !e.selected || this.settings.isMultiple && this.settings.minSelected >= n.length && e.selected && !a) return;
1246
- let o = !1, s = this.store.getSelectedOptions(), c = [];
1265
+ let n = this.store.getSelected(), r = t.currentTarget, i = String(r.dataset.id), a = this.store.getOptionByID(i) ?? e, o = n.includes(i), s = t.ctrlKey || t.metaKey;
1266
+ if (e.disabled) return;
1267
+ if (!this.settings.isMultiple && o && !this.settings.allowDeselect) {
1268
+ this.closeOnSingleSelectReclick(a);
1269
+ return;
1270
+ }
1271
+ if (o && a.mandatory) {
1272
+ this.closeOnSingleSelectReclick(a);
1273
+ return;
1274
+ }
1275
+ if (this.settings.isMultiple && this.settings.maxSelected <= n.length && !o || this.settings.isMultiple && this.settings.minSelected >= n.length && o && !s) return;
1276
+ let c = !1, l = this.store.getSelectedOptions(), u = [];
1247
1277
  if (this.settings.isMultiple) {
1248
- let n = s.some((e) => e.id === i);
1278
+ let n = l.some((e) => e.id === i);
1249
1279
  if (t.shiftKey && this.lastSelectedOption) {
1250
1280
  let t = this.lastRenderedOptions, n = t.findIndex((e) => e.id === this.lastSelectedOption.id), r = t.findIndex((t) => t.id === e.id);
1251
1281
  if (n >= 0 && r >= 0) {
1252
- let e = Math.min(n, r), i = Math.max(n, r), a = t.slice(e, i + 1).filter((e) => !s.find((t) => t.id === e.id));
1253
- c = s.length + a.length <= this.settings.maxSelected ? s.concat(a) : s;
1254
- } else c = s;
1255
- } else c = n ? s.filter((e) => e.id !== i) : s.concat(e), this.lastSelectedOption = e;
1282
+ let e = Math.min(n, r), i = Math.max(n, r), a = t.slice(e, i + 1).filter((e) => !l.find((t) => t.id === e.id));
1283
+ u = l.length + a.length <= this.settings.maxSelected ? l.concat(a) : l;
1284
+ } else u = l;
1285
+ } else u = n ? l.filter((e) => e.id !== i) : l.concat(a), this.lastSelectedOption = a;
1256
1286
  }
1257
- if (this.settings.isMultiple || (c = e.selected ? [] : [e]), this.callbacks.beforeChange || (o = !0), this.callbacks.beforeChange && (o = this.callbacks.beforeChange(c, s) !== !1), o) {
1258
- this.store.getOptionByID(i) || this.callbacks.addOption(e), this.callbacks.setSelected(c.map((e) => e.id), !1);
1287
+ if (this.settings.isMultiple || (u = o ? [] : [a]), this.callbacks.beforeChange || (c = !0), this.callbacks.beforeChange && (c = this.callbacks.beforeChange(u, l) !== !1), c) {
1288
+ this.store.getOptionByID(i) || this.callbacks.addOption(e), this.callbacks.setSelected(u.map((e) => e.id), !1);
1259
1289
  let n = t.ctrlKey || t.metaKey || t.shiftKey;
1260
- this.settings.closeOnSelect && !(this.settings.isMultiple && n) && this.callbacks.close(), this.callbacks.afterChange && this.callbacks.afterChange(c);
1290
+ this.settings.closeOnSelect && !(this.settings.isMultiple && n) && this.requestClose("select", {
1291
+ option: a,
1292
+ selectionChanged: this.selectionChanged(l, u)
1293
+ }), this.callbacks.afterChange && this.callbacks.afterChange(u);
1261
1294
  }
1262
1295
  }), t;
1263
1296
  }
@@ -1762,7 +1795,6 @@ var U = class {
1762
1795
  let t = [
1763
1796
  "beforeOpen",
1764
1797
  "afterOpen",
1765
- "beforeClose",
1766
1798
  "afterClose"
1767
1799
  ];
1768
1800
  for (let n in e.events) e.events.hasOwnProperty(n) && (t.indexOf(n) === -1 ? this.events[n] = e.events[n] : this.events[n] = S(e.events[n], 100));
@@ -1771,7 +1803,10 @@ var U = class {
1771
1803
  }, this.select.onDisabledChange = (e) => {
1772
1804
  e ? this.disable() : this.enable();
1773
1805
  }, this.select.onLabelClick = () => {
1774
- this.settings.disabled || (this.settings.isOpen ? this.close() : this.open());
1806
+ this.settings.disabled || (this.settings.isOpen ? this.close({
1807
+ source: "toggle",
1808
+ selectionChanged: !1
1809
+ }) : this.open());
1775
1810
  };
1776
1811
  let n = e.data ? e.data : this.select.getData();
1777
1812
  this.store = new L(this.settings.isMultiple ? "multiple" : "single", n), e.data && this.select.updateOptions(this.store.getData());
@@ -1819,7 +1854,8 @@ var U = class {
1819
1854
  }, this.lifecycle = new P({
1820
1855
  beforeOpen: this.events.beforeOpen,
1821
1856
  afterOpen: this.events.afterOpen,
1822
- beforeClose: this.events.beforeClose,
1857
+ beforeClose: (e) => this.events.beforeClose?.(e),
1858
+ onClose: (e) => this.applyClose(e),
1823
1859
  afterClose: () => {
1824
1860
  this.render.clearDirectionClasses(), this.render.finalizeModalClose(), this.events.afterClose && this.events.afterClose();
1825
1861
  },
@@ -1837,7 +1873,10 @@ var U = class {
1837
1873
  !this.settings.isOpen && !this.settings.isFullOpen || this.render.moveContent();
1838
1874
  },
1839
1875
  onVisibilityChange: () => {
1840
- document.hidden && this.close();
1876
+ document.hidden && this.close({
1877
+ source: "api",
1878
+ selectionChanged: !1
1879
+ });
1841
1880
  }
1842
1881
  }), this.render.renderValues(), this.render.renderOptions(this.store.getData(!1));
1843
1882
  let i = this.selectEl.getAttribute("aria-label"), a = this.selectEl.getAttribute("aria-labelledby");
@@ -1892,10 +1931,18 @@ var U = class {
1892
1931
  let e = this.render.content.search.input.value.trim();
1893
1932
  e !== "" && this.search(e);
1894
1933
  }
1895
- close(e = null) {
1896
- !this.settings.isOpen || this.settings.alwaysOpen || (this.lifecycle.cancelPending(), this.render.close(), !this.settings.keepSearch && this.render.content.search.input.value !== "" && (this.sync.flush(), this.search("")), this.render.mainFocus(e), this.settings.isOpen = !1, this.settings.isFullOpen = !1, this.lifecycle.requestClose().then(() => {
1897
- this.settings.isOpen || (this.settings.isOpen = this.lifecycle.isOpen, this.settings.isFullOpen = this.lifecycle.isFullOpen);
1898
- }), this.render.stopPositionTracking());
1934
+ close(e = {
1935
+ source: "api",
1936
+ selectionChanged: !1
1937
+ }) {
1938
+ !this.settings.isOpen || this.settings.alwaysOpen || this.lifecycle.requestClose(e).then((e) => {
1939
+ e && (this.settings.isOpen || (this.settings.isOpen = this.lifecycle.isOpen, this.settings.isFullOpen = this.lifecycle.isFullOpen));
1940
+ });
1941
+ }
1942
+ applyClose(e) {
1943
+ this.render.close(), !this.settings.keepSearch && this.render.content.search.input.value !== "" && (this.sync.flush(), this.search(""));
1944
+ let t = e.source === "outside" || e.source === "toggle";
1945
+ this.render.mainFocus(t ? "click" : null), this.settings.isOpen = !1, this.settings.isFullOpen = !1, this.render.stopPositionTracking();
1899
1946
  }
1900
1947
  search(e) {
1901
1948
  let t = e.trim();
@@ -1957,7 +2004,10 @@ var U = class {
1957
2004
  this.lifecycle.destroy(), this.render.stopPositionTracking(), this.globalEvents.detach({ listenScroll: this.settings.openPosition === "auto" }), this.store.setData([]), this.render.destroy(), this.select.destroy(), delete this.selectEl.slim;
1958
2005
  }
1959
2006
  documentClick(e) {
1960
- this.settings.isOpen && e.target && !x(e.target, this.settings.id) && this.close(e.type);
2007
+ this.settings.isOpen && e.target && !x(e.target, this.settings.id) && this.close({
2008
+ source: "outside",
2009
+ selectionChanged: !1
2010
+ });
1961
2011
  }
1962
2012
  }, G = e(({ data: e, settings: o, events: s, cssClasses: c, value: l, onChange: u, multiple: d }, f) => {
1963
2013
  let p = r(null), m = r(null), h = r(!0), g = r(l), _ = r(null), v = r(u), y = r(s), b = r(d), x = r(l), [S, C] = i(!1);