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.js
CHANGED
|
@@ -96,10 +96,10 @@ function parseOption(option) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// src/option-renderer.ts
|
|
99
|
-
function renderOptionContent(container, option, template, variant = "row") {
|
|
99
|
+
function renderOptionContent(container, option, template, variant = "row", sanitizeTemplate) {
|
|
100
100
|
if (template) {
|
|
101
101
|
const result = template(option);
|
|
102
|
-
if (typeof result === "string") container.innerHTML = result;
|
|
102
|
+
if (typeof result === "string") container.innerHTML = sanitizeTemplate ? sanitizeTemplate(result, option) : result;
|
|
103
103
|
else container.append(result);
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
@@ -136,12 +136,12 @@ function renderOptionContent(container, option, template, variant = "row") {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// src/remote.ts
|
|
139
|
-
function buildUrl(ajax, query, page) {
|
|
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
|
-
for (const [key, value] of Object.entries(ajax.params(query, page))) params.set(key, String(value));
|
|
144
|
+
for (const [key, value] of Object.entries(ajax.params(query, page, cursor))) params.set(key, String(value));
|
|
145
145
|
const separator = ajax.url.includes("?") ? "&" : "?";
|
|
146
146
|
return `${ajax.url}${separator}${params.toString()}`;
|
|
147
147
|
}
|
|
@@ -153,7 +153,12 @@ function normalizeRemoteResult(ajax, response) {
|
|
|
153
153
|
"ForgeSelect: ajax.transform must return an array of options, or an object shaped like { options: Option[], hasMore?: boolean }."
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
const nextCursor = result.nextCursor == null ? void 0 : String(result.nextCursor);
|
|
157
|
+
return {
|
|
158
|
+
options: result.options,
|
|
159
|
+
hasMore: ajax.pagination ? result.hasMore ?? nextCursor !== void 0 : false,
|
|
160
|
+
nextCursor
|
|
161
|
+
};
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
// src/remote-cache.ts
|
|
@@ -267,21 +272,6 @@ function computeCheckState(option, selected, isDisabled = defaultIsDisabled) {
|
|
|
267
272
|
if (states.every((state) => state === "none")) return "none";
|
|
268
273
|
return "some";
|
|
269
274
|
}
|
|
270
|
-
function findOption(items, value) {
|
|
271
|
-
const search = (options) => {
|
|
272
|
-
for (const option of options) {
|
|
273
|
-
if (option.value === value) return option;
|
|
274
|
-
const found = option.children ? search(option.children) : void 0;
|
|
275
|
-
if (found) return found;
|
|
276
|
-
}
|
|
277
|
-
return void 0;
|
|
278
|
-
};
|
|
279
|
-
for (const item of items) {
|
|
280
|
-
const found = search(isGroup(item) ? item.options : [item]);
|
|
281
|
-
if (found) return found;
|
|
282
|
-
}
|
|
283
|
-
return void 0;
|
|
284
|
-
}
|
|
285
275
|
function syncTreeAncestors(items, selected, isDisabled = defaultIsDisabled) {
|
|
286
276
|
const sync = (option) => {
|
|
287
277
|
if (!option.children?.length) return;
|
|
@@ -316,6 +306,8 @@ var TYPEAHEAD_RESET_MS = 500;
|
|
|
316
306
|
var uidCounter = 0;
|
|
317
307
|
var ForgeSelect = class {
|
|
318
308
|
constructor(target, options = {}) {
|
|
309
|
+
this.optionByValue = /* @__PURE__ */ new Map();
|
|
310
|
+
this.optionByLabel = /* @__PURE__ */ new Map();
|
|
319
311
|
this.selected = [];
|
|
320
312
|
this.selectedOptions = /* @__PURE__ */ new Map();
|
|
321
313
|
this.suppressNextTagClick = false;
|
|
@@ -333,6 +325,7 @@ var ForgeSelect = class {
|
|
|
333
325
|
this.typeaheadBuffer = "";
|
|
334
326
|
this.typeaheadTimer = null;
|
|
335
327
|
this.rowContentCache = /* @__PURE__ */ new Map();
|
|
328
|
+
this.rowElementCache = /* @__PURE__ */ new Map();
|
|
336
329
|
this.rowHeightCache = /* @__PURE__ */ new Map();
|
|
337
330
|
this.rowOffsetsCache = null;
|
|
338
331
|
this.scrollRafId = null;
|
|
@@ -348,6 +341,8 @@ var ForgeSelect = class {
|
|
|
348
341
|
this.ajaxController = null;
|
|
349
342
|
this.remoteLoaded = false;
|
|
350
343
|
this.remoteCache = new RemoteCache();
|
|
344
|
+
this.remoteInFlight = /* @__PURE__ */ new Map();
|
|
345
|
+
this.prefetchControllers = /* @__PURE__ */ new Set();
|
|
351
346
|
this.loadError = null;
|
|
352
347
|
this.originalDisplay = "";
|
|
353
348
|
this.originalDisabled = false;
|
|
@@ -416,6 +411,13 @@ var ForgeSelect = class {
|
|
|
416
411
|
ajax: options.ajax,
|
|
417
412
|
templateResult: options.templateResult,
|
|
418
413
|
templateSelection: options.templateSelection,
|
|
414
|
+
sanitizeTemplate: options.sanitizeTemplate,
|
|
415
|
+
beforeSelect: options.beforeSelect,
|
|
416
|
+
beforeUnselect: options.beforeUnselect,
|
|
417
|
+
beforeCreate: options.beforeCreate,
|
|
418
|
+
createOption: options.createOption,
|
|
419
|
+
missingSelectionPolicy: options.missingSelectionPolicy ?? "preserve",
|
|
420
|
+
duplicateValuePolicy: options.duplicateValuePolicy ?? "warn",
|
|
419
421
|
filterOption: options.filterOption,
|
|
420
422
|
searchFields: options.searchFields ?? ["label", "description"],
|
|
421
423
|
tokenSearch: options.tokenSearch ?? true,
|
|
@@ -437,6 +439,7 @@ var ForgeSelect = class {
|
|
|
437
439
|
this.plugins = this.opts.plugins;
|
|
438
440
|
if (nativeSelect) nativeSelect.required = this.opts.required;
|
|
439
441
|
this.data = this.opts.data ?? (nativeSelect ? parseNativeOptions(nativeSelect) : []);
|
|
442
|
+
this.rebuildOptionIndexes();
|
|
440
443
|
if (nativeSelect && !this.opts.data) {
|
|
441
444
|
const nativeOptions = Array.from(nativeSelect.options);
|
|
442
445
|
const hasIntentionalSelection = nativeSelect.multiple || nativeSelect.selectedIndex > 0 || nativeOptions.some((option) => option.defaultSelected);
|
|
@@ -474,6 +477,8 @@ var ForgeSelect = class {
|
|
|
474
477
|
this.renderList();
|
|
475
478
|
this.positionDropdown();
|
|
476
479
|
window.addEventListener("resize", this.onWindowResize);
|
|
480
|
+
window.visualViewport?.addEventListener("resize", this.onWindowResize);
|
|
481
|
+
window.visualViewport?.addEventListener("scroll", this.onWindowResize);
|
|
477
482
|
document.addEventListener("scroll", this.onAncestorScroll, true);
|
|
478
483
|
if (this.searchInput && !this.searchInput.hidden) this.searchInput.focus();
|
|
479
484
|
this.emitter.emit("open");
|
|
@@ -488,6 +493,8 @@ var ForgeSelect = class {
|
|
|
488
493
|
this.control.setAttribute("aria-expanded", "false");
|
|
489
494
|
document.removeEventListener("mousedown", this.onDocumentMouseDown);
|
|
490
495
|
window.removeEventListener("resize", this.onWindowResize);
|
|
496
|
+
window.visualViewport?.removeEventListener("resize", this.onWindowResize);
|
|
497
|
+
window.visualViewport?.removeEventListener("scroll", this.onWindowResize);
|
|
491
498
|
document.removeEventListener("scroll", this.onAncestorScroll, true);
|
|
492
499
|
if (this.ancestorScrollRafId != null) {
|
|
493
500
|
cancelAnimationFrame(this.ancestorScrollRafId);
|
|
@@ -519,11 +526,16 @@ var ForgeSelect = class {
|
|
|
519
526
|
*/
|
|
520
527
|
positionDropdown() {
|
|
521
528
|
const controlRect = this.control.getBoundingClientRect();
|
|
522
|
-
const
|
|
529
|
+
const viewport = window.visualViewport;
|
|
530
|
+
const placement = computeDropdownPlacement(
|
|
531
|
+
controlRect,
|
|
532
|
+
this.dropdown.offsetHeight,
|
|
533
|
+
viewport?.height ?? window.innerHeight
|
|
534
|
+
);
|
|
523
535
|
this.root.classList.toggle("forge-select--drop-up", placement.dropUp);
|
|
524
536
|
if (this.portalHost) {
|
|
525
|
-
this.portalHost.style.top = `${placement.top}px`;
|
|
526
|
-
this.portalHost.style.left = `${controlRect.left}px`;
|
|
537
|
+
this.portalHost.style.top = `${placement.top + (viewport?.offsetTop ?? 0)}px`;
|
|
538
|
+
this.portalHost.style.left = `${controlRect.left + (viewport?.offsetLeft ?? 0)}px`;
|
|
527
539
|
this.portalHost.style.width = `${controlRect.width}px`;
|
|
528
540
|
}
|
|
529
541
|
}
|
|
@@ -534,13 +546,15 @@ var ForgeSelect = class {
|
|
|
534
546
|
this.destroyed = true;
|
|
535
547
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
536
548
|
this.ajaxController?.abort();
|
|
549
|
+
for (const controller of this.prefetchControllers) controller.abort();
|
|
550
|
+
this.prefetchControllers.clear();
|
|
551
|
+
this.remoteInFlight.clear();
|
|
537
552
|
if (this.scrollRafId != null) cancelAnimationFrame(this.scrollRafId);
|
|
538
553
|
if (this.typeaheadTimer) clearTimeout(this.typeaheadTimer);
|
|
539
554
|
this.nativeSelect?.removeEventListener("change", this.onNativeChange);
|
|
540
555
|
this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
|
|
541
556
|
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
542
|
-
this.
|
|
543
|
-
this.rowHeightCache.clear();
|
|
557
|
+
this.clearRowCaches();
|
|
544
558
|
this.searchIndex.clear();
|
|
545
559
|
this.portalHost?.remove();
|
|
546
560
|
this.root.remove();
|
|
@@ -588,10 +602,23 @@ var ForgeSelect = class {
|
|
|
588
602
|
}
|
|
589
603
|
if (options.templateResult !== void 0) this.opts.templateResult = options.templateResult;
|
|
590
604
|
if (options.templateSelection !== void 0) this.opts.templateSelection = options.templateSelection;
|
|
605
|
+
if (options.sanitizeTemplate !== void 0) this.opts.sanitizeTemplate = options.sanitizeTemplate;
|
|
606
|
+
if (options.beforeSelect !== void 0) this.opts.beforeSelect = options.beforeSelect;
|
|
607
|
+
if (options.beforeUnselect !== void 0) this.opts.beforeUnselect = options.beforeUnselect;
|
|
608
|
+
if (options.beforeCreate !== void 0) this.opts.beforeCreate = options.beforeCreate;
|
|
609
|
+
if (options.createOption !== void 0) this.opts.createOption = options.createOption;
|
|
610
|
+
if (options.missingSelectionPolicy !== void 0) this.opts.missingSelectionPolicy = options.missingSelectionPolicy;
|
|
611
|
+
if (options.duplicateValuePolicy !== void 0) {
|
|
612
|
+
this.opts.duplicateValuePolicy = options.duplicateValuePolicy;
|
|
613
|
+
this.rebuildOptionIndexes();
|
|
614
|
+
}
|
|
591
615
|
if (options.filterOption !== void 0) this.opts.filterOption = options.filterOption;
|
|
592
616
|
if (options.searchFields !== void 0) this.opts.searchFields = options.searchFields;
|
|
593
617
|
if (options.tokenSearch !== void 0) this.opts.tokenSearch = options.tokenSearch;
|
|
594
|
-
if (options.accentInsensitive !== void 0)
|
|
618
|
+
if (options.accentInsensitive !== void 0) {
|
|
619
|
+
this.opts.accentInsensitive = options.accentInsensitive;
|
|
620
|
+
this.rebuildOptionIndexes();
|
|
621
|
+
}
|
|
595
622
|
if (options.searchScorer !== void 0) this.opts.searchScorer = options.searchScorer;
|
|
596
623
|
if (options.highlightSearch !== void 0) this.opts.highlightSearch = options.highlightSearch;
|
|
597
624
|
if (options.minSearchLength !== void 0)
|
|
@@ -619,8 +646,7 @@ var ForgeSelect = class {
|
|
|
619
646
|
}
|
|
620
647
|
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
621
648
|
this.updateSearchVisibility();
|
|
622
|
-
this.
|
|
623
|
-
this.rowHeightCache.clear();
|
|
649
|
+
this.clearRowCaches();
|
|
624
650
|
this.searchIndex.clear();
|
|
625
651
|
this.renderValue();
|
|
626
652
|
if (this.isOpen) this.renderList();
|
|
@@ -652,6 +678,7 @@ var ForgeSelect = class {
|
|
|
652
678
|
}
|
|
653
679
|
clearRemoteCache() {
|
|
654
680
|
this.remoteCache.clear();
|
|
681
|
+
this.remoteInFlight.clear();
|
|
655
682
|
}
|
|
656
683
|
setValue(value, options = {}) {
|
|
657
684
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
@@ -681,11 +708,29 @@ var ForgeSelect = class {
|
|
|
681
708
|
this.remoteLoaded = true;
|
|
682
709
|
this.page = 0;
|
|
683
710
|
this.hasMore = false;
|
|
711
|
+
this.nextCursor = void 0;
|
|
712
|
+
const previousData = this.data;
|
|
684
713
|
this.data = data;
|
|
714
|
+
try {
|
|
715
|
+
this.rebuildOptionIndexes();
|
|
716
|
+
} catch (error) {
|
|
717
|
+
this.data = previousData;
|
|
718
|
+
this.rebuildOptionIndexes();
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
const missing = this.selected.filter((value) => !this.optionByValue.has(value));
|
|
722
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "error") {
|
|
723
|
+
this.data = previousData;
|
|
724
|
+
this.rebuildOptionIndexes();
|
|
725
|
+
throw new Error(`ForgeSelect: setData() is missing selected value(s): ${missing.join(", ")}`);
|
|
726
|
+
}
|
|
685
727
|
this.opts.data = data;
|
|
728
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "prune") {
|
|
729
|
+
this.selected = this.selected.filter((value) => this.optionByValue.has(value));
|
|
730
|
+
this.afterSelectionChange();
|
|
731
|
+
}
|
|
686
732
|
this.updateSearchVisibility();
|
|
687
|
-
this.
|
|
688
|
-
this.rowHeightCache.clear();
|
|
733
|
+
this.clearRowCaches();
|
|
689
734
|
this.searchIndex.clear();
|
|
690
735
|
this.highlightedIndex = -1;
|
|
691
736
|
if (this.isOpen) this.renderList();
|
|
@@ -826,6 +871,7 @@ var ForgeSelect = class {
|
|
|
826
871
|
if (portalParent) {
|
|
827
872
|
this.portalHost = document.createElement("div");
|
|
828
873
|
this.portalHost.className = "forge-select forge-select--portal-host";
|
|
874
|
+
this.portalHost.style.direction = getComputedStyle(this.root).direction;
|
|
829
875
|
this.portalHost.dataset.theme = this.opts.theme;
|
|
830
876
|
this.portalHost.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
831
877
|
this.portalHost.append(this.dropdown);
|
|
@@ -856,10 +902,24 @@ var ForgeSelect = class {
|
|
|
856
902
|
});
|
|
857
903
|
this.clearBtn.addEventListener("click", (event) => {
|
|
858
904
|
event.stopPropagation();
|
|
905
|
+
if (this.selected.some((value) => {
|
|
906
|
+
const option = this.findOption(value) ?? this.selectedOptions.get(value) ?? { value, label: value };
|
|
907
|
+
return this.opts.beforeUnselect?.(option) === false;
|
|
908
|
+
}))
|
|
909
|
+
return;
|
|
859
910
|
this.clearSelection();
|
|
860
911
|
});
|
|
861
912
|
if (this.searchInput) {
|
|
913
|
+
let composing = false;
|
|
914
|
+
this.searchInput.addEventListener("compositionstart", () => {
|
|
915
|
+
composing = true;
|
|
916
|
+
});
|
|
917
|
+
this.searchInput.addEventListener("compositionend", () => {
|
|
918
|
+
composing = false;
|
|
919
|
+
this.applySearchQuery(this.searchInput.value, true);
|
|
920
|
+
});
|
|
862
921
|
this.searchInput.addEventListener("input", () => {
|
|
922
|
+
if (composing) return;
|
|
863
923
|
this.applySearchQuery(this.searchInput.value, true);
|
|
864
924
|
});
|
|
865
925
|
this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
@@ -869,21 +929,22 @@ var ForgeSelect = class {
|
|
|
869
929
|
const labels = text.split(/[,\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
870
930
|
if (labels.length < 2) return;
|
|
871
931
|
event.preventDefault();
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (
|
|
883
|
-
this.
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
|
|
932
|
+
void Promise.all(labels.map((label) => this.createTag(label))).then((results) => {
|
|
933
|
+
const created = results.filter((result) => result !== void 0);
|
|
934
|
+
if (created.length === 0 || this.destroyed) return;
|
|
935
|
+
this.searchInput.value = "";
|
|
936
|
+
this.query = "";
|
|
937
|
+
this.afterSelectionChange();
|
|
938
|
+
for (const result of created) {
|
|
939
|
+
if (result.created) this.emitter.emit("create", result.option);
|
|
940
|
+
this.emitter.emit("select", result.option);
|
|
941
|
+
}
|
|
942
|
+
if (this.opts.closeOnSelect) this.close();
|
|
943
|
+
else this.renderList();
|
|
944
|
+
}).catch((cause) => {
|
|
945
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
946
|
+
this.emitter.emit("error", error);
|
|
947
|
+
});
|
|
887
948
|
});
|
|
888
949
|
}
|
|
889
950
|
this.list.addEventListener("click", (event) => {
|
|
@@ -938,7 +999,7 @@ var ForgeSelect = class {
|
|
|
938
999
|
this.renderList();
|
|
939
1000
|
}
|
|
940
1001
|
handleKeydown(event) {
|
|
941
|
-
if (this.isDisabled) return;
|
|
1002
|
+
if (this.isDisabled || event.isComposing || event.keyCode === 229) return;
|
|
942
1003
|
switch (event.key) {
|
|
943
1004
|
case "Enter":
|
|
944
1005
|
event.preventDefault();
|
|
@@ -1156,23 +1217,27 @@ var ForgeSelect = class {
|
|
|
1156
1217
|
}
|
|
1157
1218
|
}
|
|
1158
1219
|
findOption(value) {
|
|
1159
|
-
return
|
|
1220
|
+
return this.optionByValue.get(value);
|
|
1160
1221
|
}
|
|
1161
1222
|
findOptionByLabel(label) {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1223
|
+
return this.optionByLabel.get(normalizeSearchText(label, this.opts.accentInsensitive));
|
|
1224
|
+
}
|
|
1225
|
+
rebuildOptionIndexes() {
|
|
1226
|
+
this.optionByValue.clear();
|
|
1227
|
+
this.optionByLabel.clear();
|
|
1228
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1229
|
+
const visit = (option) => {
|
|
1230
|
+
if (this.optionByValue.has(option.value)) duplicates.add(option.value);
|
|
1231
|
+
else this.optionByValue.set(option.value, option);
|
|
1232
|
+
const label = normalizeSearchText(option.label, this.opts.accentInsensitive);
|
|
1233
|
+
if (!this.optionByLabel.has(label)) this.optionByLabel.set(label, option);
|
|
1234
|
+
option.children?.forEach(visit);
|
|
1170
1235
|
};
|
|
1171
|
-
for (const item of this.data)
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1236
|
+
for (const item of this.data) (isGroup(item) ? item.options : [item]).forEach(visit);
|
|
1237
|
+
if (duplicates.size === 0 || this.opts.duplicateValuePolicy === "ignore") return;
|
|
1238
|
+
const message = `ForgeSelect: duplicate option value(s): ${[...duplicates].join(", ")}`;
|
|
1239
|
+
if (this.opts.duplicateValuePolicy === "error") throw new Error(message);
|
|
1240
|
+
console.warn(message);
|
|
1176
1241
|
}
|
|
1177
1242
|
/** Selects an existing option matching `label` exactly, or creates and selects a new one. */
|
|
1178
1243
|
createTag(label) {
|
|
@@ -1185,15 +1250,31 @@ var ForgeSelect = class {
|
|
|
1185
1250
|
this.announceMaximum(existing);
|
|
1186
1251
|
return void 0;
|
|
1187
1252
|
}
|
|
1253
|
+
if (this.opts.beforeSelect?.(existing) === false) return void 0;
|
|
1188
1254
|
this.selectValue(existing.value, false);
|
|
1189
1255
|
return { option: existing, created: false };
|
|
1190
1256
|
}
|
|
1191
|
-
|
|
1257
|
+
if (this.opts.beforeCreate?.(trimmed) === false) return void 0;
|
|
1258
|
+
const created = this.opts.createOption?.(trimmed) ?? { value: trimmed, label: trimmed };
|
|
1259
|
+
if (created instanceof Promise) {
|
|
1260
|
+
return created.then((option) => option ? this.addCreatedOption(option) : void 0);
|
|
1261
|
+
}
|
|
1262
|
+
return created ? this.addCreatedOption(created) : void 0;
|
|
1263
|
+
}
|
|
1264
|
+
addCreatedOption(option) {
|
|
1192
1265
|
if (this.opts.multiple && !this.canSelectOption(option)) {
|
|
1193
1266
|
this.announceMaximum(option);
|
|
1194
1267
|
return void 0;
|
|
1195
1268
|
}
|
|
1269
|
+
const duplicate = this.findOption(option.value);
|
|
1270
|
+
if (duplicate) {
|
|
1271
|
+
if (this.selected.includes(duplicate.value)) return void 0;
|
|
1272
|
+
if (this.opts.beforeSelect?.(duplicate) === false) return void 0;
|
|
1273
|
+
this.selectValue(duplicate.value, false);
|
|
1274
|
+
return { option: duplicate, created: false };
|
|
1275
|
+
}
|
|
1196
1276
|
this.data.push(option);
|
|
1277
|
+
this.rebuildOptionIndexes();
|
|
1197
1278
|
this.selectValue(option.value, false);
|
|
1198
1279
|
return { option, created: true };
|
|
1199
1280
|
}
|
|
@@ -1201,8 +1282,18 @@ var ForgeSelect = class {
|
|
|
1201
1282
|
const label = this.query.trim();
|
|
1202
1283
|
if (!label) return;
|
|
1203
1284
|
const result = this.createTag(label);
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1285
|
+
if (result instanceof Promise) {
|
|
1286
|
+
void result.then((created) => this.finishCreateFromQuery(created, label)).catch((cause) => {
|
|
1287
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1288
|
+
this.emitter.emit("error", error);
|
|
1289
|
+
});
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
this.finishCreateFromQuery(result, label);
|
|
1293
|
+
}
|
|
1294
|
+
finishCreateFromQuery(result, sourceLabel) {
|
|
1295
|
+
if (!result || this.destroyed) return;
|
|
1296
|
+
if (this.searchInput && this.query.trim() === sourceLabel) {
|
|
1206
1297
|
this.searchInput.value = "";
|
|
1207
1298
|
this.query = "";
|
|
1208
1299
|
}
|
|
@@ -1223,9 +1314,11 @@ var ForgeSelect = class {
|
|
|
1223
1314
|
if (this.opts.multiple) {
|
|
1224
1315
|
let changed = false;
|
|
1225
1316
|
if (this.selected.includes(value)) {
|
|
1317
|
+
if (this.opts.beforeUnselect?.(item.option) === false) return;
|
|
1226
1318
|
this.deselectValue(value, true);
|
|
1227
1319
|
changed = true;
|
|
1228
1320
|
} else if (this.canSelectOption(item.option)) {
|
|
1321
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1229
1322
|
this.selectValue(value, true);
|
|
1230
1323
|
changed = true;
|
|
1231
1324
|
} else {
|
|
@@ -1233,6 +1326,7 @@ var ForgeSelect = class {
|
|
|
1233
1326
|
}
|
|
1234
1327
|
if (changed && this.opts.closeOnSelect) this.close();
|
|
1235
1328
|
} else {
|
|
1329
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1236
1330
|
this.selectValue(value, true);
|
|
1237
1331
|
this.close();
|
|
1238
1332
|
this.control.focus();
|
|
@@ -1257,7 +1351,7 @@ var ForgeSelect = class {
|
|
|
1257
1351
|
tag.className = "forge-select__tag";
|
|
1258
1352
|
const label = document.createElement("span");
|
|
1259
1353
|
label.className = "forge-select__tag-label";
|
|
1260
|
-
renderOptionContent(label, option, this.opts.templateSelection, "inline");
|
|
1354
|
+
renderOptionContent(label, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1261
1355
|
const remove = document.createElement("button");
|
|
1262
1356
|
remove.type = "button";
|
|
1263
1357
|
remove.className = "forge-select__tag-remove";
|
|
@@ -1265,7 +1359,7 @@ var ForgeSelect = class {
|
|
|
1265
1359
|
remove.textContent = "\xD7";
|
|
1266
1360
|
remove.addEventListener("click", (event) => {
|
|
1267
1361
|
event.stopPropagation();
|
|
1268
|
-
if (!this.isDisabled) this.deselectValue(value, true);
|
|
1362
|
+
if (!this.isDisabled && this.opts.beforeUnselect?.(option) !== false) this.deselectValue(value, true);
|
|
1269
1363
|
});
|
|
1270
1364
|
tag.append(label, remove);
|
|
1271
1365
|
if (this.opts.sortable) {
|
|
@@ -1285,7 +1379,7 @@ var ForgeSelect = class {
|
|
|
1285
1379
|
};
|
|
1286
1380
|
const span = document.createElement("span");
|
|
1287
1381
|
span.className = "forge-select__single-value";
|
|
1288
|
-
renderOptionContent(span, option, this.opts.templateSelection, "inline");
|
|
1382
|
+
renderOptionContent(span, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1289
1383
|
this.valueEl.append(span);
|
|
1290
1384
|
}
|
|
1291
1385
|
}
|
|
@@ -1394,7 +1488,14 @@ var ForgeSelect = class {
|
|
|
1394
1488
|
accentInsensitive: this.opts.accentInsensitive,
|
|
1395
1489
|
scorer: this.opts.searchScorer
|
|
1396
1490
|
}) > 0);
|
|
1397
|
-
const
|
|
1491
|
+
const subtreeMatchCache = /* @__PURE__ */ new Map();
|
|
1492
|
+
const subtreeMatches = (option) => {
|
|
1493
|
+
const cached = subtreeMatchCache.get(option);
|
|
1494
|
+
if (cached !== void 0) return cached;
|
|
1495
|
+
const result = query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
1496
|
+
subtreeMatchCache.set(option, result);
|
|
1497
|
+
return result;
|
|
1498
|
+
};
|
|
1398
1499
|
const pushOption = (option, depth, parentValue) => {
|
|
1399
1500
|
let navIndex = -1;
|
|
1400
1501
|
const interactionDisabled = this.isOptionDisabled(option) || this.hasReachedMaximum() && !this.selected.includes(option.value);
|
|
@@ -1449,8 +1550,20 @@ var ForgeSelect = class {
|
|
|
1449
1550
|
usesVirtualScroll() {
|
|
1450
1551
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1451
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
|
+
}
|
|
1452
1565
|
rowKey(row, index) {
|
|
1453
|
-
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1566
|
+
if (row.kind === "option") return `option:${row.option.value}:${index}`;
|
|
1454
1567
|
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1455
1568
|
return `${row.kind}:${index}`;
|
|
1456
1569
|
}
|
|
@@ -1492,7 +1605,14 @@ var ForgeSelect = class {
|
|
|
1492
1605
|
if (virtual) {
|
|
1493
1606
|
const viewport = clientHeight || rowHeight * 8;
|
|
1494
1607
|
if (this.opts.variableItemHeight) {
|
|
1495
|
-
|
|
1608
|
+
let low = 0;
|
|
1609
|
+
let high = this.rows.length;
|
|
1610
|
+
while (low < high) {
|
|
1611
|
+
const middle = low + high >>> 1;
|
|
1612
|
+
if (offsets[middle + 1] < scrollTop) low = middle + 1;
|
|
1613
|
+
else high = middle;
|
|
1614
|
+
}
|
|
1615
|
+
start = low;
|
|
1496
1616
|
start = Math.max(0, start - VIRTUAL_BUFFER);
|
|
1497
1617
|
end = start;
|
|
1498
1618
|
const target = scrollTop + viewport + VIRTUAL_BUFFER * rowHeight;
|
|
@@ -1509,7 +1629,13 @@ var ForgeSelect = class {
|
|
|
1509
1629
|
}
|
|
1510
1630
|
const appended = [];
|
|
1511
1631
|
for (let i = start; i < end; i++) {
|
|
1512
|
-
const
|
|
1632
|
+
const key = this.rowKey(this.rows[i], i);
|
|
1633
|
+
const element = this.renderRow(this.rows[i], this.rowElementCache.get(key));
|
|
1634
|
+
this.rowElementCache.set(key, element);
|
|
1635
|
+
if (this.rowElementCache.size > ROW_CACHE_LIMIT) {
|
|
1636
|
+
const oldest = this.rowElementCache.keys().next().value;
|
|
1637
|
+
this.rowElementCache.delete(oldest);
|
|
1638
|
+
}
|
|
1513
1639
|
this.list.append(element);
|
|
1514
1640
|
appended.push(element);
|
|
1515
1641
|
}
|
|
@@ -1536,8 +1662,27 @@ var ForgeSelect = class {
|
|
|
1536
1662
|
}
|
|
1537
1663
|
this.updateActiveDescendant();
|
|
1538
1664
|
}
|
|
1539
|
-
renderRow(row) {
|
|
1540
|
-
const li = document.createElement("li");
|
|
1665
|
+
renderRow(row, recycled) {
|
|
1666
|
+
const li = recycled ?? document.createElement("li");
|
|
1667
|
+
li.replaceChildren();
|
|
1668
|
+
li.className = "";
|
|
1669
|
+
for (const attribute of [
|
|
1670
|
+
"role",
|
|
1671
|
+
"id",
|
|
1672
|
+
"aria-hidden",
|
|
1673
|
+
"aria-selected",
|
|
1674
|
+
"aria-disabled",
|
|
1675
|
+
"aria-expanded",
|
|
1676
|
+
"aria-level",
|
|
1677
|
+
"data-nav-index",
|
|
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"
|
|
1684
|
+
])
|
|
1685
|
+
li.removeAttribute(attribute);
|
|
1541
1686
|
switch (row.kind) {
|
|
1542
1687
|
case "group":
|
|
1543
1688
|
li.className = "forge-select__group-label";
|
|
@@ -1585,45 +1730,47 @@ var ForgeSelect = class {
|
|
|
1585
1730
|
li.textContent = format(this.strings.createOption, { query: this.query.trim() });
|
|
1586
1731
|
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
1587
1732
|
break;
|
|
1588
|
-
case "option":
|
|
1589
|
-
li
|
|
1590
|
-
li.dataset.optionValue = row.option.value;
|
|
1591
|
-
if (row.option.className) li.classList.add(...row.option.className.trim().split(/\s+/).filter(Boolean));
|
|
1592
|
-
li.setAttribute("role", "option");
|
|
1593
|
-
const isSelected = this.selected.includes(row.option.value);
|
|
1594
|
-
li.setAttribute("aria-selected", String(isSelected));
|
|
1595
|
-
if (isSelected) li.classList.add("forge-select__option--selected");
|
|
1596
|
-
if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
|
|
1597
|
-
li.classList.add("forge-select__option--indeterminate");
|
|
1598
|
-
li.dataset.selectionState = "mixed";
|
|
1599
|
-
}
|
|
1600
|
-
if (row.depth > 0) {
|
|
1601
|
-
li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
|
|
1602
|
-
}
|
|
1603
|
-
if (this.isOptionDisabled(row.option) || this.hasReachedMaximum() && !this.selected.includes(row.option.value)) {
|
|
1604
|
-
li.classList.add("forge-select__option--disabled");
|
|
1605
|
-
li.setAttribute("aria-disabled", "true");
|
|
1606
|
-
} else {
|
|
1607
|
-
li.id = `${this.uid}-nav-${row.navIndex}`;
|
|
1608
|
-
li.dataset.navIndex = String(row.navIndex);
|
|
1609
|
-
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
1610
|
-
}
|
|
1611
|
-
if (row.hasChildren) {
|
|
1612
|
-
const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
|
|
1613
|
-
li.setAttribute("aria-expanded", String(expanded));
|
|
1614
|
-
const twisty = document.createElement("span");
|
|
1615
|
-
twisty.className = "forge-select__twisty";
|
|
1616
|
-
twisty.dataset.twisty = row.option.value;
|
|
1617
|
-
twisty.setAttribute("aria-hidden", "true");
|
|
1618
|
-
twisty.textContent = expanded ? "\u25BC" : "\u25B6";
|
|
1619
|
-
li.append(twisty);
|
|
1620
|
-
}
|
|
1621
|
-
li.append(this.optionContent(row.option));
|
|
1733
|
+
case "option":
|
|
1734
|
+
this.renderOptionRow(li, row);
|
|
1622
1735
|
break;
|
|
1623
|
-
}
|
|
1624
1736
|
}
|
|
1625
1737
|
return li;
|
|
1626
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
|
+
}
|
|
1627
1774
|
/**
|
|
1628
1775
|
* Rendered row content is cached per option value and cloned on each render,
|
|
1629
1776
|
* so templates run once per option instead of once per scroll frame.
|
|
@@ -1657,7 +1804,7 @@ var ForgeSelect = class {
|
|
|
1657
1804
|
if (!cached) {
|
|
1658
1805
|
const holder = document.createElement("span");
|
|
1659
1806
|
holder.className = "forge-select__option-content";
|
|
1660
|
-
renderOptionContent(holder, option, this.opts.templateResult);
|
|
1807
|
+
renderOptionContent(holder, option, this.opts.templateResult, "row", this.opts.sanitizeTemplate);
|
|
1661
1808
|
if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
|
|
1662
1809
|
const oldest = this.rowContentCache.keys().next().value;
|
|
1663
1810
|
this.rowContentCache.delete(oldest);
|
|
@@ -1746,6 +1893,7 @@ var ForgeSelect = class {
|
|
|
1746
1893
|
this.ajaxController = null;
|
|
1747
1894
|
this.page = 0;
|
|
1748
1895
|
this.hasMore = true;
|
|
1896
|
+
this.nextCursor = void 0;
|
|
1749
1897
|
this.setLoading(true);
|
|
1750
1898
|
this.loadingMore = false;
|
|
1751
1899
|
this.loadError = null;
|
|
@@ -1760,17 +1908,27 @@ var ForgeSelect = class {
|
|
|
1760
1908
|
this.loading = loading;
|
|
1761
1909
|
this.emitter.emit("loading", loading);
|
|
1762
1910
|
}
|
|
1763
|
-
remoteCacheKey(query, page) {
|
|
1764
|
-
return `${query}\0${page}`;
|
|
1911
|
+
remoteCacheKey(query, page, cursor) {
|
|
1912
|
+
return `${query}\0${cursor ?? page}`;
|
|
1765
1913
|
}
|
|
1766
|
-
|
|
1914
|
+
fetchRemoteResult(query, page, signal, cursor) {
|
|
1915
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1916
|
+
const pending = this.remoteInFlight.get(key);
|
|
1917
|
+
if (pending) return pending;
|
|
1918
|
+
const ajax = this.opts.ajax;
|
|
1919
|
+
const request = this.requestRemote(query, page, signal, cursor).then((json) => normalizeRemoteResult(ajax, json)).finally(() => this.remoteInFlight.delete(key));
|
|
1920
|
+
this.remoteInFlight.set(key, request);
|
|
1921
|
+
return request;
|
|
1922
|
+
}
|
|
1923
|
+
async requestRemote(query, page, signal, cursor) {
|
|
1767
1924
|
const ajax = this.opts.ajax;
|
|
1768
1925
|
const attempts = Math.max(0, Math.floor(ajax.retry ?? 0)) + 1;
|
|
1769
1926
|
let lastError;
|
|
1770
1927
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1771
1928
|
try {
|
|
1772
|
-
if (ajax.request)
|
|
1773
|
-
|
|
1929
|
+
if (ajax.request)
|
|
1930
|
+
return cursor === void 0 ? await ajax.request(query, page, signal) : await ajax.request(query, page, signal, cursor);
|
|
1931
|
+
const response = await fetch(buildUrl(ajax, query, page, cursor), { signal });
|
|
1774
1932
|
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1775
1933
|
return await response.json();
|
|
1776
1934
|
} catch (error) {
|
|
@@ -1798,10 +1956,13 @@ var ForgeSelect = class {
|
|
|
1798
1956
|
const key = this.remoteCacheKey(query, 0);
|
|
1799
1957
|
if (this.remoteCache.get(key)) return;
|
|
1800
1958
|
const controller = new AbortController();
|
|
1959
|
+
this.prefetchControllers.add(controller);
|
|
1801
1960
|
try {
|
|
1802
|
-
const
|
|
1803
|
-
this.remoteCache.set(key,
|
|
1961
|
+
const result = await this.fetchRemoteResult(query, 0, controller.signal);
|
|
1962
|
+
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1804
1963
|
} catch {
|
|
1964
|
+
} finally {
|
|
1965
|
+
this.prefetchControllers.delete(controller);
|
|
1805
1966
|
}
|
|
1806
1967
|
}
|
|
1807
1968
|
/**
|
|
@@ -1827,12 +1988,12 @@ var ForgeSelect = class {
|
|
|
1827
1988
|
const controller = new AbortController();
|
|
1828
1989
|
this.ajaxController = controller;
|
|
1829
1990
|
const page = append ? this.page + 1 : 0;
|
|
1991
|
+
const cursor = append ? this.nextCursor : void 0;
|
|
1830
1992
|
try {
|
|
1831
|
-
const key = this.remoteCacheKey(query, page);
|
|
1993
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1832
1994
|
let result = this.remoteCache.get(key);
|
|
1833
1995
|
if (!result) {
|
|
1834
|
-
|
|
1835
|
-
result = normalizeRemoteResult(ajax, json);
|
|
1996
|
+
result = await this.fetchRemoteResult(query, page, controller.signal, cursor);
|
|
1836
1997
|
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1837
1998
|
}
|
|
1838
1999
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
@@ -1842,20 +2003,20 @@ var ForgeSelect = class {
|
|
|
1842
2003
|
this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
|
|
1843
2004
|
} else {
|
|
1844
2005
|
this.data = options;
|
|
1845
|
-
this.
|
|
1846
|
-
this.rowHeightCache.clear();
|
|
2006
|
+
this.clearRowCaches();
|
|
1847
2007
|
}
|
|
1848
2008
|
this.page = page;
|
|
1849
2009
|
this.hasMore = hasMore;
|
|
2010
|
+
this.nextCursor = result.nextCursor;
|
|
1850
2011
|
this.remoteLoaded = true;
|
|
1851
2012
|
this.loadError = null;
|
|
2013
|
+
this.rebuildOptionIndexes();
|
|
1852
2014
|
} catch (cause) {
|
|
1853
2015
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed || controller.signal.aborted) return;
|
|
1854
2016
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1855
2017
|
if (!append) {
|
|
1856
2018
|
this.data = [];
|
|
1857
|
-
this.
|
|
1858
|
-
this.rowHeightCache.clear();
|
|
2019
|
+
this.clearRowCaches();
|
|
1859
2020
|
}
|
|
1860
2021
|
this.hasMore = false;
|
|
1861
2022
|
this.loadError = error;
|