forge-select 0.4.0 → 0.6.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 +467 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -3
- package/dist/index.d.ts +65 -3
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +467 -46
- 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,95 @@ 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 REMOTE_CACHE_LIMIT = 50;
|
|
161
|
+
var RemoteCache = class {
|
|
162
|
+
constructor() {
|
|
163
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
164
|
+
}
|
|
165
|
+
get(key, now = Date.now()) {
|
|
166
|
+
const entry = this.entries.get(key);
|
|
167
|
+
if (!entry) return void 0;
|
|
168
|
+
if (entry.expiresAt <= now) {
|
|
169
|
+
this.entries.delete(key);
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
return entry.value;
|
|
173
|
+
}
|
|
174
|
+
set(key, value, ttl, now = Date.now()) {
|
|
175
|
+
if (ttl <= 0) return;
|
|
176
|
+
if (this.entries.size >= REMOTE_CACHE_LIMIT && !this.entries.has(key)) {
|
|
177
|
+
const oldest = this.entries.keys().next().value;
|
|
178
|
+
this.entries.delete(oldest);
|
|
179
|
+
}
|
|
180
|
+
this.entries.set(key, { value, expiresAt: now + ttl });
|
|
181
|
+
}
|
|
182
|
+
clear() {
|
|
183
|
+
this.entries.clear();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/search.ts
|
|
188
|
+
function normalizeSearchText(value, accentInsensitive = true) {
|
|
189
|
+
const lower = value.toLocaleLowerCase();
|
|
190
|
+
return accentInsensitive ? lower.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d") : lower;
|
|
191
|
+
}
|
|
192
|
+
function getSearchField(option, field) {
|
|
193
|
+
if (field === "label") return option.label;
|
|
194
|
+
if (field === "description") return option.description ?? "";
|
|
195
|
+
const path = field.slice(5).split(".");
|
|
196
|
+
let value = option.meta;
|
|
197
|
+
for (const key of path) {
|
|
198
|
+
if (!value || typeof value !== "object") return "";
|
|
199
|
+
value = value[key];
|
|
200
|
+
}
|
|
201
|
+
return value == null ? "" : String(value);
|
|
202
|
+
}
|
|
203
|
+
var SearchIndex = class {
|
|
204
|
+
constructor() {
|
|
205
|
+
this.cache = /* @__PURE__ */ new WeakMap();
|
|
206
|
+
}
|
|
207
|
+
clear() {
|
|
208
|
+
this.cache = /* @__PURE__ */ new WeakMap();
|
|
209
|
+
}
|
|
210
|
+
score(option, query, config) {
|
|
211
|
+
const normalizedQuery = normalizeSearchText(query.trim(), config.accentInsensitive);
|
|
212
|
+
if (!normalizedQuery) return 1;
|
|
213
|
+
if (config.scorer) return config.scorer(option, query.trim(), normalizedQuery);
|
|
214
|
+
const key = `${config.accentInsensitive ? "1" : "0"}:${config.fields.join("\0")}`;
|
|
215
|
+
let variants = this.cache.get(option);
|
|
216
|
+
if (!variants) {
|
|
217
|
+
variants = /* @__PURE__ */ new Map();
|
|
218
|
+
this.cache.set(option, variants);
|
|
219
|
+
}
|
|
220
|
+
let haystacks = variants.get(key);
|
|
221
|
+
if (!haystacks) {
|
|
222
|
+
haystacks = config.fields.map(
|
|
223
|
+
(field) => normalizeSearchText(getSearchField(option, field), config.accentInsensitive)
|
|
224
|
+
);
|
|
225
|
+
variants.set(key, haystacks);
|
|
226
|
+
}
|
|
227
|
+
const tokens = config.tokenSearch ? normalizedQuery.split(/\s+/).filter(Boolean) : [normalizedQuery];
|
|
228
|
+
if (!tokens.every((token) => haystacks.some((field) => field.includes(token)))) return 0;
|
|
229
|
+
const label = haystacks[config.fields.indexOf("label")];
|
|
230
|
+
if (label === normalizedQuery) return 4;
|
|
231
|
+
if (label?.startsWith(normalizedQuery)) return 3;
|
|
232
|
+
if (label?.includes(normalizedQuery)) return 2;
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
function findNormalizedRanges(label, query, accentInsensitive = true) {
|
|
237
|
+
const tokens = normalizeSearchText(query.trim(), accentInsensitive).split(/\s+/).filter(Boolean);
|
|
238
|
+
if (!tokens.length) return [];
|
|
239
|
+
const normalized = normalizeSearchText(label, accentInsensitive);
|
|
240
|
+
const ranges = [];
|
|
241
|
+
for (const token of tokens) {
|
|
242
|
+
const index = normalized.indexOf(token);
|
|
243
|
+
if (index >= 0) ranges.push([index, index + token.length]);
|
|
244
|
+
}
|
|
245
|
+
return ranges.sort((a, b) => a[0] - b[0]);
|
|
246
|
+
}
|
|
247
|
+
|
|
159
248
|
// src/selection.ts
|
|
160
249
|
function isGroup(item) {
|
|
161
250
|
return item.options !== void 0;
|
|
@@ -222,6 +311,8 @@ var DEFAULT_ITEM_HEIGHT = 36;
|
|
|
222
311
|
var VIRTUAL_BUFFER = 5;
|
|
223
312
|
var VIRTUAL_THRESHOLD = 100;
|
|
224
313
|
var ROW_CACHE_LIMIT = 2e3;
|
|
314
|
+
var PAGE_SIZE = 10;
|
|
315
|
+
var TYPEAHEAD_RESET_MS = 500;
|
|
225
316
|
var uidCounter = 0;
|
|
226
317
|
var ForgeSelect = class {
|
|
227
318
|
constructor(target, options = {}) {
|
|
@@ -239,7 +330,14 @@ var ForgeSelect = class {
|
|
|
239
330
|
this.rows = [];
|
|
240
331
|
this.navItems = [];
|
|
241
332
|
this.highlightedIndex = -1;
|
|
333
|
+
this.typeaheadBuffer = "";
|
|
334
|
+
this.typeaheadTimer = null;
|
|
242
335
|
this.rowContentCache = /* @__PURE__ */ new Map();
|
|
336
|
+
this.rowHeightCache = /* @__PURE__ */ new Map();
|
|
337
|
+
this.rowOffsetsCache = null;
|
|
338
|
+
this.scrollRafId = null;
|
|
339
|
+
this.ancestorScrollRafId = null;
|
|
340
|
+
this.searchIndex = new SearchIndex();
|
|
243
341
|
this.expandedValues = /* @__PURE__ */ new Set();
|
|
244
342
|
this.loading = false;
|
|
245
343
|
this.loadingMore = false;
|
|
@@ -249,6 +347,7 @@ var ForgeSelect = class {
|
|
|
249
347
|
this.ajaxRequestId = 0;
|
|
250
348
|
this.ajaxController = null;
|
|
251
349
|
this.remoteLoaded = false;
|
|
350
|
+
this.remoteCache = new RemoteCache();
|
|
252
351
|
this.loadError = null;
|
|
253
352
|
this.originalDisplay = "";
|
|
254
353
|
this.originalDisabled = false;
|
|
@@ -266,7 +365,12 @@ var ForgeSelect = class {
|
|
|
266
365
|
this.positionDropdown();
|
|
267
366
|
};
|
|
268
367
|
this.onAncestorScroll = () => {
|
|
269
|
-
if (this.portalHost)
|
|
368
|
+
if (!this.portalHost) return;
|
|
369
|
+
if (this.ancestorScrollRafId != null) return;
|
|
370
|
+
this.ancestorScrollRafId = requestAnimationFrame(() => {
|
|
371
|
+
this.ancestorScrollRafId = null;
|
|
372
|
+
this.positionDropdown();
|
|
373
|
+
});
|
|
270
374
|
};
|
|
271
375
|
this.onNativeInvalid = (event) => {
|
|
272
376
|
event.preventDefault();
|
|
@@ -274,6 +378,7 @@ var ForgeSelect = class {
|
|
|
274
378
|
this.control.setAttribute("aria-invalid", "true");
|
|
275
379
|
if (!this.isOpen) this.open();
|
|
276
380
|
this.control.focus();
|
|
381
|
+
this.emitter.emit("invalid", this.nativeSelect?.validationMessage ?? "");
|
|
277
382
|
};
|
|
278
383
|
this.onNativeChange = () => {
|
|
279
384
|
if (!this.nativeSelect || this.destroyed || this.syncingNative) return;
|
|
@@ -312,11 +417,17 @@ var ForgeSelect = class {
|
|
|
312
417
|
templateResult: options.templateResult,
|
|
313
418
|
templateSelection: options.templateSelection,
|
|
314
419
|
filterOption: options.filterOption,
|
|
420
|
+
searchFields: options.searchFields ?? ["label", "description"],
|
|
421
|
+
tokenSearch: options.tokenSearch ?? true,
|
|
422
|
+
accentInsensitive: options.accentInsensitive ?? true,
|
|
423
|
+
searchScorer: options.searchScorer,
|
|
424
|
+
highlightSearch: options.highlightSearch ?? false,
|
|
315
425
|
minSearchLength: Math.max(0, Math.floor(options.minSearchLength ?? 0)),
|
|
316
426
|
minResultsForSearch: Math.max(0, Math.floor(options.minResultsForSearch ?? 0)),
|
|
317
427
|
isOptionDisabled: options.isOptionDisabled,
|
|
318
428
|
virtualScroll: options.virtualScroll,
|
|
319
|
-
itemHeight: options.itemHeight
|
|
429
|
+
itemHeight: typeof options.itemHeight === "number" ? Math.max(1, options.itemHeight) : DEFAULT_ITEM_HEIGHT,
|
|
430
|
+
variableItemHeight: options.itemHeight === "auto",
|
|
320
431
|
language: options.language ?? "en",
|
|
321
432
|
plugins: options.plugins ?? [],
|
|
322
433
|
openOnFocus: options.openOnFocus ?? false,
|
|
@@ -340,6 +451,7 @@ var ForgeSelect = class {
|
|
|
340
451
|
nativeSelect?.addEventListener("invalid", this.onNativeInvalid);
|
|
341
452
|
this.nativeForm?.addEventListener("reset", this.onFormReset);
|
|
342
453
|
for (const plugin of this.plugins) plugin.onInit?.(this);
|
|
454
|
+
for (const query of this.opts.ajax?.prefetch ?? []) void this.prefetchRemote(query);
|
|
343
455
|
}
|
|
344
456
|
applyNativeValues(values) {
|
|
345
457
|
this.selected = [];
|
|
@@ -356,7 +468,7 @@ var ForgeSelect = class {
|
|
|
356
468
|
this.root.classList.add("forge-select--open");
|
|
357
469
|
this.control.setAttribute("aria-expanded", "true");
|
|
358
470
|
document.addEventListener("mousedown", this.onDocumentMouseDown);
|
|
359
|
-
if (this.opts.ajax && !this.remoteLoaded) {
|
|
471
|
+
if (this.opts.ajax && (this.opts.ajax.loadOnOpen ?? true) && !this.remoteLoaded) {
|
|
360
472
|
this.scheduleRemoteLoad(this.query, 0);
|
|
361
473
|
}
|
|
362
474
|
this.renderList();
|
|
@@ -377,6 +489,19 @@ var ForgeSelect = class {
|
|
|
377
489
|
document.removeEventListener("mousedown", this.onDocumentMouseDown);
|
|
378
490
|
window.removeEventListener("resize", this.onWindowResize);
|
|
379
491
|
document.removeEventListener("scroll", this.onAncestorScroll, true);
|
|
492
|
+
if (this.ancestorScrollRafId != null) {
|
|
493
|
+
cancelAnimationFrame(this.ancestorScrollRafId);
|
|
494
|
+
this.ancestorScrollRafId = null;
|
|
495
|
+
}
|
|
496
|
+
if (this.scrollRafId != null) {
|
|
497
|
+
cancelAnimationFrame(this.scrollRafId);
|
|
498
|
+
this.scrollRafId = null;
|
|
499
|
+
}
|
|
500
|
+
if (this.typeaheadTimer) {
|
|
501
|
+
clearTimeout(this.typeaheadTimer);
|
|
502
|
+
this.typeaheadTimer = null;
|
|
503
|
+
}
|
|
504
|
+
this.typeaheadBuffer = "";
|
|
380
505
|
this.highlightedIndex = -1;
|
|
381
506
|
if (this.searchInput) {
|
|
382
507
|
this.searchInput.value = "";
|
|
@@ -409,10 +534,14 @@ var ForgeSelect = class {
|
|
|
409
534
|
this.destroyed = true;
|
|
410
535
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
411
536
|
this.ajaxController?.abort();
|
|
537
|
+
if (this.scrollRafId != null) cancelAnimationFrame(this.scrollRafId);
|
|
538
|
+
if (this.typeaheadTimer) clearTimeout(this.typeaheadTimer);
|
|
412
539
|
this.nativeSelect?.removeEventListener("change", this.onNativeChange);
|
|
413
540
|
this.nativeSelect?.removeEventListener("invalid", this.onNativeInvalid);
|
|
414
541
|
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
415
542
|
this.rowContentCache.clear();
|
|
543
|
+
this.rowHeightCache.clear();
|
|
544
|
+
this.searchIndex.clear();
|
|
416
545
|
this.portalHost?.remove();
|
|
417
546
|
this.root.remove();
|
|
418
547
|
this.el.style.display = this.originalDisplay;
|
|
@@ -423,6 +552,107 @@ var ForgeSelect = class {
|
|
|
423
552
|
if (this.opts.multiple) return [...this.selected];
|
|
424
553
|
return this.selected[0] ?? null;
|
|
425
554
|
}
|
|
555
|
+
getSearchQuery() {
|
|
556
|
+
return this.query;
|
|
557
|
+
}
|
|
558
|
+
setSearchQuery(query, options = {}) {
|
|
559
|
+
this.applySearchQuery(query, options.emitSearch ?? true);
|
|
560
|
+
}
|
|
561
|
+
isDropdownOpen() {
|
|
562
|
+
return this.isOpen;
|
|
563
|
+
}
|
|
564
|
+
updateOptions(options) {
|
|
565
|
+
if (options.data) this.setData(options.data);
|
|
566
|
+
if ("ajax" in options && options.ajax !== this.opts.ajax) {
|
|
567
|
+
this.opts.ajax = options.ajax;
|
|
568
|
+
this.remoteLoaded = false;
|
|
569
|
+
this.clearRemoteCache();
|
|
570
|
+
}
|
|
571
|
+
if (options.placeholder !== void 0) this.opts.placeholder = options.placeholder;
|
|
572
|
+
if (options.clearable !== void 0) this.opts.clearable = options.clearable;
|
|
573
|
+
if (options.allowCreate !== void 0) this.opts.allowCreate = options.allowCreate;
|
|
574
|
+
if (options.sortable !== void 0) this.opts.sortable = options.sortable;
|
|
575
|
+
if (options.closeOnSelect !== void 0) this.opts.closeOnSelect = options.closeOnSelect;
|
|
576
|
+
if ("maxSelections" in options)
|
|
577
|
+
this.opts.maxSelections = options.maxSelections == null || !Number.isFinite(options.maxSelections) ? void 0 : Math.max(0, Math.floor(options.maxSelections));
|
|
578
|
+
if (options.theme !== void 0) {
|
|
579
|
+
this.opts.theme = options.theme;
|
|
580
|
+
this.root.dataset.theme = options.theme;
|
|
581
|
+
if (this.portalHost) this.portalHost.dataset.theme = options.theme;
|
|
582
|
+
}
|
|
583
|
+
if (options.required !== void 0) {
|
|
584
|
+
this.opts.required = options.required;
|
|
585
|
+
if (options.required) this.control.setAttribute("aria-required", "true");
|
|
586
|
+
else this.control.removeAttribute("aria-required");
|
|
587
|
+
if (this.nativeSelect) this.nativeSelect.required = options.required;
|
|
588
|
+
}
|
|
589
|
+
if (options.templateResult !== void 0) this.opts.templateResult = options.templateResult;
|
|
590
|
+
if (options.templateSelection !== void 0) this.opts.templateSelection = options.templateSelection;
|
|
591
|
+
if (options.filterOption !== void 0) this.opts.filterOption = options.filterOption;
|
|
592
|
+
if (options.searchFields !== void 0) this.opts.searchFields = options.searchFields;
|
|
593
|
+
if (options.tokenSearch !== void 0) this.opts.tokenSearch = options.tokenSearch;
|
|
594
|
+
if (options.accentInsensitive !== void 0) this.opts.accentInsensitive = options.accentInsensitive;
|
|
595
|
+
if (options.searchScorer !== void 0) this.opts.searchScorer = options.searchScorer;
|
|
596
|
+
if (options.highlightSearch !== void 0) this.opts.highlightSearch = options.highlightSearch;
|
|
597
|
+
if (options.minSearchLength !== void 0)
|
|
598
|
+
this.opts.minSearchLength = Math.max(0, Math.floor(options.minSearchLength));
|
|
599
|
+
if (options.minResultsForSearch !== void 0)
|
|
600
|
+
this.opts.minResultsForSearch = Math.max(0, Math.floor(options.minResultsForSearch));
|
|
601
|
+
if (options.isOptionDisabled !== void 0) this.opts.isOptionDisabled = options.isOptionDisabled;
|
|
602
|
+
if (options.virtualScroll !== void 0) this.opts.virtualScroll = options.virtualScroll;
|
|
603
|
+
if (options.itemHeight !== void 0) {
|
|
604
|
+
this.opts.variableItemHeight = options.itemHeight === "auto";
|
|
605
|
+
if (typeof options.itemHeight === "number") this.opts.itemHeight = Math.max(1, options.itemHeight);
|
|
606
|
+
this.root.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
607
|
+
this.portalHost?.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
608
|
+
}
|
|
609
|
+
if (options.language !== void 0) {
|
|
610
|
+
this.opts.language = options.language;
|
|
611
|
+
this.strings = getStrings(options.language);
|
|
612
|
+
this.clearBtn.setAttribute("aria-label", this.strings.clearSelection);
|
|
613
|
+
this.searchInput?.setAttribute("aria-label", this.strings.search);
|
|
614
|
+
}
|
|
615
|
+
if (options.openOnFocus !== void 0) this.opts.openOnFocus = options.openOnFocus;
|
|
616
|
+
if (options.disabled !== void 0) {
|
|
617
|
+
if (options.disabled) this.disable();
|
|
618
|
+
else this.enable();
|
|
619
|
+
}
|
|
620
|
+
this.root.classList.toggle("forge-select--sortable", this.opts.sortable && this.opts.multiple);
|
|
621
|
+
this.updateSearchVisibility();
|
|
622
|
+
this.rowContentCache.clear();
|
|
623
|
+
this.rowHeightCache.clear();
|
|
624
|
+
this.searchIndex.clear();
|
|
625
|
+
this.renderValue();
|
|
626
|
+
if (this.isOpen) this.renderList();
|
|
627
|
+
}
|
|
628
|
+
validate() {
|
|
629
|
+
const valid = (!this.opts.required || this.selected.length > 0) && (this.control.dataset.validationMessage ?? "") === "";
|
|
630
|
+
this.control.classList.toggle("forge-select__control--invalid", !valid);
|
|
631
|
+
this.control.setAttribute("aria-invalid", String(!valid));
|
|
632
|
+
return valid;
|
|
633
|
+
}
|
|
634
|
+
setCustomValidity(message) {
|
|
635
|
+
this.nativeSelect?.setCustomValidity(message);
|
|
636
|
+
this.control.dataset.validationMessage = message;
|
|
637
|
+
}
|
|
638
|
+
reportValidity() {
|
|
639
|
+
const valid = this.validate() && (this.nativeSelect?.checkValidity() ?? true);
|
|
640
|
+
if (!valid) {
|
|
641
|
+
const message = this.nativeSelect?.validationMessage ?? this.control.dataset.validationMessage ?? "";
|
|
642
|
+
if (this.nativeSelect) return this.nativeSelect.reportValidity();
|
|
643
|
+
this.emitter.emit("invalid", message);
|
|
644
|
+
}
|
|
645
|
+
return valid;
|
|
646
|
+
}
|
|
647
|
+
reload() {
|
|
648
|
+
if (!this.opts.ajax) return;
|
|
649
|
+
this.clearRemoteCache();
|
|
650
|
+
this.remoteLoaded = false;
|
|
651
|
+
this.scheduleRemoteLoad(this.query, 0);
|
|
652
|
+
}
|
|
653
|
+
clearRemoteCache() {
|
|
654
|
+
this.remoteCache.clear();
|
|
655
|
+
}
|
|
426
656
|
setValue(value, options = {}) {
|
|
427
657
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
428
658
|
const next = this.opts.multiple ? values : values.slice(0, 1);
|
|
@@ -445,7 +675,7 @@ var ForgeSelect = class {
|
|
|
445
675
|
this.ajaxController?.abort();
|
|
446
676
|
this.ajaxController = null;
|
|
447
677
|
this.ajaxRequestId += 1;
|
|
448
|
-
this.
|
|
678
|
+
this.setLoading(false);
|
|
449
679
|
this.loadingMore = false;
|
|
450
680
|
this.loadError = null;
|
|
451
681
|
this.remoteLoaded = true;
|
|
@@ -455,6 +685,8 @@ var ForgeSelect = class {
|
|
|
455
685
|
this.opts.data = data;
|
|
456
686
|
this.updateSearchVisibility();
|
|
457
687
|
this.rowContentCache.clear();
|
|
688
|
+
this.rowHeightCache.clear();
|
|
689
|
+
this.searchIndex.clear();
|
|
458
690
|
this.highlightedIndex = -1;
|
|
459
691
|
if (this.isOpen) this.renderList();
|
|
460
692
|
}
|
|
@@ -628,25 +860,7 @@ var ForgeSelect = class {
|
|
|
628
860
|
});
|
|
629
861
|
if (this.searchInput) {
|
|
630
862
|
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
|
-
}
|
|
863
|
+
this.applySearchQuery(this.searchInput.value, true);
|
|
650
864
|
});
|
|
651
865
|
this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
652
866
|
this.searchInput.addEventListener("paste", (event) => {
|
|
@@ -693,10 +907,36 @@ var ForgeSelect = class {
|
|
|
693
907
|
this.activateNavItem(navIndex);
|
|
694
908
|
});
|
|
695
909
|
this.list.addEventListener("scroll", () => {
|
|
696
|
-
if (this.
|
|
697
|
-
this.
|
|
910
|
+
if (this.scrollRafId != null) return;
|
|
911
|
+
this.scrollRafId = requestAnimationFrame(() => {
|
|
912
|
+
this.scrollRafId = null;
|
|
913
|
+
if (this.usesVirtualScroll()) this.renderRows();
|
|
914
|
+
this.maybeLoadNextPage();
|
|
915
|
+
});
|
|
698
916
|
});
|
|
699
917
|
}
|
|
918
|
+
applySearchQuery(query, emitSearch) {
|
|
919
|
+
this.query = query;
|
|
920
|
+
if (this.searchInput && this.searchInput.value !== query) this.searchInput.value = query;
|
|
921
|
+
this.highlightedIndex = -1;
|
|
922
|
+
this.list.scrollTop = 0;
|
|
923
|
+
if (emitSearch) this.emitter.emit("search", query);
|
|
924
|
+
const trimmed = query.trim();
|
|
925
|
+
const belowMinLength = trimmed !== "" && trimmed.length < this.opts.minSearchLength;
|
|
926
|
+
if (this.opts.ajax && !belowMinLength) {
|
|
927
|
+
this.scheduleRemoteLoad(query, this.opts.ajax.debounce ?? 250);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (belowMinLength) {
|
|
931
|
+
if (this.ajaxTimer) {
|
|
932
|
+
clearTimeout(this.ajaxTimer);
|
|
933
|
+
this.ajaxTimer = null;
|
|
934
|
+
}
|
|
935
|
+
this.ajaxController?.abort();
|
|
936
|
+
this.setLoading(false);
|
|
937
|
+
}
|
|
938
|
+
this.renderList();
|
|
939
|
+
}
|
|
700
940
|
handleKeydown(event) {
|
|
701
941
|
if (this.isDisabled) return;
|
|
702
942
|
switch (event.key) {
|
|
@@ -733,9 +973,63 @@ var ForgeSelect = class {
|
|
|
733
973
|
case "ArrowLeft":
|
|
734
974
|
if (this.isOpen && this.navigateTree("left")) event.preventDefault();
|
|
735
975
|
break;
|
|
976
|
+
case "Home":
|
|
977
|
+
if (this.isOpen) {
|
|
978
|
+
event.preventDefault();
|
|
979
|
+
this.focusNavIndex(0);
|
|
980
|
+
}
|
|
981
|
+
break;
|
|
982
|
+
case "End":
|
|
983
|
+
if (this.isOpen) {
|
|
984
|
+
event.preventDefault();
|
|
985
|
+
this.focusNavIndex(this.navItems.length - 1);
|
|
986
|
+
}
|
|
987
|
+
break;
|
|
988
|
+
case "PageDown":
|
|
989
|
+
if (this.isOpen) {
|
|
990
|
+
event.preventDefault();
|
|
991
|
+
this.focusNavIndex(
|
|
992
|
+
Math.min(this.navItems.length - 1, (this.highlightedIndex === -1 ? 0 : this.highlightedIndex) + PAGE_SIZE)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
break;
|
|
996
|
+
case "PageUp":
|
|
997
|
+
if (this.isOpen) {
|
|
998
|
+
event.preventDefault();
|
|
999
|
+
this.focusNavIndex(Math.max(0, (this.highlightedIndex === -1 ? 0 : this.highlightedIndex) - PAGE_SIZE));
|
|
1000
|
+
}
|
|
1001
|
+
break;
|
|
736
1002
|
case "Tab":
|
|
737
1003
|
this.close();
|
|
738
1004
|
break;
|
|
1005
|
+
default:
|
|
1006
|
+
if (this.isOpen && event.target === this.control && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1007
|
+
this.handleTypeahead(event.key);
|
|
1008
|
+
}
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Jumps the highlight to the next nav item (wrapping) whose label starts
|
|
1014
|
+
* with the accumulated buffer, matching native <select> typeahead: rapid
|
|
1015
|
+
* distinct keystrokes narrow the prefix, a pause resets it.
|
|
1016
|
+
*/
|
|
1017
|
+
handleTypeahead(char) {
|
|
1018
|
+
if (this.typeaheadTimer) clearTimeout(this.typeaheadTimer);
|
|
1019
|
+
this.typeaheadBuffer += normalizeSearchText(char, this.opts.accentInsensitive);
|
|
1020
|
+
this.typeaheadTimer = setTimeout(() => {
|
|
1021
|
+
this.typeaheadBuffer = "";
|
|
1022
|
+
this.typeaheadTimer = null;
|
|
1023
|
+
}, TYPEAHEAD_RESET_MS);
|
|
1024
|
+
const prefix = [...this.typeaheadBuffer].every((value) => value === this.typeaheadBuffer[0]) ? this.typeaheadBuffer[0] : this.typeaheadBuffer;
|
|
1025
|
+
const count = this.navItems.length;
|
|
1026
|
+
for (let step = 1; step <= count; step += 1) {
|
|
1027
|
+
const index = (this.highlightedIndex + step + count) % count;
|
|
1028
|
+
const item = this.navItems[index];
|
|
1029
|
+
if (item.kind === "option" && normalizeSearchText(item.option.label, this.opts.accentInsensitive).startsWith(prefix)) {
|
|
1030
|
+
this.focusNavIndex(index);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
739
1033
|
}
|
|
740
1034
|
}
|
|
741
1035
|
// ---------------------------------------------------------------- selection
|
|
@@ -826,7 +1120,10 @@ var ForgeSelect = class {
|
|
|
826
1120
|
this.control.classList.remove("forge-select__control--invalid");
|
|
827
1121
|
this.control.removeAttribute("aria-invalid");
|
|
828
1122
|
}
|
|
829
|
-
if (this.isOpen)
|
|
1123
|
+
if (this.isOpen) {
|
|
1124
|
+
if (this.opts.maxSelections != null) this.renderList();
|
|
1125
|
+
else this.renderRows();
|
|
1126
|
+
}
|
|
830
1127
|
if (emitChange) this.emitter.emit("change", this.getValue());
|
|
831
1128
|
}
|
|
832
1129
|
syncNativeSelect(dispatchChange = true) {
|
|
@@ -913,6 +1210,7 @@ var ForgeSelect = class {
|
|
|
913
1210
|
if (result.created) this.emitter.emit("create", result.option);
|
|
914
1211
|
this.emitter.emit("select", result.option);
|
|
915
1212
|
if (!this.opts.multiple || this.opts.closeOnSelect) this.close();
|
|
1213
|
+
else if (this.isOpen) this.renderList();
|
|
916
1214
|
}
|
|
917
1215
|
activateNavItem(navIndex) {
|
|
918
1216
|
const item = this.navItems[navIndex];
|
|
@@ -1087,9 +1385,15 @@ var ForgeSelect = class {
|
|
|
1087
1385
|
buildRows() {
|
|
1088
1386
|
this.rows = [];
|
|
1089
1387
|
this.navItems = [];
|
|
1388
|
+
this.rowOffsetsCache = null;
|
|
1090
1389
|
const trimmedQuery = this.query.trim();
|
|
1091
|
-
const query = trimmedQuery.
|
|
1092
|
-
const matches = (option) => query === "" || (this.opts.filterOption ? this.opts.filterOption(option, trimmedQuery) :
|
|
1390
|
+
const query = normalizeSearchText(trimmedQuery, this.opts.accentInsensitive);
|
|
1391
|
+
const matches = (option) => query === "" || (this.opts.filterOption ? this.opts.filterOption(option, trimmedQuery) : this.searchIndex.score(option, trimmedQuery, {
|
|
1392
|
+
fields: this.opts.searchFields,
|
|
1393
|
+
tokenSearch: this.opts.tokenSearch,
|
|
1394
|
+
accentInsensitive: this.opts.accentInsensitive,
|
|
1395
|
+
scorer: this.opts.searchScorer
|
|
1396
|
+
}) > 0);
|
|
1093
1397
|
const subtreeMatches = (option) => query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
1094
1398
|
const pushOption = (option, depth, parentValue) => {
|
|
1095
1399
|
let navIndex = -1;
|
|
@@ -1145,6 +1449,27 @@ var ForgeSelect = class {
|
|
|
1145
1449
|
usesVirtualScroll() {
|
|
1146
1450
|
return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
|
|
1147
1451
|
}
|
|
1452
|
+
rowKey(row, index) {
|
|
1453
|
+
if (row.kind === "option") return `option:${row.option.value}`;
|
|
1454
|
+
if (row.kind === "group") return `group:${row.label}:${index}`;
|
|
1455
|
+
return `${row.kind}:${index}`;
|
|
1456
|
+
}
|
|
1457
|
+
measuredRowHeight(index) {
|
|
1458
|
+
return this.opts.variableItemHeight ? this.rowHeightCache.get(this.rowKey(this.rows[index], index)) ?? this.opts.itemHeight : this.opts.itemHeight;
|
|
1459
|
+
}
|
|
1460
|
+
rowOffset(index) {
|
|
1461
|
+
if (!this.opts.variableItemHeight) return index * this.opts.itemHeight;
|
|
1462
|
+
let offset = 0;
|
|
1463
|
+
for (let i = 0; i < index; i += 1) offset += this.measuredRowHeight(i);
|
|
1464
|
+
return offset;
|
|
1465
|
+
}
|
|
1466
|
+
rowOffsets() {
|
|
1467
|
+
if (this.rowOffsetsCache) return this.rowOffsetsCache;
|
|
1468
|
+
const offsets = [0];
|
|
1469
|
+
for (let i = 0; i < this.rows.length; i += 1) offsets.push(offsets[i] + this.measuredRowHeight(i));
|
|
1470
|
+
this.rowOffsetsCache = offsets;
|
|
1471
|
+
return offsets;
|
|
1472
|
+
}
|
|
1148
1473
|
renderList() {
|
|
1149
1474
|
this.buildRows();
|
|
1150
1475
|
this.renderRows();
|
|
@@ -1161,26 +1486,49 @@ var ForgeSelect = class {
|
|
|
1161
1486
|
const virtual = this.usesVirtualScroll();
|
|
1162
1487
|
this.list.textContent = "";
|
|
1163
1488
|
const rowHeight = this.opts.itemHeight;
|
|
1489
|
+
const offsets = this.opts.variableItemHeight ? this.rowOffsets() : null;
|
|
1164
1490
|
let start = 0;
|
|
1165
1491
|
let end = this.rows.length;
|
|
1166
1492
|
if (virtual) {
|
|
1167
1493
|
const viewport = clientHeight || rowHeight * 8;
|
|
1168
|
-
|
|
1169
|
-
|
|
1494
|
+
if (this.opts.variableItemHeight) {
|
|
1495
|
+
while (start < this.rows.length && offsets[start + 1] < scrollTop) start += 1;
|
|
1496
|
+
start = Math.max(0, start - VIRTUAL_BUFFER);
|
|
1497
|
+
end = start;
|
|
1498
|
+
const target = scrollTop + viewport + VIRTUAL_BUFFER * rowHeight;
|
|
1499
|
+
while (end < this.rows.length && offsets[end] < target) end += 1;
|
|
1500
|
+
} else {
|
|
1501
|
+
start = Math.max(0, Math.floor(scrollTop / rowHeight) - VIRTUAL_BUFFER);
|
|
1502
|
+
end = Math.min(this.rows.length, start + Math.ceil(viewport / rowHeight) + VIRTUAL_BUFFER * 2);
|
|
1503
|
+
}
|
|
1170
1504
|
const topSpacer = document.createElement("li");
|
|
1171
1505
|
topSpacer.className = "forge-select__spacer";
|
|
1172
1506
|
topSpacer.setAttribute("aria-hidden", "true");
|
|
1173
|
-
topSpacer.style.height = `${start
|
|
1507
|
+
topSpacer.style.height = `${offsets?.[start] ?? this.rowOffset(start)}px`;
|
|
1174
1508
|
this.list.append(topSpacer);
|
|
1175
1509
|
}
|
|
1510
|
+
const appended = [];
|
|
1176
1511
|
for (let i = start; i < end; i++) {
|
|
1177
|
-
this.
|
|
1512
|
+
const element = this.renderRow(this.rows[i]);
|
|
1513
|
+
this.list.append(element);
|
|
1514
|
+
appended.push(element);
|
|
1515
|
+
}
|
|
1516
|
+
if (this.opts.variableItemHeight) {
|
|
1517
|
+
for (let i = start; i < end; i++) {
|
|
1518
|
+
const element = appended[i - start];
|
|
1519
|
+
const measured = element.getBoundingClientRect().height || element.offsetHeight;
|
|
1520
|
+
if (measured > 0) {
|
|
1521
|
+
const key = this.rowKey(this.rows[i], i);
|
|
1522
|
+
if (this.rowHeightCache.get(key) !== measured) this.rowOffsetsCache = null;
|
|
1523
|
+
this.rowHeightCache.set(key, measured);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1178
1526
|
}
|
|
1179
1527
|
if (virtual) {
|
|
1180
1528
|
const bottomSpacer = document.createElement("li");
|
|
1181
1529
|
bottomSpacer.className = "forge-select__spacer";
|
|
1182
1530
|
bottomSpacer.setAttribute("aria-hidden", "true");
|
|
1183
|
-
bottomSpacer.style.height = `${
|
|
1531
|
+
bottomSpacer.style.height = `${offsets ? offsets[this.rows.length] - offsets[end] : this.rowOffset(this.rows.length) - this.rowOffset(end)}px`;
|
|
1184
1532
|
this.list.append(bottomSpacer);
|
|
1185
1533
|
if (this.list.scrollTop !== scrollTop) {
|
|
1186
1534
|
this.list.scrollTop = scrollTop;
|
|
@@ -1247,6 +1595,7 @@ var ForgeSelect = class {
|
|
|
1247
1595
|
if (isSelected) li.classList.add("forge-select__option--selected");
|
|
1248
1596
|
if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected, this.isOptionDisabled) === "some") {
|
|
1249
1597
|
li.classList.add("forge-select__option--indeterminate");
|
|
1598
|
+
li.dataset.selectionState = "mixed";
|
|
1250
1599
|
}
|
|
1251
1600
|
if (row.depth > 0) {
|
|
1252
1601
|
li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
|
|
@@ -1282,6 +1631,28 @@ var ForgeSelect = class {
|
|
|
1282
1631
|
* cached content state-free.
|
|
1283
1632
|
*/
|
|
1284
1633
|
optionContent(option) {
|
|
1634
|
+
if (this.opts.highlightSearch && this.query.trim() && !this.opts.templateResult) {
|
|
1635
|
+
const holder = document.createElement("span");
|
|
1636
|
+
holder.className = "forge-select__option-content";
|
|
1637
|
+
renderOptionContent(holder, option, void 0);
|
|
1638
|
+
const label = holder.querySelector(".forge-select__option-label") ?? holder;
|
|
1639
|
+
const ranges = findNormalizedRanges(option.label, this.query, this.opts.accentInsensitive);
|
|
1640
|
+
if (ranges.length) {
|
|
1641
|
+
label.textContent = "";
|
|
1642
|
+
let cursor = 0;
|
|
1643
|
+
for (const [start, end] of ranges) {
|
|
1644
|
+
if (start < cursor) continue;
|
|
1645
|
+
label.append(document.createTextNode(option.label.slice(cursor, start)));
|
|
1646
|
+
const mark = document.createElement("mark");
|
|
1647
|
+
mark.className = "forge-select__match";
|
|
1648
|
+
mark.textContent = option.label.slice(start, end);
|
|
1649
|
+
label.append(mark);
|
|
1650
|
+
cursor = end;
|
|
1651
|
+
}
|
|
1652
|
+
label.append(document.createTextNode(option.label.slice(cursor)));
|
|
1653
|
+
}
|
|
1654
|
+
return holder;
|
|
1655
|
+
}
|
|
1285
1656
|
let cached = this.rowContentCache.get(option.value);
|
|
1286
1657
|
if (!cached) {
|
|
1287
1658
|
const holder = document.createElement("span");
|
|
@@ -1302,14 +1673,15 @@ var ForgeSelect = class {
|
|
|
1302
1673
|
this.focusNavIndex(next);
|
|
1303
1674
|
}
|
|
1304
1675
|
focusNavIndex(next) {
|
|
1676
|
+
if (this.navItems.length === 0) return;
|
|
1305
1677
|
this.highlightedIndex = next;
|
|
1306
1678
|
if (this.usesVirtualScroll()) {
|
|
1307
1679
|
const rowIndex = this.rows.findIndex(
|
|
1308
1680
|
(row) => (row.kind === "option" || row.kind === "create") && row.navIndex === next
|
|
1309
1681
|
);
|
|
1310
1682
|
if (rowIndex >= 0) {
|
|
1311
|
-
const rowHeight = this.
|
|
1312
|
-
const top = rowIndex
|
|
1683
|
+
const rowHeight = this.measuredRowHeight(rowIndex);
|
|
1684
|
+
const top = this.rowOffset(rowIndex);
|
|
1313
1685
|
const viewport = this.list.clientHeight || rowHeight * 8;
|
|
1314
1686
|
let target = this.list.scrollTop;
|
|
1315
1687
|
if (top < target) target = top;
|
|
@@ -1374,7 +1746,7 @@ var ForgeSelect = class {
|
|
|
1374
1746
|
this.ajaxController = null;
|
|
1375
1747
|
this.page = 0;
|
|
1376
1748
|
this.hasMore = true;
|
|
1377
|
-
this.
|
|
1749
|
+
this.setLoading(true);
|
|
1378
1750
|
this.loadingMore = false;
|
|
1379
1751
|
this.loadError = null;
|
|
1380
1752
|
this.renderList();
|
|
@@ -1383,6 +1755,55 @@ var ForgeSelect = class {
|
|
|
1383
1755
|
void this.loadRemote(query, { requestId });
|
|
1384
1756
|
}, delay);
|
|
1385
1757
|
}
|
|
1758
|
+
setLoading(loading) {
|
|
1759
|
+
if (this.loading === loading) return;
|
|
1760
|
+
this.loading = loading;
|
|
1761
|
+
this.emitter.emit("loading", loading);
|
|
1762
|
+
}
|
|
1763
|
+
remoteCacheKey(query, page) {
|
|
1764
|
+
return `${query}\0${page}`;
|
|
1765
|
+
}
|
|
1766
|
+
async requestRemote(query, page, signal) {
|
|
1767
|
+
const ajax = this.opts.ajax;
|
|
1768
|
+
const attempts = Math.max(0, Math.floor(ajax.retry ?? 0)) + 1;
|
|
1769
|
+
let lastError;
|
|
1770
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1771
|
+
try {
|
|
1772
|
+
if (ajax.request) return await ajax.request(query, page, signal);
|
|
1773
|
+
const response = await fetch(buildUrl(ajax, query, page), { signal });
|
|
1774
|
+
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
1775
|
+
return await response.json();
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
lastError = error;
|
|
1778
|
+
if (signal.aborted || attempt === attempts - 1) throw error;
|
|
1779
|
+
const delay = Math.max(0, ajax.retryDelay ?? 250) * 2 ** attempt;
|
|
1780
|
+
await new Promise((resolve, reject) => {
|
|
1781
|
+
const timer = setTimeout(resolve, delay);
|
|
1782
|
+
signal.addEventListener(
|
|
1783
|
+
"abort",
|
|
1784
|
+
() => {
|
|
1785
|
+
clearTimeout(timer);
|
|
1786
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1787
|
+
},
|
|
1788
|
+
{ once: true }
|
|
1789
|
+
);
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
throw lastError;
|
|
1794
|
+
}
|
|
1795
|
+
async prefetchRemote(query) {
|
|
1796
|
+
const ajax = this.opts.ajax;
|
|
1797
|
+
if (!ajax || (ajax.cacheTtl ?? 3e4) <= 0) return;
|
|
1798
|
+
const key = this.remoteCacheKey(query, 0);
|
|
1799
|
+
if (this.remoteCache.get(key)) return;
|
|
1800
|
+
const controller = new AbortController();
|
|
1801
|
+
try {
|
|
1802
|
+
const json = await this.requestRemote(query, 0, controller.signal);
|
|
1803
|
+
this.remoteCache.set(key, normalizeRemoteResult(ajax, json), ajax.cacheTtl ?? 3e4);
|
|
1804
|
+
} catch {
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1386
1807
|
/**
|
|
1387
1808
|
* Fires on every list scroll. Only acts when pagination is opted into via
|
|
1388
1809
|
* `ajax.pagination`; reads real scroll geometry rather than row counts so
|
|
@@ -1407,23 +1828,22 @@ var ForgeSelect = class {
|
|
|
1407
1828
|
this.ajaxController = controller;
|
|
1408
1829
|
const page = append ? this.page + 1 : 0;
|
|
1409
1830
|
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();
|
|
1831
|
+
const key = this.remoteCacheKey(query, page);
|
|
1832
|
+
let result = this.remoteCache.get(key);
|
|
1833
|
+
if (!result) {
|
|
1834
|
+
const json = await this.requestRemote(query, page, controller.signal);
|
|
1835
|
+
result = normalizeRemoteResult(ajax, json);
|
|
1836
|
+
this.remoteCache.set(key, result, ajax.cacheTtl ?? 3e4);
|
|
1418
1837
|
}
|
|
1419
1838
|
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
1420
|
-
const { options, hasMore } =
|
|
1839
|
+
const { options, hasMore } = result;
|
|
1421
1840
|
if (append) {
|
|
1422
1841
|
const existing = collectValues(this.data);
|
|
1423
1842
|
this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
|
|
1424
1843
|
} else {
|
|
1425
1844
|
this.data = options;
|
|
1426
1845
|
this.rowContentCache.clear();
|
|
1846
|
+
this.rowHeightCache.clear();
|
|
1427
1847
|
}
|
|
1428
1848
|
this.page = page;
|
|
1429
1849
|
this.hasMore = hasMore;
|
|
@@ -1435,6 +1855,7 @@ var ForgeSelect = class {
|
|
|
1435
1855
|
if (!append) {
|
|
1436
1856
|
this.data = [];
|
|
1437
1857
|
this.rowContentCache.clear();
|
|
1858
|
+
this.rowHeightCache.clear();
|
|
1438
1859
|
}
|
|
1439
1860
|
this.hasMore = false;
|
|
1440
1861
|
this.loadError = error;
|
|
@@ -1442,7 +1863,7 @@ var ForgeSelect = class {
|
|
|
1442
1863
|
} finally {
|
|
1443
1864
|
if (activeRequestId === this.ajaxRequestId && !this.destroyed) {
|
|
1444
1865
|
this.ajaxController = null;
|
|
1445
|
-
this.
|
|
1866
|
+
this.setLoading(false);
|
|
1446
1867
|
this.loadingMore = false;
|
|
1447
1868
|
if (this.isOpen) this.renderList();
|
|
1448
1869
|
}
|