@stackline/vue-multiselect-dropdown 3.0.3 → 3.1.0

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/dist/index.cjs CHANGED
@@ -23,7 +23,12 @@ __export(index_exports, {
23
23
  StacklineVueMultiselect: () => StacklineVueMultiselect,
24
24
  VueMultiselect: () => VueMultiselect,
25
25
  VueMultiselectDropdown: () => VueMultiselectDropdown,
26
- default: () => plugin_default
26
+ createVueMultiselectDropdown: () => createVueMultiselectDropdown,
27
+ default: () => plugin_default,
28
+ defineSettings: () => defineSettings,
29
+ defineSlots: () => defineSlots,
30
+ useMultiSelectDropdown: () => useMultiSelectDropdown,
31
+ useMultiSelectState: () => useMultiSelectState
27
32
  });
28
33
  module.exports = __toCommonJS(index_exports);
29
34
 
@@ -109,6 +114,7 @@ var styles = `
109
114
  display: flex;
110
115
  flex: 1 1 auto;
111
116
  min-width: 0;
117
+ min-height: 1.45em;
112
118
  align-items: center;
113
119
  align-content: center;
114
120
  gap: 8px;
@@ -122,6 +128,7 @@ var styles = `
122
128
  align-self: center;
123
129
  justify-content: flex-start;
124
130
  min-width: 0;
131
+ min-height: 1.45em;
125
132
  max-width: 100%;
126
133
  color: var(--vmsd-muted);
127
134
  font-size: 0.95rem;
@@ -205,33 +212,25 @@ var styles = `
205
212
  display: inline-flex;
206
213
  align-items: center;
207
214
  justify-content: center;
208
- min-width: 0;
209
- min-height: 0;
215
+ flex: 0 0 auto;
216
+ min-width: 24px;
217
+ min-height: 20px;
210
218
  color: var(--vmsd-muted);
211
219
  font-size: 0.8rem;
212
220
  font-weight: 600;
221
+ line-height: 1;
222
+ white-space: nowrap;
223
+ text-align: center;
213
224
  }
214
225
 
215
- .vmsd-root.vmsd-has-overflow:not(.skin-classic) .vmsd-trigger {
226
+ .vmsd-root.vmsd-has-overflow .vmsd-trigger {
216
227
  padding-right: 104px;
217
228
  }
218
229
 
219
- .vmsd-root.vmsd-has-overflow:not(.skin-classic):not(.vmsd-has-clear) .vmsd-trigger {
230
+ .vmsd-root.vmsd-has-overflow:not(.vmsd-has-clear) .vmsd-trigger {
220
231
  padding-right: 74px;
221
232
  }
222
233
 
223
- .vmsd-root.vmsd-has-overflow:not(.skin-classic) .vmsd-overflow {
224
- position: absolute;
225
- top: 50%;
226
- right: 76px;
227
- transform: translateY(-50%);
228
- z-index: 1;
229
- }
230
-
231
- .vmsd-root.vmsd-has-overflow:not(.skin-classic):not(.vmsd-has-clear) .vmsd-overflow {
232
- right: 42px;
233
- }
234
-
235
234
  .vmsd-actions {
236
235
  position: absolute;
237
236
  top: 50%;
@@ -508,9 +507,6 @@ var styles = `
508
507
  .vmsd-checkbox {
509
508
  position: relative;
510
509
  box-sizing: content-box;
511
- display: inline-grid;
512
- place-items: center;
513
- align-self: center;
514
510
  flex: 0 0 auto;
515
511
  width: 18px;
516
512
  height: 18px;
@@ -518,8 +514,6 @@ var styles = `
518
514
  border: 2px solid var(--vmsd-border-strong);
519
515
  border-radius: 6px;
520
516
  background-color: var(--vmsd-bg);
521
- line-height: 0;
522
- vertical-align: middle;
523
517
  }
524
518
 
525
519
  .vmsd-checkbox[data-checked="true"] {
@@ -539,7 +533,7 @@ var styles = `
539
533
  border-color: #fff;
540
534
  border-style: solid;
541
535
  border-width: 0 0 2px 2px;
542
- transform: translate(-50%, -62%) rotate(-45deg);
536
+ transform: translate(-50%, -58%) rotate(-45deg);
543
537
  transform-origin: 50%;
544
538
  }
545
539
 
@@ -801,9 +795,12 @@ var styles = `
801
795
 
802
796
  .theme-classic .vmsd-overflow,
803
797
  .skin-classic .vmsd-overflow {
798
+ min-width: 24px;
799
+ min-height: 20px;
804
800
  color: #333333;
805
801
  font-size: 14px;
806
802
  font-weight: 400;
803
+ line-height: 1;
807
804
  }
808
805
 
809
806
  .theme-classic .vmsd-actions,
@@ -1038,7 +1035,7 @@ var styles = `
1038
1035
  height: 3px;
1039
1036
  margin-top: 0;
1040
1037
  border-width: 0 0 3px 3px;
1041
- transform: translate(-50%, -62%) rotate(-45deg);
1038
+ transform: translate(-50%, -58%) rotate(-45deg);
1042
1039
  }
1043
1040
 
1044
1041
  .theme-classic .vmsd-option-label,
@@ -1071,12 +1068,10 @@ var styles = `
1071
1068
 
1072
1069
  @media (max-width: 720px) {
1073
1070
  .vmsd-trigger {
1074
- align-items: flex-start;
1071
+ align-items: center;
1075
1072
  padding-right: 54px;
1076
1073
  }
1077
1074
  }
1078
-
1079
- /* stackline-vue3-live-20260527 */
1080
1075
  `;
1081
1076
  function ensureDropdownStyles() {
1082
1077
  if (typeof document === "undefined") {
@@ -1137,7 +1132,16 @@ var DEFAULT_SETTINGS = {
1137
1132
  removeItemAriaLabel: "Remove selected option",
1138
1133
  openDropdownAriaLabel: "Open dropdown",
1139
1134
  closeDropdownAriaLabel: "Close dropdown",
1140
- loadingText: "Loading options"
1135
+ loadingText: "Loading options",
1136
+ keyboard: {
1137
+ space: true,
1138
+ spaceOptionAction: "toggle",
1139
+ tab: true,
1140
+ arrows: true,
1141
+ escape: true,
1142
+ backspaceRemovesLastWhenSearchEmpty: false,
1143
+ deleteRemovesFocusedBadge: true
1144
+ }
1141
1145
  };
1142
1146
  function iconPath(name) {
1143
1147
  if (name === "remove") {
@@ -1276,9 +1280,15 @@ function buildGroups(items, settings) {
1276
1280
  }
1277
1281
  function mergeUniqueItems(base, extra, settings) {
1278
1282
  const bucket = /* @__PURE__ */ new Map();
1279
- for (const item of base.concat(extra)) {
1283
+ for (const item of base) {
1280
1284
  bucket.set(getPrimaryValue(item, settings), item);
1281
1285
  }
1286
+ for (const item of extra) {
1287
+ const key = getPrimaryValue(item, settings);
1288
+ if (!bucket.has(key)) {
1289
+ bucket.set(key, item);
1290
+ }
1291
+ }
1282
1292
  return Array.from(bucket.values());
1283
1293
  }
1284
1294
  function callRenderFunction(renderFunction, h2, item, context) {
@@ -1287,6 +1297,10 @@ function callRenderFunction(renderFunction, h2, item, context) {
1287
1297
  }
1288
1298
  return renderFunction(item, context, h2);
1289
1299
  }
1300
+ function callSlot(vm, name, props) {
1301
+ const slot = vm.$slots && vm.$slots[name];
1302
+ return typeof slot === "function" ? slot(props) : null;
1303
+ }
1290
1304
  function escapeSelectorValue(value) {
1291
1305
  if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
1292
1306
  return CSS.escape(value);
@@ -1296,6 +1310,9 @@ function escapeSelectorValue(value) {
1296
1310
  function isActivationKey(event) {
1297
1311
  return event.key === "Enter" || event.key === " " || event.key === "Spacebar";
1298
1312
  }
1313
+ function isSpaceKey(event) {
1314
+ return event.key === " " || event.key === "Spacebar";
1315
+ }
1299
1316
  function isTextInputTarget(target) {
1300
1317
  const element = target;
1301
1318
  if (!element) {
@@ -1379,10 +1396,21 @@ var VueMultiselectDropdown = {
1379
1396
  },
1380
1397
  computed: {
1381
1398
  resolvedSettings() {
1399
+ const userSettings = this.settings || {};
1382
1400
  const merged = {
1383
1401
  ...DEFAULT_SETTINGS,
1384
- ...this.settings || {}
1402
+ ...userSettings,
1403
+ keyboard: {
1404
+ ...DEFAULT_SETTINGS.keyboard,
1405
+ ...userSettings.keyboard || {}
1406
+ }
1385
1407
  };
1408
+ if (typeof userSettings.escapeToClose === "boolean" && userSettings.keyboard?.escape == null) {
1409
+ merged.keyboard.escape = userSettings.escapeToClose;
1410
+ }
1411
+ if (typeof merged.keyboard.backspace === "boolean") {
1412
+ merged.keyboard.backspaceRemovesLastWhenSearchEmpty = merged.keyboard.backspace;
1413
+ }
1386
1414
  const skin = merged.skin || merged.theme || "classic";
1387
1415
  return {
1388
1416
  ...merged,
@@ -1476,6 +1504,9 @@ var VueMultiselectDropdown = {
1476
1504
  getKey(item) {
1477
1505
  return getPrimaryValue(item, this.resolvedSettings);
1478
1506
  },
1507
+ getOptionId(key) {
1508
+ return `${this.instanceId}-option-${key}`;
1509
+ },
1479
1510
  isSelected(item) {
1480
1511
  const key = this.getKey(item);
1481
1512
  return this.selected.some((selectedItem) => this.getKey(selectedItem) === key);
@@ -1608,6 +1639,13 @@ var VueMultiselectDropdown = {
1608
1639
  this.emitSelection(next);
1609
1640
  this.$emit("de-select", item);
1610
1641
  },
1642
+ removeLastSelected() {
1643
+ const last = this.selected[this.selected.length - 1];
1644
+ if (!last) {
1645
+ return;
1646
+ }
1647
+ this.removeItem(last);
1648
+ },
1611
1649
  addFilterItem(event) {
1612
1650
  event.preventDefault();
1613
1651
  event.stopPropagation();
@@ -1624,35 +1662,51 @@ var VueMultiselectDropdown = {
1624
1662
  this.query = "";
1625
1663
  },
1626
1664
  onTriggerKeydown(event) {
1627
- if (isActivationKey(event)) {
1665
+ const keyboard = this.resolvedSettings.keyboard;
1666
+ if (event.key === "Enter" || isSpaceKey(event) && keyboard.space !== false) {
1628
1667
  event.preventDefault();
1629
1668
  this.toggleDropdown();
1630
1669
  return;
1631
1670
  }
1632
- if (event.key === "ArrowDown") {
1671
+ if (keyboard.arrows !== false && event.key === "ArrowDown") {
1633
1672
  event.preventDefault();
1634
1673
  this.openDropdown();
1635
1674
  this.focusFirstOption();
1636
1675
  return;
1637
1676
  }
1638
- if (event.key === "ArrowUp") {
1677
+ if (keyboard.arrows !== false && event.key === "ArrowUp") {
1639
1678
  event.preventDefault();
1640
1679
  this.openDropdown();
1641
1680
  this.focusLastOption();
1642
1681
  return;
1643
1682
  }
1644
- if (event.key === "Escape") {
1683
+ if (keyboard.escape !== false && event.key === "Escape") {
1645
1684
  this.closeDropdown();
1646
1685
  }
1647
1686
  },
1648
1687
  onListKeydown(event) {
1649
- if (event.key === "Escape") {
1688
+ const keyboard = this.resolvedSettings.keyboard;
1689
+ if (keyboard.escape !== false && event.key === "Escape") {
1650
1690
  event.preventDefault();
1651
1691
  event.stopPropagation();
1652
1692
  this.closeDropdown();
1653
1693
  return;
1654
1694
  }
1655
- if (isTextInputTarget(event.target) && event.key !== "ArrowDown") {
1695
+ if (event.key === "Tab") {
1696
+ return;
1697
+ }
1698
+ if (isTextInputTarget(event.target)) {
1699
+ if (event.key === "Backspace" && !this.query && keyboard.backspaceRemovesLastWhenSearchEmpty === true) {
1700
+ event.preventDefault();
1701
+ event.stopPropagation();
1702
+ this.removeLastSelected();
1703
+ return;
1704
+ }
1705
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1706
+ return;
1707
+ }
1708
+ }
1709
+ if (keyboard.arrows === false && ["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1656
1710
  return;
1657
1711
  }
1658
1712
  const selectable = this.visibleSelectableItems();
@@ -1687,10 +1741,22 @@ var VueMultiselectDropdown = {
1687
1741
  return;
1688
1742
  }
1689
1743
  if (isActivationKey(event)) {
1744
+ if (isSpaceKey(event) && keyboard.space === false) {
1745
+ return;
1746
+ }
1690
1747
  event.preventDefault();
1691
1748
  event.stopPropagation();
1692
1749
  const activeItem = selectable.find((item) => this.getKey(item) === this.focusedKey) || selectable[0];
1693
1750
  this.toggleItem(activeItem);
1751
+ if (keyboard.spaceOptionAction === "toggle-and-next") {
1752
+ const activeIndex = selectable.findIndex((item) => this.getKey(item) === this.getKey(activeItem));
1753
+ const nextItem = selectable[Math.min(activeIndex + 1, selectable.length - 1)];
1754
+ if (nextItem) {
1755
+ this.focusOption(nextItem);
1756
+ }
1757
+ } else {
1758
+ this.focusOption(activeItem);
1759
+ }
1694
1760
  return;
1695
1761
  }
1696
1762
  },
@@ -1727,19 +1793,26 @@ var VueMultiselectDropdown = {
1727
1793
  if (isActivationKey(event)) {
1728
1794
  event.stopPropagation();
1729
1795
  }
1730
- if (event.key === "ArrowDown") {
1796
+ if (this.resolvedSettings.keyboard.arrows !== false && event.key === "ArrowDown") {
1731
1797
  event.preventDefault();
1732
1798
  event.stopPropagation();
1733
1799
  this.openDropdown();
1734
1800
  this.focusFirstOption();
1735
1801
  }
1736
- if (event.key === "ArrowUp") {
1802
+ if (this.resolvedSettings.keyboard.arrows !== false && event.key === "ArrowUp") {
1737
1803
  event.preventDefault();
1738
1804
  event.stopPropagation();
1739
1805
  this.openDropdown();
1740
1806
  this.focusLastOption();
1741
1807
  }
1742
1808
  },
1809
+ onRemoveButtonKeydown(item, event) {
1810
+ if (this.resolvedSettings.keyboard.deleteRemovesFocusedBadge !== false && (event.key === "Backspace" || event.key === "Delete")) {
1811
+ this.removeItem(item, event);
1812
+ return;
1813
+ }
1814
+ this.onInlineKeydown(event);
1815
+ },
1743
1816
  onTriggerClick(event) {
1744
1817
  const target = event.target;
1745
1818
  if (target && target.closest("button")) {
@@ -1760,7 +1833,7 @@ var VueMultiselectDropdown = {
1760
1833
  this.closeDropdown();
1761
1834
  },
1762
1835
  onDocumentKeydown(event) {
1763
- if (this.isOpen && this.resolvedSettings.escapeToClose && event.key === "Escape") {
1836
+ if (this.isOpen && this.resolvedSettings.keyboard.escape !== false && event.key === "Escape") {
1764
1837
  this.closeDropdown();
1765
1838
  }
1766
1839
  },
@@ -1878,11 +1951,14 @@ var VueMultiselectDropdown = {
1878
1951
  label,
1879
1952
  selected: true,
1880
1953
  disabled: false,
1954
+ key: this.getKey(item),
1955
+ ariaSelected: "true",
1956
+ ariaChecked: "true",
1881
1957
  query: this.query,
1882
1958
  toggle: () => this.toggleItem(item),
1883
1959
  remove: () => this.removeItem(item)
1884
1960
  };
1885
- const rendered = callRenderFunction(this.renderBadge, h, item, context);
1961
+ const rendered = callSlot(this, "badge", context) || callRenderFunction(this.renderBadge, h, item, context);
1886
1962
  const removeLabel = typeof settings.removeItemAriaLabel === "function" ? settings.removeItemAriaLabel(item) : `${settings.removeItemAriaLabel}: ${label}`;
1887
1963
  return h("span", { class: "vmsd-badge", key: this.getKey(item) }, [
1888
1964
  h("span", { class: "vmsd-badge-label" }, [rendered || label]),
@@ -1893,17 +1969,18 @@ var VueMultiselectDropdown = {
1893
1969
  attrs: { type: "button", "aria-label": removeLabel },
1894
1970
  on: {
1895
1971
  click: (event) => this.removeItem(item, event),
1896
- keydown: this.onInlineKeydown
1972
+ keydown: (event) => this.onRemoveButtonKeydown(item, event)
1897
1973
  }
1898
1974
  },
1899
1975
  [renderIcon(h, "remove")]
1900
1976
  )
1901
1977
  ]);
1902
1978
  });
1903
- const valueContent = hasSelection ? [h("span", { class: "vmsd-badge-list" }, badges)] : [h("span", { class: "vmsd-placeholder" }, [settings.text])];
1904
- if (hiddenCount > 0) {
1905
- valueContent.push(h("span", { class: "vmsd-overflow", attrs: { "aria-label": `${hiddenCount} more selected options` } }, [`+${hiddenCount}`]));
1906
- }
1979
+ const valueContent = hasSelection ? [h("span", { class: "vmsd-badge-list" }, badges)] : [
1980
+ h("span", { class: "vmsd-placeholder" }, [
1981
+ callSlot(this, "placeholder", { label: settings.text, state: { isOpen: this.isOpen, query: this.query } }) || settings.text
1982
+ ])
1983
+ ];
1907
1984
  const trigger = h(
1908
1985
  "div",
1909
1986
  {
@@ -1916,7 +1993,8 @@ var VueMultiselectDropdown = {
1916
1993
  "aria-expanded": this.isOpen ? "true" : "false",
1917
1994
  "aria-haspopup": "listbox",
1918
1995
  "aria-disabled": settings.disabled ? "true" : "false",
1919
- "aria-controls": `${this.instanceId}-listbox`
1996
+ "aria-controls": `${this.instanceId}-listbox`,
1997
+ "aria-activedescendant": this.focusedKey ? this.getOptionId(this.focusedKey) : void 0
1920
1998
  },
1921
1999
  on: {
1922
2000
  click: this.onTriggerClick,
@@ -1924,8 +2002,15 @@ var VueMultiselectDropdown = {
1924
2002
  }
1925
2003
  },
1926
2004
  [
1927
- h("div", { class: "vmsd-value" }, valueContent),
2005
+ callSlot(this, "trigger", {
2006
+ selected: this.selected,
2007
+ label: hasSelection ? this.selected.map((item) => this.getLabel(item)).join(", ") : settings.text,
2008
+ isOpen: this.isOpen,
2009
+ clearSelection: this.clearSelection,
2010
+ toggleDropdown: this.toggleDropdown
2011
+ }) || h("div", { class: "vmsd-value" }, valueContent),
1928
2012
  h("div", { class: "vmsd-actions" }, [
2013
+ hiddenCount > 0 ? h("span", { class: "vmsd-overflow", attrs: { "aria-label": `${hiddenCount} more selected options` } }, [`+${hiddenCount}`]) : null,
1929
2014
  hasClear ? h(
1930
2015
  "button",
1931
2016
  {
@@ -2024,28 +2109,37 @@ var VueMultiselectDropdown = {
2024
2109
  label,
2025
2110
  selected,
2026
2111
  disabled,
2112
+ index: this.filteredItems.findIndex((candidate) => this.getKey(candidate) === key),
2113
+ group: getGroupName(item, settings),
2114
+ key,
2115
+ optionId: this.getOptionId(key),
2116
+ ariaSelected: selected ? "true" : "false",
2117
+ ariaChecked: selected ? "true" : "false",
2027
2118
  query: this.query,
2028
2119
  toggle: () => this.toggleItem(item),
2029
2120
  remove: () => this.removeItem(item)
2030
2121
  };
2031
- const rendered = callRenderFunction(this.renderItem, h, item, context);
2122
+ const rendered = callSlot(this, "option", context) || callRenderFunction(this.renderItem, h, item, context);
2032
2123
  return h(
2033
2124
  "div",
2034
2125
  {
2035
2126
  key,
2036
2127
  class: ["vmsd-option", selected ? "vmsd-selected" : "", disabled ? "vmsd-disabled" : ""],
2037
2128
  attrs: {
2129
+ id: this.getOptionId(key),
2038
2130
  role: "option",
2039
2131
  tabindex: disabled ? "-1" : "0",
2040
2132
  "data-vmsd-option": "true",
2041
2133
  "data-vmsd-key": key,
2042
2134
  "aria-disabled": disabled ? "true" : "false",
2043
- "aria-selected": selected ? "true" : "false"
2135
+ "aria-selected": selected ? "true" : "false",
2136
+ "aria-checked": selected ? "true" : "false"
2044
2137
  },
2045
2138
  on: {
2046
2139
  click: (event) => {
2047
2140
  if (!disabled) {
2048
2141
  this.toggleItem(item, event);
2142
+ this.focusOption(item);
2049
2143
  }
2050
2144
  },
2051
2145
  focus: () => this.focusedKey = key,
@@ -2062,9 +2156,13 @@ var VueMultiselectDropdown = {
2062
2156
  ]
2063
2157
  );
2064
2158
  };
2065
- const listChildren = settings.loading ? [h("div", { class: "vmsd-state", attrs: { role: "status" } }, [settings.loadingText])] : settings.groupBy ? this.groupedItems.map(
2159
+ const listChildren = !this.isOpen ? [] : settings.loading ? [h("div", { class: "vmsd-state", attrs: { role: "status" } }, [callSlot(this, "loading", { label: settings.loadingText }) || settings.loadingText])] : settings.groupBy ? this.groupedItems.map(
2066
2160
  (group) => h("div", { class: "vmsd-group", key: group.name, attrs: { role: "group", "aria-label": group.name } }, [
2067
- h("div", { class: "vmsd-group-header" }, [
2161
+ callSlot(this, "group-header", {
2162
+ group,
2163
+ selected: group.items.filter((item) => !isDisabledItem(item)).every((item) => this.isSelected(item)),
2164
+ selectGroup: (event) => this.selectGroup(group.name, group.items, event)
2165
+ }) || h("div", { class: "vmsd-group-header" }, [
2068
2166
  h("span", [`${group.name} \xB7 ${group.items.length}`]),
2069
2167
  settings.selectGroup ? h(
2070
2168
  "button",
@@ -2079,8 +2177,8 @@ var VueMultiselectDropdown = {
2079
2177
  group.items.map(renderOption)
2080
2178
  ])
2081
2179
  ) : this.filteredItems.map(renderOption);
2082
- if (!this.filteredItems.length && !settings.loading) {
2083
- const emptyContent = this.renderEmptyState ? this.renderEmptyState(this.query, h) : settings.noDataLabel;
2180
+ if (this.isOpen && !this.filteredItems.length && !settings.loading) {
2181
+ const emptyContent = callSlot(this, "empty", { query: this.query, label: settings.noDataLabel }) || (this.renderEmptyState ? this.renderEmptyState(this.query, h) : settings.noDataLabel);
2084
2182
  listChildren.push(h("div", { class: "vmsd-state" }, [emptyContent]));
2085
2183
  }
2086
2184
  const menu = h(
@@ -2110,7 +2208,8 @@ var VueMultiselectDropdown = {
2110
2208
  on: { scroll: this.onListScroll }
2111
2209
  },
2112
2210
  listChildren
2113
- )
2211
+ ),
2212
+ this.isOpen ? callSlot(this, "menu-footer", { selected: this.selected, filteredItems: this.filteredItems, close: this.closeDropdown }) : null
2114
2213
  ]
2115
2214
  );
2116
2215
  return h(
@@ -2138,6 +2237,498 @@ var VueMultiselectDropdown = {
2138
2237
  };
2139
2238
  var StacklineVueMultiselect = VueMultiselectDropdown;
2140
2239
 
2240
+ // src/composables.ts
2241
+ var import_vue2 = require("vue");
2242
+ var DEFAULT_KEYS = {
2243
+ labelKey: "itemName",
2244
+ primaryKey: "id"
2245
+ };
2246
+ function read(value, fallback) {
2247
+ return value == null ? fallback : (0, import_vue2.unref)(value);
2248
+ }
2249
+ function isPrimitiveItem2(item) {
2250
+ return typeof item === "string" || typeof item === "number" || typeof item === "boolean";
2251
+ }
2252
+ function resolveSettings(settings) {
2253
+ const value = read(settings, {});
2254
+ return {
2255
+ ...value,
2256
+ text: value.text || "Select",
2257
+ singleSelection: Boolean(value.singleSelection),
2258
+ labelKey: value.labelKey || DEFAULT_KEYS.labelKey,
2259
+ primaryKey: value.primaryKey || DEFAULT_KEYS.primaryKey,
2260
+ searchBy: Array.isArray(value.searchBy) ? value.searchBy : [],
2261
+ groupBy: value.groupBy || ""
2262
+ };
2263
+ }
2264
+ function getLabel2(item, settings) {
2265
+ if (isPrimitiveItem2(item)) {
2266
+ return String(item);
2267
+ }
2268
+ const labelKey = settings.labelKey || DEFAULT_KEYS.labelKey;
2269
+ const keys = [labelKey, "itemName", "name", "label", "title", "value"];
2270
+ for (const key of keys) {
2271
+ if (item[key] != null) {
2272
+ return String(item[key]);
2273
+ }
2274
+ }
2275
+ return JSON.stringify(item);
2276
+ }
2277
+ function getKey(item, settings) {
2278
+ if (isPrimitiveItem2(item)) {
2279
+ return String(item);
2280
+ }
2281
+ const primaryKey = settings.primaryKey || DEFAULT_KEYS.primaryKey;
2282
+ const keys = [primaryKey, "id", "value", "key"];
2283
+ for (const key of keys) {
2284
+ if (item[key] != null) {
2285
+ return String(item[key]);
2286
+ }
2287
+ }
2288
+ return getLabel2(item, settings);
2289
+ }
2290
+ function getGroup(item, settings) {
2291
+ if (!settings.groupBy) {
2292
+ return void 0;
2293
+ }
2294
+ if (typeof settings.groupBy === "function") {
2295
+ return settings.groupBy(item);
2296
+ }
2297
+ return !isPrimitiveItem2(item) && item[settings.groupBy] != null ? String(item[settings.groupBy]) : void 0;
2298
+ }
2299
+ function isDisabled(item) {
2300
+ return !isPrimitiveItem2(item) && Boolean(item.disabled);
2301
+ }
2302
+ function matchesQuery(item, query, settings) {
2303
+ const needle = query.trim().toLowerCase();
2304
+ if (!needle) {
2305
+ return true;
2306
+ }
2307
+ const values = /* @__PURE__ */ new Set([getLabel2(item, settings).toLowerCase()]);
2308
+ if (!isPrimitiveItem2(item)) {
2309
+ const keys = settings.searchBy && settings.searchBy.length ? settings.searchBy : [settings.labelKey || DEFAULT_KEYS.labelKey];
2310
+ for (const key of keys) {
2311
+ if (key && item[key] != null) {
2312
+ values.add(String(item[key]).toLowerCase());
2313
+ }
2314
+ }
2315
+ }
2316
+ return Array.from(values).some((value) => value.includes(needle));
2317
+ }
2318
+ function mergeSelected(selected, incoming, settings) {
2319
+ const byKey = /* @__PURE__ */ new Map();
2320
+ for (const item of selected) {
2321
+ byKey.set(getKey(item, settings), item);
2322
+ }
2323
+ for (const item of incoming) {
2324
+ const key = getKey(item, settings);
2325
+ if (!byKey.has(key)) {
2326
+ byKey.set(key, item);
2327
+ }
2328
+ }
2329
+ return Array.from(byKey.values());
2330
+ }
2331
+ function readBadgeLimit(selected, settings) {
2332
+ if (settings.singleSelection) {
2333
+ return selected.length;
2334
+ }
2335
+ const value = Number(settings.badgeShowLimit);
2336
+ if (!Number.isFinite(value) || value <= 0) {
2337
+ return selected.length;
2338
+ }
2339
+ return Math.floor(value);
2340
+ }
2341
+ function normalizeExtraProps(props = {}) {
2342
+ const normalized = { ...props };
2343
+ if (normalized.className && !normalized.class) {
2344
+ normalized.class = normalized.className;
2345
+ }
2346
+ delete normalized.className;
2347
+ return normalized;
2348
+ }
2349
+ function mergePropBags(base, extra = {}) {
2350
+ const incoming = normalizeExtraProps(extra);
2351
+ const merged = { ...incoming, ...base };
2352
+ if (base.class || incoming.class) {
2353
+ merged.class = [incoming.class, base.class].filter(Boolean).join(" ");
2354
+ }
2355
+ if (base.style || incoming.style) {
2356
+ merged.style = {
2357
+ ...typeof incoming.style === "object" ? incoming.style : {},
2358
+ ...typeof base.style === "object" ? base.style : {}
2359
+ };
2360
+ }
2361
+ for (const key of Object.keys(base)) {
2362
+ if (/^on[A-Z]/.test(key) && typeof base[key] === "function" && typeof incoming[key] === "function") {
2363
+ merged[key] = (...args) => {
2364
+ incoming[key](...args);
2365
+ base[key](...args);
2366
+ };
2367
+ }
2368
+ }
2369
+ return merged;
2370
+ }
2371
+ function defineSettings(settings) {
2372
+ return settings;
2373
+ }
2374
+ function defineSlots(slots) {
2375
+ return slots;
2376
+ }
2377
+ function useMultiSelectState(options = {}) {
2378
+ const internalSelected = (0, import_vue2.ref)(options.defaultSelectedItems ? options.defaultSelectedItems.slice() : []);
2379
+ const filter = (0, import_vue2.ref)("");
2380
+ const settings = (0, import_vue2.computed)(() => resolveSettings(options.settings));
2381
+ const data = (0, import_vue2.computed)(() => read(options.data, []));
2382
+ const selectedItems = (0, import_vue2.computed)(() => read(options.selectedItems, internalSelected.value));
2383
+ const filteredItems = (0, import_vue2.computed)(() => data.value.filter((item) => matchesQuery(item, filter.value, settings.value)));
2384
+ const isSelected = (item) => {
2385
+ const key = getKey(item, settings.value);
2386
+ return selectedItems.value.some((selected) => getKey(selected, settings.value) === key);
2387
+ };
2388
+ const commit = (next) => {
2389
+ if (!options.selectedItems) {
2390
+ internalSelected.value = next;
2391
+ }
2392
+ options.onChange?.(next);
2393
+ };
2394
+ const getItemKey = (item) => getKey(item, settings.value);
2395
+ const getItemLabel = (item) => getLabel2(item, settings.value);
2396
+ const selectableItems = (0, import_vue2.computed)(() => filteredItems.value.filter((item) => !isDisabled(item)));
2397
+ const visibleBadges = (0, import_vue2.computed)(() => {
2398
+ const limit = readBadgeLimit(selectedItems.value, settings.value);
2399
+ return selectedItems.value.slice(0, limit);
2400
+ });
2401
+ const hiddenBadgeCount = (0, import_vue2.computed)(() => Math.max(selectedItems.value.length - visibleBadges.value.length, 0));
2402
+ const allFilteredSelected = (0, import_vue2.computed)(() => selectableItems.value.length > 0 && selectableItems.value.every((item) => isSelected(item)));
2403
+ const removeItem = (item) => {
2404
+ const key = getItemKey(item);
2405
+ const next = selectedItems.value.filter((selected) => getItemKey(selected) !== key);
2406
+ commit(next);
2407
+ options.onDeSelect?.(item);
2408
+ };
2409
+ const addItem = (item) => {
2410
+ if (isDisabled(item)) {
2411
+ return;
2412
+ }
2413
+ if (settings.value.singleSelection) {
2414
+ commit([item]);
2415
+ options.onSelect?.(item);
2416
+ return;
2417
+ }
2418
+ if (!isSelected(item)) {
2419
+ commit(selectedItems.value.concat(item));
2420
+ options.onSelect?.(item);
2421
+ }
2422
+ };
2423
+ const toggleItem = (item) => {
2424
+ if (isDisabled(item)) {
2425
+ return;
2426
+ }
2427
+ if (isSelected(item)) {
2428
+ removeItem(item);
2429
+ return;
2430
+ }
2431
+ addItem(item);
2432
+ };
2433
+ const clearSelection = () => {
2434
+ const previous = selectedItems.value.slice();
2435
+ commit([]);
2436
+ options.onDeSelectAll?.(previous);
2437
+ };
2438
+ const deSelectAll = (items) => {
2439
+ if (!items || !items.length) {
2440
+ clearSelection();
2441
+ return;
2442
+ }
2443
+ const keys = new Set(items.map(getItemKey));
2444
+ commit(selectedItems.value.filter((item) => !keys.has(getItemKey(item))));
2445
+ options.onDeSelectAll?.(items);
2446
+ };
2447
+ const selectAll = (items) => {
2448
+ const enabled = (items || selectableItems.value).filter((item) => !isDisabled(item));
2449
+ const before = selectedItems.value.length;
2450
+ commit(settings.value.singleSelection ? enabled.slice(0, 1) : mergeSelected(selectedItems.value, enabled, settings.value));
2451
+ options.onSelectAll?.(enabled);
2452
+ if (!enabled.length && before !== selectedItems.value.length) {
2453
+ options.onChange?.(selectedItems.value);
2454
+ }
2455
+ };
2456
+ const toggleGroup = (groupName, items) => {
2457
+ const enabled = items.filter((item) => !isDisabled(item));
2458
+ if (!enabled.length) {
2459
+ return;
2460
+ }
2461
+ const everySelected = enabled.every((item) => isSelected(item));
2462
+ if (everySelected) {
2463
+ deSelectAll(enabled);
2464
+ options.onGroupDeSelect?.(groupName, enabled);
2465
+ return;
2466
+ }
2467
+ selectAll(enabled);
2468
+ options.onGroupSelect?.(groupName, enabled);
2469
+ };
2470
+ const removeLastSelectedItem = () => {
2471
+ const last = selectedItems.value[selectedItems.value.length - 1];
2472
+ if (last) {
2473
+ removeItem(last);
2474
+ }
2475
+ };
2476
+ return {
2477
+ data,
2478
+ settings,
2479
+ filter,
2480
+ filteredItems,
2481
+ selectableItems,
2482
+ selectedItems,
2483
+ visibleBadges,
2484
+ hiddenBadgeCount,
2485
+ allFilteredSelected,
2486
+ isSelected,
2487
+ getItemKey,
2488
+ getItemLabel,
2489
+ setFilter: (value) => {
2490
+ filter.value = value;
2491
+ },
2492
+ selectItem: addItem,
2493
+ removeItem,
2494
+ removeLastSelectedItem,
2495
+ toggleItem,
2496
+ clearSelection,
2497
+ selectAll,
2498
+ deSelectAll,
2499
+ toggleGroup
2500
+ };
2501
+ }
2502
+ function useMultiSelectDropdown(options = {}) {
2503
+ const state = useMultiSelectState(options);
2504
+ const isOpen = (0, import_vue2.ref)(false);
2505
+ const query = (0, import_vue2.ref)("");
2506
+ const activeKey = (0, import_vue2.ref)("");
2507
+ const instanceId = `stackline-vmsd-headless-${Math.random().toString(36).slice(2)}`;
2508
+ const listboxId = `${instanceId}-listbox`;
2509
+ const visibleOptions = (0, import_vue2.computed)(() => {
2510
+ let index = 0;
2511
+ return state.filteredItems.value.map((item) => {
2512
+ const key = getKey(item, state.settings.value);
2513
+ return {
2514
+ item,
2515
+ key,
2516
+ label: getLabel2(item, state.settings.value),
2517
+ selected: state.isSelected(item),
2518
+ disabled: isDisabled(item),
2519
+ group: getGroup(item, state.settings.value),
2520
+ index: index++
2521
+ };
2522
+ });
2523
+ });
2524
+ const activeDescendantId = (0, import_vue2.computed)(() => activeKey.value ? `${instanceId}-option-${activeKey.value}` : void 0);
2525
+ const groups = (0, import_vue2.computed)(() => {
2526
+ const byName = /* @__PURE__ */ new Map();
2527
+ for (const option of visibleOptions.value) {
2528
+ const name = option.group || "Ungrouped";
2529
+ const existing = byName.get(name) || [];
2530
+ existing.push(option);
2531
+ byName.set(name, existing);
2532
+ }
2533
+ return Array.from(byName.entries()).map(([name, items]) => {
2534
+ const enabledItems = items.filter((option) => !option.disabled);
2535
+ return {
2536
+ name,
2537
+ items,
2538
+ enabledItems,
2539
+ selected: enabledItems.length > 0 && enabledItems.every((option) => option.selected),
2540
+ disabled: enabledItems.length === 0
2541
+ };
2542
+ });
2543
+ });
2544
+ const label = (0, import_vue2.computed)(() => {
2545
+ if (!state.selectedItems.value.length) {
2546
+ return state.settings.value.text || "Select";
2547
+ }
2548
+ return state.selectedItems.value.map((item) => getLabel2(item, state.settings.value)).join(", ");
2549
+ });
2550
+ const open = () => {
2551
+ isOpen.value = true;
2552
+ if (!activeKey.value && visibleOptions.value[0]) {
2553
+ activeKey.value = visibleOptions.value[0].key;
2554
+ }
2555
+ };
2556
+ const close = () => {
2557
+ isOpen.value = false;
2558
+ };
2559
+ const toggleOpen = () => isOpen.value ? close() : open();
2560
+ const focusNext = (direction) => {
2561
+ const optionsList = visibleOptions.value.filter((option) => !option.disabled);
2562
+ const currentIndex = Math.max(0, optionsList.findIndex((option) => option.key === activeKey.value));
2563
+ const next = optionsList[Math.min(Math.max(currentIndex + direction, 0), optionsList.length - 1)];
2564
+ if (next) {
2565
+ activeKey.value = next.key;
2566
+ }
2567
+ };
2568
+ const getRootProps = (props = {}) => mergePropBags({
2569
+ "data-stackline-multiselect": "true",
2570
+ "data-open": isOpen.value ? "true" : "false"
2571
+ }, props);
2572
+ const getTriggerProps = (props = {}) => mergePropBags({
2573
+ type: "button",
2574
+ role: "combobox",
2575
+ "aria-expanded": isOpen.value ? "true" : "false",
2576
+ "aria-haspopup": "listbox",
2577
+ "aria-controls": listboxId,
2578
+ "aria-activedescendant": activeDescendantId.value,
2579
+ onClick: toggleOpen,
2580
+ onKeydown: (event) => {
2581
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
2582
+ event.preventDefault();
2583
+ toggleOpen();
2584
+ }
2585
+ if (event.key === "ArrowDown") {
2586
+ event.preventDefault();
2587
+ open();
2588
+ focusNext(1);
2589
+ }
2590
+ if (event.key === "ArrowUp") {
2591
+ event.preventDefault();
2592
+ open();
2593
+ focusNext(-1);
2594
+ }
2595
+ if (event.key === "Escape") {
2596
+ close();
2597
+ }
2598
+ }
2599
+ }, props);
2600
+ const getSearchInputProps = (props = {}) => mergePropBags({
2601
+ value: query.value,
2602
+ "aria-label": "Search options",
2603
+ onInput: (event) => {
2604
+ query.value = String(event.target.value || "");
2605
+ state.setFilter(query.value);
2606
+ },
2607
+ onKeydown: (event) => {
2608
+ if (event.key === "ArrowDown") {
2609
+ event.preventDefault();
2610
+ focusNext(1);
2611
+ }
2612
+ if (event.key === "ArrowUp") {
2613
+ event.preventDefault();
2614
+ focusNext(-1);
2615
+ }
2616
+ if (event.key === "Escape") {
2617
+ close();
2618
+ }
2619
+ }
2620
+ }, props);
2621
+ const getListboxProps = (props = {}) => mergePropBags({
2622
+ id: listboxId,
2623
+ role: "listbox",
2624
+ "aria-multiselectable": state.settings.value.singleSelection ? "false" : "true"
2625
+ }, props);
2626
+ const getOptionProps = (option, props = {}) => mergePropBags({
2627
+ id: `${instanceId}-option-${option.key}`,
2628
+ role: "option",
2629
+ tabindex: option.disabled ? -1 : 0,
2630
+ "aria-selected": option.selected ? "true" : "false",
2631
+ "aria-checked": option.selected ? "true" : "false",
2632
+ "aria-disabled": option.disabled ? "true" : "false",
2633
+ onMouseenter: () => {
2634
+ activeKey.value = option.key;
2635
+ },
2636
+ onClick: () => {
2637
+ if (!option.disabled) {
2638
+ state.toggleItem(option.item);
2639
+ activeKey.value = option.key;
2640
+ }
2641
+ },
2642
+ onKeydown: (event) => {
2643
+ if (event.key === "ArrowDown") {
2644
+ event.preventDefault();
2645
+ focusNext(1);
2646
+ }
2647
+ if (event.key === "ArrowUp") {
2648
+ event.preventDefault();
2649
+ focusNext(-1);
2650
+ }
2651
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
2652
+ event.preventDefault();
2653
+ state.toggleItem(option.item);
2654
+ activeKey.value = option.key;
2655
+ }
2656
+ if (event.key === "Escape") {
2657
+ close();
2658
+ }
2659
+ }
2660
+ }, props);
2661
+ const getClearAllButtonProps = (props = {}) => mergePropBags({
2662
+ type: "button",
2663
+ "aria-label": "Clear selected options",
2664
+ onClick: state.clearSelection
2665
+ }, props);
2666
+ const getRemoveButtonProps = (item, props = {}) => mergePropBags({
2667
+ type: "button",
2668
+ "aria-label": `Remove ${getLabel2(item, state.settings.value)}`,
2669
+ onClick: () => {
2670
+ state.removeItem(item);
2671
+ },
2672
+ onKeydown: (event) => {
2673
+ if (event.key === "Backspace" || event.key === "Delete") {
2674
+ event.preventDefault();
2675
+ state.removeItem(item);
2676
+ }
2677
+ }
2678
+ }, props);
2679
+ return {
2680
+ isOpen,
2681
+ query,
2682
+ filter: state.filter,
2683
+ activeKey,
2684
+ activeDescendantId,
2685
+ listboxId,
2686
+ settings: state.settings,
2687
+ filteredItems: state.filteredItems,
2688
+ selectableItems: state.selectableItems,
2689
+ selectedItems: state.selectedItems,
2690
+ visibleBadges: state.visibleBadges,
2691
+ hiddenBadgeCount: state.hiddenBadgeCount,
2692
+ visibleOptions,
2693
+ groups,
2694
+ allFilteredSelected: state.allFilteredSelected,
2695
+ label,
2696
+ open,
2697
+ close,
2698
+ toggleOpen,
2699
+ clearSelection: state.clearSelection,
2700
+ selectAll: state.selectAll,
2701
+ deSelectAll: state.deSelectAll,
2702
+ toggleGroup: state.toggleGroup,
2703
+ toggleItem: state.toggleItem,
2704
+ selectItem: state.selectItem,
2705
+ removeItem: state.removeItem,
2706
+ removeLastSelectedItem: state.removeLastSelectedItem,
2707
+ isSelected: state.isSelected,
2708
+ setFilter: (value) => {
2709
+ query.value = value;
2710
+ state.setFilter(value);
2711
+ },
2712
+ getItemKey: state.getItemKey,
2713
+ getItemLabel: state.getItemLabel,
2714
+ getRootProps,
2715
+ getTriggerProps,
2716
+ getSearchInputProps,
2717
+ getListboxProps,
2718
+ getOptionProps,
2719
+ getClearAllButtonProps,
2720
+ getRemoveButtonProps
2721
+ };
2722
+ }
2723
+ function createVueMultiselectDropdown() {
2724
+ return {
2725
+ defineSettings: (settings) => defineSettings(settings),
2726
+ defineSlots,
2727
+ useMultiSelectState: (options = {}) => useMultiSelectState(options),
2728
+ useMultiSelectDropdown: (options = {}) => useMultiSelectDropdown(options)
2729
+ };
2730
+ }
2731
+
2141
2732
  // src/plugin.ts
2142
2733
  var VueMultiselect = {
2143
2734
  install(app) {
@@ -2152,5 +2743,10 @@ var plugin_default = VueMultiselect;
2152
2743
  0 && (module.exports = {
2153
2744
  StacklineVueMultiselect,
2154
2745
  VueMultiselect,
2155
- VueMultiselectDropdown
2746
+ VueMultiselectDropdown,
2747
+ createVueMultiselectDropdown,
2748
+ defineSettings,
2749
+ defineSlots,
2750
+ useMultiSelectDropdown,
2751
+ useMultiSelectState
2156
2752
  });