forge-select 0.7.0 → 0.7.1

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
@@ -165,7 +165,7 @@ function renderOptionContent(container, option, template, variant = "row", sanit
165
165
  // src/remote.ts
166
166
  function buildUrl(ajax, query, page, cursor) {
167
167
  if (!ajax.url) throw new Error("ForgeSelect: ajax requires either url or request.");
168
- if (typeof ajax.url === "function") return ajax.url(query, page);
168
+ if (typeof ajax.url === "function") return ajax.url(query, page, cursor);
169
169
  if (!ajax.params) return ajax.url;
170
170
  const params = new URLSearchParams();
171
171
  for (const [key, value] of Object.entries(ajax.params(query, page, cursor))) params.set(key, String(value));
@@ -581,9 +581,7 @@ var ForgeSelect = class {
581
581
  this.nativeSelect?.removeEventListener("change", this.onNativeChange);
582
582
  this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
583
583
  this.nativeForm?.removeEventListener("reset", this.onFormReset);
584
- this.rowContentCache.clear();
585
- this.rowElementCache.clear();
586
- this.rowHeightCache.clear();
584
+ this.clearRowCaches();
587
585
  this.searchIndex.clear();
588
586
  this.portalHost?.remove();
589
587
  this.root.remove();
@@ -675,9 +673,7 @@ var ForgeSelect = class {
675
673
  }
676
674
  this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
677
675
  this.updateSearchVisibility();
678
- this.rowContentCache.clear();
679
- this.rowElementCache.clear();
680
- this.rowHeightCache.clear();
676
+ this.clearRowCaches();
681
677
  this.searchIndex.clear();
682
678
  this.renderValue();
683
679
  if (this.isOpen) this.renderList();
@@ -761,9 +757,7 @@ var ForgeSelect = class {
761
757
  this.afterSelectionChange();
762
758
  }
763
759
  this.updateSearchVisibility();
764
- this.rowContentCache.clear();
765
- this.rowElementCache.clear();
766
- this.rowHeightCache.clear();
760
+ this.clearRowCaches();
767
761
  this.searchIndex.clear();
768
762
  this.highlightedIndex = -1;
769
763
  if (this.isOpen) this.renderList();
@@ -904,6 +898,7 @@ var ForgeSelect = class {
904
898
  if (portalParent) {
905
899
  this.portalHost = document.createElement("div");
906
900
  this.portalHost.className = "forge-select forge-select--portal-host";
901
+ this.portalHost.style.direction = getComputedStyle(this.root).direction;
907
902
  this.portalHost.dataset.theme = this.opts.theme;
908
903
  this.portalHost.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
909
904
  this.portalHost.append(this.dropdown);
@@ -1282,6 +1277,7 @@ var ForgeSelect = class {
1282
1277
  this.announceMaximum(existing);
1283
1278
  return void 0;
1284
1279
  }
1280
+ if (this.opts.beforeSelect?.(existing) === false) return void 0;
1285
1281
  this.selectValue(existing.value, false);
1286
1282
  return { option: existing, created: false };
1287
1283
  }
@@ -1300,6 +1296,7 @@ var ForgeSelect = class {
1300
1296
  const duplicate = this.findOption(option.value);
1301
1297
  if (duplicate) {
1302
1298
  if (this.selected.includes(duplicate.value)) return void 0;
1299
+ if (this.opts.beforeSelect?.(duplicate) === false) return void 0;
1303
1300
  this.selectValue(duplicate.value, false);
1304
1301
  return { option: duplicate, created: false };
1305
1302
  }
@@ -1580,6 +1577,18 @@ var ForgeSelect = class {
1580
1577
  usesVirtualScroll() {
1581
1578
  return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
1582
1579
  }
1580
+ /**
1581
+ * Drops every per-row cache at once. Rendered content, recycled <li>
1582
+ * elements, and measured heights are all keyed off the current `data`, so
1583
+ * they must be invalidated together whenever `data` is replaced — clearing
1584
+ * only some of them leaves recycled rows carrying state from the old list.
1585
+ */
1586
+ clearRowCaches() {
1587
+ this.rowContentCache.clear();
1588
+ this.rowElementCache.clear();
1589
+ this.rowHeightCache.clear();
1590
+ this.rowOffsetsCache = null;
1591
+ }
1583
1592
  rowKey(row, index) {
1584
1593
  if (row.kind === "option") return `option:${row.option.value}:${index}`;
1585
1594
  if (row.kind === "group") return `group:${row.label}:${index}`;
@@ -1693,7 +1702,12 @@ var ForgeSelect = class {
1693
1702
  "aria-expanded",
1694
1703
  "aria-level",
1695
1704
  "data-nav-index",
1696
- "data-option-value"
1705
+ "data-option-value",
1706
+ "data-selection-state",
1707
+ // Tree rows set an inline padding-left indent; clearing the whole
1708
+ // attribute keeps recycling self-contained, so a row reused at a
1709
+ // shallower depth can't inherit the previous row's indent.
1710
+ "style"
1697
1711
  ])
1698
1712
  li.removeAttribute(attribute);
1699
1713
  switch (row.kind) {
@@ -1743,45 +1757,47 @@ var ForgeSelect = class {
1743
1757
  li.textContent = format(this.strings.createOption, { query: this.query.trim() });
1744
1758
  if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
1745
1759
  break;
1746
- case "option": {
1747
- li.className = "forge-select__option";
1748
- li.dataset.optionValue = row.option.value;
1749
- if (row.option.className) li.classList.add(...row.option.className.trim().split(/\s+/).filter(Boolean));
1750
- li.setAttribute("role", "option");
1751
- const isSelected = this.selected.includes(row.option.value);
1752
- li.setAttribute("aria-selected", String(isSelected));
1753
- if (isSelected) li.classList.add("forge-select__option--selected");
1754
- if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
1755
- li.classList.add("forge-select__option--indeterminate");
1756
- li.dataset.selectionState = "mixed";
1757
- }
1758
- if (row.depth > 0) {
1759
- li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
1760
- }
1761
- if (this.isOptionDisabled(row.option) || this.hasReachedMaximum() && !this.selected.includes(row.option.value)) {
1762
- li.classList.add("forge-select__option--disabled");
1763
- li.setAttribute("aria-disabled", "true");
1764
- } else {
1765
- li.id = `${this.uid}-nav-${row.navIndex}`;
1766
- li.dataset.navIndex = String(row.navIndex);
1767
- if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
1768
- }
1769
- if (row.hasChildren) {
1770
- const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
1771
- li.setAttribute("aria-expanded", String(expanded));
1772
- const twisty = document.createElement("span");
1773
- twisty.className = "forge-select__twisty";
1774
- twisty.dataset.twisty = row.option.value;
1775
- twisty.setAttribute("aria-hidden", "true");
1776
- twisty.textContent = expanded ? "\u25BC" : "\u25B6";
1777
- li.append(twisty);
1778
- }
1779
- li.append(this.optionContent(row.option));
1760
+ case "option":
1761
+ this.renderOptionRow(li, row);
1780
1762
  break;
1781
- }
1782
1763
  }
1783
1764
  return li;
1784
1765
  }
1766
+ renderOptionRow(li, row) {
1767
+ li.className = "forge-select__option";
1768
+ li.dataset.optionValue = row.option.value;
1769
+ if (row.option.className) li.classList.add(...row.option.className.trim().split(/\s+/).filter(Boolean));
1770
+ li.setAttribute("role", "option");
1771
+ const isSelected = this.selected.includes(row.option.value);
1772
+ li.setAttribute("aria-selected", String(isSelected));
1773
+ if (isSelected) li.classList.add("forge-select__option--selected");
1774
+ if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
1775
+ li.classList.add("forge-select__option--indeterminate");
1776
+ li.dataset.selectionState = "mixed";
1777
+ }
1778
+ if (row.depth > 0) {
1779
+ li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
1780
+ }
1781
+ if (this.isOptionDisabled(row.option) || this.hasReachedMaximum() && !this.selected.includes(row.option.value)) {
1782
+ li.classList.add("forge-select__option--disabled");
1783
+ li.setAttribute("aria-disabled", "true");
1784
+ } else {
1785
+ li.id = `${this.uid}-nav-${row.navIndex}`;
1786
+ li.dataset.navIndex = String(row.navIndex);
1787
+ if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
1788
+ }
1789
+ if (row.hasChildren) {
1790
+ const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
1791
+ li.setAttribute("aria-expanded", String(expanded));
1792
+ const twisty = document.createElement("span");
1793
+ twisty.className = "forge-select__twisty";
1794
+ twisty.dataset.twisty = row.option.value;
1795
+ twisty.setAttribute("aria-hidden", "true");
1796
+ twisty.textContent = expanded ? "\u25BC" : "\u25B6";
1797
+ li.append(twisty);
1798
+ }
1799
+ li.append(this.optionContent(row.option));
1800
+ }
1785
1801
  /**
1786
1802
  * Rendered row content is cached per option value and cloned on each render,
1787
1803
  * so templates run once per option instead of once per scroll frame.
@@ -2014,8 +2030,7 @@ var ForgeSelect = class {
2014
2030
  this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
2015
2031
  } else {
2016
2032
  this.data = options;
2017
- this.rowContentCache.clear();
2018
- this.rowHeightCache.clear();
2033
+ this.clearRowCaches();
2019
2034
  }
2020
2035
  this.page = page;
2021
2036
  this.hasMore = hasMore;
@@ -2028,8 +2043,7 @@ var ForgeSelect = class {
2028
2043
  const error = cause instanceof Error ? cause : new Error(String(cause));
2029
2044
  if (!append) {
2030
2045
  this.data = [];
2031
- this.rowContentCache.clear();
2032
- this.rowHeightCache.clear();
2046
+ this.clearRowCaches();
2033
2047
  }
2034
2048
  this.hasMore = false;
2035
2049
  this.loadError = error;