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.
- package/README.md +53 -40
- package/dist/index.cjs +442 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -5
- package/dist/index.d.ts +53 -5
- package/dist/index.global.js +1 -900
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +442 -156
- package/dist/index.js.map +1 -1
- package/package.json +31 -6
- package/styles/forge-select.css +25 -1
package/dist/index.global.js
CHANGED
|
@@ -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
|