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.js CHANGED
@@ -138,7 +138,7 @@ function renderOptionContent(container, option, template, variant = "row", sanit
138
138
  // src/remote.ts
139
139
  function buildUrl(ajax, query, page, cursor) {
140
140
  if (!ajax.url) throw new Error("ForgeSelect: ajax requires either url or request.");
141
- if (typeof ajax.url === "function") return ajax.url(query, page);
141
+ if (typeof ajax.url === "function") return ajax.url(query, page, cursor);
142
142
  if (!ajax.params) return ajax.url;
143
143
  const params = new URLSearchParams();
144
144
  for (const [key, value] of Object.entries(ajax.params(query, page, cursor))) params.set(key, String(value));
@@ -554,9 +554,7 @@ var ForgeSelect = class {
554
554
  this.nativeSelect?.removeEventListener("change", this.onNativeChange);
555
555
  this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
556
556
  this.nativeForm?.removeEventListener("reset", this.onFormReset);
557
- this.rowContentCache.clear();
558
- this.rowElementCache.clear();
559
- this.rowHeightCache.clear();
557
+ this.clearRowCaches();
560
558
  this.searchIndex.clear();
561
559
  this.portalHost?.remove();
562
560
  this.root.remove();
@@ -648,9 +646,7 @@ var ForgeSelect = class {
648
646
  }
649
647
  this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
650
648
  this.updateSearchVisibility();
651
- this.rowContentCache.clear();
652
- this.rowElementCache.clear();
653
- this.rowHeightCache.clear();
649
+ this.clearRowCaches();
654
650
  this.searchIndex.clear();
655
651
  this.renderValue();
656
652
  if (this.isOpen) this.renderList();
@@ -734,9 +730,7 @@ var ForgeSelect = class {
734
730
  this.afterSelectionChange();
735
731
  }
736
732
  this.updateSearchVisibility();
737
- this.rowContentCache.clear();
738
- this.rowElementCache.clear();
739
- this.rowHeightCache.clear();
733
+ this.clearRowCaches();
740
734
  this.searchIndex.clear();
741
735
  this.highlightedIndex = -1;
742
736
  if (this.isOpen) this.renderList();
@@ -877,6 +871,7 @@ var ForgeSelect = class {
877
871
  if (portalParent) {
878
872
  this.portalHost = document.createElement("div");
879
873
  this.portalHost.className = "forge-select forge-select--portal-host";
874
+ this.portalHost.style.direction = getComputedStyle(this.root).direction;
880
875
  this.portalHost.dataset.theme = this.opts.theme;
881
876
  this.portalHost.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
882
877
  this.portalHost.append(this.dropdown);
@@ -1255,6 +1250,7 @@ var ForgeSelect = class {
1255
1250
  this.announceMaximum(existing);
1256
1251
  return void 0;
1257
1252
  }
1253
+ if (this.opts.beforeSelect?.(existing) === false) return void 0;
1258
1254
  this.selectValue(existing.value, false);
1259
1255
  return { option: existing, created: false };
1260
1256
  }
@@ -1273,6 +1269,7 @@ var ForgeSelect = class {
1273
1269
  const duplicate = this.findOption(option.value);
1274
1270
  if (duplicate) {
1275
1271
  if (this.selected.includes(duplicate.value)) return void 0;
1272
+ if (this.opts.beforeSelect?.(duplicate) === false) return void 0;
1276
1273
  this.selectValue(duplicate.value, false);
1277
1274
  return { option: duplicate, created: false };
1278
1275
  }
@@ -1553,6 +1550,18 @@ var ForgeSelect = class {
1553
1550
  usesVirtualScroll() {
1554
1551
  return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
1555
1552
  }
1553
+ /**
1554
+ * Drops every per-row cache at once. Rendered content, recycled <li>
1555
+ * elements, and measured heights are all keyed off the current `data`, so
1556
+ * they must be invalidated together whenever `data` is replaced — clearing
1557
+ * only some of them leaves recycled rows carrying state from the old list.
1558
+ */
1559
+ clearRowCaches() {
1560
+ this.rowContentCache.clear();
1561
+ this.rowElementCache.clear();
1562
+ this.rowHeightCache.clear();
1563
+ this.rowOffsetsCache = null;
1564
+ }
1556
1565
  rowKey(row, index) {
1557
1566
  if (row.kind === "option") return `option:${row.option.value}:${index}`;
1558
1567
  if (row.kind === "group") return `group:${row.label}:${index}`;
@@ -1666,7 +1675,12 @@ var ForgeSelect = class {
1666
1675
  "aria-expanded",
1667
1676
  "aria-level",
1668
1677
  "data-nav-index",
1669
- "data-option-value"
1678
+ "data-option-value",
1679
+ "data-selection-state",
1680
+ // Tree rows set an inline padding-left indent; clearing the whole
1681
+ // attribute keeps recycling self-contained, so a row reused at a
1682
+ // shallower depth can't inherit the previous row's indent.
1683
+ "style"
1670
1684
  ])
1671
1685
  li.removeAttribute(attribute);
1672
1686
  switch (row.kind) {
@@ -1716,45 +1730,47 @@ var ForgeSelect = class {
1716
1730
  li.textContent = format(this.strings.createOption, { query: this.query.trim() });
1717
1731
  if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
1718
1732
  break;
1719
- case "option": {
1720
- li.className = "forge-select__option";
1721
- li.dataset.optionValue = row.option.value;
1722
- if (row.option.className) li.classList.add(...row.option.className.trim().split(/\s+/).filter(Boolean));
1723
- li.setAttribute("role", "option");
1724
- const isSelected = this.selected.includes(row.option.value);
1725
- li.setAttribute("aria-selected", String(isSelected));
1726
- if (isSelected) li.classList.add("forge-select__option--selected");
1727
- if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
1728
- li.classList.add("forge-select__option--indeterminate");
1729
- li.dataset.selectionState = "mixed";
1730
- }
1731
- if (row.depth > 0) {
1732
- li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
1733
- }
1734
- if (this.isOptionDisabled(row.option) || this.hasReachedMaximum() && !this.selected.includes(row.option.value)) {
1735
- li.classList.add("forge-select__option--disabled");
1736
- li.setAttribute("aria-disabled", "true");
1737
- } else {
1738
- li.id = `${this.uid}-nav-${row.navIndex}`;
1739
- li.dataset.navIndex = String(row.navIndex);
1740
- if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
1741
- }
1742
- if (row.hasChildren) {
1743
- const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
1744
- li.setAttribute("aria-expanded", String(expanded));
1745
- const twisty = document.createElement("span");
1746
- twisty.className = "forge-select__twisty";
1747
- twisty.dataset.twisty = row.option.value;
1748
- twisty.setAttribute("aria-hidden", "true");
1749
- twisty.textContent = expanded ? "\u25BC" : "\u25B6";
1750
- li.append(twisty);
1751
- }
1752
- li.append(this.optionContent(row.option));
1733
+ case "option":
1734
+ this.renderOptionRow(li, row);
1753
1735
  break;
1754
- }
1755
1736
  }
1756
1737
  return li;
1757
1738
  }
1739
+ renderOptionRow(li, row) {
1740
+ li.className = "forge-select__option";
1741
+ li.dataset.optionValue = row.option.value;
1742
+ if (row.option.className) li.classList.add(...row.option.className.trim().split(/\s+/).filter(Boolean));
1743
+ li.setAttribute("role", "option");
1744
+ const isSelected = this.selected.includes(row.option.value);
1745
+ li.setAttribute("aria-selected", String(isSelected));
1746
+ if (isSelected) li.classList.add("forge-select__option--selected");
1747
+ if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
1748
+ li.classList.add("forge-select__option--indeterminate");
1749
+ li.dataset.selectionState = "mixed";
1750
+ }
1751
+ if (row.depth > 0) {
1752
+ li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
1753
+ }
1754
+ if (this.isOptionDisabled(row.option) || this.hasReachedMaximum() && !this.selected.includes(row.option.value)) {
1755
+ li.classList.add("forge-select__option--disabled");
1756
+ li.setAttribute("aria-disabled", "true");
1757
+ } else {
1758
+ li.id = `${this.uid}-nav-${row.navIndex}`;
1759
+ li.dataset.navIndex = String(row.navIndex);
1760
+ if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
1761
+ }
1762
+ if (row.hasChildren) {
1763
+ const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
1764
+ li.setAttribute("aria-expanded", String(expanded));
1765
+ const twisty = document.createElement("span");
1766
+ twisty.className = "forge-select__twisty";
1767
+ twisty.dataset.twisty = row.option.value;
1768
+ twisty.setAttribute("aria-hidden", "true");
1769
+ twisty.textContent = expanded ? "\u25BC" : "\u25B6";
1770
+ li.append(twisty);
1771
+ }
1772
+ li.append(this.optionContent(row.option));
1773
+ }
1758
1774
  /**
1759
1775
  * Rendered row content is cached per option value and cloned on each render,
1760
1776
  * so templates run once per option instead of once per scroll frame.
@@ -1987,8 +2003,7 @@ var ForgeSelect = class {
1987
2003
  this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
1988
2004
  } else {
1989
2005
  this.data = options;
1990
- this.rowContentCache.clear();
1991
- this.rowHeightCache.clear();
2006
+ this.clearRowCaches();
1992
2007
  }
1993
2008
  this.page = page;
1994
2009
  this.hasMore = hasMore;
@@ -2001,8 +2016,7 @@ var ForgeSelect = class {
2001
2016
  const error = cause instanceof Error ? cause : new Error(String(cause));
2002
2017
  if (!append) {
2003
2018
  this.data = [];
2004
- this.rowContentCache.clear();
2005
- this.rowHeightCache.clear();
2019
+ this.clearRowCaches();
2006
2020
  }
2007
2021
  this.hasMore = false;
2008
2022
  this.loadError = error;