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.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
141
|
if (typeof ajax.url === "function") return ajax.url(query, page);
|
|
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,12 +546,16 @@ 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
557
|
this.rowContentCache.clear();
|
|
558
|
+
this.rowElementCache.clear();
|
|
543
559
|
this.rowHeightCache.clear();
|
|
544
560
|
this.searchIndex.clear();
|
|
545
561
|
this.portalHost?.remove();
|
|
@@ -588,10 +604,23 @@ var ForgeSelect = class {
|
|
|
588
604
|
}
|
|
589
605
|
if (options.templateResult !== void 0) this.opts.templateResult = options.templateResult;
|
|
590
606
|
if (options.templateSelection !== void 0) this.opts.templateSelection = options.templateSelection;
|
|
607
|
+
if (options.sanitizeTemplate !== void 0) this.opts.sanitizeTemplate = options.sanitizeTemplate;
|
|
608
|
+
if (options.beforeSelect !== void 0) this.opts.beforeSelect = options.beforeSelect;
|
|
609
|
+
if (options.beforeUnselect !== void 0) this.opts.beforeUnselect = options.beforeUnselect;
|
|
610
|
+
if (options.beforeCreate !== void 0) this.opts.beforeCreate = options.beforeCreate;
|
|
611
|
+
if (options.createOption !== void 0) this.opts.createOption = options.createOption;
|
|
612
|
+
if (options.missingSelectionPolicy !== void 0) this.opts.missingSelectionPolicy = options.missingSelectionPolicy;
|
|
613
|
+
if (options.duplicateValuePolicy !== void 0) {
|
|
614
|
+
this.opts.duplicateValuePolicy = options.duplicateValuePolicy;
|
|
615
|
+
this.rebuildOptionIndexes();
|
|
616
|
+
}
|
|
591
617
|
if (options.filterOption !== void 0) this.opts.filterOption = options.filterOption;
|
|
592
618
|
if (options.searchFields !== void 0) this.opts.searchFields = options.searchFields;
|
|
593
619
|
if (options.tokenSearch !== void 0) this.opts.tokenSearch = options.tokenSearch;
|
|
594
|
-
if (options.accentInsensitive !== void 0)
|
|
620
|
+
if (options.accentInsensitive !== void 0) {
|
|
621
|
+
this.opts.accentInsensitive = options.accentInsensitive;
|
|
622
|
+
this.rebuildOptionIndexes();
|
|
623
|
+
}
|
|
595
624
|
if (options.searchScorer !== void 0) this.opts.searchScorer = options.searchScorer;
|
|
596
625
|
if (options.highlightSearch !== void 0) this.opts.highlightSearch = options.highlightSearch;
|
|
597
626
|
if (options.minSearchLength !== void 0)
|
|
@@ -620,6 +649,7 @@ var ForgeSelect = class {
|
|
|
620
649
|
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
621
650
|
this.updateSearchVisibility();
|
|
622
651
|
this.rowContentCache.clear();
|
|
652
|
+
this.rowElementCache.clear();
|
|
623
653
|
this.rowHeightCache.clear();
|
|
624
654
|
this.searchIndex.clear();
|
|
625
655
|
this.renderValue();
|
|
@@ -652,6 +682,7 @@ var ForgeSelect = class {
|
|
|
652
682
|
}
|
|
653
683
|
clearRemoteCache() {
|
|
654
684
|
this.remoteCache.clear();
|
|
685
|
+
this.remoteInFlight.clear();
|
|
655
686
|
}
|
|
656
687
|
setValue(value, options = {}) {
|
|
657
688
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
@@ -681,10 +712,30 @@ var ForgeSelect = class {
|
|
|
681
712
|
this.remoteLoaded = true;
|
|
682
713
|
this.page = 0;
|
|
683
714
|
this.hasMore = false;
|
|
715
|
+
this.nextCursor = void 0;
|
|
716
|
+
const previousData = this.data;
|
|
684
717
|
this.data = data;
|
|
718
|
+
try {
|
|
719
|
+
this.rebuildOptionIndexes();
|
|
720
|
+
} catch (error) {
|
|
721
|
+
this.data = previousData;
|
|
722
|
+
this.rebuildOptionIndexes();
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
const missing = this.selected.filter((value) => !this.optionByValue.has(value));
|
|
726
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "error") {
|
|
727
|
+
this.data = previousData;
|
|
728
|
+
this.rebuildOptionIndexes();
|
|
729
|
+
throw new Error(`ForgeSelect: setData() is missing selected value(s): ${missing.join(", ")}`);
|
|
730
|
+
}
|
|
685
731
|
this.opts.data = data;
|
|
732
|
+
if (missing.length > 0 && this.opts.missingSelectionPolicy === "prune") {
|
|
733
|
+
this.selected = this.selected.filter((value) => this.optionByValue.has(value));
|
|
734
|
+
this.afterSelectionChange();
|
|
735
|
+
}
|
|
686
736
|
this.updateSearchVisibility();
|
|
687
737
|
this.rowContentCache.clear();
|
|
738
|
+
this.rowElementCache.clear();
|
|
688
739
|
this.rowHeightCache.clear();
|
|
689
740
|
this.searchIndex.clear();
|
|
690
741
|
this.highlightedIndex = -1;
|
|
@@ -856,10 +907,24 @@ var ForgeSelect = class {
|
|
|
856
907
|
});
|
|
857
908
|
this.clearBtn.addEventListener("click", (event) => {
|
|
858
909
|
event.stopPropagation();
|
|
910
|
+
if (this.selected.some((value) => {
|
|
911
|
+
const option = this.findOption(value) ?? this.selectedOptions.get(value) ?? { value, label: value };
|
|
912
|
+
return this.opts.beforeUnselect?.(option) === false;
|
|
913
|
+
}))
|
|
914
|
+
return;
|
|
859
915
|
this.clearSelection();
|
|
860
916
|
});
|
|
861
917
|
if (this.searchInput) {
|
|
918
|
+
let composing = false;
|
|
919
|
+
this.searchInput.addEventListener("compositionstart", () => {
|
|
920
|
+
composing = true;
|
|
921
|
+
});
|
|
922
|
+
this.searchInput.addEventListener("compositionend", () => {
|
|
923
|
+
composing = false;
|
|
924
|
+
this.applySearchQuery(this.searchInput.value, true);
|
|
925
|
+
});
|
|
862
926
|
this.searchInput.addEventListener("input", () => {
|
|
927
|
+
if (composing) return;
|
|
863
928
|
this.applySearchQuery(this.searchInput.value, true);
|
|
864
929
|
});
|
|
865
930
|
this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
@@ -869,21 +934,22 @@ var ForgeSelect = class {
|
|
|
869
934
|
const labels = text.split(/[,\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
870
935
|
if (labels.length < 2) return;
|
|
871
936
|
event.preventDefault();
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (
|
|
883
|
-
this.
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
|
|
937
|
+
void Promise.all(labels.map((label) => this.createTag(label))).then((results) => {
|
|
938
|
+
const created = results.filter((result) => result !== void 0);
|
|
939
|
+
if (created.length === 0 || this.destroyed) return;
|
|
940
|
+
this.searchInput.value = "";
|
|
941
|
+
this.query = "";
|
|
942
|
+
this.afterSelectionChange();
|
|
943
|
+
for (const result of created) {
|
|
944
|
+
if (result.created) this.emitter.emit("create", result.option);
|
|
945
|
+
this.emitter.emit("select", result.option);
|
|
946
|
+
}
|
|
947
|
+
if (this.opts.closeOnSelect) this.close();
|
|
948
|
+
else this.renderList();
|
|
949
|
+
}).catch((cause) => {
|
|
950
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
951
|
+
this.emitter.emit("error", error);
|
|
952
|
+
});
|
|
887
953
|
});
|
|
888
954
|
}
|
|
889
955
|
this.list.addEventListener("click", (event) => {
|
|
@@ -938,7 +1004,7 @@ var ForgeSelect = class {
|
|
|
938
1004
|
this.renderList();
|
|
939
1005
|
}
|
|
940
1006
|
handleKeydown(event) {
|
|
941
|
-
if (this.isDisabled) return;
|
|
1007
|
+
if (this.isDisabled || event.isComposing || event.keyCode === 229) return;
|
|
942
1008
|
switch (event.key) {
|
|
943
1009
|
case "Enter":
|
|
944
1010
|
event.preventDefault();
|
|
@@ -1156,23 +1222,27 @@ var ForgeSelect = class {
|
|
|
1156
1222
|
}
|
|
1157
1223
|
}
|
|
1158
1224
|
findOption(value) {
|
|
1159
|
-
return
|
|
1225
|
+
return this.optionByValue.get(value);
|
|
1160
1226
|
}
|
|
1161
1227
|
findOptionByLabel(label) {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1228
|
+
return this.optionByLabel.get(normalizeSearchText(label, this.opts.accentInsensitive));
|
|
1229
|
+
}
|
|
1230
|
+
rebuildOptionIndexes() {
|
|
1231
|
+
this.optionByValue.clear();
|
|
1232
|
+
this.optionByLabel.clear();
|
|
1233
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1234
|
+
const visit = (option) => {
|
|
1235
|
+
if (this.optionByValue.has(option.value)) duplicates.add(option.value);
|
|
1236
|
+
else this.optionByValue.set(option.value, option);
|
|
1237
|
+
const label = normalizeSearchText(option.label, this.opts.accentInsensitive);
|
|
1238
|
+
if (!this.optionByLabel.has(label)) this.optionByLabel.set(label, option);
|
|
1239
|
+
option.children?.forEach(visit);
|
|
1170
1240
|
};
|
|
1171
|
-
for (const item of this.data)
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1241
|
+
for (const item of this.data) (isGroup(item) ? item.options : [item]).forEach(visit);
|
|
1242
|
+
if (duplicates.size === 0 || this.opts.duplicateValuePolicy === "ignore") return;
|
|
1243
|
+
const message = `ForgeSelect: duplicate option value(s): ${[...duplicates].join(", ")}`;
|
|
1244
|
+
if (this.opts.duplicateValuePolicy === "error") throw new Error(message);
|
|
1245
|
+
console.warn(message);
|
|
1176
1246
|
}
|
|
1177
1247
|
/** Selects an existing option matching `label` exactly, or creates and selects a new one. */
|
|
1178
1248
|
createTag(label) {
|
|
@@ -1188,12 +1258,26 @@ var ForgeSelect = class {
|
|
|
1188
1258
|
this.selectValue(existing.value, false);
|
|
1189
1259
|
return { option: existing, created: false };
|
|
1190
1260
|
}
|
|
1191
|
-
|
|
1261
|
+
if (this.opts.beforeCreate?.(trimmed) === false) return void 0;
|
|
1262
|
+
const created = this.opts.createOption?.(trimmed) ?? { value: trimmed, label: trimmed };
|
|
1263
|
+
if (created instanceof Promise) {
|
|
1264
|
+
return created.then((option) => option ? this.addCreatedOption(option) : void 0);
|
|
1265
|
+
}
|
|
1266
|
+
return created ? this.addCreatedOption(created) : void 0;
|
|
1267
|
+
}
|
|
1268
|
+
addCreatedOption(option) {
|
|
1192
1269
|
if (this.opts.multiple && !this.canSelectOption(option)) {
|
|
1193
1270
|
this.announceMaximum(option);
|
|
1194
1271
|
return void 0;
|
|
1195
1272
|
}
|
|
1273
|
+
const duplicate = this.findOption(option.value);
|
|
1274
|
+
if (duplicate) {
|
|
1275
|
+
if (this.selected.includes(duplicate.value)) return void 0;
|
|
1276
|
+
this.selectValue(duplicate.value, false);
|
|
1277
|
+
return { option: duplicate, created: false };
|
|
1278
|
+
}
|
|
1196
1279
|
this.data.push(option);
|
|
1280
|
+
this.rebuildOptionIndexes();
|
|
1197
1281
|
this.selectValue(option.value, false);
|
|
1198
1282
|
return { option, created: true };
|
|
1199
1283
|
}
|
|
@@ -1201,8 +1285,18 @@ var ForgeSelect = class {
|
|
|
1201
1285
|
const label = this.query.trim();
|
|
1202
1286
|
if (!label) return;
|
|
1203
1287
|
const result = this.createTag(label);
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1288
|
+
if (result instanceof Promise) {
|
|
1289
|
+
void result.then((created) => this.finishCreateFromQuery(created, label)).catch((cause) => {
|
|
1290
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1291
|
+
this.emitter.emit("error", error);
|
|
1292
|
+
});
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
this.finishCreateFromQuery(result, label);
|
|
1296
|
+
}
|
|
1297
|
+
finishCreateFromQuery(result, sourceLabel) {
|
|
1298
|
+
if (!result || this.destroyed) return;
|
|
1299
|
+
if (this.searchInput && this.query.trim() === sourceLabel) {
|
|
1206
1300
|
this.searchInput.value = "";
|
|
1207
1301
|
this.query = "";
|
|
1208
1302
|
}
|
|
@@ -1223,9 +1317,11 @@ var ForgeSelect = class {
|
|
|
1223
1317
|
if (this.opts.multiple) {
|
|
1224
1318
|
let changed = false;
|
|
1225
1319
|
if (this.selected.includes(value)) {
|
|
1320
|
+
if (this.opts.beforeUnselect?.(item.option) === false) return;
|
|
1226
1321
|
this.deselectValue(value, true);
|
|
1227
1322
|
changed = true;
|
|
1228
1323
|
} else if (this.canSelectOption(item.option)) {
|
|
1324
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1229
1325
|
this.selectValue(value, true);
|
|
1230
1326
|
changed = true;
|
|
1231
1327
|
} else {
|
|
@@ -1233,6 +1329,7 @@ var ForgeSelect = class {
|
|
|
1233
1329
|
}
|
|
1234
1330
|
if (changed && this.opts.closeOnSelect) this.close();
|
|
1235
1331
|
} else {
|
|
1332
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1236
1333
|
this.selectValue(value, true);
|
|
1237
1334
|
this.close();
|
|
1238
1335
|
this.control.focus();
|
|
@@ -1257,7 +1354,7 @@ var ForgeSelect = class {
|
|
|
1257
1354
|
tag.className = "forge-select__tag";
|
|
1258
1355
|
const label = document.createElement("span");
|
|
1259
1356
|
label.className = "forge-select__tag-label";
|
|
1260
|
-
renderOptionContent(label, option, this.opts.templateSelection, "inline");
|
|
1357
|
+
renderOptionContent(label, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1261
1358
|
const remove = document.createElement("button");
|
|
1262
1359
|
remove.type = "button";
|
|
1263
1360
|
remove.className = "forge-select__tag-remove";
|
|
@@ -1265,7 +1362,7 @@ var ForgeSelect = class {
|
|
|
1265
1362
|
remove.textContent = "\xD7";
|
|
1266
1363
|
remove.addEventListener("click", (event) => {
|
|
1267
1364
|
event.stopPropagation();
|
|
1268
|
-
if (!this.isDisabled) this.deselectValue(value, true);
|
|
1365
|
+
if (!this.isDisabled && this.opts.beforeUnselect?.(option) !== false) this.deselectValue(value, true);
|
|
1269
1366
|
});
|
|
1270
1367
|
tag.append(label, remove);
|
|
1271
1368
|
if (this.opts.sortable) {
|
|
@@ -1285,7 +1382,7 @@ var ForgeSelect = class {
|
|
|
1285
1382
|
};
|
|
1286
1383
|
const span = document.createElement("span");
|
|
1287
1384
|
span.className = "forge-select__single-value";
|
|
1288
|
-
renderOptionContent(span, option, this.opts.templateSelection, "inline");
|
|
1385
|
+
renderOptionContent(span, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1289
1386
|
this.valueEl.append(span);
|
|
1290
1387
|
}
|
|
1291
1388
|
}
|
|
@@ -1394,7 +1491,14 @@ var ForgeSelect = class {
|
|
|
1394
1491
|
accentInsensitive: this.opts.accentInsensitive,
|
|
1395
1492
|
scorer: this.opts.searchScorer
|
|
1396
1493
|
}) > 0);
|
|
1397
|
-
const
|
|
1494
|
+
const subtreeMatchCache = /* @__PURE__ */ new Map();
|
|
1495
|
+
const subtreeMatches = (option) => {
|
|
1496
|
+
const cached = subtreeMatchCache.get(option);
|
|
1497
|
+
if (cached !== void 0) return cached;
|
|
1498
|
+
const result = query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
1499
|
+
subtreeMatchCache.set(option, result);
|
|
1500
|
+
return result;
|
|
1501
|
+
};
|
|
1398
1502
|
const pushOption = (option, depth, parentValue) => {
|
|
1399
1503
|
let navIndex = -1;
|
|
1400
1504
|
const interactionDisabled = this.isOptionDisabled(option) || this.hasReachedMaximum() && !this.selected.includes(option.value);
|
|
@@ -1450,7 +1554,7 @@ var ForgeSelect = class {
|
|
|
1450
1554
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1451
1555
|
}
|
|
1452
1556
|
rowKey(row, index) {
|
|
1453
|
-
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1557
|
+
if (row.kind === "option") return `option:${row.option.value}:${index}`;
|
|
1454
1558
|
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1455
1559
|
return `${row.kind}:${index}`;
|
|
1456
1560
|
}
|
|
@@ -1492,7 +1596,14 @@ var ForgeSelect = class {
|
|
|
1492
1596
|
if (virtual) {
|
|
1493
1597
|
const viewport = clientHeight || rowHeight * 8;
|
|
1494
1598
|
if (this.opts.variableItemHeight) {
|
|
1495
|
-
|
|
1599
|
+
let low = 0;
|
|
1600
|
+
let high = this.rows.length;
|
|
1601
|
+
while (low < high) {
|
|
1602
|
+
const middle = low + high >>> 1;
|
|
1603
|
+
if (offsets[middle + 1] < scrollTop) low = middle + 1;
|
|
1604
|
+
else high = middle;
|
|
1605
|
+
}
|
|
1606
|
+
start = low;
|
|
1496
1607
|
start = Math.max(0, start - VIRTUAL_BUFFER);
|
|
1497
1608
|
end = start;
|
|
1498
1609
|
const target = scrollTop + viewport + VIRTUAL_BUFFER * rowHeight;
|
|
@@ -1509,7 +1620,13 @@ var ForgeSelect = class {
|
|
|
1509
1620
|
}
|
|
1510
1621
|
const appended = [];
|
|
1511
1622
|
for (let i = start; i < end; i++) {
|
|
1512
|
-
const
|
|
1623
|
+
const key = this.rowKey(this.rows[i], i);
|
|
1624
|
+
const element = this.renderRow(this.rows[i], this.rowElementCache.get(key));
|
|
1625
|
+
this.rowElementCache.set(key, element);
|
|
1626
|
+
if (this.rowElementCache.size > ROW_CACHE_LIMIT) {
|
|
1627
|
+
const oldest = this.rowElementCache.keys().next().value;
|
|
1628
|
+
this.rowElementCache.delete(oldest);
|
|
1629
|
+
}
|
|
1513
1630
|
this.list.append(element);
|
|
1514
1631
|
appended.push(element);
|
|
1515
1632
|
}
|
|
@@ -1536,8 +1653,22 @@ var ForgeSelect = class {
|
|
|
1536
1653
|
}
|
|
1537
1654
|
this.updateActiveDescendant();
|
|
1538
1655
|
}
|
|
1539
|
-
renderRow(row) {
|
|
1540
|
-
const li = document.createElement("li");
|
|
1656
|
+
renderRow(row, recycled) {
|
|
1657
|
+
const li = recycled ?? document.createElement("li");
|
|
1658
|
+
li.replaceChildren();
|
|
1659
|
+
li.className = "";
|
|
1660
|
+
for (const attribute of [
|
|
1661
|
+
"role",
|
|
1662
|
+
"id",
|
|
1663
|
+
"aria-hidden",
|
|
1664
|
+
"aria-selected",
|
|
1665
|
+
"aria-disabled",
|
|
1666
|
+
"aria-expanded",
|
|
1667
|
+
"aria-level",
|
|
1668
|
+
"data-nav-index",
|
|
1669
|
+
"data-option-value"
|
|
1670
|
+
])
|
|
1671
|
+
li.removeAttribute(attribute);
|
|
1541
1672
|
switch (row.kind) {
|
|
1542
1673
|
case "group":
|
|
1543
1674
|
li.className = "forge-select__group-label";
|
|
@@ -1657,7 +1788,7 @@ var ForgeSelect = class {
|
|
|
1657
1788
|
if (!cached) {
|
|
1658
1789
|
const holder = document.createElement("span");
|
|
1659
1790
|
holder.className = "forge-select__option-content";
|
|
1660
|
-
renderOptionContent(holder, option, this.opts.templateResult);
|
|
1791
|
+
renderOptionContent(holder, option, this.opts.templateResult, "row", this.opts.sanitizeTemplate);
|
|
1661
1792
|
if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
|
|
1662
1793
|
const oldest = this.rowContentCache.keys().next().value;
|
|
1663
1794
|
this.rowContentCache.delete(oldest);
|
|
@@ -1746,6 +1877,7 @@ var ForgeSelect = class {
|
|
|
1746
1877
|
this.ajaxController = null;
|
|
1747
1878
|
this.page = 0;
|
|
1748
1879
|
this.hasMore = true;
|
|
1880
|
+
this.nextCursor = void 0;
|
|
1749
1881
|
this.setLoading(true);
|
|
1750
1882
|
this.loadingMore = false;
|
|
1751
1883
|
this.loadError = null;
|
|
@@ -1760,17 +1892,27 @@ var ForgeSelect = class {
|
|
|
1760
1892
|
this.loading = loading;
|
|
1761
1893
|
this.emitter.emit("loading", loading);
|
|
1762
1894
|
}
|
|
1763
|
-
remoteCacheKey(query, page) {
|
|
1764
|
-
return `${query}\0${page}`;
|
|
1895
|
+
remoteCacheKey(query, page, cursor) {
|
|
1896
|
+
return `${query}\0${cursor ?? page}`;
|
|
1765
1897
|
}
|
|
1766
|
-
|
|
1898
|
+
fetchRemoteResult(query, page, signal, cursor) {
|
|
1899
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1900
|
+
const pending = this.remoteInFlight.get(key);
|
|
1901
|
+
if (pending) return pending;
|
|
1902
|
+
const ajax = this.opts.ajax;
|
|
1903
|
+
const request = this.requestRemote(query, page, signal, cursor).then((json) => normalizeRemoteResult(ajax, json)).finally(() => this.remoteInFlight.delete(key));
|
|
1904
|
+
this.remoteInFlight.set(key, request);
|
|
1905
|
+
return request;
|
|
1906
|
+
}
|
|
1907
|
+
async requestRemote(query, page, signal, cursor) {
|
|
1767
1908
|
const ajax = this.opts.ajax;
|
|
1768
1909
|
const attempts = Math.max(0, Math.floor(ajax.retry ?? 0)) + 1;
|
|
1769
1910
|
let lastError;
|
|
1770
1911
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1771
1912
|
try {
|
|
1772
|
-
if (ajax.request)
|
|
1773
|
-
|
|
1913
|
+
if (ajax.request)
|
|
1914
|
+
return cursor === void 0 ? await ajax.request(query, page, signal) : await ajax.request(query, page, signal, cursor);
|
|
1915
|
+
const response = await fetch(buildUrl(ajax, query, page, cursor), { signal });
|
|
1774
1916
|
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1775
1917
|
return await response.json();
|
|
1776
1918
|
} catch (error) {
|
|
@@ -1798,10 +1940,13 @@ var ForgeSelect = class {
|
|
|
1798
1940
|
const key = this.remoteCacheKey(query, 0);
|
|
1799
1941
|
if (this.remoteCache.get(key)) return;
|
|
1800
1942
|
const controller = new AbortController();
|
|
1943
|
+
this.prefetchControllers.add(controller);
|
|
1801
1944
|
try {
|
|
1802
|
-
const
|
|
1803
|
-
this.remoteCache.set(key,
|
|
1945
|
+
const result = await this.fetchRemoteResult(query, 0, controller.signal);
|
|
1946
|
+
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1804
1947
|
} catch {
|
|
1948
|
+
} finally {
|
|
1949
|
+
this.prefetchControllers.delete(controller);
|
|
1805
1950
|
}
|
|
1806
1951
|
}
|
|
1807
1952
|
/**
|
|
@@ -1827,12 +1972,12 @@ var ForgeSelect = class {
|
|
|
1827
1972
|
const controller = new AbortController();
|
|
1828
1973
|
this.ajaxController = controller;
|
|
1829
1974
|
const page = append ? this.page + 1 : 0;
|
|
1975
|
+
const cursor = append ? this.nextCursor : void 0;
|
|
1830
1976
|
try {
|
|
1831
|
-
const key = this.remoteCacheKey(query, page);
|
|
1977
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1832
1978
|
let result = this.remoteCache.get(key);
|
|
1833
1979
|
if (!result) {
|
|
1834
|
-
|
|
1835
|
-
result = normalizeRemoteResult(ajax, json);
|
|
1980
|
+
result = await this.fetchRemoteResult(query, page, controller.signal, cursor);
|
|
1836
1981
|
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1837
1982
|
}
|
|
1838
1983
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
@@ -1847,8 +1992,10 @@ var ForgeSelect = class {
|
|
|
1847
1992
|
}
|
|
1848
1993
|
this.page = page;
|
|
1849
1994
|
this.hasMore = hasMore;
|
|
1995
|
+
this.nextCursor = result.nextCursor;
|
|
1850
1996
|
this.remoteLoaded = true;
|
|
1851
1997
|
this.loadError = null;
|
|
1998
|
+
this.rebuildOptionIndexes();
|
|
1852
1999
|
} catch (cause) {
|
|
1853
2000
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed || controller.signal.aborted) return;
|
|
1854
2001
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|