@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.js CHANGED
@@ -80,6 +80,7 @@ var styles = `
80
80
  display: flex;
81
81
  flex: 1 1 auto;
82
82
  min-width: 0;
83
+ min-height: 1.45em;
83
84
  align-items: center;
84
85
  align-content: center;
85
86
  gap: 8px;
@@ -93,6 +94,7 @@ var styles = `
93
94
  align-self: center;
94
95
  justify-content: flex-start;
95
96
  min-width: 0;
97
+ min-height: 1.45em;
96
98
  max-width: 100%;
97
99
  color: var(--vmsd-muted);
98
100
  font-size: 0.95rem;
@@ -176,33 +178,25 @@ var styles = `
176
178
  display: inline-flex;
177
179
  align-items: center;
178
180
  justify-content: center;
179
- min-width: 0;
180
- min-height: 0;
181
+ flex: 0 0 auto;
182
+ min-width: 24px;
183
+ min-height: 20px;
181
184
  color: var(--vmsd-muted);
182
185
  font-size: 0.8rem;
183
186
  font-weight: 600;
187
+ line-height: 1;
188
+ white-space: nowrap;
189
+ text-align: center;
184
190
  }
185
191
 
186
- .vmsd-root.vmsd-has-overflow:not(.skin-classic) .vmsd-trigger {
192
+ .vmsd-root.vmsd-has-overflow .vmsd-trigger {
187
193
  padding-right: 104px;
188
194
  }
189
195
 
190
- .vmsd-root.vmsd-has-overflow:not(.skin-classic):not(.vmsd-has-clear) .vmsd-trigger {
196
+ .vmsd-root.vmsd-has-overflow:not(.vmsd-has-clear) .vmsd-trigger {
191
197
  padding-right: 74px;
192
198
  }
193
199
 
194
- .vmsd-root.vmsd-has-overflow:not(.skin-classic) .vmsd-overflow {
195
- position: absolute;
196
- top: 50%;
197
- right: 76px;
198
- transform: translateY(-50%);
199
- z-index: 1;
200
- }
201
-
202
- .vmsd-root.vmsd-has-overflow:not(.skin-classic):not(.vmsd-has-clear) .vmsd-overflow {
203
- right: 42px;
204
- }
205
-
206
200
  .vmsd-actions {
207
201
  position: absolute;
208
202
  top: 50%;
@@ -479,9 +473,6 @@ var styles = `
479
473
  .vmsd-checkbox {
480
474
  position: relative;
481
475
  box-sizing: content-box;
482
- display: inline-grid;
483
- place-items: center;
484
- align-self: center;
485
476
  flex: 0 0 auto;
486
477
  width: 18px;
487
478
  height: 18px;
@@ -489,8 +480,6 @@ var styles = `
489
480
  border: 2px solid var(--vmsd-border-strong);
490
481
  border-radius: 6px;
491
482
  background-color: var(--vmsd-bg);
492
- line-height: 0;
493
- vertical-align: middle;
494
483
  }
495
484
 
496
485
  .vmsd-checkbox[data-checked="true"] {
@@ -510,7 +499,7 @@ var styles = `
510
499
  border-color: #fff;
511
500
  border-style: solid;
512
501
  border-width: 0 0 2px 2px;
513
- transform: translate(-50%, -62%) rotate(-45deg);
502
+ transform: translate(-50%, -58%) rotate(-45deg);
514
503
  transform-origin: 50%;
515
504
  }
516
505
 
@@ -772,9 +761,12 @@ var styles = `
772
761
 
773
762
  .theme-classic .vmsd-overflow,
774
763
  .skin-classic .vmsd-overflow {
764
+ min-width: 24px;
765
+ min-height: 20px;
775
766
  color: #333333;
776
767
  font-size: 14px;
777
768
  font-weight: 400;
769
+ line-height: 1;
778
770
  }
779
771
 
780
772
  .theme-classic .vmsd-actions,
@@ -1009,7 +1001,7 @@ var styles = `
1009
1001
  height: 3px;
1010
1002
  margin-top: 0;
1011
1003
  border-width: 0 0 3px 3px;
1012
- transform: translate(-50%, -62%) rotate(-45deg);
1004
+ transform: translate(-50%, -58%) rotate(-45deg);
1013
1005
  }
1014
1006
 
1015
1007
  .theme-classic .vmsd-option-label,
@@ -1042,12 +1034,10 @@ var styles = `
1042
1034
 
1043
1035
  @media (max-width: 720px) {
1044
1036
  .vmsd-trigger {
1045
- align-items: flex-start;
1037
+ align-items: center;
1046
1038
  padding-right: 54px;
1047
1039
  }
1048
1040
  }
1049
-
1050
- /* stackline-vue3-live-20260527 */
1051
1041
  `;
1052
1042
  function ensureDropdownStyles() {
1053
1043
  if (typeof document === "undefined") {
@@ -1108,7 +1098,16 @@ var DEFAULT_SETTINGS = {
1108
1098
  removeItemAriaLabel: "Remove selected option",
1109
1099
  openDropdownAriaLabel: "Open dropdown",
1110
1100
  closeDropdownAriaLabel: "Close dropdown",
1111
- loadingText: "Loading options"
1101
+ loadingText: "Loading options",
1102
+ keyboard: {
1103
+ space: true,
1104
+ spaceOptionAction: "toggle",
1105
+ tab: true,
1106
+ arrows: true,
1107
+ escape: true,
1108
+ backspaceRemovesLastWhenSearchEmpty: false,
1109
+ deleteRemovesFocusedBadge: true
1110
+ }
1112
1111
  };
1113
1112
  function iconPath(name) {
1114
1113
  if (name === "remove") {
@@ -1247,9 +1246,15 @@ function buildGroups(items, settings) {
1247
1246
  }
1248
1247
  function mergeUniqueItems(base, extra, settings) {
1249
1248
  const bucket = /* @__PURE__ */ new Map();
1250
- for (const item of base.concat(extra)) {
1249
+ for (const item of base) {
1251
1250
  bucket.set(getPrimaryValue(item, settings), item);
1252
1251
  }
1252
+ for (const item of extra) {
1253
+ const key = getPrimaryValue(item, settings);
1254
+ if (!bucket.has(key)) {
1255
+ bucket.set(key, item);
1256
+ }
1257
+ }
1253
1258
  return Array.from(bucket.values());
1254
1259
  }
1255
1260
  function callRenderFunction(renderFunction, h2, item, context) {
@@ -1258,6 +1263,10 @@ function callRenderFunction(renderFunction, h2, item, context) {
1258
1263
  }
1259
1264
  return renderFunction(item, context, h2);
1260
1265
  }
1266
+ function callSlot(vm, name, props) {
1267
+ const slot = vm.$slots && vm.$slots[name];
1268
+ return typeof slot === "function" ? slot(props) : null;
1269
+ }
1261
1270
  function escapeSelectorValue(value) {
1262
1271
  if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
1263
1272
  return CSS.escape(value);
@@ -1267,6 +1276,9 @@ function escapeSelectorValue(value) {
1267
1276
  function isActivationKey(event) {
1268
1277
  return event.key === "Enter" || event.key === " " || event.key === "Spacebar";
1269
1278
  }
1279
+ function isSpaceKey(event) {
1280
+ return event.key === " " || event.key === "Spacebar";
1281
+ }
1270
1282
  function isTextInputTarget(target) {
1271
1283
  const element = target;
1272
1284
  if (!element) {
@@ -1350,10 +1362,21 @@ var VueMultiselectDropdown = {
1350
1362
  },
1351
1363
  computed: {
1352
1364
  resolvedSettings() {
1365
+ const userSettings = this.settings || {};
1353
1366
  const merged = {
1354
1367
  ...DEFAULT_SETTINGS,
1355
- ...this.settings || {}
1368
+ ...userSettings,
1369
+ keyboard: {
1370
+ ...DEFAULT_SETTINGS.keyboard,
1371
+ ...userSettings.keyboard || {}
1372
+ }
1356
1373
  };
1374
+ if (typeof userSettings.escapeToClose === "boolean" && userSettings.keyboard?.escape == null) {
1375
+ merged.keyboard.escape = userSettings.escapeToClose;
1376
+ }
1377
+ if (typeof merged.keyboard.backspace === "boolean") {
1378
+ merged.keyboard.backspaceRemovesLastWhenSearchEmpty = merged.keyboard.backspace;
1379
+ }
1357
1380
  const skin = merged.skin || merged.theme || "classic";
1358
1381
  return {
1359
1382
  ...merged,
@@ -1447,6 +1470,9 @@ var VueMultiselectDropdown = {
1447
1470
  getKey(item) {
1448
1471
  return getPrimaryValue(item, this.resolvedSettings);
1449
1472
  },
1473
+ getOptionId(key) {
1474
+ return `${this.instanceId}-option-${key}`;
1475
+ },
1450
1476
  isSelected(item) {
1451
1477
  const key = this.getKey(item);
1452
1478
  return this.selected.some((selectedItem) => this.getKey(selectedItem) === key);
@@ -1579,6 +1605,13 @@ var VueMultiselectDropdown = {
1579
1605
  this.emitSelection(next);
1580
1606
  this.$emit("de-select", item);
1581
1607
  },
1608
+ removeLastSelected() {
1609
+ const last = this.selected[this.selected.length - 1];
1610
+ if (!last) {
1611
+ return;
1612
+ }
1613
+ this.removeItem(last);
1614
+ },
1582
1615
  addFilterItem(event) {
1583
1616
  event.preventDefault();
1584
1617
  event.stopPropagation();
@@ -1595,35 +1628,51 @@ var VueMultiselectDropdown = {
1595
1628
  this.query = "";
1596
1629
  },
1597
1630
  onTriggerKeydown(event) {
1598
- if (isActivationKey(event)) {
1631
+ const keyboard = this.resolvedSettings.keyboard;
1632
+ if (event.key === "Enter" || isSpaceKey(event) && keyboard.space !== false) {
1599
1633
  event.preventDefault();
1600
1634
  this.toggleDropdown();
1601
1635
  return;
1602
1636
  }
1603
- if (event.key === "ArrowDown") {
1637
+ if (keyboard.arrows !== false && event.key === "ArrowDown") {
1604
1638
  event.preventDefault();
1605
1639
  this.openDropdown();
1606
1640
  this.focusFirstOption();
1607
1641
  return;
1608
1642
  }
1609
- if (event.key === "ArrowUp") {
1643
+ if (keyboard.arrows !== false && event.key === "ArrowUp") {
1610
1644
  event.preventDefault();
1611
1645
  this.openDropdown();
1612
1646
  this.focusLastOption();
1613
1647
  return;
1614
1648
  }
1615
- if (event.key === "Escape") {
1649
+ if (keyboard.escape !== false && event.key === "Escape") {
1616
1650
  this.closeDropdown();
1617
1651
  }
1618
1652
  },
1619
1653
  onListKeydown(event) {
1620
- if (event.key === "Escape") {
1654
+ const keyboard = this.resolvedSettings.keyboard;
1655
+ if (keyboard.escape !== false && event.key === "Escape") {
1621
1656
  event.preventDefault();
1622
1657
  event.stopPropagation();
1623
1658
  this.closeDropdown();
1624
1659
  return;
1625
1660
  }
1626
- if (isTextInputTarget(event.target) && event.key !== "ArrowDown") {
1661
+ if (event.key === "Tab") {
1662
+ return;
1663
+ }
1664
+ if (isTextInputTarget(event.target)) {
1665
+ if (event.key === "Backspace" && !this.query && keyboard.backspaceRemovesLastWhenSearchEmpty === true) {
1666
+ event.preventDefault();
1667
+ event.stopPropagation();
1668
+ this.removeLastSelected();
1669
+ return;
1670
+ }
1671
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1672
+ return;
1673
+ }
1674
+ }
1675
+ if (keyboard.arrows === false && ["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1627
1676
  return;
1628
1677
  }
1629
1678
  const selectable = this.visibleSelectableItems();
@@ -1658,10 +1707,22 @@ var VueMultiselectDropdown = {
1658
1707
  return;
1659
1708
  }
1660
1709
  if (isActivationKey(event)) {
1710
+ if (isSpaceKey(event) && keyboard.space === false) {
1711
+ return;
1712
+ }
1661
1713
  event.preventDefault();
1662
1714
  event.stopPropagation();
1663
1715
  const activeItem = selectable.find((item) => this.getKey(item) === this.focusedKey) || selectable[0];
1664
1716
  this.toggleItem(activeItem);
1717
+ if (keyboard.spaceOptionAction === "toggle-and-next") {
1718
+ const activeIndex = selectable.findIndex((item) => this.getKey(item) === this.getKey(activeItem));
1719
+ const nextItem = selectable[Math.min(activeIndex + 1, selectable.length - 1)];
1720
+ if (nextItem) {
1721
+ this.focusOption(nextItem);
1722
+ }
1723
+ } else {
1724
+ this.focusOption(activeItem);
1725
+ }
1665
1726
  return;
1666
1727
  }
1667
1728
  },
@@ -1698,19 +1759,26 @@ var VueMultiselectDropdown = {
1698
1759
  if (isActivationKey(event)) {
1699
1760
  event.stopPropagation();
1700
1761
  }
1701
- if (event.key === "ArrowDown") {
1762
+ if (this.resolvedSettings.keyboard.arrows !== false && event.key === "ArrowDown") {
1702
1763
  event.preventDefault();
1703
1764
  event.stopPropagation();
1704
1765
  this.openDropdown();
1705
1766
  this.focusFirstOption();
1706
1767
  }
1707
- if (event.key === "ArrowUp") {
1768
+ if (this.resolvedSettings.keyboard.arrows !== false && event.key === "ArrowUp") {
1708
1769
  event.preventDefault();
1709
1770
  event.stopPropagation();
1710
1771
  this.openDropdown();
1711
1772
  this.focusLastOption();
1712
1773
  }
1713
1774
  },
1775
+ onRemoveButtonKeydown(item, event) {
1776
+ if (this.resolvedSettings.keyboard.deleteRemovesFocusedBadge !== false && (event.key === "Backspace" || event.key === "Delete")) {
1777
+ this.removeItem(item, event);
1778
+ return;
1779
+ }
1780
+ this.onInlineKeydown(event);
1781
+ },
1714
1782
  onTriggerClick(event) {
1715
1783
  const target = event.target;
1716
1784
  if (target && target.closest("button")) {
@@ -1731,7 +1799,7 @@ var VueMultiselectDropdown = {
1731
1799
  this.closeDropdown();
1732
1800
  },
1733
1801
  onDocumentKeydown(event) {
1734
- if (this.isOpen && this.resolvedSettings.escapeToClose && event.key === "Escape") {
1802
+ if (this.isOpen && this.resolvedSettings.keyboard.escape !== false && event.key === "Escape") {
1735
1803
  this.closeDropdown();
1736
1804
  }
1737
1805
  },
@@ -1849,11 +1917,14 @@ var VueMultiselectDropdown = {
1849
1917
  label,
1850
1918
  selected: true,
1851
1919
  disabled: false,
1920
+ key: this.getKey(item),
1921
+ ariaSelected: "true",
1922
+ ariaChecked: "true",
1852
1923
  query: this.query,
1853
1924
  toggle: () => this.toggleItem(item),
1854
1925
  remove: () => this.removeItem(item)
1855
1926
  };
1856
- const rendered = callRenderFunction(this.renderBadge, h, item, context);
1927
+ const rendered = callSlot(this, "badge", context) || callRenderFunction(this.renderBadge, h, item, context);
1857
1928
  const removeLabel = typeof settings.removeItemAriaLabel === "function" ? settings.removeItemAriaLabel(item) : `${settings.removeItemAriaLabel}: ${label}`;
1858
1929
  return h("span", { class: "vmsd-badge", key: this.getKey(item) }, [
1859
1930
  h("span", { class: "vmsd-badge-label" }, [rendered || label]),
@@ -1864,17 +1935,18 @@ var VueMultiselectDropdown = {
1864
1935
  attrs: { type: "button", "aria-label": removeLabel },
1865
1936
  on: {
1866
1937
  click: (event) => this.removeItem(item, event),
1867
- keydown: this.onInlineKeydown
1938
+ keydown: (event) => this.onRemoveButtonKeydown(item, event)
1868
1939
  }
1869
1940
  },
1870
1941
  [renderIcon(h, "remove")]
1871
1942
  )
1872
1943
  ]);
1873
1944
  });
1874
- const valueContent = hasSelection ? [h("span", { class: "vmsd-badge-list" }, badges)] : [h("span", { class: "vmsd-placeholder" }, [settings.text])];
1875
- if (hiddenCount > 0) {
1876
- valueContent.push(h("span", { class: "vmsd-overflow", attrs: { "aria-label": `${hiddenCount} more selected options` } }, [`+${hiddenCount}`]));
1877
- }
1945
+ const valueContent = hasSelection ? [h("span", { class: "vmsd-badge-list" }, badges)] : [
1946
+ h("span", { class: "vmsd-placeholder" }, [
1947
+ callSlot(this, "placeholder", { label: settings.text, state: { isOpen: this.isOpen, query: this.query } }) || settings.text
1948
+ ])
1949
+ ];
1878
1950
  const trigger = h(
1879
1951
  "div",
1880
1952
  {
@@ -1887,7 +1959,8 @@ var VueMultiselectDropdown = {
1887
1959
  "aria-expanded": this.isOpen ? "true" : "false",
1888
1960
  "aria-haspopup": "listbox",
1889
1961
  "aria-disabled": settings.disabled ? "true" : "false",
1890
- "aria-controls": `${this.instanceId}-listbox`
1962
+ "aria-controls": `${this.instanceId}-listbox`,
1963
+ "aria-activedescendant": this.focusedKey ? this.getOptionId(this.focusedKey) : void 0
1891
1964
  },
1892
1965
  on: {
1893
1966
  click: this.onTriggerClick,
@@ -1895,8 +1968,15 @@ var VueMultiselectDropdown = {
1895
1968
  }
1896
1969
  },
1897
1970
  [
1898
- h("div", { class: "vmsd-value" }, valueContent),
1971
+ callSlot(this, "trigger", {
1972
+ selected: this.selected,
1973
+ label: hasSelection ? this.selected.map((item) => this.getLabel(item)).join(", ") : settings.text,
1974
+ isOpen: this.isOpen,
1975
+ clearSelection: this.clearSelection,
1976
+ toggleDropdown: this.toggleDropdown
1977
+ }) || h("div", { class: "vmsd-value" }, valueContent),
1899
1978
  h("div", { class: "vmsd-actions" }, [
1979
+ hiddenCount > 0 ? h("span", { class: "vmsd-overflow", attrs: { "aria-label": `${hiddenCount} more selected options` } }, [`+${hiddenCount}`]) : null,
1900
1980
  hasClear ? h(
1901
1981
  "button",
1902
1982
  {
@@ -1995,28 +2075,37 @@ var VueMultiselectDropdown = {
1995
2075
  label,
1996
2076
  selected,
1997
2077
  disabled,
2078
+ index: this.filteredItems.findIndex((candidate) => this.getKey(candidate) === key),
2079
+ group: getGroupName(item, settings),
2080
+ key,
2081
+ optionId: this.getOptionId(key),
2082
+ ariaSelected: selected ? "true" : "false",
2083
+ ariaChecked: selected ? "true" : "false",
1998
2084
  query: this.query,
1999
2085
  toggle: () => this.toggleItem(item),
2000
2086
  remove: () => this.removeItem(item)
2001
2087
  };
2002
- const rendered = callRenderFunction(this.renderItem, h, item, context);
2088
+ const rendered = callSlot(this, "option", context) || callRenderFunction(this.renderItem, h, item, context);
2003
2089
  return h(
2004
2090
  "div",
2005
2091
  {
2006
2092
  key,
2007
2093
  class: ["vmsd-option", selected ? "vmsd-selected" : "", disabled ? "vmsd-disabled" : ""],
2008
2094
  attrs: {
2095
+ id: this.getOptionId(key),
2009
2096
  role: "option",
2010
2097
  tabindex: disabled ? "-1" : "0",
2011
2098
  "data-vmsd-option": "true",
2012
2099
  "data-vmsd-key": key,
2013
2100
  "aria-disabled": disabled ? "true" : "false",
2014
- "aria-selected": selected ? "true" : "false"
2101
+ "aria-selected": selected ? "true" : "false",
2102
+ "aria-checked": selected ? "true" : "false"
2015
2103
  },
2016
2104
  on: {
2017
2105
  click: (event) => {
2018
2106
  if (!disabled) {
2019
2107
  this.toggleItem(item, event);
2108
+ this.focusOption(item);
2020
2109
  }
2021
2110
  },
2022
2111
  focus: () => this.focusedKey = key,
@@ -2033,9 +2122,13 @@ var VueMultiselectDropdown = {
2033
2122
  ]
2034
2123
  );
2035
2124
  };
2036
- const listChildren = settings.loading ? [h("div", { class: "vmsd-state", attrs: { role: "status" } }, [settings.loadingText])] : settings.groupBy ? this.groupedItems.map(
2125
+ 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(
2037
2126
  (group) => h("div", { class: "vmsd-group", key: group.name, attrs: { role: "group", "aria-label": group.name } }, [
2038
- h("div", { class: "vmsd-group-header" }, [
2127
+ callSlot(this, "group-header", {
2128
+ group,
2129
+ selected: group.items.filter((item) => !isDisabledItem(item)).every((item) => this.isSelected(item)),
2130
+ selectGroup: (event) => this.selectGroup(group.name, group.items, event)
2131
+ }) || h("div", { class: "vmsd-group-header" }, [
2039
2132
  h("span", [`${group.name} \xB7 ${group.items.length}`]),
2040
2133
  settings.selectGroup ? h(
2041
2134
  "button",
@@ -2050,8 +2143,8 @@ var VueMultiselectDropdown = {
2050
2143
  group.items.map(renderOption)
2051
2144
  ])
2052
2145
  ) : this.filteredItems.map(renderOption);
2053
- if (!this.filteredItems.length && !settings.loading) {
2054
- const emptyContent = this.renderEmptyState ? this.renderEmptyState(this.query, h) : settings.noDataLabel;
2146
+ if (this.isOpen && !this.filteredItems.length && !settings.loading) {
2147
+ const emptyContent = callSlot(this, "empty", { query: this.query, label: settings.noDataLabel }) || (this.renderEmptyState ? this.renderEmptyState(this.query, h) : settings.noDataLabel);
2055
2148
  listChildren.push(h("div", { class: "vmsd-state" }, [emptyContent]));
2056
2149
  }
2057
2150
  const menu = h(
@@ -2081,7 +2174,8 @@ var VueMultiselectDropdown = {
2081
2174
  on: { scroll: this.onListScroll }
2082
2175
  },
2083
2176
  listChildren
2084
- )
2177
+ ),
2178
+ this.isOpen ? callSlot(this, "menu-footer", { selected: this.selected, filteredItems: this.filteredItems, close: this.closeDropdown }) : null
2085
2179
  ]
2086
2180
  );
2087
2181
  return h(
@@ -2109,6 +2203,498 @@ var VueMultiselectDropdown = {
2109
2203
  };
2110
2204
  var StacklineVueMultiselect = VueMultiselectDropdown;
2111
2205
 
2206
+ // src/composables.ts
2207
+ import { computed, ref, unref } from "vue";
2208
+ var DEFAULT_KEYS = {
2209
+ labelKey: "itemName",
2210
+ primaryKey: "id"
2211
+ };
2212
+ function read(value, fallback) {
2213
+ return value == null ? fallback : unref(value);
2214
+ }
2215
+ function isPrimitiveItem2(item) {
2216
+ return typeof item === "string" || typeof item === "number" || typeof item === "boolean";
2217
+ }
2218
+ function resolveSettings(settings) {
2219
+ const value = read(settings, {});
2220
+ return {
2221
+ ...value,
2222
+ text: value.text || "Select",
2223
+ singleSelection: Boolean(value.singleSelection),
2224
+ labelKey: value.labelKey || DEFAULT_KEYS.labelKey,
2225
+ primaryKey: value.primaryKey || DEFAULT_KEYS.primaryKey,
2226
+ searchBy: Array.isArray(value.searchBy) ? value.searchBy : [],
2227
+ groupBy: value.groupBy || ""
2228
+ };
2229
+ }
2230
+ function getLabel2(item, settings) {
2231
+ if (isPrimitiveItem2(item)) {
2232
+ return String(item);
2233
+ }
2234
+ const labelKey = settings.labelKey || DEFAULT_KEYS.labelKey;
2235
+ const keys = [labelKey, "itemName", "name", "label", "title", "value"];
2236
+ for (const key of keys) {
2237
+ if (item[key] != null) {
2238
+ return String(item[key]);
2239
+ }
2240
+ }
2241
+ return JSON.stringify(item);
2242
+ }
2243
+ function getKey(item, settings) {
2244
+ if (isPrimitiveItem2(item)) {
2245
+ return String(item);
2246
+ }
2247
+ const primaryKey = settings.primaryKey || DEFAULT_KEYS.primaryKey;
2248
+ const keys = [primaryKey, "id", "value", "key"];
2249
+ for (const key of keys) {
2250
+ if (item[key] != null) {
2251
+ return String(item[key]);
2252
+ }
2253
+ }
2254
+ return getLabel2(item, settings);
2255
+ }
2256
+ function getGroup(item, settings) {
2257
+ if (!settings.groupBy) {
2258
+ return void 0;
2259
+ }
2260
+ if (typeof settings.groupBy === "function") {
2261
+ return settings.groupBy(item);
2262
+ }
2263
+ return !isPrimitiveItem2(item) && item[settings.groupBy] != null ? String(item[settings.groupBy]) : void 0;
2264
+ }
2265
+ function isDisabled(item) {
2266
+ return !isPrimitiveItem2(item) && Boolean(item.disabled);
2267
+ }
2268
+ function matchesQuery(item, query, settings) {
2269
+ const needle = query.trim().toLowerCase();
2270
+ if (!needle) {
2271
+ return true;
2272
+ }
2273
+ const values = /* @__PURE__ */ new Set([getLabel2(item, settings).toLowerCase()]);
2274
+ if (!isPrimitiveItem2(item)) {
2275
+ const keys = settings.searchBy && settings.searchBy.length ? settings.searchBy : [settings.labelKey || DEFAULT_KEYS.labelKey];
2276
+ for (const key of keys) {
2277
+ if (key && item[key] != null) {
2278
+ values.add(String(item[key]).toLowerCase());
2279
+ }
2280
+ }
2281
+ }
2282
+ return Array.from(values).some((value) => value.includes(needle));
2283
+ }
2284
+ function mergeSelected(selected, incoming, settings) {
2285
+ const byKey = /* @__PURE__ */ new Map();
2286
+ for (const item of selected) {
2287
+ byKey.set(getKey(item, settings), item);
2288
+ }
2289
+ for (const item of incoming) {
2290
+ const key = getKey(item, settings);
2291
+ if (!byKey.has(key)) {
2292
+ byKey.set(key, item);
2293
+ }
2294
+ }
2295
+ return Array.from(byKey.values());
2296
+ }
2297
+ function readBadgeLimit(selected, settings) {
2298
+ if (settings.singleSelection) {
2299
+ return selected.length;
2300
+ }
2301
+ const value = Number(settings.badgeShowLimit);
2302
+ if (!Number.isFinite(value) || value <= 0) {
2303
+ return selected.length;
2304
+ }
2305
+ return Math.floor(value);
2306
+ }
2307
+ function normalizeExtraProps(props = {}) {
2308
+ const normalized = { ...props };
2309
+ if (normalized.className && !normalized.class) {
2310
+ normalized.class = normalized.className;
2311
+ }
2312
+ delete normalized.className;
2313
+ return normalized;
2314
+ }
2315
+ function mergePropBags(base, extra = {}) {
2316
+ const incoming = normalizeExtraProps(extra);
2317
+ const merged = { ...incoming, ...base };
2318
+ if (base.class || incoming.class) {
2319
+ merged.class = [incoming.class, base.class].filter(Boolean).join(" ");
2320
+ }
2321
+ if (base.style || incoming.style) {
2322
+ merged.style = {
2323
+ ...typeof incoming.style === "object" ? incoming.style : {},
2324
+ ...typeof base.style === "object" ? base.style : {}
2325
+ };
2326
+ }
2327
+ for (const key of Object.keys(base)) {
2328
+ if (/^on[A-Z]/.test(key) && typeof base[key] === "function" && typeof incoming[key] === "function") {
2329
+ merged[key] = (...args) => {
2330
+ incoming[key](...args);
2331
+ base[key](...args);
2332
+ };
2333
+ }
2334
+ }
2335
+ return merged;
2336
+ }
2337
+ function defineSettings(settings) {
2338
+ return settings;
2339
+ }
2340
+ function defineSlots(slots) {
2341
+ return slots;
2342
+ }
2343
+ function useMultiSelectState(options = {}) {
2344
+ const internalSelected = ref(options.defaultSelectedItems ? options.defaultSelectedItems.slice() : []);
2345
+ const filter = ref("");
2346
+ const settings = computed(() => resolveSettings(options.settings));
2347
+ const data = computed(() => read(options.data, []));
2348
+ const selectedItems = computed(() => read(options.selectedItems, internalSelected.value));
2349
+ const filteredItems = computed(() => data.value.filter((item) => matchesQuery(item, filter.value, settings.value)));
2350
+ const isSelected = (item) => {
2351
+ const key = getKey(item, settings.value);
2352
+ return selectedItems.value.some((selected) => getKey(selected, settings.value) === key);
2353
+ };
2354
+ const commit = (next) => {
2355
+ if (!options.selectedItems) {
2356
+ internalSelected.value = next;
2357
+ }
2358
+ options.onChange?.(next);
2359
+ };
2360
+ const getItemKey = (item) => getKey(item, settings.value);
2361
+ const getItemLabel = (item) => getLabel2(item, settings.value);
2362
+ const selectableItems = computed(() => filteredItems.value.filter((item) => !isDisabled(item)));
2363
+ const visibleBadges = computed(() => {
2364
+ const limit = readBadgeLimit(selectedItems.value, settings.value);
2365
+ return selectedItems.value.slice(0, limit);
2366
+ });
2367
+ const hiddenBadgeCount = computed(() => Math.max(selectedItems.value.length - visibleBadges.value.length, 0));
2368
+ const allFilteredSelected = computed(() => selectableItems.value.length > 0 && selectableItems.value.every((item) => isSelected(item)));
2369
+ const removeItem = (item) => {
2370
+ const key = getItemKey(item);
2371
+ const next = selectedItems.value.filter((selected) => getItemKey(selected) !== key);
2372
+ commit(next);
2373
+ options.onDeSelect?.(item);
2374
+ };
2375
+ const addItem = (item) => {
2376
+ if (isDisabled(item)) {
2377
+ return;
2378
+ }
2379
+ if (settings.value.singleSelection) {
2380
+ commit([item]);
2381
+ options.onSelect?.(item);
2382
+ return;
2383
+ }
2384
+ if (!isSelected(item)) {
2385
+ commit(selectedItems.value.concat(item));
2386
+ options.onSelect?.(item);
2387
+ }
2388
+ };
2389
+ const toggleItem = (item) => {
2390
+ if (isDisabled(item)) {
2391
+ return;
2392
+ }
2393
+ if (isSelected(item)) {
2394
+ removeItem(item);
2395
+ return;
2396
+ }
2397
+ addItem(item);
2398
+ };
2399
+ const clearSelection = () => {
2400
+ const previous = selectedItems.value.slice();
2401
+ commit([]);
2402
+ options.onDeSelectAll?.(previous);
2403
+ };
2404
+ const deSelectAll = (items) => {
2405
+ if (!items || !items.length) {
2406
+ clearSelection();
2407
+ return;
2408
+ }
2409
+ const keys = new Set(items.map(getItemKey));
2410
+ commit(selectedItems.value.filter((item) => !keys.has(getItemKey(item))));
2411
+ options.onDeSelectAll?.(items);
2412
+ };
2413
+ const selectAll = (items) => {
2414
+ const enabled = (items || selectableItems.value).filter((item) => !isDisabled(item));
2415
+ const before = selectedItems.value.length;
2416
+ commit(settings.value.singleSelection ? enabled.slice(0, 1) : mergeSelected(selectedItems.value, enabled, settings.value));
2417
+ options.onSelectAll?.(enabled);
2418
+ if (!enabled.length && before !== selectedItems.value.length) {
2419
+ options.onChange?.(selectedItems.value);
2420
+ }
2421
+ };
2422
+ const toggleGroup = (groupName, items) => {
2423
+ const enabled = items.filter((item) => !isDisabled(item));
2424
+ if (!enabled.length) {
2425
+ return;
2426
+ }
2427
+ const everySelected = enabled.every((item) => isSelected(item));
2428
+ if (everySelected) {
2429
+ deSelectAll(enabled);
2430
+ options.onGroupDeSelect?.(groupName, enabled);
2431
+ return;
2432
+ }
2433
+ selectAll(enabled);
2434
+ options.onGroupSelect?.(groupName, enabled);
2435
+ };
2436
+ const removeLastSelectedItem = () => {
2437
+ const last = selectedItems.value[selectedItems.value.length - 1];
2438
+ if (last) {
2439
+ removeItem(last);
2440
+ }
2441
+ };
2442
+ return {
2443
+ data,
2444
+ settings,
2445
+ filter,
2446
+ filteredItems,
2447
+ selectableItems,
2448
+ selectedItems,
2449
+ visibleBadges,
2450
+ hiddenBadgeCount,
2451
+ allFilteredSelected,
2452
+ isSelected,
2453
+ getItemKey,
2454
+ getItemLabel,
2455
+ setFilter: (value) => {
2456
+ filter.value = value;
2457
+ },
2458
+ selectItem: addItem,
2459
+ removeItem,
2460
+ removeLastSelectedItem,
2461
+ toggleItem,
2462
+ clearSelection,
2463
+ selectAll,
2464
+ deSelectAll,
2465
+ toggleGroup
2466
+ };
2467
+ }
2468
+ function useMultiSelectDropdown(options = {}) {
2469
+ const state = useMultiSelectState(options);
2470
+ const isOpen = ref(false);
2471
+ const query = ref("");
2472
+ const activeKey = ref("");
2473
+ const instanceId = `stackline-vmsd-headless-${Math.random().toString(36).slice(2)}`;
2474
+ const listboxId = `${instanceId}-listbox`;
2475
+ const visibleOptions = computed(() => {
2476
+ let index = 0;
2477
+ return state.filteredItems.value.map((item) => {
2478
+ const key = getKey(item, state.settings.value);
2479
+ return {
2480
+ item,
2481
+ key,
2482
+ label: getLabel2(item, state.settings.value),
2483
+ selected: state.isSelected(item),
2484
+ disabled: isDisabled(item),
2485
+ group: getGroup(item, state.settings.value),
2486
+ index: index++
2487
+ };
2488
+ });
2489
+ });
2490
+ const activeDescendantId = computed(() => activeKey.value ? `${instanceId}-option-${activeKey.value}` : void 0);
2491
+ const groups = computed(() => {
2492
+ const byName = /* @__PURE__ */ new Map();
2493
+ for (const option of visibleOptions.value) {
2494
+ const name = option.group || "Ungrouped";
2495
+ const existing = byName.get(name) || [];
2496
+ existing.push(option);
2497
+ byName.set(name, existing);
2498
+ }
2499
+ return Array.from(byName.entries()).map(([name, items]) => {
2500
+ const enabledItems = items.filter((option) => !option.disabled);
2501
+ return {
2502
+ name,
2503
+ items,
2504
+ enabledItems,
2505
+ selected: enabledItems.length > 0 && enabledItems.every((option) => option.selected),
2506
+ disabled: enabledItems.length === 0
2507
+ };
2508
+ });
2509
+ });
2510
+ const label = computed(() => {
2511
+ if (!state.selectedItems.value.length) {
2512
+ return state.settings.value.text || "Select";
2513
+ }
2514
+ return state.selectedItems.value.map((item) => getLabel2(item, state.settings.value)).join(", ");
2515
+ });
2516
+ const open = () => {
2517
+ isOpen.value = true;
2518
+ if (!activeKey.value && visibleOptions.value[0]) {
2519
+ activeKey.value = visibleOptions.value[0].key;
2520
+ }
2521
+ };
2522
+ const close = () => {
2523
+ isOpen.value = false;
2524
+ };
2525
+ const toggleOpen = () => isOpen.value ? close() : open();
2526
+ const focusNext = (direction) => {
2527
+ const optionsList = visibleOptions.value.filter((option) => !option.disabled);
2528
+ const currentIndex = Math.max(0, optionsList.findIndex((option) => option.key === activeKey.value));
2529
+ const next = optionsList[Math.min(Math.max(currentIndex + direction, 0), optionsList.length - 1)];
2530
+ if (next) {
2531
+ activeKey.value = next.key;
2532
+ }
2533
+ };
2534
+ const getRootProps = (props = {}) => mergePropBags({
2535
+ "data-stackline-multiselect": "true",
2536
+ "data-open": isOpen.value ? "true" : "false"
2537
+ }, props);
2538
+ const getTriggerProps = (props = {}) => mergePropBags({
2539
+ type: "button",
2540
+ role: "combobox",
2541
+ "aria-expanded": isOpen.value ? "true" : "false",
2542
+ "aria-haspopup": "listbox",
2543
+ "aria-controls": listboxId,
2544
+ "aria-activedescendant": activeDescendantId.value,
2545
+ onClick: toggleOpen,
2546
+ onKeydown: (event) => {
2547
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
2548
+ event.preventDefault();
2549
+ toggleOpen();
2550
+ }
2551
+ if (event.key === "ArrowDown") {
2552
+ event.preventDefault();
2553
+ open();
2554
+ focusNext(1);
2555
+ }
2556
+ if (event.key === "ArrowUp") {
2557
+ event.preventDefault();
2558
+ open();
2559
+ focusNext(-1);
2560
+ }
2561
+ if (event.key === "Escape") {
2562
+ close();
2563
+ }
2564
+ }
2565
+ }, props);
2566
+ const getSearchInputProps = (props = {}) => mergePropBags({
2567
+ value: query.value,
2568
+ "aria-label": "Search options",
2569
+ onInput: (event) => {
2570
+ query.value = String(event.target.value || "");
2571
+ state.setFilter(query.value);
2572
+ },
2573
+ onKeydown: (event) => {
2574
+ if (event.key === "ArrowDown") {
2575
+ event.preventDefault();
2576
+ focusNext(1);
2577
+ }
2578
+ if (event.key === "ArrowUp") {
2579
+ event.preventDefault();
2580
+ focusNext(-1);
2581
+ }
2582
+ if (event.key === "Escape") {
2583
+ close();
2584
+ }
2585
+ }
2586
+ }, props);
2587
+ const getListboxProps = (props = {}) => mergePropBags({
2588
+ id: listboxId,
2589
+ role: "listbox",
2590
+ "aria-multiselectable": state.settings.value.singleSelection ? "false" : "true"
2591
+ }, props);
2592
+ const getOptionProps = (option, props = {}) => mergePropBags({
2593
+ id: `${instanceId}-option-${option.key}`,
2594
+ role: "option",
2595
+ tabindex: option.disabled ? -1 : 0,
2596
+ "aria-selected": option.selected ? "true" : "false",
2597
+ "aria-checked": option.selected ? "true" : "false",
2598
+ "aria-disabled": option.disabled ? "true" : "false",
2599
+ onMouseenter: () => {
2600
+ activeKey.value = option.key;
2601
+ },
2602
+ onClick: () => {
2603
+ if (!option.disabled) {
2604
+ state.toggleItem(option.item);
2605
+ activeKey.value = option.key;
2606
+ }
2607
+ },
2608
+ onKeydown: (event) => {
2609
+ if (event.key === "ArrowDown") {
2610
+ event.preventDefault();
2611
+ focusNext(1);
2612
+ }
2613
+ if (event.key === "ArrowUp") {
2614
+ event.preventDefault();
2615
+ focusNext(-1);
2616
+ }
2617
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
2618
+ event.preventDefault();
2619
+ state.toggleItem(option.item);
2620
+ activeKey.value = option.key;
2621
+ }
2622
+ if (event.key === "Escape") {
2623
+ close();
2624
+ }
2625
+ }
2626
+ }, props);
2627
+ const getClearAllButtonProps = (props = {}) => mergePropBags({
2628
+ type: "button",
2629
+ "aria-label": "Clear selected options",
2630
+ onClick: state.clearSelection
2631
+ }, props);
2632
+ const getRemoveButtonProps = (item, props = {}) => mergePropBags({
2633
+ type: "button",
2634
+ "aria-label": `Remove ${getLabel2(item, state.settings.value)}`,
2635
+ onClick: () => {
2636
+ state.removeItem(item);
2637
+ },
2638
+ onKeydown: (event) => {
2639
+ if (event.key === "Backspace" || event.key === "Delete") {
2640
+ event.preventDefault();
2641
+ state.removeItem(item);
2642
+ }
2643
+ }
2644
+ }, props);
2645
+ return {
2646
+ isOpen,
2647
+ query,
2648
+ filter: state.filter,
2649
+ activeKey,
2650
+ activeDescendantId,
2651
+ listboxId,
2652
+ settings: state.settings,
2653
+ filteredItems: state.filteredItems,
2654
+ selectableItems: state.selectableItems,
2655
+ selectedItems: state.selectedItems,
2656
+ visibleBadges: state.visibleBadges,
2657
+ hiddenBadgeCount: state.hiddenBadgeCount,
2658
+ visibleOptions,
2659
+ groups,
2660
+ allFilteredSelected: state.allFilteredSelected,
2661
+ label,
2662
+ open,
2663
+ close,
2664
+ toggleOpen,
2665
+ clearSelection: state.clearSelection,
2666
+ selectAll: state.selectAll,
2667
+ deSelectAll: state.deSelectAll,
2668
+ toggleGroup: state.toggleGroup,
2669
+ toggleItem: state.toggleItem,
2670
+ selectItem: state.selectItem,
2671
+ removeItem: state.removeItem,
2672
+ removeLastSelectedItem: state.removeLastSelectedItem,
2673
+ isSelected: state.isSelected,
2674
+ setFilter: (value) => {
2675
+ query.value = value;
2676
+ state.setFilter(value);
2677
+ },
2678
+ getItemKey: state.getItemKey,
2679
+ getItemLabel: state.getItemLabel,
2680
+ getRootProps,
2681
+ getTriggerProps,
2682
+ getSearchInputProps,
2683
+ getListboxProps,
2684
+ getOptionProps,
2685
+ getClearAllButtonProps,
2686
+ getRemoveButtonProps
2687
+ };
2688
+ }
2689
+ function createVueMultiselectDropdown() {
2690
+ return {
2691
+ defineSettings: (settings) => defineSettings(settings),
2692
+ defineSlots,
2693
+ useMultiSelectState: (options = {}) => useMultiSelectState(options),
2694
+ useMultiSelectDropdown: (options = {}) => useMultiSelectDropdown(options)
2695
+ };
2696
+ }
2697
+
2112
2698
  // src/plugin.ts
2113
2699
  var VueMultiselect = {
2114
2700
  install(app) {
@@ -2123,5 +2709,10 @@ export {
2123
2709
  StacklineVueMultiselect,
2124
2710
  VueMultiselect,
2125
2711
  VueMultiselectDropdown,
2126
- plugin_default as default
2712
+ createVueMultiselectDropdown,
2713
+ plugin_default as default,
2714
+ defineSettings,
2715
+ defineSlots,
2716
+ useMultiSelectDropdown,
2717
+ useMultiSelectState
2127
2718
  };