forge-select 0.1.0 → 0.3.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.
@@ -1,751 +1,2 @@
1
- "use strict";
2
- var ForgeSelectBundle = (() => {
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // src/index.ts
22
- var index_exports = {};
23
- __export(index_exports, {
24
- ForgeSelect: () => ForgeSelect,
25
- default: () => ForgeSelect
26
- });
27
-
28
- // src/emitter.ts
29
- var Emitter = class {
30
- constructor() {
31
- this.handlers = /* @__PURE__ */ new Map();
32
- }
33
- on(event, handler) {
34
- let set = this.handlers.get(event);
35
- if (!set) {
36
- set = /* @__PURE__ */ new Set();
37
- this.handlers.set(event, set);
38
- }
39
- set.add(handler);
40
- }
41
- off(event, handler) {
42
- this.handlers.get(event)?.delete(handler);
43
- }
44
- emit(event, ...args) {
45
- const set = this.handlers.get(event);
46
- if (!set) return;
47
- for (const handler of [...set]) handler(...args);
48
- }
49
- clear() {
50
- this.handlers.clear();
51
- }
52
- };
53
-
54
- // src/i18n.ts
55
- var locales = {
56
- en: {
57
- noResults: "No results found",
58
- loading: "Loading\u2026",
59
- createOption: 'Create "{query}"',
60
- clearSelection: "Clear selection",
61
- removeItem: "Remove {label}",
62
- search: "Search"
63
- },
64
- vi: {
65
- noResults: "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3",
66
- loading: "\u0110ang t\u1EA3i\u2026",
67
- createOption: 'T\u1EA1o "{query}"',
68
- clearSelection: "X\xF3a l\u1EF1a ch\u1ECDn",
69
- removeItem: "X\xF3a {label}",
70
- search: "T\xECm ki\u1EBFm"
71
- }
72
- };
73
- function getStrings(language) {
74
- if (typeof language === "string") {
75
- return locales[language] ?? locales.en;
76
- }
77
- return { ...locales.en, ...language };
78
- }
79
- function format(template, vars) {
80
- return template.replace(/\{(\w+)\}/g, (match, key) => vars[key] ?? match);
81
- }
82
-
83
- // src/ForgeSelect.ts
84
- var DEFAULT_ITEM_HEIGHT = 36;
85
- var VIRTUAL_BUFFER = 5;
86
- var VIRTUAL_THRESHOLD = 100;
87
- var ROW_CACHE_LIMIT = 2e3;
88
- var uidCounter = 0;
89
- function isGroup(item) {
90
- return item.options !== void 0;
91
- }
92
- var ForgeSelect = class {
93
- constructor(target, options = {}) {
94
- this.selected = [];
95
- this.selectedOptions = /* @__PURE__ */ new Map();
96
- this.emitter = new Emitter();
97
- this.uid = `forge-select-${++uidCounter}`;
98
- this.searchInput = null;
99
- this.isOpen = false;
100
- this.isDisabled = false;
101
- this.destroyed = false;
102
- this.query = "";
103
- this.rows = [];
104
- this.navItems = [];
105
- this.highlightedIndex = -1;
106
- this.rowContentCache = /* @__PURE__ */ new Map();
107
- this.loading = false;
108
- this.ajaxTimer = null;
109
- this.ajaxRequestId = 0;
110
- this.remoteLoaded = false;
111
- this.onDocumentMouseDown = (event) => {
112
- if (!this.root.contains(event.target)) this.close();
113
- };
114
- const el = typeof target === "string" ? document.querySelector(target) : target;
115
- if (!el) {
116
- throw new Error(`ForgeSelect: target element not found: ${String(target)}`);
117
- }
118
- this.el = el;
119
- const nativeSelect = el instanceof HTMLSelectElement ? el : null;
120
- this.opts = {
121
- placeholder: options.placeholder ?? "",
122
- searchable: options.searchable ?? true,
123
- multiple: options.multiple ?? nativeSelect?.multiple ?? false,
124
- clearable: options.clearable ?? false,
125
- allowCreate: options.allowCreate ?? false,
126
- theme: options.theme ?? "default",
127
- disabled: options.disabled ?? false,
128
- data: options.data,
129
- ajax: options.ajax,
130
- templateResult: options.templateResult,
131
- templateSelection: options.templateSelection,
132
- virtualScroll: options.virtualScroll,
133
- itemHeight: options.itemHeight ?? DEFAULT_ITEM_HEIGHT,
134
- language: options.language ?? "en",
135
- plugins: options.plugins ?? []
136
- };
137
- this.strings = getStrings(this.opts.language);
138
- this.plugins = this.opts.plugins;
139
- this.data = this.opts.data ?? (nativeSelect ? parseNativeOptions(nativeSelect) : []);
140
- if (nativeSelect && !this.opts.data) {
141
- for (const option of Array.from(nativeSelect.querySelectorAll("option"))) {
142
- if (option.hasAttribute("selected")) this.selectValue(option.value, false);
143
- }
144
- }
145
- this.buildDom();
146
- this.renderValue();
147
- if (this.opts.disabled) this.disable();
148
- for (const plugin of this.plugins) plugin.onInit?.(this);
149
- }
150
- // ---------------------------------------------------------------- public API
151
- open() {
152
- if (this.isOpen || this.isDisabled || this.destroyed) return;
153
- this.isOpen = true;
154
- this.dropdown.hidden = false;
155
- this.root.classList.add("forge-select--open");
156
- this.control.setAttribute("aria-expanded", "true");
157
- document.addEventListener("mousedown", this.onDocumentMouseDown);
158
- if (this.opts.ajax && !this.remoteLoaded) {
159
- this.scheduleRemoteLoad(this.query, 0);
160
- }
161
- this.renderList();
162
- if (this.searchInput) this.searchInput.focus();
163
- this.emitter.emit("open");
164
- for (const plugin of this.plugins) plugin.onOpen?.(this);
165
- }
166
- close() {
167
- if (!this.isOpen) return;
168
- this.isOpen = false;
169
- this.dropdown.hidden = true;
170
- this.root.classList.remove("forge-select--open");
171
- this.control.setAttribute("aria-expanded", "false");
172
- document.removeEventListener("mousedown", this.onDocumentMouseDown);
173
- this.highlightedIndex = -1;
174
- if (this.searchInput) {
175
- this.searchInput.value = "";
176
- this.query = "";
177
- }
178
- this.emitter.emit("close");
179
- for (const plugin of this.plugins) plugin.onClose?.(this);
180
- }
181
- destroy() {
182
- if (this.destroyed) return;
183
- this.close();
184
- for (const plugin of this.plugins) plugin.onDestroy?.(this);
185
- this.destroyed = true;
186
- if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
187
- this.rowContentCache.clear();
188
- this.root.remove();
189
- this.el.style.display = "";
190
- this.emitter.clear();
191
- }
192
- getValue() {
193
- if (this.opts.multiple) return [...this.selected];
194
- return this.selected[0] ?? null;
195
- }
196
- setValue(value) {
197
- const values = value == null ? [] : Array.isArray(value) ? value : [value];
198
- const next = this.opts.multiple ? values : values.slice(0, 1);
199
- if (arraysEqual(next, this.selected)) return;
200
- this.selected = [];
201
- for (const v of next) this.selectValue(v, false);
202
- this.afterSelectionChange();
203
- }
204
- enable() {
205
- this.isDisabled = false;
206
- this.root.classList.remove("forge-select--disabled");
207
- this.control.tabIndex = 0;
208
- this.control.setAttribute("aria-disabled", "false");
209
- }
210
- disable() {
211
- this.close();
212
- this.isDisabled = true;
213
- this.root.classList.add("forge-select--disabled");
214
- this.control.tabIndex = -1;
215
- this.control.setAttribute("aria-disabled", "true");
216
- }
217
- on(event, handler) {
218
- this.emitter.on(event, handler);
219
- }
220
- off(event, handler) {
221
- this.emitter.off(event, handler);
222
- }
223
- // ---------------------------------------------------------------- DOM setup
224
- buildDom() {
225
- this.root = document.createElement("div");
226
- this.root.className = "forge-select";
227
- this.root.dataset.theme = this.opts.theme;
228
- this.root.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
229
- this.control = document.createElement("div");
230
- this.control.className = "forge-select__control";
231
- this.control.setAttribute("role", "combobox");
232
- this.control.setAttribute("aria-haspopup", "listbox");
233
- this.control.setAttribute("aria-expanded", "false");
234
- this.control.setAttribute("aria-controls", `${this.uid}-list`);
235
- this.control.tabIndex = 0;
236
- this.valueEl = document.createElement("div");
237
- this.valueEl.className = "forge-select__value";
238
- this.clearBtn = document.createElement("button");
239
- this.clearBtn.type = "button";
240
- this.clearBtn.className = "forge-select__clear";
241
- this.clearBtn.setAttribute("aria-label", this.strings.clearSelection);
242
- this.clearBtn.textContent = "\xD7";
243
- this.clearBtn.hidden = true;
244
- const arrow = document.createElement("span");
245
- arrow.className = "forge-select__arrow";
246
- arrow.setAttribute("aria-hidden", "true");
247
- this.control.append(this.valueEl, this.clearBtn, arrow);
248
- this.dropdown = document.createElement("div");
249
- this.dropdown.className = "forge-select__dropdown";
250
- this.dropdown.hidden = true;
251
- if (this.opts.searchable) {
252
- this.searchInput = document.createElement("input");
253
- this.searchInput.type = "search";
254
- this.searchInput.className = "forge-select__search";
255
- this.searchInput.setAttribute("aria-label", this.strings.search);
256
- this.searchInput.setAttribute("aria-autocomplete", "list");
257
- this.searchInput.setAttribute("aria-controls", `${this.uid}-list`);
258
- this.dropdown.append(this.searchInput);
259
- }
260
- this.list = document.createElement("ul");
261
- this.list.className = "forge-select__list";
262
- this.list.id = `${this.uid}-list`;
263
- this.list.setAttribute("role", "listbox");
264
- if (this.opts.multiple) this.list.setAttribute("aria-multiselectable", "true");
265
- this.dropdown.append(this.list);
266
- this.root.append(this.control, this.dropdown);
267
- this.el.style.display = "none";
268
- this.el.insertAdjacentElement("afterend", this.root);
269
- this.bindEvents();
270
- }
271
- bindEvents() {
272
- this.control.addEventListener("click", (event) => {
273
- if (event.target === this.clearBtn) return;
274
- if (this.isDisabled) return;
275
- this.isOpen ? this.close() : this.open();
276
- });
277
- this.control.addEventListener("keydown", (event) => this.handleKeydown(event));
278
- this.clearBtn.addEventListener("click", (event) => {
279
- event.stopPropagation();
280
- this.clearSelection();
281
- });
282
- if (this.searchInput) {
283
- this.searchInput.addEventListener("input", () => {
284
- this.query = this.searchInput.value;
285
- this.highlightedIndex = -1;
286
- this.list.scrollTop = 0;
287
- this.emitter.emit("search", this.query);
288
- if (this.opts.ajax) {
289
- this.scheduleRemoteLoad(this.query, this.opts.ajax.debounce ?? 250);
290
- } else {
291
- this.renderList();
292
- }
293
- });
294
- this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
295
- }
296
- this.list.addEventListener("click", (event) => {
297
- const li = event.target.closest("li[data-nav-index]");
298
- if (!li) return;
299
- const navIndex = Number(li.dataset.navIndex);
300
- this.activateNavItem(navIndex);
301
- });
302
- this.list.addEventListener("scroll", () => {
303
- if (this.usesVirtualScroll()) this.renderRows();
304
- });
305
- }
306
- handleKeydown(event) {
307
- if (this.isDisabled) return;
308
- switch (event.key) {
309
- case "Enter":
310
- event.preventDefault();
311
- if (!this.isOpen) this.open();
312
- else if (this.highlightedIndex >= 0) this.activateNavItem(this.highlightedIndex);
313
- break;
314
- case " ":
315
- if (event.target === this.control) {
316
- event.preventDefault();
317
- if (!this.isOpen) this.open();
318
- }
319
- break;
320
- case "ArrowDown":
321
- event.preventDefault();
322
- if (!this.isOpen) this.open();
323
- else this.moveHighlight(1);
324
- break;
325
- case "ArrowUp":
326
- event.preventDefault();
327
- if (this.isOpen) this.moveHighlight(-1);
328
- break;
329
- case "Escape":
330
- if (this.isOpen) {
331
- event.preventDefault();
332
- this.close();
333
- this.control.focus();
334
- }
335
- break;
336
- case "Tab":
337
- this.close();
338
- break;
339
- }
340
- }
341
- // ---------------------------------------------------------------- selection
342
- selectValue(value, notify) {
343
- if (this.selected.includes(value)) return;
344
- const option = this.findOption(value) ?? this.selectedOptions.get(value) ?? { value, label: value };
345
- this.selectedOptions.set(value, option);
346
- if (this.opts.multiple) this.selected.push(value);
347
- else this.selected = [value];
348
- if (notify) this.afterSelectionChange();
349
- }
350
- deselectValue(value, notify) {
351
- const index = this.selected.indexOf(value);
352
- if (index === -1) return;
353
- this.selected.splice(index, 1);
354
- if (notify) this.afterSelectionChange();
355
- }
356
- clearSelection() {
357
- if (this.selected.length === 0) return;
358
- this.selected = [];
359
- this.emitter.emit("clear");
360
- this.afterSelectionChange();
361
- }
362
- afterSelectionChange() {
363
- this.renderValue();
364
- this.syncNativeSelect();
365
- if (this.isOpen) this.renderList();
366
- this.emitter.emit("change", this.getValue());
367
- }
368
- syncNativeSelect() {
369
- if (!(this.el instanceof HTMLSelectElement)) return;
370
- const existing = /* @__PURE__ */ new Set();
371
- for (const option of Array.from(this.el.options)) {
372
- existing.add(option.value);
373
- option.selected = this.selected.includes(option.value);
374
- }
375
- for (const value of this.selected) {
376
- if (existing.has(value)) continue;
377
- const option = document.createElement("option");
378
- option.value = value;
379
- option.textContent = this.selectedOptions.get(value)?.label ?? value;
380
- option.selected = true;
381
- this.el.append(option);
382
- }
383
- this.el.dispatchEvent(new Event("change", { bubbles: true }));
384
- }
385
- findOption(value) {
386
- for (const item of this.data) {
387
- if (isGroup(item)) {
388
- const found = item.options.find((o) => o.value === value);
389
- if (found) return found;
390
- } else if (item.value === value) {
391
- return item;
392
- }
393
- }
394
- return void 0;
395
- }
396
- createFromQuery() {
397
- const label = this.query.trim();
398
- if (!label) return;
399
- const option = { value: label, label };
400
- this.data.push(option);
401
- if (this.searchInput) {
402
- this.searchInput.value = "";
403
- this.query = "";
404
- }
405
- this.selectValue(option.value, true);
406
- if (!this.opts.multiple) this.close();
407
- }
408
- activateNavItem(navIndex) {
409
- const item = this.navItems[navIndex];
410
- if (!item) return;
411
- if (item.kind === "create") {
412
- this.createFromQuery();
413
- return;
414
- }
415
- const { value } = item.option;
416
- if (this.opts.multiple) {
417
- this.selected.includes(value) ? this.deselectValue(value, true) : this.selectValue(value, true);
418
- } else {
419
- this.selectValue(value, true);
420
- this.close();
421
- this.control.focus();
422
- }
423
- }
424
- // ---------------------------------------------------------------- rendering
425
- renderValue() {
426
- this.valueEl.textContent = "";
427
- const hasValue = this.selected.length > 0;
428
- this.clearBtn.hidden = !(this.opts.clearable && hasValue);
429
- if (!hasValue) {
430
- const placeholder = document.createElement("span");
431
- placeholder.className = "forge-select__placeholder";
432
- placeholder.textContent = this.opts.placeholder;
433
- this.valueEl.append(placeholder);
434
- return;
435
- }
436
- if (this.opts.multiple) {
437
- for (const value of this.selected) {
438
- const option = this.selectedOptions.get(value) ?? { value, label: value };
439
- const tag = document.createElement("span");
440
- tag.className = "forge-select__tag";
441
- const label = document.createElement("span");
442
- label.className = "forge-select__tag-label";
443
- this.renderTemplate(label, option, this.opts.templateSelection, "inline");
444
- const remove = document.createElement("button");
445
- remove.type = "button";
446
- remove.className = "forge-select__tag-remove";
447
- remove.setAttribute("aria-label", format(this.strings.removeItem, { label: option.label }));
448
- remove.textContent = "\xD7";
449
- remove.addEventListener("click", (event) => {
450
- event.stopPropagation();
451
- if (!this.isDisabled) this.deselectValue(value, true);
452
- });
453
- tag.append(label, remove);
454
- this.valueEl.append(tag);
455
- }
456
- } else {
457
- const option = this.selectedOptions.get(this.selected[0]) ?? {
458
- value: this.selected[0],
459
- label: this.selected[0]
460
- };
461
- const span = document.createElement("span");
462
- span.className = "forge-select__single-value";
463
- this.renderTemplate(span, option, this.opts.templateSelection, "inline");
464
- this.valueEl.append(span);
465
- }
466
- }
467
- renderTemplate(container, option, template, variant = "row") {
468
- if (template) {
469
- const result = template(option);
470
- if (typeof result === "string") container.innerHTML = result;
471
- else container.append(result);
472
- return;
473
- }
474
- if (!option.avatar && !option.description) {
475
- container.textContent = option.label;
476
- return;
477
- }
478
- if (option.avatar) {
479
- const avatar = document.createElement("img");
480
- avatar.className = variant === "row" ? "forge-select__option-avatar" : "forge-select__inline-avatar";
481
- avatar.src = option.avatar;
482
- avatar.alt = "";
483
- avatar.setAttribute("loading", "lazy");
484
- avatar.setAttribute("decoding", "async");
485
- container.append(avatar);
486
- }
487
- if (variant === "row" && option.description) {
488
- const body = document.createElement("span");
489
- body.className = "forge-select__option-body";
490
- const label = document.createElement("span");
491
- label.className = "forge-select__option-label";
492
- label.textContent = option.label;
493
- const desc = document.createElement("span");
494
- desc.className = "forge-select__option-desc";
495
- desc.textContent = option.description;
496
- body.append(label, desc);
497
- container.append(body);
498
- } else {
499
- const label = document.createElement("span");
500
- label.className = "forge-select__option-label";
501
- label.textContent = option.label;
502
- container.append(label);
503
- }
504
- }
505
- buildRows() {
506
- this.rows = [];
507
- this.navItems = [];
508
- const query = this.query.trim().toLowerCase();
509
- const matches = (option) => query === "" || option.label.toLowerCase().includes(query) || (option.description?.toLowerCase().includes(query) ?? false);
510
- const pushOption = (option) => {
511
- let navIndex = -1;
512
- if (!option.disabled) {
513
- navIndex = this.navItems.length;
514
- this.navItems.push({ kind: "option", option });
515
- }
516
- this.rows.push({ kind: "option", option, navIndex });
517
- };
518
- if (this.loading) {
519
- this.rows.push({ kind: "loading" });
520
- return;
521
- }
522
- for (const item of this.data) {
523
- if (isGroup(item)) {
524
- const visible = item.options.filter(matches);
525
- if (visible.length === 0) continue;
526
- this.rows.push({ kind: "group", label: item.label });
527
- visible.forEach(pushOption);
528
- } else if (matches(item)) {
529
- pushOption(item);
530
- }
531
- }
532
- if (this.opts.allowCreate && query !== "" && !this.hasExactMatch(query)) {
533
- const navIndex = this.navItems.length;
534
- this.navItems.push({ kind: "create" });
535
- this.rows.push({ kind: "create", navIndex });
536
- }
537
- if (this.rows.length === 0) this.rows.push({ kind: "empty" });
538
- }
539
- hasExactMatch(lowerQuery) {
540
- for (const item of this.data) {
541
- const options = isGroup(item) ? item.options : [item];
542
- if (options.some((o) => o.label.toLowerCase() === lowerQuery)) return true;
543
- }
544
- return false;
545
- }
546
- usesVirtualScroll() {
547
- return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
548
- }
549
- renderList() {
550
- this.buildRows();
551
- this.renderRows();
552
- }
553
- renderRows() {
554
- const scrollTop = this.list.scrollTop;
555
- const clientHeight = this.list.clientHeight;
556
- const virtual = this.usesVirtualScroll();
557
- this.list.textContent = "";
558
- const rowHeight = this.opts.itemHeight;
559
- let start = 0;
560
- let end = this.rows.length;
561
- if (virtual) {
562
- const viewport = clientHeight || rowHeight * 8;
563
- start = Math.max(0, Math.floor(scrollTop / rowHeight) - VIRTUAL_BUFFER);
564
- end = Math.min(this.rows.length, start + Math.ceil(viewport / rowHeight) + VIRTUAL_BUFFER * 2);
565
- const topSpacer = document.createElement("li");
566
- topSpacer.className = "forge-select__spacer";
567
- topSpacer.setAttribute("aria-hidden", "true");
568
- topSpacer.style.height = `${start * rowHeight}px`;
569
- this.list.append(topSpacer);
570
- }
571
- for (let i = start; i < end; i++) {
572
- this.list.append(this.renderRow(this.rows[i]));
573
- }
574
- if (virtual) {
575
- const bottomSpacer = document.createElement("li");
576
- bottomSpacer.className = "forge-select__spacer";
577
- bottomSpacer.setAttribute("aria-hidden", "true");
578
- bottomSpacer.style.height = `${(this.rows.length - end) * rowHeight}px`;
579
- this.list.append(bottomSpacer);
580
- if (this.list.scrollTop !== scrollTop) {
581
- this.list.scrollTop = scrollTop;
582
- }
583
- }
584
- this.updateActiveDescendant();
585
- }
586
- renderRow(row) {
587
- const li = document.createElement("li");
588
- switch (row.kind) {
589
- case "group":
590
- li.className = "forge-select__group-label";
591
- li.setAttribute("role", "presentation");
592
- li.textContent = row.label;
593
- break;
594
- case "empty":
595
- li.className = "forge-select__empty";
596
- li.textContent = this.strings.noResults;
597
- break;
598
- case "loading":
599
- li.className = "forge-select__loading";
600
- li.textContent = this.strings.loading;
601
- break;
602
- case "create":
603
- li.className = "forge-select__option forge-select__option--create";
604
- li.setAttribute("role", "option");
605
- li.id = `${this.uid}-nav-${row.navIndex}`;
606
- li.dataset.navIndex = String(row.navIndex);
607
- li.textContent = format(this.strings.createOption, { query: this.query.trim() });
608
- if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
609
- break;
610
- case "option": {
611
- li.className = "forge-select__option";
612
- li.setAttribute("role", "option");
613
- const isSelected = this.selected.includes(row.option.value);
614
- li.setAttribute("aria-selected", String(isSelected));
615
- if (isSelected) li.classList.add("forge-select__option--selected");
616
- if (row.option.disabled) {
617
- li.classList.add("forge-select__option--disabled");
618
- li.setAttribute("aria-disabled", "true");
619
- } else {
620
- li.id = `${this.uid}-nav-${row.navIndex}`;
621
- li.dataset.navIndex = String(row.navIndex);
622
- if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
623
- }
624
- li.append(this.optionContent(row.option));
625
- break;
626
- }
627
- }
628
- return li;
629
- }
630
- /**
631
- * Rendered row content is cached per option value and cloned on each render,
632
- * so templates run once per option instead of once per scroll frame.
633
- * State classes (selected/highlighted/disabled) live on the <li>, keeping the
634
- * cached content state-free.
635
- */
636
- optionContent(option) {
637
- let cached = this.rowContentCache.get(option.value);
638
- if (!cached) {
639
- const holder = document.createElement("span");
640
- holder.className = "forge-select__option-content";
641
- this.renderTemplate(holder, option, this.opts.templateResult);
642
- if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
643
- const oldest = this.rowContentCache.keys().next().value;
644
- this.rowContentCache.delete(oldest);
645
- }
646
- this.rowContentCache.set(option.value, holder);
647
- cached = holder;
648
- }
649
- return cached.cloneNode(true);
650
- }
651
- moveHighlight(delta) {
652
- if (this.navItems.length === 0) return;
653
- const next = this.highlightedIndex === -1 && delta > 0 ? 0 : (this.highlightedIndex + delta + this.navItems.length) % this.navItems.length;
654
- this.highlightedIndex = next;
655
- if (this.usesVirtualScroll()) {
656
- const rowIndex = this.rows.findIndex(
657
- (row) => (row.kind === "option" || row.kind === "create") && row.navIndex === next
658
- );
659
- if (rowIndex >= 0) {
660
- const rowHeight = this.opts.itemHeight;
661
- const top = rowIndex * rowHeight;
662
- const viewport = this.list.clientHeight || rowHeight * 8;
663
- let target = this.list.scrollTop;
664
- if (top < target) target = top;
665
- else if (top + rowHeight > target + viewport) target = top + rowHeight - viewport;
666
- if (target !== this.list.scrollTop) this.list.scrollTop = target;
667
- }
668
- this.renderRows();
669
- } else {
670
- this.renderRows();
671
- const highlighted = this.list.querySelector(".forge-select__option--highlighted");
672
- highlighted?.scrollIntoView?.({ block: "nearest" });
673
- }
674
- }
675
- updateActiveDescendant() {
676
- const target = this.searchInput ?? this.control;
677
- if (this.highlightedIndex >= 0) {
678
- target.setAttribute("aria-activedescendant", `${this.uid}-nav-${this.highlightedIndex}`);
679
- } else {
680
- target.removeAttribute("aria-activedescendant");
681
- }
682
- }
683
- // ---------------------------------------------------------------- remote data
684
- scheduleRemoteLoad(query, delay) {
685
- if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
686
- this.loading = true;
687
- this.renderList();
688
- this.ajaxTimer = setTimeout(() => {
689
- void this.loadRemote(query);
690
- }, delay);
691
- }
692
- async loadRemote(query) {
693
- const ajax = this.opts.ajax;
694
- const requestId = ++this.ajaxRequestId;
695
- try {
696
- const url = buildUrl(ajax, query);
697
- const response = await fetch(url);
698
- const json = await response.json();
699
- if (requestId !== this.ajaxRequestId || this.destroyed) return;
700
- this.data = ajax.transform ? ajax.transform(json) : json;
701
- this.rowContentCache.clear();
702
- this.remoteLoaded = true;
703
- } catch {
704
- if (requestId !== this.ajaxRequestId || this.destroyed) return;
705
- this.data = [];
706
- this.rowContentCache.clear();
707
- } finally {
708
- if (requestId === this.ajaxRequestId && !this.destroyed) {
709
- this.loading = false;
710
- if (this.isOpen) this.renderList();
711
- }
712
- }
713
- }
714
- };
715
- function parseNativeOptions(select) {
716
- const data = [];
717
- for (const child of Array.from(select.children)) {
718
- if (child instanceof HTMLOptGroupElement) {
719
- data.push({
720
- label: child.label,
721
- options: Array.from(child.querySelectorAll("option")).map(parseOption)
722
- });
723
- } else if (child instanceof HTMLOptionElement) {
724
- data.push(parseOption(child));
725
- }
726
- }
727
- return data;
728
- }
729
- function parseOption(option) {
730
- return {
731
- value: option.value,
732
- label: option.textContent?.trim() ?? option.value,
733
- disabled: option.disabled || void 0
734
- };
735
- }
736
- function buildUrl(ajax, query) {
737
- if (typeof ajax.url === "function") return ajax.url(query);
738
- if (!ajax.params) return ajax.url;
739
- const params = new URLSearchParams();
740
- for (const [key, value] of Object.entries(ajax.params(query))) {
741
- params.set(key, String(value));
742
- }
743
- const separator = ajax.url.includes("?") ? "&" : "?";
744
- return `${ajax.url}${separator}${params.toString()}`;
745
- }
746
- function arraysEqual(a, b) {
747
- return a.length === b.length && a.every((value, index) => value === b[index]);
748
- }
749
- return __toCommonJS(index_exports);
750
- })();
1
+ "use strict";var ForgeSelectBundle=(()=>{var I=Object.defineProperty;var V=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var q=(r,e)=>{for(var t in e)I(r,t,{get:e[t],enumerable:!0})},P=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of F(e))!j.call(r,s)&&s!==t&&I(r,s,{get:()=>e[s],enumerable:!(i=V(e,s))||i.enumerable});return r};var B=r=>P(I({},"__esModule",{value:!0}),r);var X={};q(X,{ForgeSelect:()=>p,default:()=>p});var v=class{constructor(){this.handlers=new Map}on(e,t){let i=this.handlers.get(e);i||(i=new Set,this.handlers.set(e,i)),i.add(t)}off(e,t){this.handlers.get(e)?.delete(t)}emit(e,...t){let i=this.handlers.get(e);if(i)for(let s of[...i])s(...t)}clear(){this.handlers.clear()}};var L={en:{noResults:"No results found",loading:"Loading\u2026",loadingMore:"Loading more\u2026",errorLoading:"Could not load options",createOption:'Create "{query}"',clearSelection:"Clear selection",removeItem:"Remove {label}",search:"Search",reorderHint:"{label}. Press Alt+Left or Alt+Right to reorder."},vi:{noResults:"Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3",loading:"\u0110ang t\u1EA3i\u2026",loadingMore:"\u0110ang t\u1EA3i th\xEAm\u2026",errorLoading:"Kh\xF4ng th\u1EC3 t\u1EA3i t\xF9y ch\u1ECDn",createOption:'T\u1EA1o "{query}"',clearSelection:"X\xF3a l\u1EF1a ch\u1ECDn",removeItem:"X\xF3a {label}",search:"T\xECm ki\u1EBFm",reorderHint:"{label}. Nh\u1EA5n Alt+Tr\xE1i ho\u1EB7c Alt+Ph\u1EA3i \u0111\u1EC3 s\u1EAFp x\u1EBFp l\u1EA1i."}};function C(r){return typeof r=="string"?L[r]??L.en:{...L.en,...r}}function b(r,e){return r.replace(/\{(\w+)\}/g,(t,i)=>e[i]??t)}function T(r){let e=[];for(let t of Array.from(r.children))t instanceof HTMLOptGroupElement?e.push({label:t.label,options:Array.from(t.querySelectorAll("option")).map(O)}):t instanceof HTMLOptionElement&&e.push(O(t));return e}function O(r){let e=r.parentElement instanceof HTMLOptGroupElement&&r.parentElement.disabled;return{value:r.value,label:r.textContent?.trim()??r.value,disabled:r.disabled||e||void 0}}function x(r,e,t,i="row"){if(t){let s=t(e);typeof s=="string"?r.innerHTML=s:r.append(s);return}if(!e.avatar&&!e.description){r.textContent=e.label;return}if(e.avatar){let s=document.createElement("img");s.className=i==="row"?"forge-select__option-avatar":"forge-select__inline-avatar",s.src=e.avatar,s.alt="",s.setAttribute("loading","lazy"),s.setAttribute("decoding","async"),r.append(s)}if(i==="row"&&e.description){let s=document.createElement("span");s.className="forge-select__option-body";let n=document.createElement("span");n.className="forge-select__option-label",n.textContent=e.label;let a=document.createElement("span");a.className="forge-select__option-desc",a.textContent=e.description,s.append(n,a),r.append(s)}else{let s=document.createElement("span");s.className="forge-select__option-label",s.textContent=e.label,r.append(s)}}function _(r,e,t){if(typeof r.url=="function")return r.url(e,t);if(!r.params)return r.url;let i=new URLSearchParams;for(let[n,a]of Object.entries(r.params(e,t)))i.set(n,String(a));let s=r.url.includes("?")?"&":"?";return`${r.url}${s}${i.toString()}`}function R(r,e){let t=r.transform?r.transform(e):e;if(Array.isArray(t))return{options:t,hasMore:!1};if(!t||!Array.isArray(t.options))throw new Error("ForgeSelect: ajax.transform must return an array of options, or an object shaped like { options: Option[], hasMore?: boolean }.");return{options:t.options,hasMore:r.pagination?!!t.hasMore:!1}}function u(r){return r.options!==void 0}function y(r){if(!r.children)return[];let e=[];for(let t of r.children)t.disabled||e.push(t.value),e.push(...y(t));return e}function E(r,e){if(!r.children?.length)return e.includes(r.value)?"all":"none";let t=r.children.filter(i=>!i.disabled).map(i=>E(i,e));return t.length===0?"none":t.every(i=>i==="all")?"all":t.every(i=>i==="none")?"none":"some"}function k(r,e){let t=i=>{for(let s of i){if(s.value===e)return s;let n=s.children?t(s.children):void 0;if(n)return n}};for(let i of r){let s=t(u(i)?i.options:[i]);if(s)return s}}function M(r,e){let t=i=>{if(!i.children?.length)return;for(let a of i.children)t(a);let s=E(i,e),n=e.indexOf(i.value);s==="all"&&n===-1?e.push(i.value):s!=="all"&&n!==-1&&e.splice(n,1)};for(let i of r)(u(i)?i.options:[i]).forEach(t)}function N(r){let e=new Set,t=i=>{e.add(i.value),i.children?.forEach(t)};for(let i of r)(u(i)?i.options:[i]).forEach(t);return e}function H(r,e){return r.length===e.length&&r.every((t,i)=>t===e[i])}var $=36,D=5,G=100,K=2e3,U=0,p=class{constructor(e,t={}){this.selected=[];this.selectedOptions=new Map;this.suppressNextTagClick=!1;this.emitter=new v;this.uid=`forge-select-${++U}`;this.searchInput=null;this.isOpen=!1;this.isDisabled=!1;this.destroyed=!1;this.query="";this.rows=[];this.navItems=[];this.highlightedIndex=-1;this.rowContentCache=new Map;this.expandedValues=new Set;this.loading=!1;this.loadingMore=!1;this.page=0;this.hasMore=!0;this.ajaxTimer=null;this.ajaxRequestId=0;this.ajaxController=null;this.remoteLoaded=!1;this.loadError=null;this.originalDisplay="";this.originalDisabled=!1;this.nativeSelect=null;this.nativeForm=null;this.syncingNative=!1;this.onDocumentMouseDown=e=>{this.root.contains(e.target)||this.close()};this.onNativeChange=()=>{if(!this.nativeSelect||this.destroyed||this.syncingNative)return;let e=Array.from(this.nativeSelect.selectedOptions,t=>t.value);this.applyNativeValues(e)};this.onFormReset=()=>{if(!this.nativeSelect||this.destroyed)return;let e=Array.from(this.nativeSelect.options).filter(t=>t.defaultSelected).map(t=>t.value);this.applyNativeValues(e)};let i=typeof e=="string"?document.querySelector(e):e;if(!i)throw new Error(`ForgeSelect: target element not found: ${String(e)}`);this.el=i;let s=i instanceof HTMLSelectElement?i:null;if(this.nativeSelect=s,this.nativeForm=s?.form??null,this.originalDisplay=i.style.display,this.originalDisabled=s?.disabled??!1,this.opts={placeholder:t.placeholder??"",searchable:t.searchable??!0,multiple:t.multiple??s?.multiple??!1,clearable:t.clearable??!1,allowCreate:t.allowCreate??!1,sortable:t.sortable??!1,theme:t.theme??"default",disabled:t.disabled??s?.disabled??!1,data:t.data,ajax:t.ajax,templateResult:t.templateResult,templateSelection:t.templateSelection,virtualScroll:t.virtualScroll,itemHeight:t.itemHeight??$,language:t.language??"en",plugins:t.plugins??[]},this.strings=C(this.opts.language),this.plugins=this.opts.plugins,this.data=this.opts.data??(s?T(s):[]),s&&!this.opts.data){let n=Array.from(s.options),a=s.multiple||s.selectedIndex>0||n.some(o=>o.defaultSelected);for(let o of n)a&&o.selected&&this.selectValue(o.value,!1)}this.buildDom(),this.renderValue(),this.opts.disabled&&this.disable(),s?.addEventListener("change",this.onNativeChange),this.nativeForm?.addEventListener("reset",this.onFormReset);for(let n of this.plugins)n.onInit?.(this)}applyNativeValues(e){this.selected=[];for(let t of this.opts.multiple?e:e.slice(0,1))this.selectValue(t,!1);this.renderValue(),this.isOpen&&this.renderList(),this.emitter.emit("change",this.getValue())}open(){if(!(this.isOpen||this.isDisabled||this.destroyed)){this.isOpen=!0,this.dropdown.hidden=!1,this.root.classList.add("forge-select--open"),this.control.setAttribute("aria-expanded","true"),document.addEventListener("mousedown",this.onDocumentMouseDown),this.opts.ajax&&!this.remoteLoaded&&this.scheduleRemoteLoad(this.query,0),this.renderList(),this.searchInput&&this.searchInput.focus(),this.emitter.emit("open");for(let e of this.plugins)e.onOpen?.(this)}}close(){if(this.isOpen){this.isOpen=!1,this.dropdown.hidden=!0,this.root.classList.remove("forge-select--open"),this.control.setAttribute("aria-expanded","false"),document.removeEventListener("mousedown",this.onDocumentMouseDown),this.highlightedIndex=-1,this.searchInput&&(this.searchInput.value="",this.query=""),this.emitter.emit("close");for(let e of this.plugins)e.onClose?.(this)}}destroy(){if(!this.destroyed){this.close();for(let e of this.plugins)e.onDestroy?.(this);this.destroyed=!0,this.ajaxTimer&&clearTimeout(this.ajaxTimer),this.ajaxController?.abort(),this.nativeSelect?.removeEventListener("change",this.onNativeChange),this.nativeForm?.removeEventListener("reset",this.onFormReset),this.rowContentCache.clear(),this.root.remove(),this.el.style.display=this.originalDisplay,this.nativeSelect&&(this.nativeSelect.disabled=this.originalDisabled),this.emitter.clear()}}getValue(){return this.opts.multiple?[...this.selected]:this.selected[0]??null}setValue(e,t={}){let i=e==null?[]:Array.isArray(e)?e:[e],s=this.opts.multiple?i:i.slice(0,1);if(!H(s,this.selected)){this.selected=[];for(let n of s)this.selectValue(n,!1);this.afterSelectionChange(t.emitChange??!0)}}enable(){this.isDisabled=!1,this.root.classList.remove("forge-select--disabled"),this.control.tabIndex=0,this.control.setAttribute("aria-disabled","false"),this.nativeSelect&&(this.nativeSelect.disabled=!1)}disable(){this.close(),this.isDisabled=!0,this.root.classList.add("forge-select--disabled"),this.control.tabIndex=-1,this.control.setAttribute("aria-disabled","true"),this.nativeSelect&&(this.nativeSelect.disabled=!0)}on(e,t){this.emitter.on(e,t)}off(e,t){this.emitter.off(e,t)}applyAccessibleName(){let e=this.el.getAttribute("aria-labelledby"),t=this.el.getAttribute("aria-label");if(e)this.control.setAttribute("aria-labelledby",e);else if(t)this.control.setAttribute("aria-label",t);else if(this.el.id){let i=Array.from(document.getElementsByTagName("label")).find(s=>s.htmlFor===this.el.id);i&&(i.id||(i.id=`${this.uid}-label`),this.control.setAttribute("aria-labelledby",i.id))}}buildDom(){this.root=document.createElement("div"),this.root.className="forge-select",this.root.dataset.theme=this.opts.theme,this.root.style.setProperty("--fs-item-height",`${this.opts.itemHeight}px`),this.opts.sortable&&this.opts.multiple&&this.root.classList.add("forge-select--sortable"),this.control=document.createElement("div"),this.control.className="forge-select__control",this.control.setAttribute("role","combobox"),this.control.setAttribute("aria-haspopup","listbox"),this.control.setAttribute("aria-expanded","false"),this.control.setAttribute("aria-controls",`${this.uid}-list`),this.control.tabIndex=0,this.applyAccessibleName(),this.valueEl=document.createElement("div"),this.valueEl.className="forge-select__value",this.clearBtn=document.createElement("button"),this.clearBtn.type="button",this.clearBtn.className="forge-select__clear",this.clearBtn.setAttribute("aria-label",this.strings.clearSelection),this.clearBtn.textContent="\xD7",this.clearBtn.hidden=!0;let e=document.createElement("span");e.className="forge-select__arrow",e.setAttribute("aria-hidden","true"),this.control.append(this.valueEl,this.clearBtn,e),this.dropdown=document.createElement("div"),this.dropdown.className="forge-select__dropdown",this.dropdown.hidden=!0,this.opts.searchable&&(this.searchInput=document.createElement("input"),this.searchInput.type="search",this.searchInput.className="forge-select__search",this.searchInput.setAttribute("aria-label",this.strings.search),this.searchInput.setAttribute("aria-autocomplete","list"),this.searchInput.setAttribute("aria-controls",`${this.uid}-list`),this.dropdown.append(this.searchInput)),this.list=document.createElement("ul"),this.list.className="forge-select__list",this.list.id=`${this.uid}-list`,this.list.setAttribute("role","listbox"),this.opts.multiple&&this.list.setAttribute("aria-multiselectable","true"),this.dropdown.append(this.list),this.liveRegion=document.createElement("div"),this.liveRegion.className="forge-select__sr-only",this.liveRegion.setAttribute("role","status"),this.liveRegion.setAttribute("aria-live","polite"),this.root.append(this.control,this.dropdown,this.liveRegion),this.el.style.display="none",this.el.insertAdjacentElement("afterend",this.root),this.bindEvents()}bindEvents(){this.control.addEventListener("click",e=>{if(e.target!==this.clearBtn){if(this.suppressNextTagClick){this.suppressNextTagClick=!1;return}this.isDisabled||(this.isOpen?this.close():this.open())}}),this.control.addEventListener("keydown",e=>this.handleKeydown(e)),this.clearBtn.addEventListener("click",e=>{e.stopPropagation(),this.clearSelection()}),this.searchInput&&(this.searchInput.addEventListener("input",()=>{this.query=this.searchInput.value,this.highlightedIndex=-1,this.list.scrollTop=0,this.emitter.emit("search",this.query),this.opts.ajax?this.scheduleRemoteLoad(this.query,this.opts.ajax.debounce??250):this.renderList()}),this.searchInput.addEventListener("keydown",e=>this.handleKeydown(e))),this.list.addEventListener("click",e=>{let t=e.target,i=t.closest("[data-twisty]");if(i){let a=i.dataset.twisty;this.expandedValues.has(a)?this.expandedValues.delete(a):this.expandedValues.add(a),this.renderList();return}let s=t.closest("li[data-nav-index]");if(!s)return;let n=Number(s.dataset.navIndex);this.activateNavItem(n)}),this.list.addEventListener("scroll",()=>{this.usesVirtualScroll()&&this.renderRows(),this.maybeLoadNextPage()})}handleKeydown(e){if(!this.isDisabled)switch(e.key){case"Enter":e.preventDefault(),this.isOpen?this.highlightedIndex>=0&&this.activateNavItem(this.highlightedIndex):this.open();break;case" ":e.target===this.control&&(e.preventDefault(),this.isOpen||this.open());break;case"ArrowDown":e.preventDefault(),this.isOpen?this.moveHighlight(1):this.open();break;case"ArrowUp":e.preventDefault(),this.isOpen&&this.moveHighlight(-1);break;case"Escape":this.isOpen&&(e.preventDefault(),this.close(),this.control.focus());break;case"ArrowRight":this.isOpen&&this.navigateTree("right")&&e.preventDefault();break;case"ArrowLeft":this.isOpen&&this.navigateTree("left")&&e.preventDefault();break;case"Tab":this.close();break}}selectValue(e,t){if(this.selected.includes(e))return;let i=this.findOption(e)??this.selectedOptions.get(e)??{value:e,label:e};if(this.selectedOptions.set(e,i),this.opts.multiple){this.selected.push(e);for(let s of y(i))this.selected.includes(s)||this.selected.push(s);this.syncTreeAncestors()}else this.selected=[e];t&&this.afterSelectionChange()}deselectValue(e,t){let i=this.selected.indexOf(e);if(i!==-1){if(this.selected.splice(i,1),this.opts.multiple){let s=this.findOption(e)??this.selectedOptions.get(e);if(s)for(let n of y(s)){let a=this.selected.indexOf(n);a!==-1&&this.selected.splice(a,1)}this.syncTreeAncestors()}t&&this.afterSelectionChange()}}syncTreeAncestors(){M(this.data,this.selected)}clearSelection(){this.selected.length!==0&&(this.selected=[],this.emitter.emit("clear"),this.afterSelectionChange())}afterSelectionChange(e=!0){this.renderValue(),this.syncNativeSelect(e),this.isOpen&&this.renderList(),e&&this.emitter.emit("change",this.getValue())}syncNativeSelect(e=!0){if(!(this.el instanceof HTMLSelectElement))return;let t=new Set;for(let i of Array.from(this.el.options))t.add(i.value),i.selected=this.selected.includes(i.value);for(let i of this.selected){if(t.has(i))continue;let s=document.createElement("option");s.value=i,s.textContent=this.selectedOptions.get(i)?.label??i,s.selected=!0,this.el.append(s)}if(this.opts.sortable&&this.opts.multiple)for(let i of this.selected){let s=Array.from(this.el.options).find(n=>n.value===i);s&&this.el.append(s)}if(e){this.syncingNative=!0;try{this.el.dispatchEvent(new Event("change",{bubbles:!0}))}finally{this.syncingNative=!1}}}findOption(e){return k(this.data,e)}createFromQuery(){let e=this.query.trim();if(!e)return;let t={value:e,label:e};this.data.push(t),this.searchInput&&(this.searchInput.value="",this.query=""),this.selectValue(t.value,!0),this.opts.multiple||this.close()}activateNavItem(e){let t=this.navItems[e];if(!t)return;if(t.kind==="create"){this.createFromQuery();return}let{value:i}=t.option;this.opts.multiple?this.selected.includes(i)?this.deselectValue(i,!0):this.selectValue(i,!0):(this.selectValue(i,!0),this.close(),this.control.focus())}renderValue(){this.valueEl.textContent="";let e=this.selected.length>0;if(this.clearBtn.hidden=!(this.opts.clearable&&e),!e){let t=document.createElement("span");t.className="forge-select__placeholder",t.textContent=this.opts.placeholder,this.valueEl.append(t);return}if(this.opts.multiple)for(let t of this.selected){let i=this.selectedOptions.get(t)??{value:t,label:t},s=document.createElement("span");s.className="forge-select__tag";let n=document.createElement("span");n.className="forge-select__tag-label",x(n,i,this.opts.templateSelection,"inline");let a=document.createElement("button");a.type="button",a.className="forge-select__tag-remove",a.setAttribute("aria-label",b(this.strings.removeItem,{label:i.label})),a.textContent="\xD7",a.addEventListener("click",o=>{o.stopPropagation(),this.isDisabled||this.deselectValue(t,!0)}),s.append(n,a),this.opts.sortable&&(s.dataset.value=t,s.tabIndex=0,s.setAttribute("aria-roledescription","draggable item"),s.setAttribute("aria-label",b(this.strings.reorderHint,{label:i.label})),s.addEventListener("keydown",o=>this.handleTagKeydown(o,t)),this.bindTagDrag(s,t)),this.valueEl.append(s)}else{let t=this.selectedOptions.get(this.selected[0])??{value:this.selected[0],label:this.selected[0]},i=document.createElement("span");i.className="forge-select__single-value",x(i,t,this.opts.templateSelection,"inline"),this.valueEl.append(i)}}bindTagDrag(e,t){let s=0,n=!1,a=[],o=h=>{if(!n){if(Math.abs(h.clientX-s)<4)return;n=!0,a=[...this.selected],typeof this.valueEl.setPointerCapture=="function"&&this.valueEl.setPointerCapture(h.pointerId),e.classList.add("forge-select__tag--dragging")}h.preventDefault();let f=a.indexOf(t),d=Array.from(this.valueEl.querySelectorAll(".forge-select__tag"));for(let c of d){if(c===e)continue;let m=c.dataset.value;if(!m)continue;let g=a.indexOf(m);if(g===-1)continue;let S=c.getBoundingClientRect(),w=S.left+S.width/2,A=f<g;if(A?h.clientX>w:h.clientX<w){a.splice(f,1),a.splice(g,0,t),A?this.valueEl.insertBefore(e,c.nextSibling):this.valueEl.insertBefore(e,c);break}}},l=h=>{this.valueEl.removeEventListener("pointermove",o),this.valueEl.removeEventListener("pointerup",l),this.valueEl.removeEventListener("pointercancel",l),n&&(typeof this.valueEl.releasePointerCapture=="function"&&this.valueEl.releasePointerCapture(h.pointerId),e.classList.remove("forge-select__tag--dragging"),this.selected=a,this.suppressNextTagClick=!0,this.afterSelectionChange())};e.addEventListener("pointerdown",h=>{this.isDisabled||h.button!==0||h.target.closest(".forge-select__tag-remove")||(s=h.clientX,n=!1,this.valueEl.addEventListener("pointermove",o),this.valueEl.addEventListener("pointerup",l),this.valueEl.addEventListener("pointercancel",l))})}handleTagKeydown(e,t){if(!e.altKey||e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;let i=this.selected.indexOf(t),s=e.key==="ArrowLeft"?i-1:i+1;if(i===-1||s<0||s>=this.selected.length)return;e.preventDefault(),e.stopPropagation();let n=[...this.selected];[n[i],n[s]]=[n[s],n[i]],this.selected=n,this.afterSelectionChange(),this.focusTagByValue(t)}focusTagByValue(e){for(let t of Array.from(this.valueEl.querySelectorAll(".forge-select__tag")))if(t.dataset.value===e){t.focus();return}}buildRows(){this.rows=[],this.navItems=[];let e=this.query.trim().toLowerCase(),t=n=>e===""||n.label.toLowerCase().includes(e)||(n.description?.toLowerCase().includes(e)??!1),i=n=>e===""||t(n)||(n.children??[]).some(i),s=(n,a,o)=>{let l=-1;n.disabled||(l=this.navItems.length,this.navItems.push({kind:"option",option:n,parentValue:o}));let h=!!n.children&&n.children.length>0;if(this.rows.push({kind:"option",option:n,navIndex:l,depth:a,hasChildren:h}),h&&(e!==""||this.expandedValues.has(n.value)))for(let d of n.children)i(d)&&s(d,a+1,n.value)};if(this.loading){this.rows.push({kind:"loading"});return}if(this.loadError){this.rows.push({kind:"error"});return}for(let n of this.data)if(u(n)){let a=n.options.filter(i);if(a.length===0)continue;this.rows.push({kind:"group",label:n.label}),a.forEach(o=>s(o,0))}else i(n)&&s(n,0);if(this.opts.allowCreate&&e!==""&&!this.hasExactMatch(e)){let n=this.navItems.length;this.navItems.push({kind:"create"}),this.rows.push({kind:"create",navIndex:n})}this.rows.length===0?this.rows.push({kind:"empty"}):this.loadingMore&&this.rows.push({kind:"loading-more"})}hasExactMatch(e){let t=i=>i.label.toLowerCase()===e||(i.children??[]).some(t);for(let i of this.data)if((u(i)?i.options:[i]).some(t))return!0;return!1}usesVirtualScroll(){return this.opts.virtualScroll!==!1&&this.rows.length>G}renderList(){this.buildRows(),this.renderRows(),this.announceStatus()}announceStatus(){let e=this.rows[0],t=e?.kind==="loading"?this.strings.loading:e?.kind==="error"?this.strings.errorLoading:e?.kind==="empty"?this.strings.noResults:"";this.liveRegion.textContent!==t&&(this.liveRegion.textContent=t)}renderRows(){let e=this.list.scrollTop,t=this.list.clientHeight,i=this.usesVirtualScroll();this.list.textContent="";let s=this.opts.itemHeight,n=0,a=this.rows.length;if(i){let o=t||s*8;n=Math.max(0,Math.floor(e/s)-D),a=Math.min(this.rows.length,n+Math.ceil(o/s)+D*2);let l=document.createElement("li");l.className="forge-select__spacer",l.setAttribute("aria-hidden","true"),l.style.height=`${n*s}px`,this.list.append(l)}for(let o=n;o<a;o++)this.list.append(this.renderRow(this.rows[o]));if(i){let o=document.createElement("li");o.className="forge-select__spacer",o.setAttribute("aria-hidden","true"),o.style.height=`${(this.rows.length-a)*s}px`,this.list.append(o),this.list.scrollTop!==e&&(this.list.scrollTop=e)}this.updateActiveDescendant()}renderRow(e){let t=document.createElement("li");switch(e.kind){case"group":t.className="forge-select__group-label",t.setAttribute("role","presentation"),t.textContent=e.label;break;case"empty":t.className="forge-select__empty",t.setAttribute("role","option"),t.setAttribute("aria-disabled","true"),t.setAttribute("aria-selected","false"),t.textContent=this.strings.noResults;break;case"error":t.className="forge-select__error",t.setAttribute("role","option"),t.setAttribute("aria-disabled","true"),t.setAttribute("aria-selected","false"),t.textContent=this.strings.errorLoading;break;case"loading":t.className="forge-select__loading",t.setAttribute("role","option"),t.setAttribute("aria-disabled","true"),t.setAttribute("aria-selected","false"),t.textContent=this.strings.loading;break;case"loading-more":t.className="forge-select__loading-more",t.setAttribute("aria-hidden","true"),t.textContent=this.strings.loadingMore;break;case"create":t.className="forge-select__option forge-select__option--create",t.setAttribute("role","option"),t.id=`${this.uid}-nav-${e.navIndex}`,t.dataset.navIndex=String(e.navIndex),t.textContent=b(this.strings.createOption,{query:this.query.trim()}),e.navIndex===this.highlightedIndex&&t.classList.add("forge-select__option--highlighted");break;case"option":{t.className="forge-select__option",t.setAttribute("role","option");let i=this.selected.includes(e.option.value);if(t.setAttribute("aria-selected",String(i)),i&&t.classList.add("forge-select__option--selected"),this.opts.multiple&&e.hasChildren&&E(e.option,this.selected)==="some"&&t.classList.add("forge-select__option--indeterminate"),e.depth>0&&(t.style.paddingLeft=`calc(12px + ${e.depth} * var(--fs-tree-indent, 18px))`),e.option.disabled?(t.classList.add("forge-select__option--disabled"),t.setAttribute("aria-disabled","true")):(t.id=`${this.uid}-nav-${e.navIndex}`,t.dataset.navIndex=String(e.navIndex),e.navIndex===this.highlightedIndex&&t.classList.add("forge-select__option--highlighted")),e.hasChildren){let s=this.query!==""||this.expandedValues.has(e.option.value);t.setAttribute("aria-expanded",String(s));let n=document.createElement("span");n.className="forge-select__twisty",n.dataset.twisty=e.option.value,n.setAttribute("aria-hidden","true"),n.textContent=s?"\u25BC":"\u25B6",t.append(n)}t.append(this.optionContent(e.option));break}}return t}optionContent(e){let t=this.rowContentCache.get(e.value);if(!t){let i=document.createElement("span");if(i.className="forge-select__option-content",x(i,e,this.opts.templateResult),this.rowContentCache.size>=K){let s=this.rowContentCache.keys().next().value;this.rowContentCache.delete(s)}this.rowContentCache.set(e.value,i),t=i}return t.cloneNode(!0)}moveHighlight(e){if(this.navItems.length===0)return;let t=this.highlightedIndex===-1?e>0?0:this.navItems.length-1:(this.highlightedIndex+e+this.navItems.length)%this.navItems.length;this.focusNavIndex(t)}focusNavIndex(e){if(this.highlightedIndex=e,this.usesVirtualScroll()){let t=this.rows.findIndex(i=>(i.kind==="option"||i.kind==="create")&&i.navIndex===e);if(t>=0){let i=this.opts.itemHeight,s=t*i,n=this.list.clientHeight||i*8,a=this.list.scrollTop;s<a?a=s:s+i>a+n&&(a=s+i-n),a!==this.list.scrollTop&&(this.list.scrollTop=a)}this.renderRows()}else this.renderRows(),this.list.querySelector(".forge-select__option--highlighted")?.scrollIntoView?.({block:"nearest"})}navigateTree(e){let t=this.navItems[this.highlightedIndex];if(!t||t.kind!=="option")return!1;let{option:i,parentValue:s}=t,n=!!i.children?.length,a=this.query!==""||this.expandedValues.has(i.value);if(e==="right"){if(n&&!a)return this.expandedValues.add(i.value),this.renderList(),!0;if(n){let o=this.navItems.findIndex(l=>l.kind==="option"&&l.parentValue===i.value);if(o>=0)return this.focusNavIndex(o),!0}return!1}if(n&&a&&this.query==="")return this.expandedValues.delete(i.value),this.renderList(),!0;if(s){let o=this.navItems.findIndex(l=>l.kind==="option"&&l.option.value===s);if(o>=0)return this.focusNavIndex(o),!0}return!1}updateActiveDescendant(){let e=this.searchInput??this.control;this.highlightedIndex>=0?e.setAttribute("aria-activedescendant",`${this.uid}-nav-${this.highlightedIndex}`):e.removeAttribute("aria-activedescendant")}scheduleRemoteLoad(e,t){this.ajaxTimer&&clearTimeout(this.ajaxTimer);let i=++this.ajaxRequestId;this.ajaxController?.abort(),this.ajaxController=null,this.page=0,this.hasMore=!0,this.loading=!0,this.loadingMore=!1,this.loadError=null,this.renderList(),this.ajaxTimer=setTimeout(()=>{this.ajaxTimer=null,this.loadRemote(e,{requestId:i})},t)}maybeLoadNextPage(){if(!this.opts.ajax?.pagination||!this.hasMore||this.loading||this.loadingMore)return;let{scrollHeight:t,scrollTop:i,clientHeight:s}=this.list,n=this.opts.itemHeight*2;t-i-s>=n||(this.loadingMore=!0,this.renderList(),this.loadRemote(this.query,{append:!0}))}async loadRemote(e,{append:t=!1,requestId:i}={}){let s=this.opts.ajax,n=i??++this.ajaxRequestId;if(n!==this.ajaxRequestId)return;this.ajaxController?.abort();let a=new AbortController;this.ajaxController=a;let o=t?this.page+1:0;try{let l=_(s,e,o),h=await fetch(l,{signal:a.signal});if(h.ok===!1)throw new Error(`ForgeSelect: remote request failed with HTTP ${h.status}`);let f=await h.json();if(n!==this.ajaxRequestId||this.destroyed)return;let{options:d,hasMore:c}=R(s,f);if(t){let m=N(this.data);this.data=[...this.data,...d.filter(g=>!m.has(g.value))]}else this.data=d,this.rowContentCache.clear();this.page=o,this.hasMore=c,this.remoteLoaded=!0,this.loadError=null}catch(l){if(n!==this.ajaxRequestId||this.destroyed||a.signal.aborted)return;let h=l instanceof Error?l:new Error(String(l));t||(this.data=[],this.rowContentCache.clear()),this.hasMore=!1,this.loadError=h,this.emitter.emit("error",h)}finally{n===this.ajaxRequestId&&!this.destroyed&&(this.ajaxController=null,this.loading=!1,this.loadingMore=!1,this.isOpen&&this.renderList())}}};return B(X);})();
751
2
  //# sourceMappingURL=index.global.js.map