forge-select 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -6
- package/dist/index.cjs +107 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +107 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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));
|
|
@@ -324,6 +324,12 @@ var ForgeSelect = class {
|
|
|
324
324
|
this.highlightedIndex = -1;
|
|
325
325
|
this.typeaheadBuffer = "";
|
|
326
326
|
this.typeaheadTimer = null;
|
|
327
|
+
// Keyed by the option object, not its value: duplicateValuePolicy only warns
|
|
328
|
+
// by default, so two options can share a value while carrying different
|
|
329
|
+
// labels, and a value-keyed cache would render the first one's content for
|
|
330
|
+
// both. Still explicitly bounded — a WeakMap would not collect anything while
|
|
331
|
+
// `data` holds every key, so scrolling a long list would retain a detached
|
|
332
|
+
// node per option visited.
|
|
327
333
|
this.rowContentCache = /* @__PURE__ */ new Map();
|
|
328
334
|
this.rowElementCache = /* @__PURE__ */ new Map();
|
|
329
335
|
this.rowHeightCache = /* @__PURE__ */ new Map();
|
|
@@ -511,8 +517,21 @@ var ForgeSelect = class {
|
|
|
511
517
|
this.typeaheadBuffer = "";
|
|
512
518
|
this.highlightedIndex = -1;
|
|
513
519
|
if (this.searchInput) {
|
|
520
|
+
const hadQuery = this.query !== "";
|
|
514
521
|
this.searchInput.value = "";
|
|
515
522
|
this.query = "";
|
|
523
|
+
if (hadQuery && this.opts.ajax) {
|
|
524
|
+
this.ajaxRequestId += 1;
|
|
525
|
+
if (this.ajaxTimer) {
|
|
526
|
+
clearTimeout(this.ajaxTimer);
|
|
527
|
+
this.ajaxTimer = null;
|
|
528
|
+
}
|
|
529
|
+
this.ajaxController?.abort();
|
|
530
|
+
this.ajaxController = null;
|
|
531
|
+
this.setLoading(false);
|
|
532
|
+
this.loadingMore = false;
|
|
533
|
+
this.remoteLoaded = false;
|
|
534
|
+
}
|
|
516
535
|
}
|
|
517
536
|
this.emitter.emit("close");
|
|
518
537
|
for (const plugin of this.plugins) plugin.onClose?.(this);
|
|
@@ -554,9 +573,7 @@ var ForgeSelect = class {
|
|
|
554
573
|
this.nativeSelect?.removeEventListener("change", this.onNativeChange);
|
|
555
574
|
this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
|
|
556
575
|
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
557
|
-
this.
|
|
558
|
-
this.rowElementCache.clear();
|
|
559
|
-
this.rowHeightCache.clear();
|
|
576
|
+
this.clearRowCaches();
|
|
560
577
|
this.searchIndex.clear();
|
|
561
578
|
this.portalHost?.remove();
|
|
562
579
|
this.root.remove();
|
|
@@ -648,9 +665,7 @@ var ForgeSelect = class {
|
|
|
648
665
|
}
|
|
649
666
|
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
650
667
|
this.updateSearchVisibility();
|
|
651
|
-
this.
|
|
652
|
-
this.rowElementCache.clear();
|
|
653
|
-
this.rowHeightCache.clear();
|
|
668
|
+
this.clearRowCaches();
|
|
654
669
|
this.searchIndex.clear();
|
|
655
670
|
this.renderValue();
|
|
656
671
|
if (this.isOpen) this.renderList();
|
|
@@ -734,9 +749,7 @@ var ForgeSelect = class {
|
|
|
734
749
|
this.afterSelectionChange();
|
|
735
750
|
}
|
|
736
751
|
this.updateSearchVisibility();
|
|
737
|
-
this.
|
|
738
|
-
this.rowElementCache.clear();
|
|
739
|
-
this.rowHeightCache.clear();
|
|
752
|
+
this.clearRowCaches();
|
|
740
753
|
this.searchIndex.clear();
|
|
741
754
|
this.highlightedIndex = -1;
|
|
742
755
|
if (this.isOpen) this.renderList();
|
|
@@ -877,6 +890,7 @@ var ForgeSelect = class {
|
|
|
877
890
|
if (portalParent) {
|
|
878
891
|
this.portalHost = document.createElement("div");
|
|
879
892
|
this.portalHost.className = "forge-select forge-select--portal-host";
|
|
893
|
+
this.portalHost.style.direction = getComputedStyle(this.root).direction;
|
|
880
894
|
this.portalHost.dataset.theme = this.opts.theme;
|
|
881
895
|
this.portalHost.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
882
896
|
this.portalHost.append(this.dropdown);
|
|
@@ -1255,6 +1269,7 @@ var ForgeSelect = class {
|
|
|
1255
1269
|
this.announceMaximum(existing);
|
|
1256
1270
|
return void 0;
|
|
1257
1271
|
}
|
|
1272
|
+
if (this.opts.beforeSelect?.(existing) === false) return void 0;
|
|
1258
1273
|
this.selectValue(existing.value, false);
|
|
1259
1274
|
return { option: existing, created: false };
|
|
1260
1275
|
}
|
|
@@ -1273,6 +1288,7 @@ var ForgeSelect = class {
|
|
|
1273
1288
|
const duplicate = this.findOption(option.value);
|
|
1274
1289
|
if (duplicate) {
|
|
1275
1290
|
if (this.selected.includes(duplicate.value)) return void 0;
|
|
1291
|
+
if (this.opts.beforeSelect?.(duplicate) === false) return void 0;
|
|
1276
1292
|
this.selectValue(duplicate.value, false);
|
|
1277
1293
|
return { option: duplicate, created: false };
|
|
1278
1294
|
}
|
|
@@ -1553,11 +1569,40 @@ var ForgeSelect = class {
|
|
|
1553
1569
|
usesVirtualScroll() {
|
|
1554
1570
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1555
1571
|
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Drops every per-row cache at once. Rendered content, recycled <li>
|
|
1574
|
+
* elements, and measured heights are all keyed off the current `data`, so
|
|
1575
|
+
* they must be invalidated together whenever `data` is replaced — clearing
|
|
1576
|
+
* only some of them leaves recycled rows carrying state from the old list.
|
|
1577
|
+
*/
|
|
1578
|
+
clearRowCaches() {
|
|
1579
|
+
this.rowContentCache.clear();
|
|
1580
|
+
this.rowElementCache.clear();
|
|
1581
|
+
this.rowHeightCache.clear();
|
|
1582
|
+
this.rowOffsetsCache = null;
|
|
1583
|
+
}
|
|
1584
|
+
/** Identifies a row *position*, which is what measured heights are tied to. */
|
|
1556
1585
|
rowKey(row, index) {
|
|
1557
1586
|
if (row.kind === "option") return `option:${row.option.value}:${index}`;
|
|
1558
1587
|
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1559
1588
|
return `${row.kind}:${index}`;
|
|
1560
1589
|
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Identifies a row's *content* for `<li>` recycling, deliberately without the
|
|
1592
|
+
* row index: filtering shifts every index below the first change, so an
|
|
1593
|
+
* index-keyed element cache misses on every keystroke — exactly when the list
|
|
1594
|
+
* re-renders most. renderRow() rewrites the element completely, so reusing it
|
|
1595
|
+
* at a new position is safe.
|
|
1596
|
+
*
|
|
1597
|
+
* Keys are not guaranteed unique within a render — duplicateValuePolicy
|
|
1598
|
+
* defaults to warning rather than rejecting duplicate values — so callers
|
|
1599
|
+
* must not hand the same element to two rows of one render.
|
|
1600
|
+
*/
|
|
1601
|
+
rowElementKey(row) {
|
|
1602
|
+
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1603
|
+
if (row.kind === "group") return `group:${row.label}`;
|
|
1604
|
+
return row.kind;
|
|
1605
|
+
}
|
|
1561
1606
|
measuredRowHeight(index) {
|
|
1562
1607
|
return this.opts.variableItemHeight ? this.rowHeightCache.get(this.rowKey(this.rows[index], index)) ?? this.opts.itemHeight : this.opts.itemHeight;
|
|
1563
1608
|
}
|
|
@@ -1619,9 +1664,12 @@ var ForgeSelect = class {
|
|
|
1619
1664
|
this.list.append(topSpacer);
|
|
1620
1665
|
}
|
|
1621
1666
|
const appended = [];
|
|
1667
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1622
1668
|
for (let i = start; i < end; i++) {
|
|
1623
|
-
const key = this.
|
|
1624
|
-
const
|
|
1669
|
+
const key = this.rowElementKey(this.rows[i]);
|
|
1670
|
+
const cached = this.rowElementCache.get(key);
|
|
1671
|
+
const element = this.renderRow(this.rows[i], cached && !claimed.has(cached) ? cached : void 0);
|
|
1672
|
+
claimed.add(element);
|
|
1625
1673
|
this.rowElementCache.set(key, element);
|
|
1626
1674
|
if (this.rowElementCache.size > ROW_CACHE_LIMIT) {
|
|
1627
1675
|
const oldest = this.rowElementCache.keys().next().value;
|
|
@@ -1666,7 +1714,12 @@ var ForgeSelect = class {
|
|
|
1666
1714
|
"aria-expanded",
|
|
1667
1715
|
"aria-level",
|
|
1668
1716
|
"data-nav-index",
|
|
1669
|
-
"data-option-value"
|
|
1717
|
+
"data-option-value",
|
|
1718
|
+
"data-selection-state",
|
|
1719
|
+
// Tree rows set an inline padding-left indent; clearing the whole
|
|
1720
|
+
// attribute keeps recycling self-contained, so a row reused at a
|
|
1721
|
+
// shallower depth can't inherit the previous row's indent.
|
|
1722
|
+
"style"
|
|
1670
1723
|
])
|
|
1671
1724
|
li.removeAttribute(attribute);
|
|
1672
1725
|
switch (row.kind) {
|
|
@@ -1716,45 +1769,47 @@ var ForgeSelect = class {
|
|
|
1716
1769
|
li.textContent = format(this.strings.createOption, { query: this.query.trim() });
|
|
1717
1770
|
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
1718
1771
|
break;
|
|
1719
|
-
case "option":
|
|
1720
|
-
li
|
|
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));
|
|
1772
|
+
case "option":
|
|
1773
|
+
this.renderOptionRow(li, row);
|
|
1753
1774
|
break;
|
|
1754
|
-
}
|
|
1755
1775
|
}
|
|
1756
1776
|
return li;
|
|
1757
1777
|
}
|
|
1778
|
+
renderOptionRow(li, row) {
|
|
1779
|
+
li.className = "forge-select__option";
|
|
1780
|
+
li.dataset.optionValue = row.option.value;
|
|
1781
|
+
if (row.option.className) li.classList.add(...row.option.className.trim().split(/\s+/).filter(Boolean));
|
|
1782
|
+
li.setAttribute("role", "option");
|
|
1783
|
+
const isSelected = this.selected.includes(row.option.value);
|
|
1784
|
+
li.setAttribute("aria-selected", String(isSelected));
|
|
1785
|
+
if (isSelected) li.classList.add("forge-select__option--selected");
|
|
1786
|
+
if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
|
|
1787
|
+
li.classList.add("forge-select__option--indeterminate");
|
|
1788
|
+
li.dataset.selectionState = "mixed";
|
|
1789
|
+
}
|
|
1790
|
+
if (row.depth > 0) {
|
|
1791
|
+
li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
|
|
1792
|
+
}
|
|
1793
|
+
if (this.isOptionDisabled(row.option) || this.hasReachedMaximum() && !this.selected.includes(row.option.value)) {
|
|
1794
|
+
li.classList.add("forge-select__option--disabled");
|
|
1795
|
+
li.setAttribute("aria-disabled", "true");
|
|
1796
|
+
} else {
|
|
1797
|
+
li.id = `${this.uid}-nav-${row.navIndex}`;
|
|
1798
|
+
li.dataset.navIndex = String(row.navIndex);
|
|
1799
|
+
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
1800
|
+
}
|
|
1801
|
+
if (row.hasChildren) {
|
|
1802
|
+
const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
|
|
1803
|
+
li.setAttribute("aria-expanded", String(expanded));
|
|
1804
|
+
const twisty = document.createElement("span");
|
|
1805
|
+
twisty.className = "forge-select__twisty";
|
|
1806
|
+
twisty.dataset.twisty = row.option.value;
|
|
1807
|
+
twisty.setAttribute("aria-hidden", "true");
|
|
1808
|
+
twisty.textContent = expanded ? "\u25BC" : "\u25B6";
|
|
1809
|
+
li.append(twisty);
|
|
1810
|
+
}
|
|
1811
|
+
li.append(this.optionContent(row.option));
|
|
1812
|
+
}
|
|
1758
1813
|
/**
|
|
1759
1814
|
* Rendered row content is cached per option value and cloned on each render,
|
|
1760
1815
|
* so templates run once per option instead of once per scroll frame.
|
|
@@ -1784,7 +1839,7 @@ var ForgeSelect = class {
|
|
|
1784
1839
|
}
|
|
1785
1840
|
return holder;
|
|
1786
1841
|
}
|
|
1787
|
-
let cached = this.rowContentCache.get(option
|
|
1842
|
+
let cached = this.rowContentCache.get(option);
|
|
1788
1843
|
if (!cached) {
|
|
1789
1844
|
const holder = document.createElement("span");
|
|
1790
1845
|
holder.className = "forge-select__option-content";
|
|
@@ -1793,7 +1848,7 @@ var ForgeSelect = class {
|
|
|
1793
1848
|
const oldest = this.rowContentCache.keys().next().value;
|
|
1794
1849
|
this.rowContentCache.delete(oldest);
|
|
1795
1850
|
}
|
|
1796
|
-
this.rowContentCache.set(option
|
|
1851
|
+
this.rowContentCache.set(option, holder);
|
|
1797
1852
|
cached = holder;
|
|
1798
1853
|
}
|
|
1799
1854
|
return cached.cloneNode(true);
|
|
@@ -1987,8 +2042,7 @@ var ForgeSelect = class {
|
|
|
1987
2042
|
this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
|
|
1988
2043
|
} else {
|
|
1989
2044
|
this.data = options;
|
|
1990
|
-
this.
|
|
1991
|
-
this.rowHeightCache.clear();
|
|
2045
|
+
this.clearRowCaches();
|
|
1992
2046
|
}
|
|
1993
2047
|
this.page = page;
|
|
1994
2048
|
this.hasMore = hasMore;
|
|
@@ -2001,8 +2055,7 @@ var ForgeSelect = class {
|
|
|
2001
2055
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
2002
2056
|
if (!append) {
|
|
2003
2057
|
this.data = [];
|
|
2004
|
-
this.
|
|
2005
|
-
this.rowHeightCache.clear();
|
|
2058
|
+
this.clearRowCaches();
|
|
2006
2059
|
}
|
|
2007
2060
|
this.hasMore = false;
|
|
2008
2061
|
this.loadError = error;
|