forge-select 0.2.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,901 +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
- loadingMore: "Loading more\u2026",
60
- createOption: 'Create "{query}"',
61
- clearSelection: "Clear selection",
62
- removeItem: "Remove {label}",
63
- search: "Search"
64
- },
65
- vi: {
66
- noResults: "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3",
67
- loading: "\u0110ang t\u1EA3i\u2026",
68
- loadingMore: "\u0110ang t\u1EA3i th\xEAm\u2026",
69
- createOption: 'T\u1EA1o "{query}"',
70
- clearSelection: "X\xF3a l\u1EF1a ch\u1ECDn",
71
- removeItem: "X\xF3a {label}",
72
- search: "T\xECm ki\u1EBFm"
73
- }
74
- };
75
- function getStrings(language) {
76
- if (typeof language === "string") {
77
- return locales[language] ?? locales.en;
78
- }
79
- return { ...locales.en, ...language };
80
- }
81
- function format(template, vars) {
82
- return template.replace(/\{(\w+)\}/g, (match, key) => vars[key] ?? match);
83
- }
84
-
85
- // src/ForgeSelect.ts
86
- var DEFAULT_ITEM_HEIGHT = 36;
87
- var VIRTUAL_BUFFER = 5;
88
- var VIRTUAL_THRESHOLD = 100;
89
- var ROW_CACHE_LIMIT = 2e3;
90
- var uidCounter = 0;
91
- function isGroup(item) {
92
- return item.options !== void 0;
93
- }
94
- function collectDescendantValues(option) {
95
- if (!option.children) return [];
96
- const values = [];
97
- for (const child of option.children) {
98
- values.push(child.value, ...collectDescendantValues(child));
99
- }
100
- return values;
101
- }
102
- function computeCheckState(option, selected) {
103
- if (!option.children || option.children.length === 0) {
104
- return selected.includes(option.value) ? "all" : "none";
105
- }
106
- const states = option.children.map((child) => computeCheckState(child, selected));
107
- if (states.every((s) => s === "all")) return "all";
108
- if (states.every((s) => s === "none")) return "none";
109
- return "some";
110
- }
111
- var ForgeSelect = class {
112
- constructor(target, options = {}) {
113
- this.selected = [];
114
- this.selectedOptions = /* @__PURE__ */ new Map();
115
- this.emitter = new Emitter();
116
- this.uid = `forge-select-${++uidCounter}`;
117
- this.searchInput = null;
118
- this.isOpen = false;
119
- this.isDisabled = false;
120
- this.destroyed = false;
121
- this.query = "";
122
- this.rows = [];
123
- this.navItems = [];
124
- this.highlightedIndex = -1;
125
- this.rowContentCache = /* @__PURE__ */ new Map();
126
- this.expandedValues = /* @__PURE__ */ new Set();
127
- this.loading = false;
128
- this.loadingMore = false;
129
- this.page = 0;
130
- this.hasMore = true;
131
- this.ajaxTimer = null;
132
- this.ajaxRequestId = 0;
133
- this.remoteLoaded = false;
134
- this.onDocumentMouseDown = (event) => {
135
- if (!this.root.contains(event.target)) this.close();
136
- };
137
- const el = typeof target === "string" ? document.querySelector(target) : target;
138
- if (!el) {
139
- throw new Error(`ForgeSelect: target element not found: ${String(target)}`);
140
- }
141
- this.el = el;
142
- const nativeSelect = el instanceof HTMLSelectElement ? el : null;
143
- this.opts = {
144
- placeholder: options.placeholder ?? "",
145
- searchable: options.searchable ?? true,
146
- multiple: options.multiple ?? nativeSelect?.multiple ?? false,
147
- clearable: options.clearable ?? false,
148
- allowCreate: options.allowCreate ?? false,
149
- theme: options.theme ?? "default",
150
- disabled: options.disabled ?? false,
151
- data: options.data,
152
- ajax: options.ajax,
153
- templateResult: options.templateResult,
154
- templateSelection: options.templateSelection,
155
- virtualScroll: options.virtualScroll,
156
- itemHeight: options.itemHeight ?? DEFAULT_ITEM_HEIGHT,
157
- language: options.language ?? "en",
158
- plugins: options.plugins ?? []
159
- };
160
- this.strings = getStrings(this.opts.language);
161
- this.plugins = this.opts.plugins;
162
- this.data = this.opts.data ?? (nativeSelect ? parseNativeOptions(nativeSelect) : []);
163
- if (nativeSelect && !this.opts.data) {
164
- for (const option of Array.from(nativeSelect.querySelectorAll("option"))) {
165
- if (option.hasAttribute("selected")) this.selectValue(option.value, false);
166
- }
167
- }
168
- this.buildDom();
169
- this.renderValue();
170
- if (this.opts.disabled) this.disable();
171
- for (const plugin of this.plugins) plugin.onInit?.(this);
172
- }
173
- // ---------------------------------------------------------------- public API
174
- open() {
175
- if (this.isOpen || this.isDisabled || this.destroyed) return;
176
- this.isOpen = true;
177
- this.dropdown.hidden = false;
178
- this.root.classList.add("forge-select--open");
179
- this.control.setAttribute("aria-expanded", "true");
180
- document.addEventListener("mousedown", this.onDocumentMouseDown);
181
- if (this.opts.ajax && !this.remoteLoaded) {
182
- this.scheduleRemoteLoad(this.query, 0);
183
- }
184
- this.renderList();
185
- if (this.searchInput) this.searchInput.focus();
186
- this.emitter.emit("open");
187
- for (const plugin of this.plugins) plugin.onOpen?.(this);
188
- }
189
- close() {
190
- if (!this.isOpen) return;
191
- this.isOpen = false;
192
- this.dropdown.hidden = true;
193
- this.root.classList.remove("forge-select--open");
194
- this.control.setAttribute("aria-expanded", "false");
195
- document.removeEventListener("mousedown", this.onDocumentMouseDown);
196
- this.highlightedIndex = -1;
197
- if (this.searchInput) {
198
- this.searchInput.value = "";
199
- this.query = "";
200
- }
201
- this.emitter.emit("close");
202
- for (const plugin of this.plugins) plugin.onClose?.(this);
203
- }
204
- destroy() {
205
- if (this.destroyed) return;
206
- this.close();
207
- for (const plugin of this.plugins) plugin.onDestroy?.(this);
208
- this.destroyed = true;
209
- if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
210
- this.rowContentCache.clear();
211
- this.root.remove();
212
- this.el.style.display = "";
213
- this.emitter.clear();
214
- }
215
- getValue() {
216
- if (this.opts.multiple) return [...this.selected];
217
- return this.selected[0] ?? null;
218
- }
219
- setValue(value) {
220
- const values = value == null ? [] : Array.isArray(value) ? value : [value];
221
- const next = this.opts.multiple ? values : values.slice(0, 1);
222
- if (arraysEqual(next, this.selected)) return;
223
- this.selected = [];
224
- for (const v of next) this.selectValue(v, false);
225
- this.afterSelectionChange();
226
- }
227
- enable() {
228
- this.isDisabled = false;
229
- this.root.classList.remove("forge-select--disabled");
230
- this.control.tabIndex = 0;
231
- this.control.setAttribute("aria-disabled", "false");
232
- }
233
- disable() {
234
- this.close();
235
- this.isDisabled = true;
236
- this.root.classList.add("forge-select--disabled");
237
- this.control.tabIndex = -1;
238
- this.control.setAttribute("aria-disabled", "true");
239
- }
240
- on(event, handler) {
241
- this.emitter.on(event, handler);
242
- }
243
- off(event, handler) {
244
- this.emitter.off(event, handler);
245
- }
246
- // ---------------------------------------------------------------- DOM setup
247
- buildDom() {
248
- this.root = document.createElement("div");
249
- this.root.className = "forge-select";
250
- this.root.dataset.theme = this.opts.theme;
251
- this.root.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
252
- this.control = document.createElement("div");
253
- this.control.className = "forge-select__control";
254
- this.control.setAttribute("role", "combobox");
255
- this.control.setAttribute("aria-haspopup", "listbox");
256
- this.control.setAttribute("aria-expanded", "false");
257
- this.control.setAttribute("aria-controls", `${this.uid}-list`);
258
- this.control.tabIndex = 0;
259
- this.valueEl = document.createElement("div");
260
- this.valueEl.className = "forge-select__value";
261
- this.clearBtn = document.createElement("button");
262
- this.clearBtn.type = "button";
263
- this.clearBtn.className = "forge-select__clear";
264
- this.clearBtn.setAttribute("aria-label", this.strings.clearSelection);
265
- this.clearBtn.textContent = "\xD7";
266
- this.clearBtn.hidden = true;
267
- const arrow = document.createElement("span");
268
- arrow.className = "forge-select__arrow";
269
- arrow.setAttribute("aria-hidden", "true");
270
- this.control.append(this.valueEl, this.clearBtn, arrow);
271
- this.dropdown = document.createElement("div");
272
- this.dropdown.className = "forge-select__dropdown";
273
- this.dropdown.hidden = true;
274
- if (this.opts.searchable) {
275
- this.searchInput = document.createElement("input");
276
- this.searchInput.type = "search";
277
- this.searchInput.className = "forge-select__search";
278
- this.searchInput.setAttribute("aria-label", this.strings.search);
279
- this.searchInput.setAttribute("aria-autocomplete", "list");
280
- this.searchInput.setAttribute("aria-controls", `${this.uid}-list`);
281
- this.dropdown.append(this.searchInput);
282
- }
283
- this.list = document.createElement("ul");
284
- this.list.className = "forge-select__list";
285
- this.list.id = `${this.uid}-list`;
286
- this.list.setAttribute("role", "listbox");
287
- if (this.opts.multiple) this.list.setAttribute("aria-multiselectable", "true");
288
- this.dropdown.append(this.list);
289
- this.root.append(this.control, this.dropdown);
290
- this.el.style.display = "none";
291
- this.el.insertAdjacentElement("afterend", this.root);
292
- this.bindEvents();
293
- }
294
- bindEvents() {
295
- this.control.addEventListener("click", (event) => {
296
- if (event.target === this.clearBtn) return;
297
- if (this.isDisabled) return;
298
- this.isOpen ? this.close() : this.open();
299
- });
300
- this.control.addEventListener("keydown", (event) => this.handleKeydown(event));
301
- this.clearBtn.addEventListener("click", (event) => {
302
- event.stopPropagation();
303
- this.clearSelection();
304
- });
305
- if (this.searchInput) {
306
- this.searchInput.addEventListener("input", () => {
307
- this.query = this.searchInput.value;
308
- this.highlightedIndex = -1;
309
- this.list.scrollTop = 0;
310
- this.emitter.emit("search", this.query);
311
- if (this.opts.ajax) {
312
- this.scheduleRemoteLoad(this.query, this.opts.ajax.debounce ?? 250);
313
- } else {
314
- this.renderList();
315
- }
316
- });
317
- this.searchInput.addEventListener("keydown", (event) => this.handleKeydown(event));
318
- }
319
- this.list.addEventListener("click", (event) => {
320
- const target = event.target;
321
- const twisty = target.closest("[data-twisty]");
322
- if (twisty) {
323
- const value = twisty.dataset.twisty;
324
- if (this.expandedValues.has(value)) this.expandedValues.delete(value);
325
- else this.expandedValues.add(value);
326
- this.renderList();
327
- return;
328
- }
329
- const li = target.closest("li[data-nav-index]");
330
- if (!li) return;
331
- const navIndex = Number(li.dataset.navIndex);
332
- this.activateNavItem(navIndex);
333
- });
334
- this.list.addEventListener("scroll", () => {
335
- if (this.usesVirtualScroll()) this.renderRows();
336
- this.maybeLoadNextPage();
337
- });
338
- }
339
- handleKeydown(event) {
340
- if (this.isDisabled) return;
341
- switch (event.key) {
342
- case "Enter":
343
- event.preventDefault();
344
- if (!this.isOpen) this.open();
345
- else if (this.highlightedIndex >= 0) this.activateNavItem(this.highlightedIndex);
346
- break;
347
- case " ":
348
- if (event.target === this.control) {
349
- event.preventDefault();
350
- if (!this.isOpen) this.open();
351
- }
352
- break;
353
- case "ArrowDown":
354
- event.preventDefault();
355
- if (!this.isOpen) this.open();
356
- else this.moveHighlight(1);
357
- break;
358
- case "ArrowUp":
359
- event.preventDefault();
360
- if (this.isOpen) this.moveHighlight(-1);
361
- break;
362
- case "Escape":
363
- if (this.isOpen) {
364
- event.preventDefault();
365
- this.close();
366
- this.control.focus();
367
- }
368
- break;
369
- case "Tab":
370
- this.close();
371
- break;
372
- }
373
- }
374
- // ---------------------------------------------------------------- selection
375
- selectValue(value, notify) {
376
- if (this.selected.includes(value)) return;
377
- const option = this.findOption(value) ?? this.selectedOptions.get(value) ?? { value, label: value };
378
- this.selectedOptions.set(value, option);
379
- if (this.opts.multiple) {
380
- this.selected.push(value);
381
- for (const v of collectDescendantValues(option)) {
382
- if (!this.selected.includes(v)) this.selected.push(v);
383
- }
384
- this.syncTreeAncestors();
385
- } else {
386
- this.selected = [value];
387
- }
388
- if (notify) this.afterSelectionChange();
389
- }
390
- deselectValue(value, notify) {
391
- const index = this.selected.indexOf(value);
392
- if (index === -1) return;
393
- this.selected.splice(index, 1);
394
- if (this.opts.multiple) {
395
- const option = this.findOption(value) ?? this.selectedOptions.get(value);
396
- if (option) {
397
- for (const v of collectDescendantValues(option)) {
398
- const i = this.selected.indexOf(v);
399
- if (i !== -1) this.selected.splice(i, 1);
400
- }
401
- }
402
- this.syncTreeAncestors();
403
- }
404
- if (notify) this.afterSelectionChange();
405
- }
406
- /**
407
- * Keeps every tree parent's own membership in `selected` consistent with
408
- * its descendants (post-order, so parents see already-corrected children):
409
- * a parent counts as selected only when `computeCheckState` says "all".
410
- * No-op for data with no `children` anywhere.
411
- */
412
- syncTreeAncestors() {
413
- const sync = (option) => {
414
- if (!option.children || option.children.length === 0) return;
415
- for (const child of option.children) sync(child);
416
- const state = computeCheckState(option, this.selected);
417
- const index = this.selected.indexOf(option.value);
418
- if (state === "all" && index === -1) this.selected.push(option.value);
419
- else if (state !== "all" && index !== -1) this.selected.splice(index, 1);
420
- };
421
- for (const item of this.data) {
422
- const options = isGroup(item) ? item.options : [item];
423
- options.forEach(sync);
424
- }
425
- }
426
- clearSelection() {
427
- if (this.selected.length === 0) return;
428
- this.selected = [];
429
- this.emitter.emit("clear");
430
- this.afterSelectionChange();
431
- }
432
- afterSelectionChange() {
433
- this.renderValue();
434
- this.syncNativeSelect();
435
- if (this.isOpen) this.renderList();
436
- this.emitter.emit("change", this.getValue());
437
- }
438
- syncNativeSelect() {
439
- if (!(this.el instanceof HTMLSelectElement)) return;
440
- const existing = /* @__PURE__ */ new Set();
441
- for (const option of Array.from(this.el.options)) {
442
- existing.add(option.value);
443
- option.selected = this.selected.includes(option.value);
444
- }
445
- for (const value of this.selected) {
446
- if (existing.has(value)) continue;
447
- const option = document.createElement("option");
448
- option.value = value;
449
- option.textContent = this.selectedOptions.get(value)?.label ?? value;
450
- option.selected = true;
451
- this.el.append(option);
452
- }
453
- this.el.dispatchEvent(new Event("change", { bubbles: true }));
454
- }
455
- findOption(value) {
456
- const search = (options) => {
457
- for (const option of options) {
458
- if (option.value === value) return option;
459
- if (option.children) {
460
- const found = search(option.children);
461
- if (found) return found;
462
- }
463
- }
464
- return void 0;
465
- };
466
- for (const item of this.data) {
467
- const found = search(isGroup(item) ? item.options : [item]);
468
- if (found) return found;
469
- }
470
- return void 0;
471
- }
472
- createFromQuery() {
473
- const label = this.query.trim();
474
- if (!label) return;
475
- const option = { value: label, label };
476
- this.data.push(option);
477
- if (this.searchInput) {
478
- this.searchInput.value = "";
479
- this.query = "";
480
- }
481
- this.selectValue(option.value, true);
482
- if (!this.opts.multiple) this.close();
483
- }
484
- activateNavItem(navIndex) {
485
- const item = this.navItems[navIndex];
486
- if (!item) return;
487
- if (item.kind === "create") {
488
- this.createFromQuery();
489
- return;
490
- }
491
- const { value } = item.option;
492
- if (this.opts.multiple) {
493
- this.selected.includes(value) ? this.deselectValue(value, true) : this.selectValue(value, true);
494
- } else {
495
- this.selectValue(value, true);
496
- this.close();
497
- this.control.focus();
498
- }
499
- }
500
- // ---------------------------------------------------------------- rendering
501
- renderValue() {
502
- this.valueEl.textContent = "";
503
- const hasValue = this.selected.length > 0;
504
- this.clearBtn.hidden = !(this.opts.clearable && hasValue);
505
- if (!hasValue) {
506
- const placeholder = document.createElement("span");
507
- placeholder.className = "forge-select__placeholder";
508
- placeholder.textContent = this.opts.placeholder;
509
- this.valueEl.append(placeholder);
510
- return;
511
- }
512
- if (this.opts.multiple) {
513
- for (const value of this.selected) {
514
- const option = this.selectedOptions.get(value) ?? { value, label: value };
515
- const tag = document.createElement("span");
516
- tag.className = "forge-select__tag";
517
- const label = document.createElement("span");
518
- label.className = "forge-select__tag-label";
519
- this.renderTemplate(label, option, this.opts.templateSelection, "inline");
520
- const remove = document.createElement("button");
521
- remove.type = "button";
522
- remove.className = "forge-select__tag-remove";
523
- remove.setAttribute("aria-label", format(this.strings.removeItem, { label: option.label }));
524
- remove.textContent = "\xD7";
525
- remove.addEventListener("click", (event) => {
526
- event.stopPropagation();
527
- if (!this.isDisabled) this.deselectValue(value, true);
528
- });
529
- tag.append(label, remove);
530
- this.valueEl.append(tag);
531
- }
532
- } else {
533
- const option = this.selectedOptions.get(this.selected[0]) ?? {
534
- value: this.selected[0],
535
- label: this.selected[0]
536
- };
537
- const span = document.createElement("span");
538
- span.className = "forge-select__single-value";
539
- this.renderTemplate(span, option, this.opts.templateSelection, "inline");
540
- this.valueEl.append(span);
541
- }
542
- }
543
- renderTemplate(container, option, template, variant = "row") {
544
- if (template) {
545
- const result = template(option);
546
- if (typeof result === "string") container.innerHTML = result;
547
- else container.append(result);
548
- return;
549
- }
550
- if (!option.avatar && !option.description) {
551
- container.textContent = option.label;
552
- return;
553
- }
554
- if (option.avatar) {
555
- const avatar = document.createElement("img");
556
- avatar.className = variant === "row" ? "forge-select__option-avatar" : "forge-select__inline-avatar";
557
- avatar.src = option.avatar;
558
- avatar.alt = "";
559
- avatar.setAttribute("loading", "lazy");
560
- avatar.setAttribute("decoding", "async");
561
- container.append(avatar);
562
- }
563
- if (variant === "row" && option.description) {
564
- const body = document.createElement("span");
565
- body.className = "forge-select__option-body";
566
- const label = document.createElement("span");
567
- label.className = "forge-select__option-label";
568
- label.textContent = option.label;
569
- const desc = document.createElement("span");
570
- desc.className = "forge-select__option-desc";
571
- desc.textContent = option.description;
572
- body.append(label, desc);
573
- container.append(body);
574
- } else {
575
- const label = document.createElement("span");
576
- label.className = "forge-select__option-label";
577
- label.textContent = option.label;
578
- container.append(label);
579
- }
580
- }
581
- buildRows() {
582
- this.rows = [];
583
- this.navItems = [];
584
- const query = this.query.trim().toLowerCase();
585
- const matches = (option) => query === "" || option.label.toLowerCase().includes(query) || (option.description?.toLowerCase().includes(query) ?? false);
586
- const subtreeMatches = (option) => query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
587
- const pushOption = (option, depth) => {
588
- let navIndex = -1;
589
- if (!option.disabled) {
590
- navIndex = this.navItems.length;
591
- this.navItems.push({ kind: "option", option });
592
- }
593
- const hasChildren = !!option.children && option.children.length > 0;
594
- this.rows.push({ kind: "option", option, navIndex, depth, hasChildren });
595
- if (hasChildren) {
596
- const expanded = query !== "" || this.expandedValues.has(option.value);
597
- if (expanded) {
598
- for (const child of option.children) {
599
- if (subtreeMatches(child)) pushOption(child, depth + 1);
600
- }
601
- }
602
- }
603
- };
604
- if (this.loading) {
605
- this.rows.push({ kind: "loading" });
606
- return;
607
- }
608
- for (const item of this.data) {
609
- if (isGroup(item)) {
610
- const visible = item.options.filter(subtreeMatches);
611
- if (visible.length === 0) continue;
612
- this.rows.push({ kind: "group", label: item.label });
613
- visible.forEach((o) => pushOption(o, 0));
614
- } else if (subtreeMatches(item)) {
615
- pushOption(item, 0);
616
- }
617
- }
618
- if (this.opts.allowCreate && query !== "" && !this.hasExactMatch(query)) {
619
- const navIndex = this.navItems.length;
620
- this.navItems.push({ kind: "create" });
621
- this.rows.push({ kind: "create", navIndex });
622
- }
623
- if (this.rows.length === 0) this.rows.push({ kind: "empty" });
624
- else if (this.loadingMore) this.rows.push({ kind: "loading-more" });
625
- }
626
- hasExactMatch(lowerQuery) {
627
- const matchesExactly = (option) => option.label.toLowerCase() === lowerQuery || (option.children ?? []).some(matchesExactly);
628
- for (const item of this.data) {
629
- const options = isGroup(item) ? item.options : [item];
630
- if (options.some(matchesExactly)) return true;
631
- }
632
- return false;
633
- }
634
- usesVirtualScroll() {
635
- return this.opts.virtualScroll !== false && this.rows.length > VIRTUAL_THRESHOLD;
636
- }
637
- renderList() {
638
- this.buildRows();
639
- this.renderRows();
640
- }
641
- renderRows() {
642
- const scrollTop = this.list.scrollTop;
643
- const clientHeight = this.list.clientHeight;
644
- const virtual = this.usesVirtualScroll();
645
- this.list.textContent = "";
646
- const rowHeight = this.opts.itemHeight;
647
- let start = 0;
648
- let end = this.rows.length;
649
- if (virtual) {
650
- const viewport = clientHeight || rowHeight * 8;
651
- start = Math.max(0, Math.floor(scrollTop / rowHeight) - VIRTUAL_BUFFER);
652
- end = Math.min(this.rows.length, start + Math.ceil(viewport / rowHeight) + VIRTUAL_BUFFER * 2);
653
- const topSpacer = document.createElement("li");
654
- topSpacer.className = "forge-select__spacer";
655
- topSpacer.setAttribute("aria-hidden", "true");
656
- topSpacer.style.height = `${start * rowHeight}px`;
657
- this.list.append(topSpacer);
658
- }
659
- for (let i = start; i < end; i++) {
660
- this.list.append(this.renderRow(this.rows[i]));
661
- }
662
- if (virtual) {
663
- const bottomSpacer = document.createElement("li");
664
- bottomSpacer.className = "forge-select__spacer";
665
- bottomSpacer.setAttribute("aria-hidden", "true");
666
- bottomSpacer.style.height = `${(this.rows.length - end) * rowHeight}px`;
667
- this.list.append(bottomSpacer);
668
- if (this.list.scrollTop !== scrollTop) {
669
- this.list.scrollTop = scrollTop;
670
- }
671
- }
672
- this.updateActiveDescendant();
673
- }
674
- renderRow(row) {
675
- const li = document.createElement("li");
676
- switch (row.kind) {
677
- case "group":
678
- li.className = "forge-select__group-label";
679
- li.setAttribute("role", "presentation");
680
- li.textContent = row.label;
681
- break;
682
- case "empty":
683
- li.className = "forge-select__empty";
684
- li.textContent = this.strings.noResults;
685
- break;
686
- case "loading":
687
- li.className = "forge-select__loading";
688
- li.textContent = this.strings.loading;
689
- break;
690
- case "loading-more":
691
- li.className = "forge-select__loading-more";
692
- li.setAttribute("aria-hidden", "true");
693
- li.textContent = this.strings.loadingMore;
694
- break;
695
- case "create":
696
- li.className = "forge-select__option forge-select__option--create";
697
- li.setAttribute("role", "option");
698
- li.id = `${this.uid}-nav-${row.navIndex}`;
699
- li.dataset.navIndex = String(row.navIndex);
700
- li.textContent = format(this.strings.createOption, { query: this.query.trim() });
701
- if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
702
- break;
703
- case "option": {
704
- li.className = "forge-select__option";
705
- li.setAttribute("role", "option");
706
- const isSelected = this.selected.includes(row.option.value);
707
- li.setAttribute("aria-selected", String(isSelected));
708
- if (isSelected) li.classList.add("forge-select__option--selected");
709
- if (this.opts.multiple && row.hasChildren && computeCheckState(row.option, this.selected) === "some") {
710
- li.classList.add("forge-select__option--indeterminate");
711
- }
712
- if (row.depth > 0) {
713
- li.style.paddingLeft = `calc(12px + ${row.depth} * var(--fs-tree-indent, 18px))`;
714
- }
715
- if (row.option.disabled) {
716
- li.classList.add("forge-select__option--disabled");
717
- li.setAttribute("aria-disabled", "true");
718
- } else {
719
- li.id = `${this.uid}-nav-${row.navIndex}`;
720
- li.dataset.navIndex = String(row.navIndex);
721
- if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
722
- }
723
- if (row.hasChildren) {
724
- const twisty = document.createElement("span");
725
- twisty.className = "forge-select__twisty";
726
- twisty.dataset.twisty = row.option.value;
727
- twisty.setAttribute("aria-hidden", "true");
728
- twisty.textContent = this.expandedValues.has(row.option.value) ? "\u25BC" : "\u25B6";
729
- li.append(twisty);
730
- }
731
- li.append(this.optionContent(row.option));
732
- break;
733
- }
734
- }
735
- return li;
736
- }
737
- /**
738
- * Rendered row content is cached per option value and cloned on each render,
739
- * so templates run once per option instead of once per scroll frame.
740
- * State classes (selected/highlighted/disabled) live on the <li>, keeping the
741
- * cached content state-free.
742
- */
743
- optionContent(option) {
744
- let cached = this.rowContentCache.get(option.value);
745
- if (!cached) {
746
- const holder = document.createElement("span");
747
- holder.className = "forge-select__option-content";
748
- this.renderTemplate(holder, option, this.opts.templateResult);
749
- if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
750
- const oldest = this.rowContentCache.keys().next().value;
751
- this.rowContentCache.delete(oldest);
752
- }
753
- this.rowContentCache.set(option.value, holder);
754
- cached = holder;
755
- }
756
- return cached.cloneNode(true);
757
- }
758
- moveHighlight(delta) {
759
- if (this.navItems.length === 0) return;
760
- const next = this.highlightedIndex === -1 && delta > 0 ? 0 : (this.highlightedIndex + delta + this.navItems.length) % this.navItems.length;
761
- this.highlightedIndex = next;
762
- if (this.usesVirtualScroll()) {
763
- const rowIndex = this.rows.findIndex(
764
- (row) => (row.kind === "option" || row.kind === "create") && row.navIndex === next
765
- );
766
- if (rowIndex >= 0) {
767
- const rowHeight = this.opts.itemHeight;
768
- const top = rowIndex * rowHeight;
769
- const viewport = this.list.clientHeight || rowHeight * 8;
770
- let target = this.list.scrollTop;
771
- if (top < target) target = top;
772
- else if (top + rowHeight > target + viewport) target = top + rowHeight - viewport;
773
- if (target !== this.list.scrollTop) this.list.scrollTop = target;
774
- }
775
- this.renderRows();
776
- } else {
777
- this.renderRows();
778
- const highlighted = this.list.querySelector(".forge-select__option--highlighted");
779
- highlighted?.scrollIntoView?.({ block: "nearest" });
780
- }
781
- }
782
- updateActiveDescendant() {
783
- const target = this.searchInput ?? this.control;
784
- if (this.highlightedIndex >= 0) {
785
- target.setAttribute("aria-activedescendant", `${this.uid}-nav-${this.highlightedIndex}`);
786
- } else {
787
- target.removeAttribute("aria-activedescendant");
788
- }
789
- }
790
- // ---------------------------------------------------------------- remote data
791
- scheduleRemoteLoad(query, delay) {
792
- if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
793
- this.page = 0;
794
- this.hasMore = true;
795
- this.loading = true;
796
- this.renderList();
797
- this.ajaxTimer = setTimeout(() => {
798
- void this.loadRemote(query);
799
- }, delay);
800
- }
801
- /**
802
- * Fires on every list scroll. Only acts when pagination is opted into via
803
- * `ajax.pagination`; reads real scroll geometry rather than row counts so
804
- * it works whether or not virtual scrolling is active for this list.
805
- */
806
- maybeLoadNextPage() {
807
- const ajax = this.opts.ajax;
808
- if (!ajax?.pagination || !this.hasMore || this.loading || this.loadingMore) return;
809
- const { scrollHeight, scrollTop, clientHeight } = this.list;
810
- const threshold = this.opts.itemHeight * 2;
811
- if (scrollHeight - scrollTop - clientHeight >= threshold) return;
812
- this.loadingMore = true;
813
- this.renderList();
814
- void this.loadRemote(this.query, { append: true });
815
- }
816
- async loadRemote(query, { append = false } = {}) {
817
- const ajax = this.opts.ajax;
818
- const requestId = ++this.ajaxRequestId;
819
- const page = append ? this.page + 1 : 0;
820
- try {
821
- const url = buildUrl(ajax, query, page);
822
- const response = await fetch(url);
823
- const json = await response.json();
824
- if (requestId !== this.ajaxRequestId || this.destroyed) return;
825
- const result = ajax.transform ? ajax.transform(json) : json;
826
- const options = Array.isArray(result) ? result : result.options;
827
- const hasMore = ajax.pagination ? Array.isArray(result) ? false : result.hasMore : false;
828
- if (append) {
829
- const existing = collectValues(this.data);
830
- this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
831
- } else {
832
- this.data = options;
833
- this.rowContentCache.clear();
834
- }
835
- this.page = page;
836
- this.hasMore = hasMore;
837
- this.remoteLoaded = true;
838
- } catch {
839
- if (requestId !== this.ajaxRequestId || this.destroyed) return;
840
- if (!append) {
841
- this.data = [];
842
- this.rowContentCache.clear();
843
- }
844
- this.hasMore = false;
845
- } finally {
846
- if (requestId === this.ajaxRequestId && !this.destroyed) {
847
- this.loading = false;
848
- this.loadingMore = false;
849
- if (this.isOpen) this.renderList();
850
- }
851
- }
852
- }
853
- };
854
- function parseNativeOptions(select) {
855
- const data = [];
856
- for (const child of Array.from(select.children)) {
857
- if (child instanceof HTMLOptGroupElement) {
858
- data.push({
859
- label: child.label,
860
- options: Array.from(child.querySelectorAll("option")).map(parseOption)
861
- });
862
- } else if (child instanceof HTMLOptionElement) {
863
- data.push(parseOption(child));
864
- }
865
- }
866
- return data;
867
- }
868
- function parseOption(option) {
869
- return {
870
- value: option.value,
871
- label: option.textContent?.trim() ?? option.value,
872
- disabled: option.disabled || void 0
873
- };
874
- }
875
- function buildUrl(ajax, query, page) {
876
- if (typeof ajax.url === "function") return ajax.url(query);
877
- if (!ajax.params) return ajax.url;
878
- const params = new URLSearchParams();
879
- for (const [key, value] of Object.entries(ajax.params(query, page))) {
880
- params.set(key, String(value));
881
- }
882
- const separator = ajax.url.includes("?") ? "&" : "?";
883
- return `${ajax.url}${separator}${params.toString()}`;
884
- }
885
- function collectValues(items) {
886
- const values = /* @__PURE__ */ new Set();
887
- for (const item of items) {
888
- if (isGroup(item)) {
889
- for (const option of item.options) values.add(option.value);
890
- } else {
891
- values.add(item.value);
892
- }
893
- }
894
- return values;
895
- }
896
- function arraysEqual(a, b) {
897
- return a.length === b.length && a.every((value, index) => value === b[index]);
898
- }
899
- return __toCommonJS(index_exports);
900
- })();
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);})();
901
2
  //# sourceMappingURL=index.global.js.map