forge-select 0.5.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 +343 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -4
- package/dist/index.d.ts +44 -4
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +343 -85
- 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,10 +153,16 @@ 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
|
|
165
|
+
var REMOTE_CACHE_LIMIT = 50;
|
|
160
166
|
var RemoteCache = class {
|
|
161
167
|
constructor() {
|
|
162
168
|
this.entries = /* @__PURE__ */ new Map();
|
|
@@ -171,7 +177,12 @@ var RemoteCache = class {
|
|
|
171
177
|
return entry.value;
|
|
172
178
|
}
|
|
173
179
|
set(key, value, ttl, now = Date.now()) {
|
|
174
|
-
if (ttl
|
|
180
|
+
if (ttl <= 0) return;
|
|
181
|
+
if (this.entries.size >= REMOTE_CACHE_LIMIT && !this.entries.has(key)) {
|
|
182
|
+
const oldest = this.entries.keys().next().value;
|
|
183
|
+
this.entries.delete(oldest);
|
|
184
|
+
}
|
|
185
|
+
this.entries.set(key, { value, expiresAt: now + ttl });
|
|
175
186
|
}
|
|
176
187
|
clear() {
|
|
177
188
|
this.entries.clear();
|
|
@@ -261,21 +272,6 @@ function computeCheckState(option, selected, isDisabled = defaultIsDisabled) {
|
|
|
261
272
|
if (states.every((state) => state === "none")) return "none";
|
|
262
273
|
return "some";
|
|
263
274
|
}
|
|
264
|
-
function findOption(items, value) {
|
|
265
|
-
const search = (options) => {
|
|
266
|
-
for (const option of options) {
|
|
267
|
-
if (option.value === value) return option;
|
|
268
|
-
const found = option.children ? search(option.children) : void 0;
|
|
269
|
-
if (found) return found;
|
|
270
|
-
}
|
|
271
|
-
return void 0;
|
|
272
|
-
};
|
|
273
|
-
for (const item of items) {
|
|
274
|
-
const found = search(isGroup(item) ? item.options : [item]);
|
|
275
|
-
if (found) return found;
|
|
276
|
-
}
|
|
277
|
-
return void 0;
|
|
278
|
-
}
|
|
279
275
|
function syncTreeAncestors(items, selected, isDisabled = defaultIsDisabled) {
|
|
280
276
|
const sync = (option) => {
|
|
281
277
|
if (!option.children?.length) return;
|
|
@@ -305,9 +301,13 @@ var DEFAULT_ITEM_HEIGHT = 36;
|
|
|
305
301
|
var VIRTUAL_BUFFER = 5;
|
|
306
302
|
var VIRTUAL_THRESHOLD = 100;
|
|
307
303
|
var ROW_CACHE_LIMIT = 2e3;
|
|
304
|
+
var PAGE_SIZE = 10;
|
|
305
|
+
var TYPEAHEAD_RESET_MS = 500;
|
|
308
306
|
var uidCounter = 0;
|
|
309
307
|
var ForgeSelect = class {
|
|
310
308
|
constructor(target, options = {}) {
|
|
309
|
+
this.optionByValue = /* @__PURE__ */ new Map();
|
|
310
|
+
this.optionByLabel = /* @__PURE__ */ new Map();
|
|
311
311
|
this.selected = [];
|
|
312
312
|
this.selectedOptions = /* @__PURE__ */ new Map();
|
|
313
313
|
this.suppressNextTagClick = false;
|
|
@@ -322,8 +322,14 @@ var ForgeSelect = class {
|
|
|
322
322
|
this.rows = [];
|
|
323
323
|
this.navItems = [];
|
|
324
324
|
this.highlightedIndex = -1;
|
|
325
|
+
this.typeaheadBuffer = "";
|
|
326
|
+
this.typeaheadTimer = null;
|
|
325
327
|
this.rowContentCache = /* @__PURE__ */ new Map();
|
|
328
|
+
this.rowElementCache = /* @__PURE__ */ new Map();
|
|
326
329
|
this.rowHeightCache = /* @__PURE__ */ new Map();
|
|
330
|
+
this.rowOffsetsCache = null;
|
|
331
|
+
this.scrollRafId = null;
|
|
332
|
+
this.ancestorScrollRafId = null;
|
|
327
333
|
this.searchIndex = new SearchIndex();
|
|
328
334
|
this.expandedValues = /* @__PURE__ */ new Set();
|
|
329
335
|
this.loading = false;
|
|
@@ -335,6 +341,8 @@ var ForgeSelect = class {
|
|
|
335
341
|
this.ajaxController = null;
|
|
336
342
|
this.remoteLoaded = false;
|
|
337
343
|
this.remoteCache = new RemoteCache();
|
|
344
|
+
this.remoteInFlight = /* @__PURE__ */ new Map();
|
|
345
|
+
this.prefetchControllers = /* @__PURE__ */ new Set();
|
|
338
346
|
this.loadError = null;
|
|
339
347
|
this.originalDisplay = "";
|
|
340
348
|
this.originalDisabled = false;
|
|
@@ -352,7 +360,12 @@ var ForgeSelect = class {
|
|
|
352
360
|
this.positionDropdown();
|
|
353
361
|
};
|
|
354
362
|
this.onAncestorScroll = () => {
|
|
355
|
-
if (this.portalHost)
|
|
363
|
+
if (!this.portalHost) return;
|
|
364
|
+
if (this.ancestorScrollRafId != null) return;
|
|
365
|
+
this.ancestorScrollRafId = requestAnimationFrame(() => {
|
|
366
|
+
this.ancestorScrollRafId = null;
|
|
367
|
+
this.positionDropdown();
|
|
368
|
+
});
|
|
356
369
|
};
|
|
357
370
|
this.onNativeInvalid = (event) => {
|
|
358
371
|
event.preventDefault();
|
|
@@ -398,6 +411,13 @@ var ForgeSelect = class {
|
|
|
398
411
|
ajax: options.ajax,
|
|
399
412
|
templateResult: options.templateResult,
|
|
400
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",
|
|
401
421
|
filterOption: options.filterOption,
|
|
402
422
|
searchFields: options.searchFields ?? ["label", "description"],
|
|
403
423
|
tokenSearch: options.tokenSearch ?? true,
|
|
@@ -419,6 +439,7 @@ var ForgeSelect = class {
|
|
|
419
439
|
this.plugins = this.opts.plugins;
|
|
420
440
|
if (nativeSelect) nativeSelect.required = this.opts.required;
|
|
421
441
|
this.data = this.opts.data ?? (nativeSelect ? parseNativeOptions(nativeSelect) : []);
|
|
442
|
+
this.rebuildOptionIndexes();
|
|
422
443
|
if (nativeSelect && !this.opts.data) {
|
|
423
444
|
const nativeOptions = Array.from(nativeSelect.options);
|
|
424
445
|
const hasIntentionalSelection = nativeSelect.multiple || nativeSelect.selectedIndex > 0 || nativeOptions.some((option) => option.defaultSelected);
|
|
@@ -456,6 +477,8 @@ var ForgeSelect = class {
|
|
|
456
477
|
this.renderList();
|
|
457
478
|
this.positionDropdown();
|
|
458
479
|
window.addEventListener("resize", this.onWindowResize);
|
|
480
|
+
window.visualViewport?.addEventListener("resize", this.onWindowResize);
|
|
481
|
+
window.visualViewport?.addEventListener("scroll", this.onWindowResize);
|
|
459
482
|
document.addEventListener("scroll", this.onAncestorScroll, true);
|
|
460
483
|
if (this.searchInput && !this.searchInput.hidden) this.searchInput.focus();
|
|
461
484
|
this.emitter.emit("open");
|
|
@@ -470,7 +493,22 @@ var ForgeSelect = class {
|
|
|
470
493
|
this.control.setAttribute("aria-expanded", "false");
|
|
471
494
|
document.removeEventListener("mousedown", this.onDocumentMouseDown);
|
|
472
495
|
window.removeEventListener("resize", this.onWindowResize);
|
|
496
|
+
window.visualViewport?.removeEventListener("resize", this.onWindowResize);
|
|
497
|
+
window.visualViewport?.removeEventListener("scroll", this.onWindowResize);
|
|
473
498
|
document.removeEventListener("scroll", this.onAncestorScroll, true);
|
|
499
|
+
if (this.ancestorScrollRafId != null) {
|
|
500
|
+
cancelAnimationFrame(this.ancestorScrollRafId);
|
|
501
|
+
this.ancestorScrollRafId = null;
|
|
502
|
+
}
|
|
503
|
+
if (this.scrollRafId != null) {
|
|
504
|
+
cancelAnimationFrame(this.scrollRafId);
|
|
505
|
+
this.scrollRafId = null;
|
|
506
|
+
}
|
|
507
|
+
if (this.typeaheadTimer) {
|
|
508
|
+
clearTimeout(this.typeaheadTimer);
|
|
509
|
+
this.typeaheadTimer = null;
|
|
510
|
+
}
|
|
511
|
+
this.typeaheadBuffer = "";
|
|
474
512
|
this.highlightedIndex = -1;
|
|
475
513
|
if (this.searchInput) {
|
|
476
514
|
this.searchInput.value = "";
|
|
@@ -488,11 +526,16 @@ var ForgeSelect = class {
|
|
|
488
526
|
*/
|
|
489
527
|
positionDropdown() {
|
|
490
528
|
const controlRect = this.control.getBoundingClientRect();
|
|
491
|
-
const
|
|
529
|
+
const viewport = window.visualViewport;
|
|
530
|
+
const placement = computeDropdownPlacement(
|
|
531
|
+
controlRect,
|
|
532
|
+
this.dropdown.offsetHeight,
|
|
533
|
+
viewport?.height ?? window.innerHeight
|
|
534
|
+
);
|
|
492
535
|
this.root.classList.toggle("forge-select--drop-up", placement.dropUp);
|
|
493
536
|
if (this.portalHost) {
|
|
494
|
-
this.portalHost.style.top = `${placement.top}px`;
|
|
495
|
-
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`;
|
|
496
539
|
this.portalHost.style.width = `${controlRect.width}px`;
|
|
497
540
|
}
|
|
498
541
|
}
|
|
@@ -503,10 +546,16 @@ var ForgeSelect = class {
|
|
|
503
546
|
this.destroyed = true;
|
|
504
547
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
505
548
|
this.ajaxController?.abort();
|
|
549
|
+
for (const controller of this.prefetchControllers) controller.abort();
|
|
550
|
+
this.prefetchControllers.clear();
|
|
551
|
+
this.remoteInFlight.clear();
|
|
552
|
+
if (this.scrollRafId != null) cancelAnimationFrame(this.scrollRafId);
|
|
553
|
+
if (this.typeaheadTimer) clearTimeout(this.typeaheadTimer);
|
|
506
554
|
this.nativeSelect?.removeEventListener("change", this.onNativeChange);
|
|
507
555
|
this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
|
|
508
556
|
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
509
557
|
this.rowContentCache.clear();
|
|
558
|
+
this.rowElementCache.clear();
|
|
510
559
|
this.rowHeightCache.clear();
|
|
511
560
|
this.searchIndex.clear();
|
|
512
561
|
this.portalHost?.remove();
|
|
@@ -555,10 +604,23 @@ var ForgeSelect = class {
|
|
|
555
604
|
}
|
|
556
605
|
if (options.templateResult !== void 0) this.opts.templateResult = options.templateResult;
|
|
557
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
|
+
}
|
|
558
617
|
if (options.filterOption !== void 0) this.opts.filterOption = options.filterOption;
|
|
559
618
|
if (options.searchFields !== void 0) this.opts.searchFields = options.searchFields;
|
|
560
619
|
if (options.tokenSearch !== void 0) this.opts.tokenSearch = options.tokenSearch;
|
|
561
|
-
if (options.accentInsensitive !== void 0)
|
|
620
|
+
if (options.accentInsensitive !== void 0) {
|
|
621
|
+
this.opts.accentInsensitive = options.accentInsensitive;
|
|
622
|
+
this.rebuildOptionIndexes();
|
|
623
|
+
}
|
|
562
624
|
if (options.searchScorer !== void 0) this.opts.searchScorer = options.searchScorer;
|
|
563
625
|
if (options.highlightSearch !== void 0) this.opts.highlightSearch = options.highlightSearch;
|
|
564
626
|
if (options.minSearchLength !== void 0)
|
|
@@ -587,6 +649,8 @@ var ForgeSelect = class {
|
|
|
587
649
|
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
588
650
|
this.updateSearchVisibility();
|
|
589
651
|
this.rowContentCache.clear();
|
|
652
|
+
this.rowElementCache.clear();
|
|
653
|
+
this.rowHeightCache.clear();
|
|
590
654
|
this.searchIndex.clear();
|
|
591
655
|
this.renderValue();
|
|
592
656
|
if (this.isOpen) this.renderList();
|
|
@@ -618,6 +682,7 @@ var ForgeSelect = class {
|
|
|
618
682
|
}
|
|
619
683
|
clearRemoteCache() {
|
|
620
684
|
this.remoteCache.clear();
|
|
685
|
+
this.remoteInFlight.clear();
|
|
621
686
|
}
|
|
622
687
|
setValue(value, options = {}) {
|
|
623
688
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
@@ -647,10 +712,31 @@ var ForgeSelect = class {
|
|
|
647
712
|
this.remoteLoaded = true;
|
|
648
713
|
this.page = 0;
|
|
649
714
|
this.hasMore = false;
|
|
715
|
+
this.nextCursor = void 0;
|
|
716
|
+
const previousData = this.data;
|
|
650
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
|
+
}
|
|
651
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
|
+
}
|
|
652
736
|
this.updateSearchVisibility();
|
|
653
737
|
this.rowContentCache.clear();
|
|
738
|
+
this.rowElementCache.clear();
|
|
739
|
+
this.rowHeightCache.clear();
|
|
654
740
|
this.searchIndex.clear();
|
|
655
741
|
this.highlightedIndex = -1;
|
|
656
742
|
if (this.isOpen) this.renderList();
|
|
@@ -821,10 +907,24 @@ var ForgeSelect = class {
|
|
|
821
907
|
});
|
|
822
908
|
this.clearBtn.addEventListener("click", (event) => {
|
|
823
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;
|
|
824
915
|
this.clearSelection();
|
|
825
916
|
});
|
|
826
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
|
+
});
|
|
827
926
|
this.searchInput.addEventListener("input", () => {
|
|
927
|
+
if (composing) return;
|
|
828
928
|
this.applySearchQuery(this.searchInput.value, true);
|
|
829
929
|
});
|
|
830
930
|
this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
@@ -834,21 +934,22 @@ var ForgeSelect = class {
|
|
|
834
934
|
const labels = text.split(/[,\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
835
935
|
if (labels.length < 2) return;
|
|
836
936
|
event.preventDefault();
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (
|
|
848
|
-
this.
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
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
|
+
});
|
|
852
953
|
});
|
|
853
954
|
}
|
|
854
955
|
this.list.addEventListener("click", (event) => {
|
|
@@ -872,8 +973,12 @@ var ForgeSelect = class {
|
|
|
872
973
|
this.activateNavItem(navIndex);
|
|
873
974
|
});
|
|
874
975
|
this.list.addEventListener("scroll", () => {
|
|
875
|
-
if (this.
|
|
876
|
-
this.
|
|
976
|
+
if (this.scrollRafId != null) return;
|
|
977
|
+
this.scrollRafId = requestAnimationFrame(() => {
|
|
978
|
+
this.scrollRafId = null;
|
|
979
|
+
if (this.usesVirtualScroll()) this.renderRows();
|
|
980
|
+
this.maybeLoadNextPage();
|
|
981
|
+
});
|
|
877
982
|
});
|
|
878
983
|
}
|
|
879
984
|
applySearchQuery(query, emitSearch) {
|
|
@@ -881,7 +986,6 @@ var ForgeSelect = class {
|
|
|
881
986
|
if (this.searchInput && this.searchInput.value !== query) this.searchInput.value = query;
|
|
882
987
|
this.highlightedIndex = -1;
|
|
883
988
|
this.list.scrollTop = 0;
|
|
884
|
-
this.rowContentCache.clear();
|
|
885
989
|
if (emitSearch) this.emitter.emit("search", query);
|
|
886
990
|
const trimmed = query.trim();
|
|
887
991
|
const belowMinLength = trimmed !== "" && trimmed.length < this.opts.minSearchLength;
|
|
@@ -900,7 +1004,7 @@ var ForgeSelect = class {
|
|
|
900
1004
|
this.renderList();
|
|
901
1005
|
}
|
|
902
1006
|
handleKeydown(event) {
|
|
903
|
-
if (this.isDisabled) return;
|
|
1007
|
+
if (this.isDisabled || event.isComposing || event.keyCode === 229) return;
|
|
904
1008
|
switch (event.key) {
|
|
905
1009
|
case "Enter":
|
|
906
1010
|
event.preventDefault();
|
|
@@ -935,9 +1039,63 @@ var ForgeSelect = class {
|
|
|
935
1039
|
case "ArrowLeft":
|
|
936
1040
|
if (this.isOpen && this.navigateTree("left")) event.preventDefault();
|
|
937
1041
|
break;
|
|
1042
|
+
case "Home":
|
|
1043
|
+
if (this.isOpen) {
|
|
1044
|
+
event.preventDefault();
|
|
1045
|
+
this.focusNavIndex(0);
|
|
1046
|
+
}
|
|
1047
|
+
break;
|
|
1048
|
+
case "End":
|
|
1049
|
+
if (this.isOpen) {
|
|
1050
|
+
event.preventDefault();
|
|
1051
|
+
this.focusNavIndex(this.navItems.length - 1);
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
case "PageDown":
|
|
1055
|
+
if (this.isOpen) {
|
|
1056
|
+
event.preventDefault();
|
|
1057
|
+
this.focusNavIndex(
|
|
1058
|
+
Math.min(this.navItems.length - 1, (this.highlightedIndex === -1 ? 0 : this.highlightedIndex) + PAGE_SIZE)
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
break;
|
|
1062
|
+
case "PageUp":
|
|
1063
|
+
if (this.isOpen) {
|
|
1064
|
+
event.preventDefault();
|
|
1065
|
+
this.focusNavIndex(Math.max(0, (this.highlightedIndex === -1 ? 0 : this.highlightedIndex) - PAGE_SIZE));
|
|
1066
|
+
}
|
|
1067
|
+
break;
|
|
938
1068
|
case "Tab":
|
|
939
1069
|
this.close();
|
|
940
1070
|
break;
|
|
1071
|
+
default:
|
|
1072
|
+
if (this.isOpen && event.target === this.control && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1073
|
+
this.handleTypeahead(event.key);
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Jumps the highlight to the next nav item (wrapping) whose label starts
|
|
1080
|
+
* with the accumulated buffer, matching native <select> typeahead: rapid
|
|
1081
|
+
* distinct keystrokes narrow the prefix, a pause resets it.
|
|
1082
|
+
*/
|
|
1083
|
+
handleTypeahead(char) {
|
|
1084
|
+
if (this.typeaheadTimer) clearTimeout(this.typeaheadTimer);
|
|
1085
|
+
this.typeaheadBuffer += normalizeSearchText(char, this.opts.accentInsensitive);
|
|
1086
|
+
this.typeaheadTimer = setTimeout(() => {
|
|
1087
|
+
this.typeaheadBuffer = "";
|
|
1088
|
+
this.typeaheadTimer = null;
|
|
1089
|
+
}, TYPEAHEAD_RESET_MS);
|
|
1090
|
+
const prefix = [...this.typeaheadBuffer].every((value) => value === this.typeaheadBuffer[0]) ? this.typeaheadBuffer[0] : this.typeaheadBuffer;
|
|
1091
|
+
const count = this.navItems.length;
|
|
1092
|
+
for (let step = 1; step <= count; step += 1) {
|
|
1093
|
+
const index = (this.highlightedIndex + step + count) % count;
|
|
1094
|
+
const item = this.navItems[index];
|
|
1095
|
+
if (item.kind === "option" && normalizeSearchText(item.option.label, this.opts.accentInsensitive).startsWith(prefix)) {
|
|
1096
|
+
this.focusNavIndex(index);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
941
1099
|
}
|
|
942
1100
|
}
|
|
943
1101
|
// ---------------------------------------------------------------- selection
|
|
@@ -1028,7 +1186,10 @@ var ForgeSelect = class {
|
|
|
1028
1186
|
this.control.classList.remove("forge-select__control--invalid");
|
|
1029
1187
|
this.control.removeAttribute("aria-invalid");
|
|
1030
1188
|
}
|
|
1031
|
-
if (this.isOpen)
|
|
1189
|
+
if (this.isOpen) {
|
|
1190
|
+
if (this.opts.maxSelections != null) this.renderList();
|
|
1191
|
+
else this.renderRows();
|
|
1192
|
+
}
|
|
1032
1193
|
if (emitChange) this.emitter.emit("change", this.getValue());
|
|
1033
1194
|
}
|
|
1034
1195
|
syncNativeSelect(dispatchChange = true) {
|
|
@@ -1061,23 +1222,27 @@ var ForgeSelect = class {
|
|
|
1061
1222
|
}
|
|
1062
1223
|
}
|
|
1063
1224
|
findOption(value) {
|
|
1064
|
-
return
|
|
1225
|
+
return this.optionByValue.get(value);
|
|
1065
1226
|
}
|
|
1066
1227
|
findOptionByLabel(label) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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);
|
|
1075
1240
|
};
|
|
1076
|
-
for (const item of this.data)
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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);
|
|
1081
1246
|
}
|
|
1082
1247
|
/** Selects an existing option matching `label` exactly, or creates and selects a new one. */
|
|
1083
1248
|
createTag(label) {
|
|
@@ -1093,12 +1258,26 @@ var ForgeSelect = class {
|
|
|
1093
1258
|
this.selectValue(existing.value, false);
|
|
1094
1259
|
return { option: existing, created: false };
|
|
1095
1260
|
}
|
|
1096
|
-
|
|
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) {
|
|
1097
1269
|
if (this.opts.multiple && !this.canSelectOption(option)) {
|
|
1098
1270
|
this.announceMaximum(option);
|
|
1099
1271
|
return void 0;
|
|
1100
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
|
+
}
|
|
1101
1279
|
this.data.push(option);
|
|
1280
|
+
this.rebuildOptionIndexes();
|
|
1102
1281
|
this.selectValue(option.value, false);
|
|
1103
1282
|
return { option, created: true };
|
|
1104
1283
|
}
|
|
@@ -1106,8 +1285,18 @@ var ForgeSelect = class {
|
|
|
1106
1285
|
const label = this.query.trim();
|
|
1107
1286
|
if (!label) return;
|
|
1108
1287
|
const result = this.createTag(label);
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
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) {
|
|
1111
1300
|
this.searchInput.value = "";
|
|
1112
1301
|
this.query = "";
|
|
1113
1302
|
}
|
|
@@ -1115,6 +1304,7 @@ var ForgeSelect = class {
|
|
|
1115
1304
|
if (result.created) this.emitter.emit("create", result.option);
|
|
1116
1305
|
this.emitter.emit("select", result.option);
|
|
1117
1306
|
if (!this.opts.multiple || this.opts.closeOnSelect) this.close();
|
|
1307
|
+
else if (this.isOpen) this.renderList();
|
|
1118
1308
|
}
|
|
1119
1309
|
activateNavItem(navIndex) {
|
|
1120
1310
|
const item = this.navItems[navIndex];
|
|
@@ -1127,9 +1317,11 @@ var ForgeSelect = class {
|
|
|
1127
1317
|
if (this.opts.multiple) {
|
|
1128
1318
|
let changed = false;
|
|
1129
1319
|
if (this.selected.includes(value)) {
|
|
1320
|
+
if (this.opts.beforeUnselect?.(item.option) === false) return;
|
|
1130
1321
|
this.deselectValue(value, true);
|
|
1131
1322
|
changed = true;
|
|
1132
1323
|
} else if (this.canSelectOption(item.option)) {
|
|
1324
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1133
1325
|
this.selectValue(value, true);
|
|
1134
1326
|
changed = true;
|
|
1135
1327
|
} else {
|
|
@@ -1137,6 +1329,7 @@ var ForgeSelect = class {
|
|
|
1137
1329
|
}
|
|
1138
1330
|
if (changed && this.opts.closeOnSelect) this.close();
|
|
1139
1331
|
} else {
|
|
1332
|
+
if (this.opts.beforeSelect?.(item.option) === false) return;
|
|
1140
1333
|
this.selectValue(value, true);
|
|
1141
1334
|
this.close();
|
|
1142
1335
|
this.control.focus();
|
|
@@ -1161,7 +1354,7 @@ var ForgeSelect = class {
|
|
|
1161
1354
|
tag.className = "forge-select__tag";
|
|
1162
1355
|
const label = document.createElement("span");
|
|
1163
1356
|
label.className = "forge-select__tag-label";
|
|
1164
|
-
renderOptionContent(label, option, this.opts.templateSelection, "inline");
|
|
1357
|
+
renderOptionContent(label, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1165
1358
|
const remove = document.createElement("button");
|
|
1166
1359
|
remove.type = "button";
|
|
1167
1360
|
remove.className = "forge-select__tag-remove";
|
|
@@ -1169,7 +1362,7 @@ var ForgeSelect = class {
|
|
|
1169
1362
|
remove.textContent = "\xD7";
|
|
1170
1363
|
remove.addEventListener("click", (event) => {
|
|
1171
1364
|
event.stopPropagation();
|
|
1172
|
-
if (!this.isDisabled) this.deselectValue(value, true);
|
|
1365
|
+
if (!this.isDisabled && this.opts.beforeUnselect?.(option) !== false) this.deselectValue(value, true);
|
|
1173
1366
|
});
|
|
1174
1367
|
tag.append(label, remove);
|
|
1175
1368
|
if (this.opts.sortable) {
|
|
@@ -1189,7 +1382,7 @@ var ForgeSelect = class {
|
|
|
1189
1382
|
};
|
|
1190
1383
|
const span = document.createElement("span");
|
|
1191
1384
|
span.className = "forge-select__single-value";
|
|
1192
|
-
renderOptionContent(span, option, this.opts.templateSelection, "inline");
|
|
1385
|
+
renderOptionContent(span, option, this.opts.templateSelection, "inline", this.opts.sanitizeTemplate);
|
|
1193
1386
|
this.valueEl.append(span);
|
|
1194
1387
|
}
|
|
1195
1388
|
}
|
|
@@ -1289,6 +1482,7 @@ var ForgeSelect = class {
|
|
|
1289
1482
|
buildRows() {
|
|
1290
1483
|
this.rows = [];
|
|
1291
1484
|
this.navItems = [];
|
|
1485
|
+
this.rowOffsetsCache = null;
|
|
1292
1486
|
const trimmedQuery = this.query.trim();
|
|
1293
1487
|
const query = normalizeSearchText(trimmedQuery, this.opts.accentInsensitive);
|
|
1294
1488
|
const matches = (option) => query === "" || (this.opts.filterOption ? this.opts.filterOption(option, trimmedQuery) : this.searchIndex.score(option, trimmedQuery, {
|
|
@@ -1297,7 +1491,14 @@ var ForgeSelect = class {
|
|
|
1297
1491
|
accentInsensitive: this.opts.accentInsensitive,
|
|
1298
1492
|
scorer: this.opts.searchScorer
|
|
1299
1493
|
}) > 0);
|
|
1300
|
-
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
|
+
};
|
|
1301
1502
|
const pushOption = (option, depth, parentValue) => {
|
|
1302
1503
|
let navIndex = -1;
|
|
1303
1504
|
const interactionDisabled = this.isOptionDisabled(option) || this.hasReachedMaximum() && !this.selected.includes(option.value);
|
|
@@ -1353,7 +1554,7 @@ var ForgeSelect = class {
|
|
|
1353
1554
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1354
1555
|
}
|
|
1355
1556
|
rowKey(row, index) {
|
|
1356
|
-
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1557
|
+
if (row.kind === "option") return `option:${row.option.value}:${index}`;
|
|
1357
1558
|
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1358
1559
|
return `${row.kind}:${index}`;
|
|
1359
1560
|
}
|
|
@@ -1367,8 +1568,10 @@ var ForgeSelect = class {
|
|
|
1367
1568
|
return offset;
|
|
1368
1569
|
}
|
|
1369
1570
|
rowOffsets() {
|
|
1571
|
+
if (this.rowOffsetsCache) return this.rowOffsetsCache;
|
|
1370
1572
|
const offsets = [0];
|
|
1371
1573
|
for (let i = 0; i < this.rows.length; i += 1) offsets.push(offsets[i] + this.measuredRowHeight(i));
|
|
1574
|
+
this.rowOffsetsCache = offsets;
|
|
1372
1575
|
return offsets;
|
|
1373
1576
|
}
|
|
1374
1577
|
renderList() {
|
|
@@ -1393,7 +1596,14 @@ var ForgeSelect = class {
|
|
|
1393
1596
|
if (virtual) {
|
|
1394
1597
|
const viewport = clientHeight || rowHeight * 8;
|
|
1395
1598
|
if (this.opts.variableItemHeight) {
|
|
1396
|
-
|
|
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;
|
|
1397
1607
|
start = Math.max(0, start - VIRTUAL_BUFFER);
|
|
1398
1608
|
end = start;
|
|
1399
1609
|
const target = scrollTop + viewport + VIRTUAL_BUFFER * rowHeight;
|
|
@@ -1408,12 +1618,27 @@ var ForgeSelect = class {
|
|
|
1408
1618
|
topSpacer.style.height = `${offsets?.[start] ?? this.rowOffset(start)}px`;
|
|
1409
1619
|
this.list.append(topSpacer);
|
|
1410
1620
|
}
|
|
1621
|
+
const appended = [];
|
|
1411
1622
|
for (let i = start; i < end; i++) {
|
|
1412
|
-
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
|
+
}
|
|
1413
1630
|
this.list.append(element);
|
|
1414
|
-
|
|
1631
|
+
appended.push(element);
|
|
1632
|
+
}
|
|
1633
|
+
if (this.opts.variableItemHeight) {
|
|
1634
|
+
for (let i = start; i < end; i++) {
|
|
1635
|
+
const element = appended[i - start];
|
|
1415
1636
|
const measured = element.getBoundingClientRect().height || element.offsetHeight;
|
|
1416
|
-
if (measured > 0)
|
|
1637
|
+
if (measured > 0) {
|
|
1638
|
+
const key = this.rowKey(this.rows[i], i);
|
|
1639
|
+
if (this.rowHeightCache.get(key) !== measured) this.rowOffsetsCache = null;
|
|
1640
|
+
this.rowHeightCache.set(key, measured);
|
|
1641
|
+
}
|
|
1417
1642
|
}
|
|
1418
1643
|
}
|
|
1419
1644
|
if (virtual) {
|
|
@@ -1428,8 +1653,22 @@ var ForgeSelect = class {
|
|
|
1428
1653
|
}
|
|
1429
1654
|
this.updateActiveDescendant();
|
|
1430
1655
|
}
|
|
1431
|
-
renderRow(row) {
|
|
1432
|
-
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);
|
|
1433
1672
|
switch (row.kind) {
|
|
1434
1673
|
case "group":
|
|
1435
1674
|
li.className = "forge-select__group-label";
|
|
@@ -1549,7 +1788,7 @@ var ForgeSelect = class {
|
|
|
1549
1788
|
if (!cached) {
|
|
1550
1789
|
const holder = document.createElement("span");
|
|
1551
1790
|
holder.className = "forge-select__option-content";
|
|
1552
|
-
renderOptionContent(holder, option, this.opts.templateResult);
|
|
1791
|
+
renderOptionContent(holder, option, this.opts.templateResult, "row", this.opts.sanitizeTemplate);
|
|
1553
1792
|
if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
|
|
1554
1793
|
const oldest = this.rowContentCache.keys().next().value;
|
|
1555
1794
|
this.rowContentCache.delete(oldest);
|
|
@@ -1565,6 +1804,7 @@ var ForgeSelect = class {
|
|
|
1565
1804
|
this.focusNavIndex(next);
|
|
1566
1805
|
}
|
|
1567
1806
|
focusNavIndex(next) {
|
|
1807
|
+
if (this.navItems.length === 0) return;
|
|
1568
1808
|
this.highlightedIndex = next;
|
|
1569
1809
|
if (this.usesVirtualScroll()) {
|
|
1570
1810
|
const rowIndex = this.rows.findIndex(
|
|
@@ -1637,6 +1877,7 @@ var ForgeSelect = class {
|
|
|
1637
1877
|
this.ajaxController = null;
|
|
1638
1878
|
this.page = 0;
|
|
1639
1879
|
this.hasMore = true;
|
|
1880
|
+
this.nextCursor = void 0;
|
|
1640
1881
|
this.setLoading(true);
|
|
1641
1882
|
this.loadingMore = false;
|
|
1642
1883
|
this.loadError = null;
|
|
@@ -1651,17 +1892,27 @@ var ForgeSelect = class {
|
|
|
1651
1892
|
this.loading = loading;
|
|
1652
1893
|
this.emitter.emit("loading", loading);
|
|
1653
1894
|
}
|
|
1654
|
-
remoteCacheKey(query, page) {
|
|
1655
|
-
return `${query}\0${page}`;
|
|
1895
|
+
remoteCacheKey(query, page, cursor) {
|
|
1896
|
+
return `${query}\0${cursor ?? page}`;
|
|
1656
1897
|
}
|
|
1657
|
-
|
|
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) {
|
|
1658
1908
|
const ajax = this.opts.ajax;
|
|
1659
1909
|
const attempts = Math.max(0, Math.floor(ajax.retry ?? 0)) + 1;
|
|
1660
1910
|
let lastError;
|
|
1661
1911
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1662
1912
|
try {
|
|
1663
|
-
if (ajax.request)
|
|
1664
|
-
|
|
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 });
|
|
1665
1916
|
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1666
1917
|
return await response.json();
|
|
1667
1918
|
} catch (error) {
|
|
@@ -1689,10 +1940,13 @@ var ForgeSelect = class {
|
|
|
1689
1940
|
const key = this.remoteCacheKey(query, 0);
|
|
1690
1941
|
if (this.remoteCache.get(key)) return;
|
|
1691
1942
|
const controller = new AbortController();
|
|
1943
|
+
this.prefetchControllers.add(controller);
|
|
1692
1944
|
try {
|
|
1693
|
-
const
|
|
1694
|
-
this.remoteCache.set(key,
|
|
1945
|
+
const result = await this.fetchRemoteResult(query, 0, controller.signal);
|
|
1946
|
+
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1695
1947
|
} catch {
|
|
1948
|
+
} finally {
|
|
1949
|
+
this.prefetchControllers.delete(controller);
|
|
1696
1950
|
}
|
|
1697
1951
|
}
|
|
1698
1952
|
/**
|
|
@@ -1718,12 +1972,12 @@ var ForgeSelect = class {
|
|
|
1718
1972
|
const controller = new AbortController();
|
|
1719
1973
|
this.ajaxController = controller;
|
|
1720
1974
|
const page = append ? this.page + 1 : 0;
|
|
1975
|
+
const cursor = append ? this.nextCursor : void 0;
|
|
1721
1976
|
try {
|
|
1722
|
-
const key = this.remoteCacheKey(query, page);
|
|
1977
|
+
const key = this.remoteCacheKey(query, page, cursor);
|
|
1723
1978
|
let result = this.remoteCache.get(key);
|
|
1724
1979
|
if (!result) {
|
|
1725
|
-
|
|
1726
|
-
result = normalizeRemoteResult(ajax, json);
|
|
1980
|
+
result = await this.fetchRemoteResult(query, page, controller.signal, cursor);
|
|
1727
1981
|
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1728
1982
|
}
|
|
1729
1983
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
@@ -1734,17 +1988,21 @@ var ForgeSelect = class {
|
|
|
1734
1988
|
} else {
|
|
1735
1989
|
this.data = options;
|
|
1736
1990
|
this.rowContentCache.clear();
|
|
1991
|
+
this.rowHeightCache.clear();
|
|
1737
1992
|
}
|
|
1738
1993
|
this.page = page;
|
|
1739
1994
|
this.hasMore = hasMore;
|
|
1995
|
+
this.nextCursor = result.nextCursor;
|
|
1740
1996
|
this.remoteLoaded = true;
|
|
1741
1997
|
this.loadError = null;
|
|
1998
|
+
this.rebuildOptionIndexes();
|
|
1742
1999
|
} catch (cause) {
|
|
1743
2000
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed || controller.signal.aborted) return;
|
|
1744
2001
|
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1745
2002
|
if (!append) {
|
|
1746
2003
|
this.data = [];
|
|
1747
2004
|
this.rowContentCache.clear();
|
|
2005
|
+
this.rowHeightCache.clear();
|
|
1748
2006
|
}
|
|
1749
2007
|
this.hasMore = false;
|
|
1750
2008
|
this.loadError = error;
|