forge-select 0.6.0 → 0.7.0
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 +224 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -4
- package/dist/index.d.ts +33 -4
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +224 -77
- 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
168
|
if (typeof ajax.url === "function") return ajax.url(query, page);
|
|
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,12 +573,16 @@ 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
584
|
this.rowContentCache.clear();
|
|
585
|
+
this.rowElementCache.clear();
|
|
570
586
|
this.rowHeightCache.clear();
|
|
571
587
|
this.searchIndex.clear();
|
|
572
588
|
this.portalHost?.remove();
|
|
@@ -615,10 +631,23 @@ var ForgeSelect = class {
|
|
|
615
631
|
}
|
|
616
632
|
if (options.templateResult !== void 0) this.opts.templateResult = options.templateResult;
|
|
617
633
|
if (options.templateSelection !== void 0) this.opts.templateSelection = options.templateSelection;
|
|
634
|
+
if (options.sanitizeTemplate !== void 0) this.opts.sanitizeTemplate = options.sanitizeTemplate;
|
|
635
|
+
if (options.beforeSelect !== void 0) this.opts.beforeSelect = options.beforeSelect;
|
|
636
|
+
if (options.beforeUnselect !== void 0) this.opts.beforeUnselect = options.beforeUnselect;
|
|
637
|
+
if (options.beforeCreate !== void 0) this.opts.beforeCreate = options.beforeCreate;
|
|
638
|
+
if (options.createOption !== void 0) this.opts.createOption = options.createOption;
|
|
639
|
+
if (options.missingSelectionPolicy !== void 0) this.opts.missingSelectionPolicy = options.missingSelectionPolicy;
|
|
640
|
+
if (options.duplicateValuePolicy !== void 0) {
|
|
641
|
+
this.opts.duplicateValuePolicy = options.duplicateValuePolicy;
|
|
642
|
+
this.rebuildOptionIndexes();
|
|
643
|
+
}
|
|
618
644
|
if (options.filterOption !== void 0) this.opts.filterOption = options.filterOption;
|
|
619
645
|
if (options.searchFields !== void 0) this.opts.searchFields = options.searchFields;
|
|
620
646
|
if (options.tokenSearch !== void 0) this.opts.tokenSearch = options.tokenSearch;
|
|
621
|
-
if (options.accentInsensitive !== void 0)
|
|
647
|
+
if (options.accentInsensitive !== void 0) {
|
|
648
|
+
this.opts.accentInsensitive = options.accentInsensitive;
|
|
649
|
+
this.rebuildOptionIndexes();
|
|
650
|
+
}
|
|
622
651
|
if (options.searchScorer !== void 0) this.opts.searchScorer = options.searchScorer;
|
|
623
652
|
if (options.highlightSearch !== void 0) this.opts.highlightSearch = options.highlightSearch;
|
|
624
653
|
if (options.minSearchLength !== void 0)
|
|
@@ -647,6 +676,7 @@ var ForgeSelect = class {
|
|
|
647
676
|
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
648
677
|
this.updateSearchVisibility();
|
|
649
678
|
this.rowContentCache.clear();
|
|
679
|
+
this.rowElementCache.clear();
|
|
650
680
|
this.rowHeightCache.clear();
|
|
651
681
|
this.searchIndex.clear();
|
|
652
682
|
this.renderValue();
|
|
@@ -679,6 +709,7 @@ var ForgeSelect = class {
|
|
|
679
709
|
}
|
|
680
710
|
clearRemoteCache() {
|
|
681
711
|
this.remoteCache.clear();
|
|
712
|
+
this.remoteInFlight.clear();
|
|
682
713
|
}
|
|
683
714
|
setValue(value, options = {}) {
|
|
684
715
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
@@ -708,10 +739,30 @@ var ForgeSelect = class {
|
|
|
708
739
|
this.remoteLoaded = true;
|
|
709
740
|
this.page = 0;
|
|
710
741
|
this.hasMore = false;
|
|
742
|
+
this.nextCursor = void 0;
|
|
743
|
+
const previousData = this.data;
|
|
711
744
|
this.data = data;
|
|
745
|
+
try {
|
|
746
|
+
this.rebuildOptionIndexes();
|
|
747
|
+
} catch (error) {
|
|
748
|
+
this.data = previousData;
|
|
749
|
+
this.rebuildOptionIndexes();
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
const missing = this.selected.filter((value) => !this.optionByValue.has(value));
|
|
753
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "error") {
|
|
754
|
+
this.data = previousData;
|
|
755
|
+
this.rebuildOptionIndexes();
|
|
756
|
+
throw new Error(`ForgeSelect: setData() is missing selected value(s): ${missing.join(", ")}`);
|
|
757
|
+
}
|
|
712
758
|
this.opts.data = data;
|
|
759
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "prune") {
|
|
760
|
+
this.selected = this.selected.filter((value) => this.optionByValue.has(value));
|
|
761
|
+
this.afterSelectionChange();
|
|
762
|
+
}
|
|
713
763
|
this.updateSearchVisibility();
|
|
714
764
|
this.rowContentCache.clear();
|
|
765
|
+
this.rowElementCache.clear();
|
|
715
766
|
this.rowHeightCache.clear();
|
|
716
767
|
this.searchIndex.clear();
|
|
717
768
|
this.highlightedIndex = -1;
|
|
@@ -883,10 +934,24 @@ var ForgeSelect = class {
|
|
|
883
934
|
});
|
|
884
935
|
this.clearBtn.addEventListener("click", (event) => {
|
|
885
936
|
event.stopPropagation();
|
|
937
|
+
if (this.selected.some((value) => {
|
|
938
|
+
const option = this.findOption(value) ?? this.selectedOptions.get(value) ?? { value, label: value };
|
|
939
|
+
return this.opts.beforeUnselect?.(option) === false;
|
|
940
|
+
}))
|
|
941
|
+
return;
|
|
886
942
|
this.clearSelection();
|
|
887
943
|
});
|
|
888
944
|
if (this.searchInput) {
|
|
945
|
+
let composing = false;
|
|
946
|
+
this.searchInput.addEventListener("compositionstart", () => {
|
|
947
|
+
composing = true;
|
|
948
|
+
});
|
|
949
|
+
this.searchInput.addEventListener("compositionend", () => {
|
|
950
|
+
composing = false;
|
|
951
|
+
this.applySearchQuery(this.searchInput.value, true);
|
|
952
|
+
});
|
|
889
953
|
this.searchInput.addEventListener("input", () => {
|
|
954
|
+
if (composing) return;
|
|
890
955
|
this.applySearchQuery(this.searchInput.value, true);
|
|
891
956
|
});
|
|
892
957
|
this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
@@ -896,21 +961,22 @@ var ForgeSelect = class {
|
|
|
896
961
|
const labels = text.split(/[,\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
897
962
|
if (labels.length < 2) return;
|
|
898
963
|
event.preventDefault();
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (
|
|
910
|
-
this.
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
964
|
+
void Promise.all(labels.map((label) => this.createTag(label))).then((results) => {
|
|
965
|
+
const created = results.filter((result) => result !== void 0);
|
|
966
|
+
if (created.length === 0 || this.destroyed) return;
|
|
967
|
+
this.searchInput.value = "";
|
|
968
|
+
this.query = "";
|
|
969
|
+
this.afterSelectionChange();
|
|
970
|
+
for (const result of created) {
|
|
971
|
+
if (result.created) this.emitter.emit("create", result.option);
|
|
972
|
+
this.emitter.emit("select", result.option);
|
|
973
|
+
}
|
|
974
|
+
if (this.opts.closeOnSelect) this.close();
|
|
975
|
+
else this.renderList();
|
|
976
|
+
}).catch((cause) => {
|
|
977
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
978
|
+
this.emitter.emit("error", error);
|
|
979
|
+
});
|
|
914
980
|
});
|
|
915
981
|
}
|
|
916
982
|
this.list.addEventListener("click", (event) => {
|
|
@@ -965,7 +1031,7 @@ var ForgeSelect = class {
|
|
|
965
1031
|
this.renderList();
|
|
966
1032
|
}
|
|
967
1033
|
handleKeydown(event) {
|
|
968
|
-
if (this.isDisabled) return;
|
|
1034
|
+
if (this.isDisabled || event.isComposing || event.keyCode === 229) return;
|
|
969
1035
|
switch (event.key) {
|
|
970
1036
|
case "Enter":
|
|
971
1037
|
event.preventDefault();
|
|
@@ -1183,23 +1249,27 @@ var ForgeSelect = class {
|
|
|
1183
1249
|
}
|
|
1184
1250
|
}
|
|
1185
1251
|
findOption(value) {
|
|
1186
|
-
return
|
|
1252
|
+
return this.optionByValue.get(value);
|
|
1187
1253
|
}
|
|
1188
1254
|
findOptionByLabel(label) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1255
|
+
return this.optionByLabel.get(normalizeSearchText(label, this.opts.accentInsensitive));
|
|
1256
|
+
}
|
|
1257
|
+
rebuildOptionIndexes() {
|
|
1258
|
+
this.optionByValue.clear();
|
|
1259
|
+
this.optionByLabel.clear();
|
|
1260
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1261
|
+
const visit = (option) => {
|
|
1262
|
+
if (this.optionByValue.has(option.value)) duplicates.add(option.value);
|
|
1263
|
+
else this.optionByValue.set(option.value, option);
|
|
1264
|
+
const label = normalizeSearchText(option.label, this.opts.accentInsensitive);
|
|
1265
|
+
if (!this.optionByLabel.has(label)) this.optionByLabel.set(label, option);
|
|
1266
|
+
option.children?.forEach(visit);
|
|
1197
1267
|
};
|
|
1198
|
-
for (const item of this.data)
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1268
|
+
for (const item of this.data) (isGroup(item) ? item.options : [item]).forEach(visit);
|
|
1269
|
+
if (duplicates.size === 0 || this.opts.duplicateValuePolicy === "ignore") return;
|
|
1270
|
+
const message = `ForgeSelect: duplicate option value(s): ${[...duplicates].join(", ")}`;
|
|
1271
|
+
if (this.opts.duplicateValuePolicy === "error") throw new Error(message);
|
|
1272
|
+
console.warn(message);
|
|
1203
1273
|
}
|
|
1204
1274
|
/** Selects an existing option matching `label` exactly, or creates and selects a new one. */
|
|
1205
1275
|
createTag(label) {
|
|
@@ -1215,12 +1285,26 @@ var ForgeSelect = class {
|
|
|
1215
1285
|
this.selectValue(existing.value, false);
|
|
1216
1286
|
return { option: existing, created: false };
|
|
1217
1287
|
}
|
|
1218
|
-
|
|
1288
|
+
if (this.opts.beforeCreate?.(trimmed) === false) return void 0;
|
|
1289
|
+
const created = this.opts.createOption?.(trimmed) ?? { value: trimmed, label: trimmed };
|
|
1290
|
+
if (created instanceof Promise) {
|
|
1291
|
+
return created.then((option) => option ? this.addCreatedOption(option) : void 0);
|
|
1292
|
+
}
|
|
1293
|
+
return created ? this.addCreatedOption(created) : void 0;
|
|
1294
|
+
}
|
|
1295
|
+
addCreatedOption(option) {
|
|
1219
1296
|
if (this.opts.multiple && !this.canSelectOption(option)) {
|
|
1220
1297
|
this.announceMaximum(option);
|
|
1221
1298
|
return void 0;
|
|
1222
1299
|
}
|
|
1300
|
+
const duplicate = this.findOption(option.value);
|
|
1301
|
+
if (duplicate) {
|
|
1302
|
+
if (this.selected.includes(duplicate.value)) return void 0;
|
|
1303
|
+
this.selectValue(duplicate.value, false);
|
|
1304
|
+
return { option: duplicate, created: false };
|
|
1305
|
+
}
|
|
1223
1306
|
this.data.push(option);
|
|
1307
|
+
this.rebuildOptionIndexes();
|
|
1224
1308
|
this.selectValue(option.value, false);
|
|
1225
1309
|
return { option, created: true };
|
|
1226
1310
|
}
|
|
@@ -1228,8 +1312,18 @@ var ForgeSelect = class {
|
|
|
1228
1312
|
const label = this.query.trim();
|
|
1229
1313
|
if (!label) return;
|
|
1230
1314
|
const result = this.createTag(label);
|
|
1231
|
-
if (
|
|
1232
|
-
|
|
1315
|
+
if (result instanceof Promise) {
|
|
1316
|
+
void result.then((created) => this.finishCreateFromQuery(created, label)).catch((cause) => {
|
|
1317
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1318
|
+
this.emitter.emit("error", error);
|
|
1319
|
+
});
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
this.finishCreateFromQuery(result, label);
|
|
1323
|
+
}
|
|
1324
|
+
finishCreateFromQuery(result, sourceLabel) {
|
|
1325
|
+
if (!result || this.destroyed) return;
|
|
1326
|
+
if (this.searchInput && this.query.trim() === sourceLabel) {
|
|
1233
1327
|
this.searchInput.value = "";
|
|
1234
1328
|
this.query = "";
|
|
1235
1329
|
}
|
|
@@ -1250,9 +1344,11 @@ var ForgeSelect = class {
|
|
|
1250
1344
|
if (this.opts.multiple) {
|
|
1251
1345
|
let changed = false;
|
|
1252
1346
|
if (this.selected.includes(value)) {
|
|
1347
|
+
if (this.opts.beforeUnselect?.(item.option) === false) return;
|
|
1253
1348
|
this.deselectValue(value, true);
|
|
1254
1349
|
changed = true;
|
|
1255
1350
|
} else if (this.canSelectOption(item.option)) {
|
|
1351
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1256
1352
|
this.selectValue(value, true);
|
|
1257
1353
|
changed = true;
|
|
1258
1354
|
} else {
|
|
@@ -1260,6 +1356,7 @@ var ForgeSelect = class {
|
|
|
1260
1356
|
}
|
|
1261
1357
|
if (changed && this.opts.closeOnSelect) this.close();
|
|
1262
1358
|
} else {
|
|
1359
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1263
1360
|
this.selectValue(value, true);
|
|
1264
1361
|
this.close();
|
|
1265
1362
|
this.control.focus();
|
|
@@ -1284,7 +1381,7 @@ var ForgeSelect = class {
|
|
|
1284
1381
|
tag.className = "forge-select__tag";
|
|
1285
1382
|
const label = document.createElement("span");
|
|
1286
1383
|
label.className = "forge-select__tag-label";
|
|
1287
|
-
renderOptionContent(label, option, this.opts.templateSelection, "inline");
|
|
1384
|
+
renderOptionContent(label, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1288
1385
|
const remove = document.createElement("button");
|
|
1289
1386
|
remove.type = "button";
|
|
1290
1387
|
remove.className = "forge-select__tag-remove";
|
|
@@ -1292,7 +1389,7 @@ var ForgeSelect = class {
|
|
|
1292
1389
|
remove.textContent = "\xD7";
|
|
1293
1390
|
remove.addEventListener("click", (event) => {
|
|
1294
1391
|
event.stopPropagation();
|
|
1295
|
-
if (!this.isDisabled) this.deselectValue(value, true);
|
|
1392
|
+
if (!this.isDisabled && this.opts.beforeUnselect?.(option) !== false) this.deselectValue(value, true);
|
|
1296
1393
|
});
|
|
1297
1394
|
tag.append(label, remove);
|
|
1298
1395
|
if (this.opts.sortable) {
|
|
@@ -1312,7 +1409,7 @@ var ForgeSelect = class {
|
|
|
1312
1409
|
};
|
|
1313
1410
|
const span = document.createElement("span");
|
|
1314
1411
|
span.className = "forge-select__single-value";
|
|
1315
|
-
renderOptionContent(span, option, this.opts.templateSelection, "inline");
|
|
1412
|
+
renderOptionContent(span, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1316
1413
|
this.valueEl.append(span);
|
|
1317
1414
|
}
|
|
1318
1415
|
}
|
|
@@ -1421,7 +1518,14 @@ var ForgeSelect = class {
|
|
|
1421
1518
|
accentInsensitive: this.opts.accentInsensitive,
|
|
1422
1519
|
scorer: this.opts.searchScorer
|
|
1423
1520
|
}) > 0);
|
|
1424
|
-
const
|
|
1521
|
+
const subtreeMatchCache = /* @__PURE__ */ new Map();
|
|
1522
|
+
const subtreeMatches = (option) => {
|
|
1523
|
+
const cached = subtreeMatchCache.get(option);
|
|
1524
|
+
if (cached !== void 0) return cached;
|
|
1525
|
+
const result = query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
1526
|
+
subtreeMatchCache.set(option, result);
|
|
1527
|
+
return result;
|
|
1528
|
+
};
|
|
1425
1529
|
const pushOption = (option, depth, parentValue) => {
|
|
1426
1530
|
let navIndex = -1;
|
|
1427
1531
|
const interactionDisabled = this.isOptionDisabled(option) || this.hasReachedMaximum() && !this.selected.includes(option.value);
|
|
@@ -1477,7 +1581,7 @@ var ForgeSelect = class {
|
|
|
1477
1581
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1478
1582
|
}
|
|
1479
1583
|
rowKey(row, index) {
|
|
1480
|
-
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1584
|
+
if (row.kind === "option") return `option:${row.option.value}:${index}`;
|
|
1481
1585
|
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1482
1586
|
return `${row.kind}:${index}`;
|
|
1483
1587
|
}
|
|
@@ -1519,7 +1623,14 @@ var ForgeSelect = class {
|
|
|
1519
1623
|
if (virtual) {
|
|
1520
1624
|
const viewport = clientHeight || rowHeight * 8;
|
|
1521
1625
|
if (this.opts.variableItemHeight) {
|
|
1522
|
-
|
|
1626
|
+
let low = 0;
|
|
1627
|
+
let high = this.rows.length;
|
|
1628
|
+
while (low < high) {
|
|
1629
|
+
const middle = low + high >>> 1;
|
|
1630
|
+
if (offsets[middle + 1] < scrollTop) low = middle + 1;
|
|
1631
|
+
else high = middle;
|
|
1632
|
+
}
|
|
1633
|
+
start = low;
|
|
1523
1634
|
start = Math.max(0, start - VIRTUAL_BUFFER);
|
|
1524
1635
|
end = start;
|
|
1525
1636
|
const target = scrollTop + viewport + VIRTUAL_BUFFER * rowHeight;
|
|
@@ -1536,7 +1647,13 @@ var ForgeSelect = class {
|
|
|
1536
1647
|
}
|
|
1537
1648
|
const appended = [];
|
|
1538
1649
|
for (let i = start; i < end; i++) {
|
|
1539
|
-
const
|
|
1650
|
+
const key = this.rowKey(this.rows[i], i);
|
|
1651
|
+
const element = this.renderRow(this.rows[i], this.rowElementCache.get(key));
|
|
1652
|
+
this.rowElementCache.set(key, element);
|
|
1653
|
+
if (this.rowElementCache.size > ROW_CACHE_LIMIT) {
|
|
1654
|
+
const oldest = this.rowElementCache.keys().next().value;
|
|
1655
|
+
this.rowElementCache.delete(oldest);
|
|
1656
|
+
}
|
|
1540
1657
|
this.list.append(element);
|
|
1541
1658
|
appended.push(element);
|
|
1542
1659
|
}
|
|
@@ -1563,8 +1680,22 @@ var ForgeSelect = class {
|
|
|
1563
1680
|
}
|
|
1564
1681
|
this.updateActiveDescendant();
|
|
1565
1682
|
}
|
|
1566
|
-
renderRow(row) {
|
|
1567
|
-
const li = document.createElement("li");
|
|
1683
|
+
renderRow(row, recycled) {
|
|
1684
|
+
const li = recycled ?? document.createElement("li");
|
|
1685
|
+
li.replaceChildren();
|
|
1686
|
+
li.className = "";
|
|
1687
|
+
for (const attribute of [
|
|
1688
|
+
"role",
|
|
1689
|
+
"id",
|
|
1690
|
+
"aria-hidden",
|
|
1691
|
+
"aria-selected",
|
|
1692
|
+
"aria-disabled",
|
|
1693
|
+
"aria-expanded",
|
|
1694
|
+
"aria-level",
|
|
1695
|
+
"data-nav-index",
|
|
1696
|
+
"data-option-value"
|
|
1697
|
+
])
|
|
1698
|
+
li.removeAttribute(attribute);
|
|
1568
1699
|
switch (row.kind) {
|
|
1569
1700
|
case "group":
|
|
1570
1701
|
li.className = "forge-select__group-label";
|
|
@@ -1684,7 +1815,7 @@ var ForgeSelect = class {
|
|
|
1684
1815
|
if (!cached) {
|
|
1685
1816
|
const holder = document.createElement("span");
|
|
1686
1817
|
holder.className = "forge-select__option-content";
|
|
1687
|
-
renderOptionContent(holder, option, this.opts.templateResult);
|
|
1818
|
+
renderOptionContent(holder, option, this.opts.templateResult, "row", this.opts.sanitizeTemplate);
|
|
1688
1819
|
if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
|
|
1689
1820
|
const oldest = this.rowContentCache.keys().next().value;
|
|
1690
1821
|
this.rowContentCache.delete(oldest);
|
|
@@ -1773,6 +1904,7 @@ var ForgeSelect = class {
|
|
|
1773
1904
|
this.ajaxController = null;
|
|
1774
1905
|
this.page = 0;
|
|
1775
1906
|
this.hasMore = true;
|
|
1907
|
+
this.nextCursor = void 0;
|
|
1776
1908
|
this.setLoading(true);
|
|
1777
1909
|
this.loadingMore = false;
|
|
1778
1910
|
this.loadError = null;
|
|
@@ -1787,17 +1919,27 @@ var ForgeSelect = class {
|
|
|
1787
1919
|
this.loading = loading;
|
|
1788
1920
|
this.emitter.emit("loading", loading);
|
|
1789
1921
|
}
|
|
1790
|
-
remoteCacheKey(query, page) {
|
|
1791
|
-
return `${query}\0${page}`;
|
|
1922
|
+
remoteCacheKey(query, page, cursor) {
|
|
1923
|
+
return `${query}\0${cursor ?? page}`;
|
|
1792
1924
|
}
|
|
1793
|
-
|
|
1925
|
+
fetchRemoteResult(query, page, signal, cursor) {
|
|
1926
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1927
|
+
const pending = this.remoteInFlight.get(key);
|
|
1928
|
+
if (pending) return pending;
|
|
1929
|
+
const ajax = this.opts.ajax;
|
|
1930
|
+
const request = this.requestRemote(query, page, signal, cursor).then((json) => normalizeRemoteResult(ajax, json)).finally(() => this.remoteInFlight.delete(key));
|
|
1931
|
+
this.remoteInFlight.set(key, request);
|
|
1932
|
+
return request;
|
|
1933
|
+
}
|
|
1934
|
+
async requestRemote(query, page, signal, cursor) {
|
|
1794
1935
|
const ajax = this.opts.ajax;
|
|
1795
1936
|
const attempts = Math.max(0, Math.floor(ajax.retry ?? 0)) + 1;
|
|
1796
1937
|
let lastError;
|
|
1797
1938
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1798
1939
|
try {
|
|
1799
|
-
if (ajax.request)
|
|
1800
|
-
|
|
1940
|
+
if (ajax.request)
|
|
1941
|
+
return cursor === void 0 ? await ajax.request(query, page, signal) : await ajax.request(query, page, signal, cursor);
|
|
1942
|
+
const response = await fetch(buildUrl(ajax, query, page, cursor), { signal });
|
|
1801
1943
|
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1802
1944
|
return await response.json();
|
|
1803
1945
|
} catch (error) {
|
|
@@ -1825,10 +1967,13 @@ var ForgeSelect = class {
|
|
|
1825
1967
|
const key = this.remoteCacheKey(query, 0);
|
|
1826
1968
|
if (this.remoteCache.get(key)) return;
|
|
1827
1969
|
const controller = new AbortController();
|
|
1970
|
+
this.prefetchControllers.add(controller);
|
|
1828
1971
|
try {
|
|
1829
|
-
const
|
|
1830
|
-
this.remoteCache.set(key,
|
|
1972
|
+
const result = await this.fetchRemoteResult(query, 0, controller.signal);
|
|
1973
|
+
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1831
1974
|
} catch {
|
|
1975
|
+
} finally {
|
|
1976
|
+
this.prefetchControllers.delete(controller);
|
|
1832
1977
|
}
|
|
1833
1978
|
}
|
|
1834
1979
|
/**
|
|
@@ -1854,12 +1999,12 @@ var ForgeSelect = class {
|
|
|
1854
1999
|
const controller = new AbortController();
|
|
1855
2000
|
this.ajaxController = controller;
|
|
1856
2001
|
const page = append ? this.page + 1 : 0;
|
|
2002
|
+
const cursor = append ? this.nextCursor : void 0;
|
|
1857
2003
|
try {
|
|
1858
|
-
const key = this.remoteCacheKey(query, page);
|
|
2004
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1859
2005
|
let result = this.remoteCache.get(key);
|
|
1860
2006
|
if (!result) {
|
|
1861
|
-
|
|
1862
|
-
result = normalizeRemoteResult(ajax, json);
|
|
2007
|
+
result = await this.fetchRemoteResult(query, page, controller.signal, cursor);
|
|
1863
2008
|
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1864
2009
|
}
|
|
1865
2010
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
@@ -1874,8 +2019,10 @@ var ForgeSelect = class {
|
|
|
1874
2019
|
}
|
|
1875
2020
|
this.page = page;
|
|
1876
2021
|
this.hasMore = hasMore;
|
|
2022
|
+
this.nextCursor = result.nextCursor;
|
|
1877
2023
|
this.remoteLoaded = true;
|
|
1878
2024
|
this.loadError = null;
|
|
2025
|
+
this.rebuildOptionIndexes();
|
|
1879
2026
|
} catch (cause) {
|
|
1880
2027
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed || controller.signal.aborted) return;
|
|
1881
2028
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|