forge-select 0.6.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 +284 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -5
- package/dist/index.d.ts +42 -5
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +284 -123
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/styles/forge-select.css +4 -5
package/dist/index.cjs
CHANGED
|
@@ -123,10 +123,10 @@ function parseOption(option) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// src/option-renderer.ts
|
|
126
|
-
function renderOptionContent(container, option, template, variant = "row") {
|
|
126
|
+
function renderOptionContent(container, option, template, variant = "row", sanitizeTemplate) {
|
|
127
127
|
if (template) {
|
|
128
128
|
const result = template(option);
|
|
129
|
-
if (typeof result === "string") container.innerHTML = result;
|
|
129
|
+
if (typeof result === "string") container.innerHTML = sanitizeTemplate ? sanitizeTemplate(result, option) : result;
|
|
130
130
|
else container.append(result);
|
|
131
131
|
return;
|
|
132
132
|
}
|
|
@@ -163,12 +163,12 @@ function renderOptionContent(container, option, template, variant = "row") {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// src/remote.ts
|
|
166
|
-
function buildUrl(ajax, query, page) {
|
|
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
|
-
for (const [key, value] of Object.entries(ajax.params(query, page))) params.set(key, String(value));
|
|
171
|
+
for (const [key, value] of Object.entries(ajax.params(query, page, cursor))) params.set(key, String(value));
|
|
172
172
|
const separator = ajax.url.includes("?") ? "&" : "?";
|
|
173
173
|
return `${ajax.url}${separator}${params.toString()}`;
|
|
174
174
|
}
|
|
@@ -180,7 +180,12 @@ function normalizeRemoteResult(ajax, response) {
|
|
|
180
180
|
"ForgeSelect: ajax.transform must return an array of options, or an object shaped like { options: Option[], hasMore?: boolean }."
|
|
181
181
|
);
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
const nextCursor = result.nextCursor == null ? void 0 : String(result.nextCursor);
|
|
184
|
+
return {
|
|
185
|
+
options: result.options,
|
|
186
|
+
hasMore: ajax.pagination ? result.hasMore ?? nextCursor !== void 0 : false,
|
|
187
|
+
nextCursor
|
|
188
|
+
};
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
// src/remote-cache.ts
|
|
@@ -294,21 +299,6 @@ function computeCheckState(option, selected, isDisabled = defaultIsDisabled) {
|
|
|
294
299
|
if (states.every((state) => state === "none")) return "none";
|
|
295
300
|
return "some";
|
|
296
301
|
}
|
|
297
|
-
function findOption(items, value) {
|
|
298
|
-
const search = (options) => {
|
|
299
|
-
for (const option of options) {
|
|
300
|
-
if (option.value === value) return option;
|
|
301
|
-
const found = option.children ? search(option.children) : void 0;
|
|
302
|
-
if (found) return found;
|
|
303
|
-
}
|
|
304
|
-
return void 0;
|
|
305
|
-
};
|
|
306
|
-
for (const item of items) {
|
|
307
|
-
const found = search(isGroup(item) ? item.options : [item]);
|
|
308
|
-
if (found) return found;
|
|
309
|
-
}
|
|
310
|
-
return void 0;
|
|
311
|
-
}
|
|
312
302
|
function syncTreeAncestors(items, selected, isDisabled = defaultIsDisabled) {
|
|
313
303
|
const sync = (option) => {
|
|
314
304
|
if (!option.children?.length) return;
|
|
@@ -343,6 +333,8 @@ var TYPEAHEAD_RESET_MS = 500;
|
|
|
343
333
|
var uidCounter = 0;
|
|
344
334
|
var ForgeSelect = class {
|
|
345
335
|
constructor(target, options = {}) {
|
|
336
|
+
this.optionByValue = /* @__PURE__ */ new Map();
|
|
337
|
+
this.optionByLabel = /* @__PURE__ */ new Map();
|
|
346
338
|
this.selected = [];
|
|
347
339
|
this.selectedOptions = /* @__PURE__ */ new Map();
|
|
348
340
|
this.suppressNextTagClick = false;
|
|
@@ -360,6 +352,7 @@ var ForgeSelect = class {
|
|
|
360
352
|
this.typeaheadBuffer = "";
|
|
361
353
|
this.typeaheadTimer = null;
|
|
362
354
|
this.rowContentCache = /* @__PURE__ */ new Map();
|
|
355
|
+
this.rowElementCache = /* @__PURE__ */ new Map();
|
|
363
356
|
this.rowHeightCache = /* @__PURE__ */ new Map();
|
|
364
357
|
this.rowOffsetsCache = null;
|
|
365
358
|
this.scrollRafId = null;
|
|
@@ -375,6 +368,8 @@ var ForgeSelect = class {
|
|
|
375
368
|
this.ajaxController = null;
|
|
376
369
|
this.remoteLoaded = false;
|
|
377
370
|
this.remoteCache = new RemoteCache();
|
|
371
|
+
this.remoteInFlight = /* @__PURE__ */ new Map();
|
|
372
|
+
this.prefetchControllers = /* @__PURE__ */ new Set();
|
|
378
373
|
this.loadError = null;
|
|
379
374
|
this.originalDisplay = "";
|
|
380
375
|
this.originalDisabled = false;
|
|
@@ -443,6 +438,13 @@ var ForgeSelect = class {
|
|
|
443
438
|
ajax: options.ajax,
|
|
444
439
|
templateResult: options.templateResult,
|
|
445
440
|
templateSelection: options.templateSelection,
|
|
441
|
+
sanitizeTemplate: options.sanitizeTemplate,
|
|
442
|
+
beforeSelect: options.beforeSelect,
|
|
443
|
+
beforeUnselect: options.beforeUnselect,
|
|
444
|
+
beforeCreate: options.beforeCreate,
|
|
445
|
+
createOption: options.createOption,
|
|
446
|
+
missingSelectionPolicy: options.missingSelectionPolicy ?? "preserve",
|
|
447
|
+
duplicateValuePolicy: options.duplicateValuePolicy ?? "warn",
|
|
446
448
|
filterOption: options.filterOption,
|
|
447
449
|
searchFields: options.searchFields ?? ["label", "description"],
|
|
448
450
|
tokenSearch: options.tokenSearch ?? true,
|
|
@@ -464,6 +466,7 @@ var ForgeSelect = class {
|
|
|
464
466
|
this.plugins = this.opts.plugins;
|
|
465
467
|
if (nativeSelect) nativeSelect.required = this.opts.required;
|
|
466
468
|
this.data = this.opts.data ?? (nativeSelect ? parseNativeOptions(nativeSelect) : []);
|
|
469
|
+
this.rebuildOptionIndexes();
|
|
467
470
|
if (nativeSelect && !this.opts.data) {
|
|
468
471
|
const nativeOptions = Array.from(nativeSelect.options);
|
|
469
472
|
const hasIntentionalSelection = nativeSelect.multiple || nativeSelect.selectedIndex > 0 || nativeOptions.some((option) => option.defaultSelected);
|
|
@@ -501,6 +504,8 @@ var ForgeSelect = class {
|
|
|
501
504
|
this.renderList();
|
|
502
505
|
this.positionDropdown();
|
|
503
506
|
window.addEventListener("resize", this.onWindowResize);
|
|
507
|
+
window.visualViewport?.addEventListener("resize", this.onWindowResize);
|
|
508
|
+
window.visualViewport?.addEventListener("scroll", this.onWindowResize);
|
|
504
509
|
document.addEventListener("scroll", this.onAncestorScroll, true);
|
|
505
510
|
if (this.searchInput && !this.searchInput.hidden) this.searchInput.focus();
|
|
506
511
|
this.emitter.emit("open");
|
|
@@ -515,6 +520,8 @@ var ForgeSelect = class {
|
|
|
515
520
|
this.control.setAttribute("aria-expanded", "false");
|
|
516
521
|
document.removeEventListener("mousedown", this.onDocumentMouseDown);
|
|
517
522
|
window.removeEventListener("resize", this.onWindowResize);
|
|
523
|
+
window.visualViewport?.removeEventListener("resize", this.onWindowResize);
|
|
524
|
+
window.visualViewport?.removeEventListener("scroll", this.onWindowResize);
|
|
518
525
|
document.removeEventListener("scroll", this.onAncestorScroll, true);
|
|
519
526
|
if (this.ancestorScrollRafId != null) {
|
|
520
527
|
cancelAnimationFrame(this.ancestorScrollRafId);
|
|
@@ -546,11 +553,16 @@ var ForgeSelect = class {
|
|
|
546
553
|
*/
|
|
547
554
|
positionDropdown() {
|
|
548
555
|
const controlRect = this.control.getBoundingClientRect();
|
|
549
|
-
const
|
|
556
|
+
const viewport = window.visualViewport;
|
|
557
|
+
const placement = computeDropdownPlacement(
|
|
558
|
+
controlRect,
|
|
559
|
+
this.dropdown.offsetHeight,
|
|
560
|
+
viewport?.height ?? window.innerHeight
|
|
561
|
+
);
|
|
550
562
|
this.root.classList.toggle("forge-select--drop-up", placement.dropUp);
|
|
551
563
|
if (this.portalHost) {
|
|
552
|
-
this.portalHost.style.top = `${placement.top}px`;
|
|
553
|
-
this.portalHost.style.left = `${controlRect.left}px`;
|
|
564
|
+
this.portalHost.style.top = `${placement.top + (viewport?.offsetTop ?? 0)}px`;
|
|
565
|
+
this.portalHost.style.left = `${controlRect.left + (viewport?.offsetLeft ?? 0)}px`;
|
|
554
566
|
this.portalHost.style.width = `${controlRect.width}px`;
|
|
555
567
|
}
|
|
556
568
|
}
|
|
@@ -561,13 +573,15 @@ var ForgeSelect = class {
|
|
|
561
573
|
this.destroyed = true;
|
|
562
574
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
563
575
|
this.ajaxController?.abort();
|
|
576
|
+
for (const controller of this.prefetchControllers) controller.abort();
|
|
577
|
+
this.prefetchControllers.clear();
|
|
578
|
+
this.remoteInFlight.clear();
|
|
564
579
|
if (this.scrollRafId != null) cancelAnimationFrame(this.scrollRafId);
|
|
565
580
|
if (this.typeaheadTimer) clearTimeout(this.typeaheadTimer);
|
|
566
581
|
this.nativeSelect?.removeEventListener("change", this.onNativeChange);
|
|
567
582
|
this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
|
|
568
583
|
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
569
|
-
this.
|
|
570
|
-
this.rowHeightCache.clear();
|
|
584
|
+
this.clearRowCaches();
|
|
571
585
|
this.searchIndex.clear();
|
|
572
586
|
this.portalHost?.remove();
|
|
573
587
|
this.root.remove();
|
|
@@ -615,10 +629,23 @@ var ForgeSelect = class {
|
|
|
615
629
|
}
|
|
616
630
|
if (options.templateResult !== void 0) this.opts.templateResult = options.templateResult;
|
|
617
631
|
if (options.templateSelection !== void 0) this.opts.templateSelection = options.templateSelection;
|
|
632
|
+
if (options.sanitizeTemplate !== void 0) this.opts.sanitizeTemplate = options.sanitizeTemplate;
|
|
633
|
+
if (options.beforeSelect !== void 0) this.opts.beforeSelect = options.beforeSelect;
|
|
634
|
+
if (options.beforeUnselect !== void 0) this.opts.beforeUnselect = options.beforeUnselect;
|
|
635
|
+
if (options.beforeCreate !== void 0) this.opts.beforeCreate = options.beforeCreate;
|
|
636
|
+
if (options.createOption !== void 0) this.opts.createOption = options.createOption;
|
|
637
|
+
if (options.missingSelectionPolicy !== void 0) this.opts.missingSelectionPolicy = options.missingSelectionPolicy;
|
|
638
|
+
if (options.duplicateValuePolicy !== void 0) {
|
|
639
|
+
this.opts.duplicateValuePolicy = options.duplicateValuePolicy;
|
|
640
|
+
this.rebuildOptionIndexes();
|
|
641
|
+
}
|
|
618
642
|
if (options.filterOption !== void 0) this.opts.filterOption = options.filterOption;
|
|
619
643
|
if (options.searchFields !== void 0) this.opts.searchFields = options.searchFields;
|
|
620
644
|
if (options.tokenSearch !== void 0) this.opts.tokenSearch = options.tokenSearch;
|
|
621
|
-
if (options.accentInsensitive !== void 0)
|
|
645
|
+
if (options.accentInsensitive !== void 0) {
|
|
646
|
+
this.opts.accentInsensitive = options.accentInsensitive;
|
|
647
|
+
this.rebuildOptionIndexes();
|
|
648
|
+
}
|
|
622
649
|
if (options.searchScorer !== void 0) this.opts.searchScorer = options.searchScorer;
|
|
623
650
|
if (options.highlightSearch !== void 0) this.opts.highlightSearch = options.highlightSearch;
|
|
624
651
|
if (options.minSearchLength !== void 0)
|
|
@@ -646,8 +673,7 @@ var ForgeSelect = class {
|
|
|
646
673
|
}
|
|
647
674
|
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
648
675
|
this.updateSearchVisibility();
|
|
649
|
-
this.
|
|
650
|
-
this.rowHeightCache.clear();
|
|
676
|
+
this.clearRowCaches();
|
|
651
677
|
this.searchIndex.clear();
|
|
652
678
|
this.renderValue();
|
|
653
679
|
if (this.isOpen) this.renderList();
|
|
@@ -679,6 +705,7 @@ var ForgeSelect = class {
|
|
|
679
705
|
}
|
|
680
706
|
clearRemoteCache() {
|
|
681
707
|
this.remoteCache.clear();
|
|
708
|
+
this.remoteInFlight.clear();
|
|
682
709
|
}
|
|
683
710
|
setValue(value, options = {}) {
|
|
684
711
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
@@ -708,11 +735,29 @@ var ForgeSelect = class {
|
|
|
708
735
|
this.remoteLoaded = true;
|
|
709
736
|
this.page = 0;
|
|
710
737
|
this.hasMore = false;
|
|
738
|
+
this.nextCursor = void 0;
|
|
739
|
+
const previousData = this.data;
|
|
711
740
|
this.data = data;
|
|
741
|
+
try {
|
|
742
|
+
this.rebuildOptionIndexes();
|
|
743
|
+
} catch (error) {
|
|
744
|
+
this.data = previousData;
|
|
745
|
+
this.rebuildOptionIndexes();
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
const missing = this.selected.filter((value) => !this.optionByValue.has(value));
|
|
749
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "error") {
|
|
750
|
+
this.data = previousData;
|
|
751
|
+
this.rebuildOptionIndexes();
|
|
752
|
+
throw new Error(`ForgeSelect: setData() is missing selected value(s): ${missing.join(", ")}`);
|
|
753
|
+
}
|
|
712
754
|
this.opts.data = data;
|
|
755
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "prune") {
|
|
756
|
+
this.selected = this.selected.filter((value) => this.optionByValue.has(value));
|
|
757
|
+
this.afterSelectionChange();
|
|
758
|
+
}
|
|
713
759
|
this.updateSearchVisibility();
|
|
714
|
-
this.
|
|
715
|
-
this.rowHeightCache.clear();
|
|
760
|
+
this.clearRowCaches();
|
|
716
761
|
this.searchIndex.clear();
|
|
717
762
|
this.highlightedIndex = -1;
|
|
718
763
|
if (this.isOpen) this.renderList();
|
|
@@ -853,6 +898,7 @@ var ForgeSelect = class {
|
|
|
853
898
|
if (portalParent) {
|
|
854
899
|
this.portalHost = document.createElement("div");
|
|
855
900
|
this.portalHost.className = "forge-select forge-select--portal-host";
|
|
901
|
+
this.portalHost.style.direction = getComputedStyle(this.root).direction;
|
|
856
902
|
this.portalHost.dataset.theme = this.opts.theme;
|
|
857
903
|
this.portalHost.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
858
904
|
this.portalHost.append(this.dropdown);
|
|
@@ -883,10 +929,24 @@ var ForgeSelect = class {
|
|
|
883
929
|
});
|
|
884
930
|
this.clearBtn.addEventListener("click", (event) => {
|
|
885
931
|
event.stopPropagation();
|
|
932
|
+
if (this.selected.some((value) => {
|
|
933
|
+
const option = this.findOption(value) ?? this.selectedOptions.get(value) ?? { value, label: value };
|
|
934
|
+
return this.opts.beforeUnselect?.(option) === false;
|
|
935
|
+
}))
|
|
936
|
+
return;
|
|
886
937
|
this.clearSelection();
|
|
887
938
|
});
|
|
888
939
|
if (this.searchInput) {
|
|
940
|
+
let composing = false;
|
|
941
|
+
this.searchInput.addEventListener("compositionstart", () => {
|
|
942
|
+
composing = true;
|
|
943
|
+
});
|
|
944
|
+
this.searchInput.addEventListener("compositionend", () => {
|
|
945
|
+
composing = false;
|
|
946
|
+
this.applySearchQuery(this.searchInput.value, true);
|
|
947
|
+
});
|
|
889
948
|
this.searchInput.addEventListener("input", () => {
|
|
949
|
+
if (composing) return;
|
|
890
950
|
this.applySearchQuery(this.searchInput.value, true);
|
|
891
951
|
});
|
|
892
952
|
this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
@@ -896,21 +956,22 @@ var ForgeSelect = class {
|
|
|
896
956
|
const labels = text.split(/[,\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
897
957
|
if (labels.length < 2) return;
|
|
898
958
|
event.preventDefault();
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (
|
|
910
|
-
this.
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
959
|
+
void Promise.all(labels.map((label) => this.createTag(label))).then((results) => {
|
|
960
|
+
const created = results.filter((result) => result !== void 0);
|
|
961
|
+
if (created.length === 0 || this.destroyed) return;
|
|
962
|
+
this.searchInput.value = "";
|
|
963
|
+
this.query = "";
|
|
964
|
+
this.afterSelectionChange();
|
|
965
|
+
for (const result of created) {
|
|
966
|
+
if (result.created) this.emitter.emit("create", result.option);
|
|
967
|
+
this.emitter.emit("select", result.option);
|
|
968
|
+
}
|
|
969
|
+
if (this.opts.closeOnSelect) this.close();
|
|
970
|
+
else this.renderList();
|
|
971
|
+
}).catch((cause) => {
|
|
972
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
973
|
+
this.emitter.emit("error", error);
|
|
974
|
+
});
|
|
914
975
|
});
|
|
915
976
|
}
|
|
916
977
|
this.list.addEventListener("click", (event) => {
|
|
@@ -965,7 +1026,7 @@ var ForgeSelect = class {
|
|
|
965
1026
|
this.renderList();
|
|
966
1027
|
}
|
|
967
1028
|
handleKeydown(event) {
|
|
968
|
-
if (this.isDisabled) return;
|
|
1029
|
+
if (this.isDisabled || event.isComposing || event.keyCode === 229) return;
|
|
969
1030
|
switch (event.key) {
|
|
970
1031
|
case "Enter":
|
|
971
1032
|
event.preventDefault();
|
|
@@ -1183,23 +1244,27 @@ var ForgeSelect = class {
|
|
|
1183
1244
|
}
|
|
1184
1245
|
}
|
|
1185
1246
|
findOption(value) {
|
|
1186
|
-
return
|
|
1247
|
+
return this.optionByValue.get(value);
|
|
1187
1248
|
}
|
|
1188
1249
|
findOptionByLabel(label) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1250
|
+
return this.optionByLabel.get(normalizeSearchText(label, this.opts.accentInsensitive));
|
|
1251
|
+
}
|
|
1252
|
+
rebuildOptionIndexes() {
|
|
1253
|
+
this.optionByValue.clear();
|
|
1254
|
+
this.optionByLabel.clear();
|
|
1255
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1256
|
+
const visit = (option) => {
|
|
1257
|
+
if (this.optionByValue.has(option.value)) duplicates.add(option.value);
|
|
1258
|
+
else this.optionByValue.set(option.value, option);
|
|
1259
|
+
const label = normalizeSearchText(option.label, this.opts.accentInsensitive);
|
|
1260
|
+
if (!this.optionByLabel.has(label)) this.optionByLabel.set(label, option);
|
|
1261
|
+
option.children?.forEach(visit);
|
|
1197
1262
|
};
|
|
1198
|
-
for (const item of this.data)
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1263
|
+
for (const item of this.data) (isGroup(item) ? item.options : [item]).forEach(visit);
|
|
1264
|
+
if (duplicates.size === 0 || this.opts.duplicateValuePolicy === "ignore") return;
|
|
1265
|
+
const message = `ForgeSelect: duplicate option value(s): ${[...duplicates].join(", ")}`;
|
|
1266
|
+
if (this.opts.duplicateValuePolicy === "error") throw new Error(message);
|
|
1267
|
+
console.warn(message);
|
|
1203
1268
|
}
|
|
1204
1269
|
/** Selects an existing option matching `label` exactly, or creates and selects a new one. */
|
|
1205
1270
|
createTag(label) {
|
|
@@ -1212,15 +1277,31 @@ var ForgeSelect = class {
|
|
|
1212
1277
|
this.announceMaximum(existing);
|
|
1213
1278
|
return void 0;
|
|
1214
1279
|
}
|
|
1280
|
+
if (this.opts.beforeSelect?.(existing) === false) return void 0;
|
|
1215
1281
|
this.selectValue(existing.value, false);
|
|
1216
1282
|
return { option: existing, created: false };
|
|
1217
1283
|
}
|
|
1218
|
-
|
|
1284
|
+
if (this.opts.beforeCreate?.(trimmed) === false) return void 0;
|
|
1285
|
+
const created = this.opts.createOption?.(trimmed) ?? { value: trimmed, label: trimmed };
|
|
1286
|
+
if (created instanceof Promise) {
|
|
1287
|
+
return created.then((option) => option ? this.addCreatedOption(option) : void 0);
|
|
1288
|
+
}
|
|
1289
|
+
return created ? this.addCreatedOption(created) : void 0;
|
|
1290
|
+
}
|
|
1291
|
+
addCreatedOption(option) {
|
|
1219
1292
|
if (this.opts.multiple && !this.canSelectOption(option)) {
|
|
1220
1293
|
this.announceMaximum(option);
|
|
1221
1294
|
return void 0;
|
|
1222
1295
|
}
|
|
1296
|
+
const duplicate = this.findOption(option.value);
|
|
1297
|
+
if (duplicate) {
|
|
1298
|
+
if (this.selected.includes(duplicate.value)) return void 0;
|
|
1299
|
+
if (this.opts.beforeSelect?.(duplicate) === false) return void 0;
|
|
1300
|
+
this.selectValue(duplicate.value, false);
|
|
1301
|
+
return { option: duplicate, created: false };
|
|
1302
|
+
}
|
|
1223
1303
|
this.data.push(option);
|
|
1304
|
+
this.rebuildOptionIndexes();
|
|
1224
1305
|
this.selectValue(option.value, false);
|
|
1225
1306
|
return { option, created: true };
|
|
1226
1307
|
}
|
|
@@ -1228,8 +1309,18 @@ var ForgeSelect = class {
|
|
|
1228
1309
|
const label = this.query.trim();
|
|
1229
1310
|
if (!label) return;
|
|
1230
1311
|
const result = this.createTag(label);
|
|
1231
|
-
if (
|
|
1232
|
-
|
|
1312
|
+
if (result instanceof Promise) {
|
|
1313
|
+
void result.then((created) => this.finishCreateFromQuery(created, label)).catch((cause) => {
|
|
1314
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1315
|
+
this.emitter.emit("error", error);
|
|
1316
|
+
});
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
this.finishCreateFromQuery(result, label);
|
|
1320
|
+
}
|
|
1321
|
+
finishCreateFromQuery(result, sourceLabel) {
|
|
1322
|
+
if (!result || this.destroyed) return;
|
|
1323
|
+
if (this.searchInput && this.query.trim() === sourceLabel) {
|
|
1233
1324
|
this.searchInput.value = "";
|
|
1234
1325
|
this.query = "";
|
|
1235
1326
|
}
|
|
@@ -1250,9 +1341,11 @@ var ForgeSelect = class {
|
|
|
1250
1341
|
if (this.opts.multiple) {
|
|
1251
1342
|
let changed = false;
|
|
1252
1343
|
if (this.selected.includes(value)) {
|
|
1344
|
+
if (this.opts.beforeUnselect?.(item.option) === false) return;
|
|
1253
1345
|
this.deselectValue(value, true);
|
|
1254
1346
|
changed = true;
|
|
1255
1347
|
} else if (this.canSelectOption(item.option)) {
|
|
1348
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1256
1349
|
this.selectValue(value, true);
|
|
1257
1350
|
changed = true;
|
|
1258
1351
|
} else {
|
|
@@ -1260,6 +1353,7 @@ var ForgeSelect = class {
|
|
|
1260
1353
|
}
|
|
1261
1354
|
if (changed && this.opts.closeOnSelect) this.close();
|
|
1262
1355
|
} else {
|
|
1356
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1263
1357
|
this.selectValue(value, true);
|
|
1264
1358
|
this.close();
|
|
1265
1359
|
this.control.focus();
|
|
@@ -1284,7 +1378,7 @@ var ForgeSelect = class {
|
|
|
1284
1378
|
tag.className = "forge-select__tag";
|
|
1285
1379
|
const label = document.createElement("span");
|
|
1286
1380
|
label.className = "forge-select__tag-label";
|
|
1287
|
-
renderOptionContent(label, option, this.opts.templateSelection, "inline");
|
|
1381
|
+
renderOptionContent(label, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1288
1382
|
const remove = document.createElement("button");
|
|
1289
1383
|
remove.type = "button";
|
|
1290
1384
|
remove.className = "forge-select__tag-remove";
|
|
@@ -1292,7 +1386,7 @@ var ForgeSelect = class {
|
|
|
1292
1386
|
remove.textContent = "\xD7";
|
|
1293
1387
|
remove.addEventListener("click", (event) => {
|
|
1294
1388
|
event.stopPropagation();
|
|
1295
|
-
if (!this.isDisabled) this.deselectValue(value, true);
|
|
1389
|
+
if (!this.isDisabled && this.opts.beforeUnselect?.(option) !== false) this.deselectValue(value, true);
|
|
1296
1390
|
});
|
|
1297
1391
|
tag.append(label, remove);
|
|
1298
1392
|
if (this.opts.sortable) {
|
|
@@ -1312,7 +1406,7 @@ var ForgeSelect = class {
|
|
|
1312
1406
|
};
|
|
1313
1407
|
const span = document.createElement("span");
|
|
1314
1408
|
span.className = "forge-select__single-value";
|
|
1315
|
-
renderOptionContent(span, option, this.opts.templateSelection, "inline");
|
|
1409
|
+
renderOptionContent(span, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1316
1410
|
this.valueEl.append(span);
|
|
1317
1411
|
}
|
|
1318
1412
|
}
|
|
@@ -1421,7 +1515,14 @@ var ForgeSelect = class {
|
|
|
1421
1515
|
accentInsensitive: this.opts.accentInsensitive,
|
|
1422
1516
|
scorer: this.opts.searchScorer
|
|
1423
1517
|
}) > 0);
|
|
1424
|
-
const
|
|
1518
|
+
const subtreeMatchCache = /* @__PURE__ */ new Map();
|
|
1519
|
+
const subtreeMatches = (option) => {
|
|
1520
|
+
const cached = subtreeMatchCache.get(option);
|
|
1521
|
+
if (cached !== void 0) return cached;
|
|
1522
|
+
const result = query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
1523
|
+
subtreeMatchCache.set(option, result);
|
|
1524
|
+
return result;
|
|
1525
|
+
};
|
|
1425
1526
|
const pushOption = (option, depth, parentValue) => {
|
|
1426
1527
|
let navIndex = -1;
|
|
1427
1528
|
const interactionDisabled = this.isOptionDisabled(option) || this.hasReachedMaximum() && !this.selected.includes(option.value);
|
|
@@ -1476,8 +1577,20 @@ var ForgeSelect = class {
|
|
|
1476
1577
|
usesVirtualScroll() {
|
|
1477
1578
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1478
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
|
+
}
|
|
1479
1592
|
rowKey(row, index) {
|
|
1480
|
-
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1593
|
+
if (row.kind === "option") return `option:${row.option.value}:${index}`;
|
|
1481
1594
|
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1482
1595
|
return `${row.kind}:${index}`;
|
|
1483
1596
|
}
|
|
@@ -1519,7 +1632,14 @@ var ForgeSelect = class {
|
|
|
1519
1632
|
if (virtual) {
|
|
1520
1633
|
const viewport = clientHeight || rowHeight * 8;
|
|
1521
1634
|
if (this.opts.variableItemHeight) {
|
|
1522
|
-
|
|
1635
|
+
let low = 0;
|
|
1636
|
+
let high = this.rows.length;
|
|
1637
|
+
while (low < high) {
|
|
1638
|
+
const middle = low + high >>> 1;
|
|
1639
|
+
if (offsets[middle + 1] < scrollTop) low = middle + 1;
|
|
1640
|
+
else high = middle;
|
|
1641
|
+
}
|
|
1642
|
+
start = low;
|
|
1523
1643
|
start = Math.max(0, start - VIRTUAL_BUFFER);
|
|
1524
1644
|
end = start;
|
|
1525
1645
|
const target = scrollTop + viewport + VIRTUAL_BUFFER * rowHeight;
|
|
@@ -1536,7 +1656,13 @@ var ForgeSelect = class {
|
|
|
1536
1656
|
}
|
|
1537
1657
|
const appended = [];
|
|
1538
1658
|
for (let i = start; i < end; i++) {
|
|
1539
|
-
const
|
|
1659
|
+
const key = this.rowKey(this.rows[i], i);
|
|
1660
|
+
const element = this.renderRow(this.rows[i], this.rowElementCache.get(key));
|
|
1661
|
+
this.rowElementCache.set(key, element);
|
|
1662
|
+
if (this.rowElementCache.size > ROW_CACHE_LIMIT) {
|
|
1663
|
+
const oldest = this.rowElementCache.keys().next().value;
|
|
1664
|
+
this.rowElementCache.delete(oldest);
|
|
1665
|
+
}
|
|
1540
1666
|
this.list.append(element);
|
|
1541
1667
|
appended.push(element);
|
|
1542
1668
|
}
|
|
@@ -1563,8 +1689,27 @@ var ForgeSelect = class {
|
|
|
1563
1689
|
}
|
|
1564
1690
|
this.updateActiveDescendant();
|
|
1565
1691
|
}
|
|
1566
|
-
renderRow(row) {
|
|
1567
|
-
const li = document.createElement("li");
|
|
1692
|
+
renderRow(row, recycled) {
|
|
1693
|
+
const li = recycled ?? document.createElement("li");
|
|
1694
|
+
li.replaceChildren();
|
|
1695
|
+
li.className = "";
|
|
1696
|
+
for (const attribute of [
|
|
1697
|
+
"role",
|
|
1698
|
+
"id",
|
|
1699
|
+
"aria-hidden",
|
|
1700
|
+
"aria-selected",
|
|
1701
|
+
"aria-disabled",
|
|
1702
|
+
"aria-expanded",
|
|
1703
|
+
"aria-level",
|
|
1704
|
+
"data-nav-index",
|
|
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"
|
|
1711
|
+
])
|
|
1712
|
+
li.removeAttribute(attribute);
|
|
1568
1713
|
switch (row.kind) {
|
|
1569
1714
|
case "group":
|
|
1570
1715
|
li.className = "forge-select__group-label";
|
|
@@ -1612,45 +1757,47 @@ var ForgeSelect = class {
|
|
|
1612
1757
|
li.textContent = format(this.strings.createOption, { query: this.query.trim() });
|
|
1613
1758
|
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
1614
1759
|
break;
|
|
1615
|
-
case "option":
|
|
1616
|
-
li
|
|
1617
|
-
li.dataset.optionValue = row.option.value;
|
|
1618
|
-
if (row.option.className) li.classList.add(...row.option.className.trim().split(/\s+/).filter(Boolean));
|
|
1619
|
-
li.setAttribute("role", "option");
|
|
1620
|
-
const isSelected = this.selected.includes(row.option.value);
|
|
1621
|
-
li.setAttribute("aria-selected", String(isSelected));
|
|
1622
|
-
if (isSelected) li.classList.add("forge-select__option--selected");
|
|
1623
|
-
if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
|
|
1624
|
-
li.classList.add("forge-select__option--indeterminate");
|
|
1625
|
-
li.dataset.selectionState = "mixed";
|
|
1626
|
-
}
|
|
1627
|
-
if (row.depth > 0) {
|
|
1628
|
-
li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
|
|
1629
|
-
}
|
|
1630
|
-
if (this.isOptionDisabled(row.option) || this.hasReachedMaximum() && !this.selected.includes(row.option.value)) {
|
|
1631
|
-
li.classList.add("forge-select__option--disabled");
|
|
1632
|
-
li.setAttribute("aria-disabled", "true");
|
|
1633
|
-
} else {
|
|
1634
|
-
li.id = `${this.uid}-nav-${row.navIndex}`;
|
|
1635
|
-
li.dataset.navIndex = String(row.navIndex);
|
|
1636
|
-
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
1637
|
-
}
|
|
1638
|
-
if (row.hasChildren) {
|
|
1639
|
-
const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
|
|
1640
|
-
li.setAttribute("aria-expanded", String(expanded));
|
|
1641
|
-
const twisty = document.createElement("span");
|
|
1642
|
-
twisty.className = "forge-select__twisty";
|
|
1643
|
-
twisty.dataset.twisty = row.option.value;
|
|
1644
|
-
twisty.setAttribute("aria-hidden", "true");
|
|
1645
|
-
twisty.textContent = expanded ? "\u25BC" : "\u25B6";
|
|
1646
|
-
li.append(twisty);
|
|
1647
|
-
}
|
|
1648
|
-
li.append(this.optionContent(row.option));
|
|
1760
|
+
case "option":
|
|
1761
|
+
this.renderOptionRow(li, row);
|
|
1649
1762
|
break;
|
|
1650
|
-
}
|
|
1651
1763
|
}
|
|
1652
1764
|
return li;
|
|
1653
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
|
+
}
|
|
1654
1801
|
/**
|
|
1655
1802
|
* Rendered row content is cached per option value and cloned on each render,
|
|
1656
1803
|
* so templates run once per option instead of once per scroll frame.
|
|
@@ -1684,7 +1831,7 @@ var ForgeSelect = class {
|
|
|
1684
1831
|
if (!cached) {
|
|
1685
1832
|
const holder = document.createElement("span");
|
|
1686
1833
|
holder.className = "forge-select__option-content";
|
|
1687
|
-
renderOptionContent(holder, option, this.opts.templateResult);
|
|
1834
|
+
renderOptionContent(holder, option, this.opts.templateResult, "row", this.opts.sanitizeTemplate);
|
|
1688
1835
|
if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
|
|
1689
1836
|
const oldest = this.rowContentCache.keys().next().value;
|
|
1690
1837
|
this.rowContentCache.delete(oldest);
|
|
@@ -1773,6 +1920,7 @@ var ForgeSelect = class {
|
|
|
1773
1920
|
this.ajaxController = null;
|
|
1774
1921
|
this.page = 0;
|
|
1775
1922
|
this.hasMore = true;
|
|
1923
|
+
this.nextCursor = void 0;
|
|
1776
1924
|
this.setLoading(true);
|
|
1777
1925
|
this.loadingMore = false;
|
|
1778
1926
|
this.loadError = null;
|
|
@@ -1787,17 +1935,27 @@ var ForgeSelect = class {
|
|
|
1787
1935
|
this.loading = loading;
|
|
1788
1936
|
this.emitter.emit("loading", loading);
|
|
1789
1937
|
}
|
|
1790
|
-
remoteCacheKey(query, page) {
|
|
1791
|
-
return `${query}\0${page}`;
|
|
1938
|
+
remoteCacheKey(query, page, cursor) {
|
|
1939
|
+
return `${query}\0${cursor ?? page}`;
|
|
1792
1940
|
}
|
|
1793
|
-
|
|
1941
|
+
fetchRemoteResult(query, page, signal, cursor) {
|
|
1942
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1943
|
+
const pending = this.remoteInFlight.get(key);
|
|
1944
|
+
if (pending) return pending;
|
|
1945
|
+
const ajax = this.opts.ajax;
|
|
1946
|
+
const request = this.requestRemote(query, page, signal, cursor).then((json) => normalizeRemoteResult(ajax, json)).finally(() => this.remoteInFlight.delete(key));
|
|
1947
|
+
this.remoteInFlight.set(key, request);
|
|
1948
|
+
return request;
|
|
1949
|
+
}
|
|
1950
|
+
async requestRemote(query, page, signal, cursor) {
|
|
1794
1951
|
const ajax = this.opts.ajax;
|
|
1795
1952
|
const attempts = Math.max(0, Math.floor(ajax.retry ?? 0)) + 1;
|
|
1796
1953
|
let lastError;
|
|
1797
1954
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1798
1955
|
try {
|
|
1799
|
-
if (ajax.request)
|
|
1800
|
-
|
|
1956
|
+
if (ajax.request)
|
|
1957
|
+
return cursor === void 0 ? await ajax.request(query, page, signal) : await ajax.request(query, page, signal, cursor);
|
|
1958
|
+
const response = await fetch(buildUrl(ajax, query, page, cursor), { signal });
|
|
1801
1959
|
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1802
1960
|
return await response.json();
|
|
1803
1961
|
} catch (error) {
|
|
@@ -1825,10 +1983,13 @@ var ForgeSelect = class {
|
|
|
1825
1983
|
const key = this.remoteCacheKey(query, 0);
|
|
1826
1984
|
if (this.remoteCache.get(key)) return;
|
|
1827
1985
|
const controller = new AbortController();
|
|
1986
|
+
this.prefetchControllers.add(controller);
|
|
1828
1987
|
try {
|
|
1829
|
-
const
|
|
1830
|
-
this.remoteCache.set(key,
|
|
1988
|
+
const result = await this.fetchRemoteResult(query, 0, controller.signal);
|
|
1989
|
+
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1831
1990
|
} catch {
|
|
1991
|
+
} finally {
|
|
1992
|
+
this.prefetchControllers.delete(controller);
|
|
1832
1993
|
}
|
|
1833
1994
|
}
|
|
1834
1995
|
/**
|
|
@@ -1854,12 +2015,12 @@ var ForgeSelect = class {
|
|
|
1854
2015
|
const controller = new AbortController();
|
|
1855
2016
|
this.ajaxController = controller;
|
|
1856
2017
|
const page = append ? this.page + 1 : 0;
|
|
2018
|
+
const cursor = append ? this.nextCursor : void 0;
|
|
1857
2019
|
try {
|
|
1858
|
-
const key = this.remoteCacheKey(query, page);
|
|
2020
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1859
2021
|
let result = this.remoteCache.get(key);
|
|
1860
2022
|
if (!result) {
|
|
1861
|
-
|
|
1862
|
-
result = normalizeRemoteResult(ajax, json);
|
|
2023
|
+
result = await this.fetchRemoteResult(query, page, controller.signal, cursor);
|
|
1863
2024
|
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1864
2025
|
}
|
|
1865
2026
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
@@ -1869,20 +2030,20 @@ var ForgeSelect = class {
|
|
|
1869
2030
|
this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
|
|
1870
2031
|
} else {
|
|
1871
2032
|
this.data = options;
|
|
1872
|
-
this.
|
|
1873
|
-
this.rowHeightCache.clear();
|
|
2033
|
+
this.clearRowCaches();
|
|
1874
2034
|
}
|
|
1875
2035
|
this.page = page;
|
|
1876
2036
|
this.hasMore = hasMore;
|
|
2037
|
+
this.nextCursor = result.nextCursor;
|
|
1877
2038
|
this.remoteLoaded = true;
|
|
1878
2039
|
this.loadError = null;
|
|
2040
|
+
this.rebuildOptionIndexes();
|
|
1879
2041
|
} catch (cause) {
|
|
1880
2042
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed || controller.signal.aborted) return;
|
|
1881
2043
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1882
2044
|
if (!append) {
|
|
1883
2045
|
this.data = [];
|
|
1884
|
-
this.
|
|
1885
|
-
this.rowHeightCache.clear();
|
|
2046
|
+
this.clearRowCaches();
|
|
1886
2047
|
}
|
|
1887
2048
|
this.hasMore = false;
|
|
1888
2049
|
this.loadError = error;
|