forge-select 0.4.0 → 0.5.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/README.md +1 -1
- package/dist/index.cjs +352 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -3
- package/dist/index.d.ts +54 -3
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +352 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/styles/forge-select.css +12 -0
package/dist/index.js
CHANGED
|
@@ -156,6 +156,89 @@ function normalizeRemoteResult(ajax, response) {
|
|
|
156
156
|
return { options: result.options, hasMore: ajax.pagination ? Boolean(result.hasMore) : false };
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// src/remote-cache.ts
|
|
160
|
+
var RemoteCache = class {
|
|
161
|
+
constructor() {
|
|
162
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
163
|
+
}
|
|
164
|
+
get(key, now = Date.now()) {
|
|
165
|
+
const entry = this.entries.get(key);
|
|
166
|
+
if (!entry) return void 0;
|
|
167
|
+
if (entry.expiresAt <= now) {
|
|
168
|
+
this.entries.delete(key);
|
|
169
|
+
return void 0;
|
|
170
|
+
}
|
|
171
|
+
return entry.value;
|
|
172
|
+
}
|
|
173
|
+
set(key, value, ttl, now = Date.now()) {
|
|
174
|
+
if (ttl > 0) this.entries.set(key, { value, expiresAt: now + ttl });
|
|
175
|
+
}
|
|
176
|
+
clear() {
|
|
177
|
+
this.entries.clear();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/search.ts
|
|
182
|
+
function normalizeSearchText(value, accentInsensitive = true) {
|
|
183
|
+
const lower = value.toLocaleLowerCase();
|
|
184
|
+
return accentInsensitive ? lower.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d") : lower;
|
|
185
|
+
}
|
|
186
|
+
function getSearchField(option, field) {
|
|
187
|
+
if (field === "label") return option.label;
|
|
188
|
+
if (field === "description") return option.description ?? "";
|
|
189
|
+
const path = field.slice(5).split(".");
|
|
190
|
+
let value = option.meta;
|
|
191
|
+
for (const key of path) {
|
|
192
|
+
if (!value || typeof value !== "object") return "";
|
|
193
|
+
value = value[key];
|
|
194
|
+
}
|
|
195
|
+
return value == null ? "" : String(value);
|
|
196
|
+
}
|
|
197
|
+
var SearchIndex = class {
|
|
198
|
+
constructor() {
|
|
199
|
+
this.cache = /* @__PURE__ */ new WeakMap();
|
|
200
|
+
}
|
|
201
|
+
clear() {
|
|
202
|
+
this.cache = /* @__PURE__ */ new WeakMap();
|
|
203
|
+
}
|
|
204
|
+
score(option, query, config) {
|
|
205
|
+
const normalizedQuery = normalizeSearchText(query.trim(), config.accentInsensitive);
|
|
206
|
+
if (!normalizedQuery) return 1;
|
|
207
|
+
if (config.scorer) return config.scorer(option, query.trim(), normalizedQuery);
|
|
208
|
+
const key = `${config.accentInsensitive ? "1" : "0"}:${config.fields.join("\0")}`;
|
|
209
|
+
let variants = this.cache.get(option);
|
|
210
|
+
if (!variants) {
|
|
211
|
+
variants = /* @__PURE__ */ new Map();
|
|
212
|
+
this.cache.set(option, variants);
|
|
213
|
+
}
|
|
214
|
+
let haystacks = variants.get(key);
|
|
215
|
+
if (!haystacks) {
|
|
216
|
+
haystacks = config.fields.map(
|
|
217
|
+
(field) => normalizeSearchText(getSearchField(option, field), config.accentInsensitive)
|
|
218
|
+
);
|
|
219
|
+
variants.set(key, haystacks);
|
|
220
|
+
}
|
|
221
|
+
const tokens = config.tokenSearch ? normalizedQuery.split(/\s+/).filter(Boolean) : [normalizedQuery];
|
|
222
|
+
if (!tokens.every((token) => haystacks.some((field) => field.includes(token)))) return 0;
|
|
223
|
+
const label = haystacks[config.fields.indexOf("label")];
|
|
224
|
+
if (label === normalizedQuery) return 4;
|
|
225
|
+
if (label?.startsWith(normalizedQuery)) return 3;
|
|
226
|
+
if (label?.includes(normalizedQuery)) return 2;
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function findNormalizedRanges(label, query, accentInsensitive = true) {
|
|
231
|
+
const tokens = normalizeSearchText(query.trim(), accentInsensitive).split(/\s+/).filter(Boolean);
|
|
232
|
+
if (!tokens.length) return [];
|
|
233
|
+
const normalized = normalizeSearchText(label, accentInsensitive);
|
|
234
|
+
const ranges = [];
|
|
235
|
+
for (const token of tokens) {
|
|
236
|
+
const index = normalized.indexOf(token);
|
|
237
|
+
if (index >= 0) ranges.push([index, index + token.length]);
|
|
238
|
+
}
|
|
239
|
+
return ranges.sort((a, b) => a[0] - b[0]);
|
|
240
|
+
}
|
|
241
|
+
|
|
159
242
|
// src/selection.ts
|
|
160
243
|
function isGroup(item) {
|
|
161
244
|
return item.options !== void 0;
|
|
@@ -240,6 +323,8 @@ var ForgeSelect = class {
|
|
|
240
323
|
this.navItems = [];
|
|
241
324
|
this.highlightedIndex = -1;
|
|
242
325
|
this.rowContentCache = /* @__PURE__ */ new Map();
|
|
326
|
+
this.rowHeightCache = /* @__PURE__ */ new Map();
|
|
327
|
+
this.searchIndex = new SearchIndex();
|
|
243
328
|
this.expandedValues = /* @__PURE__ */ new Set();
|
|
244
329
|
this.loading = false;
|
|
245
330
|
this.loadingMore = false;
|
|
@@ -249,6 +334,7 @@ var ForgeSelect = class {
|
|
|
249
334
|
this.ajaxRequestId = 0;
|
|
250
335
|
this.ajaxController = null;
|
|
251
336
|
this.remoteLoaded = false;
|
|
337
|
+
this.remoteCache = new RemoteCache();
|
|
252
338
|
this.loadError = null;
|
|
253
339
|
this.originalDisplay = "";
|
|
254
340
|
this.originalDisabled = false;
|
|
@@ -274,6 +360,7 @@ var ForgeSelect = class {
|
|
|
274
360
|
this.control.setAttribute("aria-invalid", "true");
|
|
275
361
|
if (!this.isOpen) this.open();
|
|
276
362
|
this.control.focus();
|
|
363
|
+
this.emitter.emit("invalid", this.nativeSelect?.validationMessage ?? "");
|
|
277
364
|
};
|
|
278
365
|
this.onNativeChange = () => {
|
|
279
366
|
if (!this.nativeSelect || this.destroyed || this.syncingNative) return;
|
|
@@ -312,11 +399,17 @@ var ForgeSelect = class {
|
|
|
312
399
|
templateResult: options.templateResult,
|
|
313
400
|
templateSelection: options.templateSelection,
|
|
314
401
|
filterOption: options.filterOption,
|
|
402
|
+
searchFields: options.searchFields ?? ["label", "description"],
|
|
403
|
+
tokenSearch: options.tokenSearch ?? true,
|
|
404
|
+
accentInsensitive: options.accentInsensitive ?? true,
|
|
405
|
+
searchScorer: options.searchScorer,
|
|
406
|
+
highlightSearch: options.highlightSearch ?? false,
|
|
315
407
|
minSearchLength: Math.max(0, Math.floor(options.minSearchLength ?? 0)),
|
|
316
408
|
minResultsForSearch: Math.max(0, Math.floor(options.minResultsForSearch ?? 0)),
|
|
317
409
|
isOptionDisabled: options.isOptionDisabled,
|
|
318
410
|
virtualScroll: options.virtualScroll,
|
|
319
|
-
itemHeight: options.itemHeight
|
|
411
|
+
itemHeight: typeof options.itemHeight === "number" ? Math.max(1, options.itemHeight) : DEFAULT_ITEM_HEIGHT,
|
|
412
|
+
variableItemHeight: options.itemHeight === "auto",
|
|
320
413
|
language: options.language ?? "en",
|
|
321
414
|
plugins: options.plugins ?? [],
|
|
322
415
|
openOnFocus: options.openOnFocus ?? false,
|
|
@@ -340,6 +433,7 @@ var ForgeSelect = class {
|
|
|
340
433
|
nativeSelect?.addEventListener("invalid", this.onNativeInvalid);
|
|
341
434
|
this.nativeForm?.addEventListener("reset", this.onFormReset);
|
|
342
435
|
for (const plugin of this.plugins) plugin.onInit?.(this);
|
|
436
|
+
for (const query of this.opts.ajax?.prefetch ?? []) void this.prefetchRemote(query);
|
|
343
437
|
}
|
|
344
438
|
applyNativeValues(values) {
|
|
345
439
|
this.selected = [];
|
|
@@ -356,7 +450,7 @@ var ForgeSelect = class {
|
|
|
356
450
|
this.root.classList.add("forge-select--open");
|
|
357
451
|
this.control.setAttribute("aria-expanded", "true");
|
|
358
452
|
document.addEventListener("mousedown", this.onDocumentMouseDown);
|
|
359
|
-
if (this.opts.ajax && !this.remoteLoaded) {
|
|
453
|
+
if (this.opts.ajax && (this.opts.ajax.loadOnOpen ?? true) && !this.remoteLoaded) {
|
|
360
454
|
this.scheduleRemoteLoad(this.query, 0);
|
|
361
455
|
}
|
|
362
456
|
this.renderList();
|
|
@@ -413,6 +507,8 @@ var ForgeSelect = class {
|
|
|
413
507
|
this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
|
|
414
508
|
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
415
509
|
this.rowContentCache.clear();
|
|
510
|
+
this.rowHeightCache.clear();
|
|
511
|
+
this.searchIndex.clear();
|
|
416
512
|
this.portalHost?.remove();
|
|
417
513
|
this.root.remove();
|
|
418
514
|
this.el.style.display = this.originalDisplay;
|
|
@@ -423,6 +519,106 @@ var ForgeSelect = class {
|
|
|
423
519
|
if (this.opts.multiple) return [...this.selected];
|
|
424
520
|
return this.selected[0] ?? null;
|
|
425
521
|
}
|
|
522
|
+
getSearchQuery() {
|
|
523
|
+
return this.query;
|
|
524
|
+
}
|
|
525
|
+
setSearchQuery(query, options = {}) {
|
|
526
|
+
this.applySearchQuery(query, options.emitSearch ?? true);
|
|
527
|
+
}
|
|
528
|
+
isDropdownOpen() {
|
|
529
|
+
return this.isOpen;
|
|
530
|
+
}
|
|
531
|
+
updateOptions(options) {
|
|
532
|
+
if (options.data) this.setData(options.data);
|
|
533
|
+
if ("ajax" in options && options.ajax !== this.opts.ajax) {
|
|
534
|
+
this.opts.ajax = options.ajax;
|
|
535
|
+
this.remoteLoaded = false;
|
|
536
|
+
this.clearRemoteCache();
|
|
537
|
+
}
|
|
538
|
+
if (options.placeholder !== void 0) this.opts.placeholder = options.placeholder;
|
|
539
|
+
if (options.clearable !== void 0) this.opts.clearable = options.clearable;
|
|
540
|
+
if (options.allowCreate !== void 0) this.opts.allowCreate = options.allowCreate;
|
|
541
|
+
if (options.sortable !== void 0) this.opts.sortable = options.sortable;
|
|
542
|
+
if (options.closeOnSelect !== void 0) this.opts.closeOnSelect = options.closeOnSelect;
|
|
543
|
+
if ("maxSelections" in options)
|
|
544
|
+
this.opts.maxSelections = options.maxSelections == null || !Number.isFinite(options.maxSelections) ? void 0 : Math.max(0, Math.floor(options.maxSelections));
|
|
545
|
+
if (options.theme !== void 0) {
|
|
546
|
+
this.opts.theme = options.theme;
|
|
547
|
+
this.root.dataset.theme = options.theme;
|
|
548
|
+
if (this.portalHost) this.portalHost.dataset.theme = options.theme;
|
|
549
|
+
}
|
|
550
|
+
if (options.required !== void 0) {
|
|
551
|
+
this.opts.required = options.required;
|
|
552
|
+
if (options.required) this.control.setAttribute("aria-required", "true");
|
|
553
|
+
else this.control.removeAttribute("aria-required");
|
|
554
|
+
if (this.nativeSelect) this.nativeSelect.required = options.required;
|
|
555
|
+
}
|
|
556
|
+
if (options.templateResult !== void 0) this.opts.templateResult = options.templateResult;
|
|
557
|
+
if (options.templateSelection !== void 0) this.opts.templateSelection = options.templateSelection;
|
|
558
|
+
if (options.filterOption !== void 0) this.opts.filterOption = options.filterOption;
|
|
559
|
+
if (options.searchFields !== void 0) this.opts.searchFields = options.searchFields;
|
|
560
|
+
if (options.tokenSearch !== void 0) this.opts.tokenSearch = options.tokenSearch;
|
|
561
|
+
if (options.accentInsensitive !== void 0) this.opts.accentInsensitive = options.accentInsensitive;
|
|
562
|
+
if (options.searchScorer !== void 0) this.opts.searchScorer = options.searchScorer;
|
|
563
|
+
if (options.highlightSearch !== void 0) this.opts.highlightSearch = options.highlightSearch;
|
|
564
|
+
if (options.minSearchLength !== void 0)
|
|
565
|
+
this.opts.minSearchLength = Math.max(0, Math.floor(options.minSearchLength));
|
|
566
|
+
if (options.minResultsForSearch !== void 0)
|
|
567
|
+
this.opts.minResultsForSearch = Math.max(0, Math.floor(options.minResultsForSearch));
|
|
568
|
+
if (options.isOptionDisabled !== void 0) this.opts.isOptionDisabled = options.isOptionDisabled;
|
|
569
|
+
if (options.virtualScroll !== void 0) this.opts.virtualScroll = options.virtualScroll;
|
|
570
|
+
if (options.itemHeight !== void 0) {
|
|
571
|
+
this.opts.variableItemHeight = options.itemHeight === "auto";
|
|
572
|
+
if (typeof options.itemHeight === "number") this.opts.itemHeight = Math.max(1, options.itemHeight);
|
|
573
|
+
this.root.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
574
|
+
this.portalHost?.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
575
|
+
}
|
|
576
|
+
if (options.language !== void 0) {
|
|
577
|
+
this.opts.language = options.language;
|
|
578
|
+
this.strings = getStrings(options.language);
|
|
579
|
+
this.clearBtn.setAttribute("aria-label", this.strings.clearSelection);
|
|
580
|
+
this.searchInput?.setAttribute("aria-label", this.strings.search);
|
|
581
|
+
}
|
|
582
|
+
if (options.openOnFocus !== void 0) this.opts.openOnFocus = options.openOnFocus;
|
|
583
|
+
if (options.disabled !== void 0) {
|
|
584
|
+
if (options.disabled) this.disable();
|
|
585
|
+
else this.enable();
|
|
586
|
+
}
|
|
587
|
+
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
588
|
+
this.updateSearchVisibility();
|
|
589
|
+
this.rowContentCache.clear();
|
|
590
|
+
this.searchIndex.clear();
|
|
591
|
+
this.renderValue();
|
|
592
|
+
if (this.isOpen) this.renderList();
|
|
593
|
+
}
|
|
594
|
+
validate() {
|
|
595
|
+
const valid = (!this.opts.required || this.selected.length > 0) && (this.control.dataset.validationMessage ?? "") === "";
|
|
596
|
+
this.control.classList.toggle("forge-select__control--invalid", !valid);
|
|
597
|
+
this.control.setAttribute("aria-invalid", String(!valid));
|
|
598
|
+
return valid;
|
|
599
|
+
}
|
|
600
|
+
setCustomValidity(message) {
|
|
601
|
+
this.nativeSelect?.setCustomValidity(message);
|
|
602
|
+
this.control.dataset.validationMessage = message;
|
|
603
|
+
}
|
|
604
|
+
reportValidity() {
|
|
605
|
+
const valid = this.validate() && (this.nativeSelect?.checkValidity() ?? true);
|
|
606
|
+
if (!valid) {
|
|
607
|
+
const message = this.nativeSelect?.validationMessage ?? this.control.dataset.validationMessage ?? "";
|
|
608
|
+
if (this.nativeSelect) return this.nativeSelect.reportValidity();
|
|
609
|
+
this.emitter.emit("invalid", message);
|
|
610
|
+
}
|
|
611
|
+
return valid;
|
|
612
|
+
}
|
|
613
|
+
reload() {
|
|
614
|
+
if (!this.opts.ajax) return;
|
|
615
|
+
this.clearRemoteCache();
|
|
616
|
+
this.remoteLoaded = false;
|
|
617
|
+
this.scheduleRemoteLoad(this.query, 0);
|
|
618
|
+
}
|
|
619
|
+
clearRemoteCache() {
|
|
620
|
+
this.remoteCache.clear();
|
|
621
|
+
}
|
|
426
622
|
setValue(value, options = {}) {
|
|
427
623
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
428
624
|
const next = this.opts.multiple ? values : values.slice(0, 1);
|
|
@@ -445,7 +641,7 @@ var ForgeSelect = class {
|
|
|
445
641
|
this.ajaxController?.abort();
|
|
446
642
|
this.ajaxController = null;
|
|
447
643
|
this.ajaxRequestId += 1;
|
|
448
|
-
this.
|
|
644
|
+
this.setLoading(false);
|
|
449
645
|
this.loadingMore = false;
|
|
450
646
|
this.loadError = null;
|
|
451
647
|
this.remoteLoaded = true;
|
|
@@ -455,6 +651,7 @@ var ForgeSelect = class {
|
|
|
455
651
|
this.opts.data = data;
|
|
456
652
|
this.updateSearchVisibility();
|
|
457
653
|
this.rowContentCache.clear();
|
|
654
|
+
this.searchIndex.clear();
|
|
458
655
|
this.highlightedIndex = -1;
|
|
459
656
|
if (this.isOpen) this.renderList();
|
|
460
657
|
}
|
|
@@ -628,25 +825,7 @@ var ForgeSelect = class {
|
|
|
628
825
|
});
|
|
629
826
|
if (this.searchInput) {
|
|
630
827
|
this.searchInput.addEventListener("input", () => {
|
|
631
|
-
this.
|
|
632
|
-
this.highlightedIndex = -1;
|
|
633
|
-
this.list.scrollTop = 0;
|
|
634
|
-
this.emitter.emit("search", this.query);
|
|
635
|
-
const trimmed = this.query.trim();
|
|
636
|
-
const belowMinLength = trimmed !== "" && trimmed.length < this.opts.minSearchLength;
|
|
637
|
-
if (this.opts.ajax && !belowMinLength) {
|
|
638
|
-
this.scheduleRemoteLoad(this.query, this.opts.ajax.debounce ?? 250);
|
|
639
|
-
} else {
|
|
640
|
-
if (belowMinLength) {
|
|
641
|
-
if (this.ajaxTimer) {
|
|
642
|
-
clearTimeout(this.ajaxTimer);
|
|
643
|
-
this.ajaxTimer = null;
|
|
644
|
-
}
|
|
645
|
-
this.ajaxController?.abort();
|
|
646
|
-
this.loading = false;
|
|
647
|
-
}
|
|
648
|
-
this.renderList();
|
|
649
|
-
}
|
|
828
|
+
this.applySearchQuery(this.searchInput.value, true);
|
|
650
829
|
});
|
|
651
830
|
this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
652
831
|
this.searchInput.addEventListener("paste", (event) => {
|
|
@@ -697,6 +876,29 @@ var ForgeSelect = class {
|
|
|
697
876
|
this.maybeLoadNextPage();
|
|
698
877
|
});
|
|
699
878
|
}
|
|
879
|
+
applySearchQuery(query, emitSearch) {
|
|
880
|
+
this.query = query;
|
|
881
|
+
if (this.searchInput && this.searchInput.value !== query) this.searchInput.value = query;
|
|
882
|
+
this.highlightedIndex = -1;
|
|
883
|
+
this.list.scrollTop = 0;
|
|
884
|
+
this.rowContentCache.clear();
|
|
885
|
+
if (emitSearch) this.emitter.emit("search", query);
|
|
886
|
+
const trimmed = query.trim();
|
|
887
|
+
const belowMinLength = trimmed !== "" && trimmed.length < this.opts.minSearchLength;
|
|
888
|
+
if (this.opts.ajax && !belowMinLength) {
|
|
889
|
+
this.scheduleRemoteLoad(query, this.opts.ajax.debounce ?? 250);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (belowMinLength) {
|
|
893
|
+
if (this.ajaxTimer) {
|
|
894
|
+
clearTimeout(this.ajaxTimer);
|
|
895
|
+
this.ajaxTimer = null;
|
|
896
|
+
}
|
|
897
|
+
this.ajaxController?.abort();
|
|
898
|
+
this.setLoading(false);
|
|
899
|
+
}
|
|
900
|
+
this.renderList();
|
|
901
|
+
}
|
|
700
902
|
handleKeydown(event) {
|
|
701
903
|
if (this.isDisabled) return;
|
|
702
904
|
switch (event.key) {
|
|
@@ -1088,8 +1290,13 @@ var ForgeSelect = class {
|
|
|
1088
1290
|
this.rows = [];
|
|
1089
1291
|
this.navItems = [];
|
|
1090
1292
|
const trimmedQuery = this.query.trim();
|
|
1091
|
-
const query = trimmedQuery.
|
|
1092
|
-
const matches = (option) => query === "" || (this.opts.filterOption ? this.opts.filterOption(option, trimmedQuery) :
|
|
1293
|
+
const query = normalizeSearchText(trimmedQuery, this.opts.accentInsensitive);
|
|
1294
|
+
const matches = (option) => query === "" || (this.opts.filterOption ? this.opts.filterOption(option, trimmedQuery) : this.searchIndex.score(option, trimmedQuery, {
|
|
1295
|
+
fields: this.opts.searchFields,
|
|
1296
|
+
tokenSearch: this.opts.tokenSearch,
|
|
1297
|
+
accentInsensitive: this.opts.accentInsensitive,
|
|
1298
|
+
scorer: this.opts.searchScorer
|
|
1299
|
+
}) > 0);
|
|
1093
1300
|
const subtreeMatches = (option) => query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
1094
1301
|
const pushOption = (option, depth, parentValue) => {
|
|
1095
1302
|
let navIndex = -1;
|
|
@@ -1145,6 +1352,25 @@ var ForgeSelect = class {
|
|
|
1145
1352
|
usesVirtualScroll() {
|
|
1146
1353
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1147
1354
|
}
|
|
1355
|
+
rowKey(row, index) {
|
|
1356
|
+
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1357
|
+
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1358
|
+
return `${row.kind}:${index}`;
|
|
1359
|
+
}
|
|
1360
|
+
measuredRowHeight(index) {
|
|
1361
|
+
return this.opts.variableItemHeight ? this.rowHeightCache.get(this.rowKey(this.rows[index], index)) ?? this.opts.itemHeight : this.opts.itemHeight;
|
|
1362
|
+
}
|
|
1363
|
+
rowOffset(index) {
|
|
1364
|
+
if (!this.opts.variableItemHeight) return index * this.opts.itemHeight;
|
|
1365
|
+
let offset = 0;
|
|
1366
|
+
for (let i = 0; i < index; i += 1) offset += this.measuredRowHeight(i);
|
|
1367
|
+
return offset;
|
|
1368
|
+
}
|
|
1369
|
+
rowOffsets() {
|
|
1370
|
+
const offsets = [0];
|
|
1371
|
+
for (let i = 0; i < this.rows.length; i += 1) offsets.push(offsets[i] + this.measuredRowHeight(i));
|
|
1372
|
+
return offsets;
|
|
1373
|
+
}
|
|
1148
1374
|
renderList() {
|
|
1149
1375
|
this.buildRows();
|
|
1150
1376
|
this.renderRows();
|
|
@@ -1161,26 +1387,40 @@ var ForgeSelect = class {
|
|
|
1161
1387
|
const virtual = this.usesVirtualScroll();
|
|
1162
1388
|
this.list.textContent = "";
|
|
1163
1389
|
const rowHeight = this.opts.itemHeight;
|
|
1390
|
+
const offsets = this.opts.variableItemHeight ? this.rowOffsets() : null;
|
|
1164
1391
|
let start = 0;
|
|
1165
1392
|
let end = this.rows.length;
|
|
1166
1393
|
if (virtual) {
|
|
1167
1394
|
const viewport = clientHeight || rowHeight * 8;
|
|
1168
|
-
|
|
1169
|
-
|
|
1395
|
+
if (this.opts.variableItemHeight) {
|
|
1396
|
+
while (start < this.rows.length && offsets[start + 1] < scrollTop) start += 1;
|
|
1397
|
+
start = Math.max(0, start - VIRTUAL_BUFFER);
|
|
1398
|
+
end = start;
|
|
1399
|
+
const target = scrollTop + viewport + VIRTUAL_BUFFER * rowHeight;
|
|
1400
|
+
while (end < this.rows.length && offsets[end] < target) end += 1;
|
|
1401
|
+
} else {
|
|
1402
|
+
start = Math.max(0, Math.floor(scrollTop / rowHeight) - VIRTUAL_BUFFER);
|
|
1403
|
+
end = Math.min(this.rows.length, start + Math.ceil(viewport / rowHeight) + VIRTUAL_BUFFER * 2);
|
|
1404
|
+
}
|
|
1170
1405
|
const topSpacer = document.createElement("li");
|
|
1171
1406
|
topSpacer.className = "forge-select__spacer";
|
|
1172
1407
|
topSpacer.setAttribute("aria-hidden", "true");
|
|
1173
|
-
topSpacer.style.height = `${start
|
|
1408
|
+
topSpacer.style.height = `${offsets?.[start] ?? this.rowOffset(start)}px`;
|
|
1174
1409
|
this.list.append(topSpacer);
|
|
1175
1410
|
}
|
|
1176
1411
|
for (let i = start; i < end; i++) {
|
|
1177
|
-
this.
|
|
1412
|
+
const element = this.renderRow(this.rows[i]);
|
|
1413
|
+
this.list.append(element);
|
|
1414
|
+
if (this.opts.variableItemHeight) {
|
|
1415
|
+
const measured = element.getBoundingClientRect().height || element.offsetHeight;
|
|
1416
|
+
if (measured > 0) this.rowHeightCache.set(this.rowKey(this.rows[i], i), measured);
|
|
1417
|
+
}
|
|
1178
1418
|
}
|
|
1179
1419
|
if (virtual) {
|
|
1180
1420
|
const bottomSpacer = document.createElement("li");
|
|
1181
1421
|
bottomSpacer.className = "forge-select__spacer";
|
|
1182
1422
|
bottomSpacer.setAttribute("aria-hidden", "true");
|
|
1183
|
-
bottomSpacer.style.height = `${
|
|
1423
|
+
bottomSpacer.style.height = `${offsets ? offsets[this.rows.length] - offsets[end] : this.rowOffset(this.rows.length) - this.rowOffset(end)}px`;
|
|
1184
1424
|
this.list.append(bottomSpacer);
|
|
1185
1425
|
if (this.list.scrollTop !== scrollTop) {
|
|
1186
1426
|
this.list.scrollTop = scrollTop;
|
|
@@ -1247,6 +1487,7 @@ var ForgeSelect = class {
|
|
|
1247
1487
|
if (isSelected) li.classList.add("forge-select__option--selected");
|
|
1248
1488
|
if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
|
|
1249
1489
|
li.classList.add("forge-select__option--indeterminate");
|
|
1490
|
+
li.dataset.selectionState = "mixed";
|
|
1250
1491
|
}
|
|
1251
1492
|
if (row.depth > 0) {
|
|
1252
1493
|
li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
|
|
@@ -1282,6 +1523,28 @@ var ForgeSelect = class {
|
|
|
1282
1523
|
* cached content state-free.
|
|
1283
1524
|
*/
|
|
1284
1525
|
optionContent(option) {
|
|
1526
|
+
if (this.opts.highlightSearch && this.query.trim() && !this.opts.templateResult) {
|
|
1527
|
+
const holder = document.createElement("span");
|
|
1528
|
+
holder.className = "forge-select__option-content";
|
|
1529
|
+
renderOptionContent(holder, option, void 0);
|
|
1530
|
+
const label = holder.querySelector(".forge-select__option-label") ?? holder;
|
|
1531
|
+
const ranges = findNormalizedRanges(option.label, this.query, this.opts.accentInsensitive);
|
|
1532
|
+
if (ranges.length) {
|
|
1533
|
+
label.textContent = "";
|
|
1534
|
+
let cursor = 0;
|
|
1535
|
+
for (const [start, end] of ranges) {
|
|
1536
|
+
if (start < cursor) continue;
|
|
1537
|
+
label.append(document.createTextNode(option.label.slice(cursor, start)));
|
|
1538
|
+
const mark = document.createElement("mark");
|
|
1539
|
+
mark.className = "forge-select__match";
|
|
1540
|
+
mark.textContent = option.label.slice(start, end);
|
|
1541
|
+
label.append(mark);
|
|
1542
|
+
cursor = end;
|
|
1543
|
+
}
|
|
1544
|
+
label.append(document.createTextNode(option.label.slice(cursor)));
|
|
1545
|
+
}
|
|
1546
|
+
return holder;
|
|
1547
|
+
}
|
|
1285
1548
|
let cached = this.rowContentCache.get(option.value);
|
|
1286
1549
|
if (!cached) {
|
|
1287
1550
|
const holder = document.createElement("span");
|
|
@@ -1308,8 +1571,8 @@ var ForgeSelect = class {
|
|
|
1308
1571
|
(row) => (row.kind === "option" || row.kind === "create") && row.navIndex === next
|
|
1309
1572
|
);
|
|
1310
1573
|
if (rowIndex >= 0) {
|
|
1311
|
-
const rowHeight = this.
|
|
1312
|
-
const top = rowIndex
|
|
1574
|
+
const rowHeight = this.measuredRowHeight(rowIndex);
|
|
1575
|
+
const top = this.rowOffset(rowIndex);
|
|
1313
1576
|
const viewport = this.list.clientHeight || rowHeight * 8;
|
|
1314
1577
|
let target = this.list.scrollTop;
|
|
1315
1578
|
if (top < target) target = top;
|
|
@@ -1374,7 +1637,7 @@ var ForgeSelect = class {
|
|
|
1374
1637
|
this.ajaxController = null;
|
|
1375
1638
|
this.page = 0;
|
|
1376
1639
|
this.hasMore = true;
|
|
1377
|
-
this.
|
|
1640
|
+
this.setLoading(true);
|
|
1378
1641
|
this.loadingMore = false;
|
|
1379
1642
|
this.loadError = null;
|
|
1380
1643
|
this.renderList();
|
|
@@ -1383,6 +1646,55 @@ var ForgeSelect = class {
|
|
|
1383
1646
|
void this.loadRemote(query, { requestId });
|
|
1384
1647
|
}, delay);
|
|
1385
1648
|
}
|
|
1649
|
+
setLoading(loading) {
|
|
1650
|
+
if (this.loading === loading) return;
|
|
1651
|
+
this.loading = loading;
|
|
1652
|
+
this.emitter.emit("loading", loading);
|
|
1653
|
+
}
|
|
1654
|
+
remoteCacheKey(query, page) {
|
|
1655
|
+
return `${query}\0${page}`;
|
|
1656
|
+
}
|
|
1657
|
+
async requestRemote(query, page, signal) {
|
|
1658
|
+
const ajax = this.opts.ajax;
|
|
1659
|
+
const attempts = Math.max(0, Math.floor(ajax.retry ?? 0)) + 1;
|
|
1660
|
+
let lastError;
|
|
1661
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1662
|
+
try {
|
|
1663
|
+
if (ajax.request) return await ajax.request(query, page, signal);
|
|
1664
|
+
const response = await fetch(buildUrl(ajax, query, page), { signal });
|
|
1665
|
+
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1666
|
+
return await response.json();
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
lastError = error;
|
|
1669
|
+
if (signal.aborted || attempt === attempts - 1) throw error;
|
|
1670
|
+
const delay = Math.max(0, ajax.retryDelay ?? 250) * 2 ** attempt;
|
|
1671
|
+
await new Promise((resolve, reject) => {
|
|
1672
|
+
const timer = setTimeout(resolve, delay);
|
|
1673
|
+
signal.addEventListener(
|
|
1674
|
+
"abort",
|
|
1675
|
+
() => {
|
|
1676
|
+
clearTimeout(timer);
|
|
1677
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1678
|
+
},
|
|
1679
|
+
{ once: true }
|
|
1680
|
+
);
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
throw lastError;
|
|
1685
|
+
}
|
|
1686
|
+
async prefetchRemote(query) {
|
|
1687
|
+
const ajax = this.opts.ajax;
|
|
1688
|
+
if (!ajax || (ajax.cacheTtl ?? 3e4) <= 0) return;
|
|
1689
|
+
const key = this.remoteCacheKey(query, 0);
|
|
1690
|
+
if (this.remoteCache.get(key)) return;
|
|
1691
|
+
const controller = new AbortController();
|
|
1692
|
+
try {
|
|
1693
|
+
const json = await this.requestRemote(query, 0, controller.signal);
|
|
1694
|
+
this.remoteCache.set(key, normalizeRemoteResult(ajax, json), ajax.cacheTtl ?? 3e4);
|
|
1695
|
+
} catch {
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1386
1698
|
/**
|
|
1387
1699
|
* Fires on every list scroll. Only acts when pagination is opted into via
|
|
1388
1700
|
* `ajax.pagination`; reads real scroll geometry rather than row counts so
|
|
@@ -1407,17 +1719,15 @@ var ForgeSelect = class {
|
|
|
1407
1719
|
this.ajaxController = controller;
|
|
1408
1720
|
const page = append ? this.page + 1 : 0;
|
|
1409
1721
|
try {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1417
|
-
json = await response.json();
|
|
1722
|
+
const key = this.remoteCacheKey(query, page);
|
|
1723
|
+
let result = this.remoteCache.get(key);
|
|
1724
|
+
if (!result) {
|
|
1725
|
+
const json = await this.requestRemote(query, page, controller.signal);
|
|
1726
|
+
result = normalizeRemoteResult(ajax, json);
|
|
1727
|
+
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1418
1728
|
}
|
|
1419
1729
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
1420
|
-
const { options, hasMore } =
|
|
1730
|
+
const { options, hasMore } = result;
|
|
1421
1731
|
if (append) {
|
|
1422
1732
|
const existing = collectValues(this.data);
|
|
1423
1733
|
this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
|
|
@@ -1442,7 +1752,7 @@ var ForgeSelect = class {
|
|
|
1442
1752
|
} finally {
|
|
1443
1753
|
if (activeRequestId === this.ajaxRequestId && !this.destroyed) {
|
|
1444
1754
|
this.ajaxController = null;
|
|
1445
|
-
this.
|
|
1755
|
+
this.setLoading(false);
|
|
1446
1756
|
this.loadingMore = false;
|
|
1447
1757
|
if (this.isOpen) this.renderList();
|
|
1448
1758
|
}
|