forge-select 0.2.0 → 0.4.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 +62 -40
- package/dist/index.cjs +754 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -9
- package/dist/index.d.ts +175 -9
- package/dist/index.global.js +1 -900
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +754 -174
- package/dist/index.js.map +1 -1
- package/package.json +32 -7
- package/styles/forge-select.css +50 -2
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 E=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var B=(o,t)=>{for(var e in t)E(o,e,{get:t[e],enumerable:!0})},$=(o,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of j(t))!P.call(o,i)&&i!==e&&E(o,i,{get:()=>t[i],enumerable:!(s=F(t,i))||s.enumerable});return o};var U=o=>$(E({},"__esModule",{value:!0}),o);var W={};B(W,{ForgeSelect:()=>f,default:()=>f});var b=class{constructor(){this.handlers=new Map}on(t,e){let s=this.handlers.get(t);s||(s=new Set,this.handlers.set(t,s)),s.add(e)}off(t,e){this.handlers.get(t)?.delete(e)}emit(t,...e){let s=this.handlers.get(t);if(s)for(let i of[...s])i(...e)}clear(){this.handlers.clear()}};function D(o,t,e,s=4){let i=e-o.bottom,r=o.top,n=t>i&&r>i;return{dropUp:n,top:n?o.top-t-s:o.bottom+s}}var O={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.",minSearchLength:"Type {count} or more characters to search",maximumSelected:"Maximum of {count} selections reached"},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.",minSearchLength:"Nh\u1EADp th\xEAm {count} k\xFD t\u1EF1 \u0111\u1EC3 t\xECm ki\u1EBFm",maximumSelected:"\u0110\xE3 \u0111\u1EA1t t\u1ED1i \u0111a {count} l\u1EF1a ch\u1ECDn"}};function _(o){return typeof o=="string"?O[o]??O.en:{...O.en,...o}}function u(o,t){return o.replace(/\{(\w+)\}/g,(e,s)=>t[s]??e)}function H(o){let t=[];for(let e of Array.from(o.children))e instanceof HTMLOptGroupElement?t.push({label:e.label,options:Array.from(e.querySelectorAll("option")).map(M)}):e instanceof HTMLOptionElement&&t.push(M(e));return t}function M(o){let t=o.parentElement instanceof HTMLOptGroupElement&&o.parentElement.disabled;return{value:o.value,label:o.textContent?.trim()??o.value,disabled:o.disabled||t||void 0}}function x(o,t,e,s="row"){if(e){let i=e(t);typeof i=="string"?o.innerHTML=i:o.append(i);return}if(!t.avatar&&!t.description){o.textContent=t.label;return}if(t.avatar){let i=document.createElement("img");i.className=s==="row"?"forge-select__option-avatar":"forge-select__inline-avatar",i.src=t.avatar,i.alt="",i.setAttribute("loading","lazy"),i.setAttribute("decoding","async"),o.append(i)}if(s==="row"&&t.description){let i=document.createElement("span");i.className="forge-select__option-body";let r=document.createElement("span");r.className="forge-select__option-label",r.textContent=t.label;let n=document.createElement("span");n.className="forge-select__option-desc",n.textContent=t.description,i.append(r,n),o.append(i)}else{let i=document.createElement("span");i.className="forge-select__option-label",i.textContent=t.label,o.append(i)}}function R(o,t,e){if(!o.url)throw new Error("ForgeSelect: ajax requires either url or request.");if(typeof o.url=="function")return o.url(t,e);if(!o.params)return o.url;let s=new URLSearchParams;for(let[r,n]of Object.entries(o.params(t,e)))s.set(r,String(n));let i=o.url.includes("?")?"&":"?";return`${o.url}${i}${s.toString()}`}function k(o,t){let e=o.transform?o.transform(t):t;if(Array.isArray(e))return{options:e,hasMore:!1};if(!e||!Array.isArray(e.options))throw new Error("ForgeSelect: ajax.transform must return an array of options, or an object shaped like { options: Option[], hasMore?: boolean }.");return{options:e.options,hasMore:o.pagination?!!e.hasMore:!1}}function p(o){return o.options!==void 0}var w=o=>!!o.disabled;function v(o,t=w){if(!o.children)return[];let e=[];for(let s of o.children)t(s)||e.push(s.value),e.push(...v(s,t));return e}function y(o,t,e=w){if(!o.children?.length)return t.includes(o.value)?"all":"none";let s=o.children.filter(i=>!e(i)).map(i=>y(i,t,e));return s.length===0?"none":s.every(i=>i==="all")?"all":s.every(i=>i==="none")?"none":"some"}function N(o,t){let e=s=>{for(let i of s){if(i.value===t)return i;let r=i.children?e(i.children):void 0;if(r)return r}};for(let s of o){let i=e(p(s)?s.options:[s]);if(i)return i}}function L(o,t,e=w){let s=i=>{if(!i.children?.length)return;for(let a of i.children)s(a);let r=y(i,t,e),n=t.indexOf(i.value);r==="all"&&n===-1?t.push(i.value):r!=="all"&&n!==-1&&t.splice(n,1)};for(let i of o)(p(i)?i.options:[i]).forEach(s)}function I(o){let t=new Set,e=s=>{t.add(s.value),s.children?.forEach(e)};for(let s of o)(p(s)?s.options:[s]).forEach(e);return t}function V(o,t){return o.length===t.length&&o.every((e,s)=>e===t[s])}var G=36,q=5,z=100,K=2e3,X=0,f=class{constructor(t,e={}){this.selected=[];this.selectedOptions=new Map;this.suppressNextTagClick=!1;this.emitter=new b;this.uid=`forge-select-${++X}`;this.searchInput=null;this.portalHost=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.isOptionDisabled=t=>t.disabled===!0||(this.opts.isOptionDisabled?.(t)??!1);this.pointerDownOnControl=!1;this.onDocumentMouseDown=t=>{let e=t.target;!this.root.contains(e)&&!this.portalHost?.contains(e)&&this.close()};this.onWindowResize=()=>{this.positionDropdown()};this.onAncestorScroll=()=>{this.portalHost&&this.positionDropdown()};this.onNativeInvalid=t=>{t.preventDefault(),this.control.classList.add("forge-select__control--invalid"),this.control.setAttribute("aria-invalid","true"),this.isOpen||this.open(),this.control.focus()};this.onNativeChange=()=>{if(!this.nativeSelect||this.destroyed||this.syncingNative)return;let t=Array.from(this.nativeSelect.selectedOptions,e=>e.value);this.applyNativeValues(t)};this.onFormReset=()=>{if(!this.nativeSelect||this.destroyed)return;let t=Array.from(this.nativeSelect.options).filter(e=>e.defaultSelected).map(e=>e.value);this.applyNativeValues(t)};let s=typeof t=="string"?document.querySelector(t):t;if(!s)throw new Error(`ForgeSelect: target element not found: ${String(t)}`);this.el=s;let i=s instanceof HTMLSelectElement?s:null;if(this.nativeSelect=i,this.nativeForm=i?.form??null,this.originalDisplay=s.style.display,this.originalDisabled=i?.disabled??!1,this.opts={placeholder:e.placeholder??"",searchable:e.searchable??!0,multiple:e.multiple??i?.multiple??!1,clearable:e.clearable??!1,allowCreate:e.allowCreate??!1,sortable:e.sortable??!1,closeOnSelect:e.closeOnSelect??!1,maxSelections:e.maxSelections==null||!Number.isFinite(e.maxSelections)?void 0:Math.max(0,Math.floor(e.maxSelections)),theme:e.theme??"default",disabled:e.disabled??i?.disabled??!1,required:e.required??i?.required??!1,data:e.data,ajax:e.ajax,templateResult:e.templateResult,templateSelection:e.templateSelection,filterOption:e.filterOption,minSearchLength:Math.max(0,Math.floor(e.minSearchLength??0)),minResultsForSearch:Math.max(0,Math.floor(e.minResultsForSearch??0)),isOptionDisabled:e.isOptionDisabled,virtualScroll:e.virtualScroll,itemHeight:e.itemHeight??G,language:e.language??"en",plugins:e.plugins??[],openOnFocus:e.openOnFocus??!1,dropdownParent:e.dropdownParent},this.strings=_(this.opts.language),this.plugins=this.opts.plugins,i&&(i.required=this.opts.required),this.data=this.opts.data??(i?H(i):[]),i&&!this.opts.data){let r=Array.from(i.options),n=i.multiple||i.selectedIndex>0||r.some(a=>a.defaultSelected);for(let a of r)n&&a.selected&&this.selectValue(a.value,!1)}this.buildDom(),this.renderValue(),this.opts.disabled&&this.disable(),i?.addEventListener("change",this.onNativeChange),i?.addEventListener("invalid",this.onNativeInvalid),this.nativeForm?.addEventListener("reset",this.onFormReset);for(let r of this.plugins)r.onInit?.(this)}applyNativeValues(t){this.selected=[];for(let e of this.opts.multiple?t:t.slice(0,1))this.selectValue(e,!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.positionDropdown(),window.addEventListener("resize",this.onWindowResize),document.addEventListener("scroll",this.onAncestorScroll,!0),this.searchInput&&!this.searchInput.hidden&&this.searchInput.focus(),this.emitter.emit("open");for(let t of this.plugins)t.onOpen?.(this)}}close(){if(this.isOpen){this.isOpen=!1,this.dropdown.hidden=!0,this.root.classList.remove("forge-select--open"),this.root.classList.remove("forge-select--drop-up"),this.control.setAttribute("aria-expanded","false"),document.removeEventListener("mousedown",this.onDocumentMouseDown),window.removeEventListener("resize",this.onWindowResize),document.removeEventListener("scroll",this.onAncestorScroll,!0),this.highlightedIndex=-1,this.searchInput&&(this.searchInput.value="",this.query=""),this.emitter.emit("close");for(let t of this.plugins)t.onClose?.(this)}}positionDropdown(){let t=this.control.getBoundingClientRect(),e=D(t,this.dropdown.offsetHeight,window.innerHeight);this.root.classList.toggle("forge-select--drop-up",e.dropUp),this.portalHost&&(this.portalHost.style.top=`${e.top}px`,this.portalHost.style.left=`${t.left}px`,this.portalHost.style.width=`${t.width}px`)}destroy(){if(!this.destroyed){this.close();for(let t of this.plugins)t.onDestroy?.(this);this.destroyed=!0,this.ajaxTimer&&clearTimeout(this.ajaxTimer),this.ajaxController?.abort(),this.nativeSelect?.removeEventListener("change",this.onNativeChange),this.nativeSelect?.removeEventListener("invalid",this.onNativeInvalid),this.nativeForm?.removeEventListener("reset",this.onFormReset),this.rowContentCache.clear(),this.portalHost?.remove(),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(t,e={}){let s=t==null?[]:Array.isArray(t)?t:[t],i=this.opts.multiple?s:s.slice(0,1);if(!V(i,this.selected)){this.selected=[];for(let r of i)this.selectValue(r,!1);this.afterSelectionChange(e.emitChange??!0)}}setData(t){this.ajaxTimer&&(clearTimeout(this.ajaxTimer),this.ajaxTimer=null),this.ajaxController?.abort(),this.ajaxController=null,this.ajaxRequestId+=1,this.loading=!1,this.loadingMore=!1,this.loadError=null,this.remoteLoaded=!0,this.page=0,this.hasMore=!1,this.data=t,this.opts.data=t,this.updateSearchVisibility(),this.rowContentCache.clear(),this.highlightedIndex=-1,this.isOpen&&this.renderList()}selectAll(){if(this.opts.multiple){this.selected=[];for(let t of this.allSelectableValues()){let e=this.findOption(t);e&&this.canSelectOption(e)&&this.selectValue(t,!1)}this.afterSelectionChange()}}clearAll(){this.clearSelection()}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(t,e){this.emitter.on(t,e)}off(t,e){this.emitter.off(t,e)}applyAccessibleName(){let t=this.el.getAttribute("aria-labelledby"),e=this.el.getAttribute("aria-label");if(t)this.control.setAttribute("aria-labelledby",t);else if(e)this.control.setAttribute("aria-label",e);else if(this.el.id){let s=Array.from(document.getElementsByTagName("label")).find(i=>i.htmlFor===this.el.id);s&&(s.id||(s.id=`${this.uid}-label`),this.control.setAttribute("aria-labelledby",s.id))}}shouldShowSearch(){return this.opts.searchable&&(this.opts.ajax!=null||I(this.data).size>=this.opts.minResultsForSearch)}updateSearchVisibility(){this.searchInput&&(this.searchInput.hidden=!this.shouldShowSearch(),this.searchInput.hidden&&(this.searchInput.value="",this.query=""))}buildDom(){let t=typeof this.opts.dropdownParent=="string"?document.querySelector(this.opts.dropdownParent):this.opts.dropdownParent;if(this.opts.dropdownParent&&!t)throw new Error(`ForgeSelect: dropdown parent not found: ${String(this.opts.dropdownParent)}`);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.opts.required&&this.control.setAttribute("aria-required","true"),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.searchInput.hidden=!this.shouldShowSearch(),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.liveRegion),t||this.root.append(this.dropdown),this.el.style.display="none",this.el.insertAdjacentElement("afterend",this.root),t&&(this.portalHost=document.createElement("div"),this.portalHost.className="forge-select forge-select--portal-host",this.portalHost.dataset.theme=this.opts.theme,this.portalHost.style.setProperty("--fs-item-height",`${this.opts.itemHeight}px`),this.portalHost.append(this.dropdown),t.append(this.portalHost)),this.bindEvents()}bindEvents(){this.control.addEventListener("click",t=>{if(t.target!==this.clearBtn){if(this.suppressNextTagClick){this.suppressNextTagClick=!1;return}this.isDisabled||(this.isOpen?this.close():this.open())}}),this.control.addEventListener("keydown",t=>this.handleKeydown(t)),this.control.addEventListener("mousedown",()=>{this.pointerDownOnControl=!0}),this.control.addEventListener("focus",()=>{this.opts.openOnFocus&&!this.pointerDownOnControl&&!this.isOpen&&!this.isDisabled&&this.open(),this.pointerDownOnControl=!1}),this.clearBtn.addEventListener("click",t=>{t.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);let t=this.query.trim(),e=t!==""&&t.length<this.opts.minSearchLength;this.opts.ajax&&!e?this.scheduleRemoteLoad(this.query,this.opts.ajax.debounce??250):(e&&(this.ajaxTimer&&(clearTimeout(this.ajaxTimer),this.ajaxTimer=null),this.ajaxController?.abort(),this.loading=!1),this.renderList())}),this.searchInput.addEventListener("keydown",t=>this.handleKeydown(t)),this.searchInput.addEventListener("paste",t=>{if(!this.opts.multiple||!this.opts.allowCreate)return;let s=(t.clipboardData?.getData("text")??"").split(/[,\n]+/).map(r=>r.trim()).filter(Boolean);if(s.length<2)return;t.preventDefault();let i=[];for(let r of s){let n=this.createTag(r);n&&i.push(n)}if(i.length!==0){this.searchInput.value="",this.query="",this.afterSelectionChange();for(let r of i)r.created&&this.emitter.emit("create",r.option),this.emitter.emit("select",r.option);this.opts.closeOnSelect?this.close():this.renderList()}})),this.list.addEventListener("click",t=>{let e=t.target,s=e.closest("[data-twisty]");if(s){let n=s.dataset.twisty;this.expandedValues.has(n)?this.expandedValues.delete(n):this.expandedValues.add(n),this.renderList();return}let i=e.closest("li[data-nav-index]");if(!i){let n=e.closest("li[data-option-value]"),a=n?this.findOption(n.dataset.optionValue):void 0;a&&this.hasReachedMaximum()&&!this.selected.includes(a.value)&&this.announceMaximum(a);return}let r=Number(i.dataset.navIndex);this.activateNavItem(r)}),this.list.addEventListener("scroll",()=>{this.usesVirtualScroll()&&this.renderRows(),this.maybeLoadNextPage()})}handleKeydown(t){if(!this.isDisabled)switch(t.key){case"Enter":t.preventDefault(),this.isOpen?this.highlightedIndex>=0&&this.activateNavItem(this.highlightedIndex):this.open();break;case" ":t.target===this.control&&(t.preventDefault(),this.isOpen||this.open());break;case"ArrowDown":t.preventDefault(),this.isOpen?this.moveHighlight(1):this.open();break;case"ArrowUp":t.preventDefault(),this.isOpen&&this.moveHighlight(-1);break;case"Escape":this.isOpen&&(t.preventDefault(),this.close(),this.control.focus());break;case"ArrowRight":this.isOpen&&this.navigateTree("right")&&t.preventDefault();break;case"ArrowLeft":this.isOpen&&this.navigateTree("left")&&t.preventDefault();break;case"Tab":this.close();break}}canSelectOption(t){if(this.opts.maxSelections==null)return!0;let e=[...this.selected];e.includes(t.value)||e.push(t.value);for(let s of v(t,this.isOptionDisabled))e.includes(s)||e.push(s);return L(this.data,e,this.isOptionDisabled),e.length<=this.opts.maxSelections}hasReachedMaximum(){return this.opts.maxSelections!=null&&this.selected.length>=this.opts.maxSelections}announceMaximum(t){let e=this.opts.maxSelections;e!=null&&(this.liveRegion.textContent=u(this.strings.maximumSelected,{count:String(e)}),this.emitter.emit("maximum",{limit:e,option:t}))}selectValue(t,e){if(this.selected.includes(t))return;let s=this.findOption(t)??this.selectedOptions.get(t)??{value:t,label:t};if(this.selectedOptions.set(t,s),this.opts.multiple){this.selected.push(t);for(let i of v(s,this.isOptionDisabled))this.selected.includes(i)||this.selected.push(i);this.syncTreeAncestors()}else this.selected=[t];e&&(this.afterSelectionChange(),this.emitter.emit("select",s))}deselectValue(t,e){let s=this.selected.indexOf(t);if(s===-1)return;let i=this.findOption(t)??this.selectedOptions.get(t);if(this.selected.splice(s,1),this.opts.multiple){if(i)for(let r of v(i,this.isOptionDisabled)){let n=this.selected.indexOf(r);n!==-1&&this.selected.splice(n,1)}this.syncTreeAncestors()}e&&(this.afterSelectionChange(),this.emitter.emit("unselect",i??{value:t,label:t}))}syncTreeAncestors(){L(this.data,this.selected,this.isOptionDisabled)}clearSelection(){this.selected.length!==0&&(this.selected=[],this.emitter.emit("clear"),this.afterSelectionChange())}allSelectableValues(){let t=[],e=s=>{this.isOptionDisabled(s)||t.push(s.value),s.children?.forEach(e)};for(let s of this.data)(p(s)?s.options:[s]).forEach(e);return t}afterSelectionChange(t=!0){this.renderValue(),this.syncNativeSelect(t),(!this.opts.required||this.selected.length>0)&&(this.control.classList.remove("forge-select__control--invalid"),this.control.removeAttribute("aria-invalid")),this.isOpen&&this.renderList(),t&&this.emitter.emit("change",this.getValue())}syncNativeSelect(t=!0){if(!(this.el instanceof HTMLSelectElement))return;let e=new Set;for(let s of Array.from(this.el.options))e.add(s.value),s.selected=this.selected.includes(s.value);for(let s of this.selected){if(e.has(s))continue;let i=document.createElement("option");i.value=s,i.textContent=this.selectedOptions.get(s)?.label??s,i.selected=!0,this.el.append(i)}if(this.opts.sortable&&this.opts.multiple)for(let s of this.selected){let i=Array.from(this.el.options).find(r=>r.value===s);i&&this.el.append(i)}if(t){this.syncingNative=!0;try{this.el.dispatchEvent(new Event("change",{bubbles:!0}))}finally{this.syncingNative=!1}}}findOption(t){return N(this.data,t)}findOptionByLabel(t){let e=t.toLowerCase(),s=i=>{for(let r of i){if(r.label.toLowerCase()===e)return r;let n=r.children?s(r.children):void 0;if(n)return n}};for(let i of this.data){let r=s(p(i)?i.options:[i]);if(r)return r}}createTag(t){let e=t.trim();if(!e)return;let s=this.findOptionByLabel(e);if(s){if(this.selected.includes(s.value))return;if(this.opts.multiple&&!this.canSelectOption(s)){this.announceMaximum(s);return}return this.selectValue(s.value,!1),{option:s,created:!1}}let i={value:e,label:e};if(this.opts.multiple&&!this.canSelectOption(i)){this.announceMaximum(i);return}return this.data.push(i),this.selectValue(i.value,!1),{option:i,created:!0}}createFromQuery(){let t=this.query.trim();if(!t)return;let e=this.createTag(t);e&&(this.searchInput&&(this.searchInput.value="",this.query=""),this.afterSelectionChange(),e.created&&this.emitter.emit("create",e.option),this.emitter.emit("select",e.option),(!this.opts.multiple||this.opts.closeOnSelect)&&this.close())}activateNavItem(t){let e=this.navItems[t];if(!e)return;if(e.kind==="create"){this.createFromQuery();return}let{value:s}=e.option;if(this.opts.multiple){let i=!1;this.selected.includes(s)?(this.deselectValue(s,!0),i=!0):this.canSelectOption(e.option)?(this.selectValue(s,!0),i=!0):this.announceMaximum(e.option),i&&this.opts.closeOnSelect&&this.close()}else this.selectValue(s,!0),this.close(),this.control.focus()}renderValue(){this.valueEl.textContent="";let t=this.selected.length>0;if(this.clearBtn.hidden=!(this.opts.clearable&&t),!t){let e=document.createElement("span");e.className="forge-select__placeholder",e.textContent=this.opts.placeholder,this.valueEl.append(e);return}if(this.opts.multiple)for(let e of this.selected){let s=this.selectedOptions.get(e)??{value:e,label:e},i=document.createElement("span");i.className="forge-select__tag";let r=document.createElement("span");r.className="forge-select__tag-label",x(r,s,this.opts.templateSelection,"inline");let n=document.createElement("button");n.type="button",n.className="forge-select__tag-remove",n.setAttribute("aria-label",u(this.strings.removeItem,{label:s.label})),n.textContent="\xD7",n.addEventListener("click",a=>{a.stopPropagation(),this.isDisabled||this.deselectValue(e,!0)}),i.append(r,n),this.opts.sortable&&(i.dataset.value=e,i.tabIndex=0,i.setAttribute("aria-roledescription","draggable item"),i.setAttribute("aria-label",u(this.strings.reorderHint,{label:s.label})),i.addEventListener("keydown",a=>this.handleTagKeydown(a,e)),this.bindTagDrag(i,e)),this.valueEl.append(i)}else{let e=this.selectedOptions.get(this.selected[0])??{value:this.selected[0],label:this.selected[0]},s=document.createElement("span");s.className="forge-select__single-value",x(s,e,this.opts.templateSelection,"inline"),this.valueEl.append(s)}}bindTagDrag(t,e){let i=0,r=!1,n=[],a=h=>{if(!r){if(Math.abs(h.clientX-i)<4)return;r=!0,n=[...this.selected],typeof this.valueEl.setPointerCapture=="function"&&this.valueEl.setPointerCapture(h.pointerId),t.classList.add("forge-select__tag--dragging")}h.preventDefault();let m=n.indexOf(e),d=Array.from(this.valueEl.querySelectorAll(".forge-select__tag"));for(let c of d){if(c===t)continue;let g=c.dataset.value;if(!g)continue;let S=n.indexOf(g);if(S===-1)continue;let C=c.getBoundingClientRect(),A=C.left+C.width/2,T=m<S;if(T?h.clientX>A:h.clientX<A){n.splice(m,1),n.splice(S,0,e),T?this.valueEl.insertBefore(t,c.nextSibling):this.valueEl.insertBefore(t,c);break}}},l=h=>{this.valueEl.removeEventListener("pointermove",a),this.valueEl.removeEventListener("pointerup",l),this.valueEl.removeEventListener("pointercancel",l),r&&(typeof this.valueEl.releasePointerCapture=="function"&&this.valueEl.releasePointerCapture(h.pointerId),t.classList.remove("forge-select__tag--dragging"),this.selected=n,this.suppressNextTagClick=!0,this.afterSelectionChange(),this.emitter.emit("reorder",[...this.selected]))};t.addEventListener("pointerdown",h=>{this.isDisabled||h.button!==0||h.target.closest(".forge-select__tag-remove")||(i=h.clientX,r=!1,this.valueEl.addEventListener("pointermove",a),this.valueEl.addEventListener("pointerup",l),this.valueEl.addEventListener("pointercancel",l))})}handleTagKeydown(t,e){if(!t.altKey||t.key!=="ArrowLeft"&&t.key!=="ArrowRight")return;let s=this.selected.indexOf(e),i=t.key==="ArrowLeft"?s-1:s+1;if(s===-1||i<0||i>=this.selected.length)return;t.preventDefault(),t.stopPropagation();let r=[...this.selected];[r[s],r[i]]=[r[i],r[s]],this.selected=r,this.afterSelectionChange(),this.emitter.emit("reorder",[...this.selected]),this.focusTagByValue(e)}focusTagByValue(t){for(let e of Array.from(this.valueEl.querySelectorAll(".forge-select__tag")))if(e.dataset.value===t){e.focus();return}}buildRows(){this.rows=[],this.navItems=[];let t=this.query.trim(),e=t.toLowerCase(),s=n=>e===""||(this.opts.filterOption?this.opts.filterOption(n,t):n.label.toLowerCase().includes(e)||(n.description?.toLowerCase().includes(e)??!1)),i=n=>e===""||s(n)||(n.children??[]).some(i),r=(n,a,l)=>{let h=-1;this.isOptionDisabled(n)||this.hasReachedMaximum()&&!this.selected.includes(n.value)||(h=this.navItems.length,this.navItems.push({kind:"option",option:n,parentValue:l}));let d=!!n.children&&n.children.length>0;if(this.rows.push({kind:"option",option:n,navIndex:h,depth:a,hasChildren:d}),d&&(e!==""||this.expandedValues.has(n.value)))for(let g of n.children)i(g)&&r(g,a+1,n.value)};if(t!==""&&t.length<this.opts.minSearchLength){this.rows.push({kind:"min-length"});return}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(p(n)){let a=n.options.filter(i);if(a.length===0)continue;this.rows.push({kind:"group",label:n.label}),a.forEach(l=>r(l,0))}else i(n)&&r(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(t){return!!this.findOptionByLabel(t)}usesVirtualScroll(){return this.opts.virtualScroll!==!1&&this.rows.length>z}renderList(){this.buildRows(),this.renderRows(),this.announceStatus()}announceStatus(){let t=this.rows[0],e=this.hasReachedMaximum()?u(this.strings.maximumSelected,{count:String(this.opts.maxSelections)}):t?.kind==="loading"?this.strings.loading:t?.kind==="error"?this.strings.errorLoading:t?.kind==="empty"?this.strings.noResults:t?.kind==="min-length"?u(this.strings.minSearchLength,{count:String(this.opts.minSearchLength)}):"";this.liveRegion.textContent!==e&&(this.liveRegion.textContent=e)}renderRows(){let t=this.list.scrollTop,e=this.list.clientHeight,s=this.usesVirtualScroll();this.list.textContent="";let i=this.opts.itemHeight,r=0,n=this.rows.length;if(s){let a=e||i*8;r=Math.max(0,Math.floor(t/i)-q),n=Math.min(this.rows.length,r+Math.ceil(a/i)+q*2);let l=document.createElement("li");l.className="forge-select__spacer",l.setAttribute("aria-hidden","true"),l.style.height=`${r*i}px`,this.list.append(l)}for(let a=r;a<n;a++)this.list.append(this.renderRow(this.rows[a]));if(s){let a=document.createElement("li");a.className="forge-select__spacer",a.setAttribute("aria-hidden","true"),a.style.height=`${(this.rows.length-n)*i}px`,this.list.append(a),this.list.scrollTop!==t&&(this.list.scrollTop=t)}this.updateActiveDescendant()}renderRow(t){let e=document.createElement("li");switch(t.kind){case"group":e.className="forge-select__group-label",e.setAttribute("role","presentation"),e.textContent=t.label;break;case"empty":e.className="forge-select__empty",e.setAttribute("role","option"),e.setAttribute("aria-disabled","true"),e.setAttribute("aria-selected","false"),e.textContent=this.strings.noResults;break;case"min-length":e.className="forge-select__min-length",e.setAttribute("role","option"),e.setAttribute("aria-disabled","true"),e.setAttribute("aria-selected","false"),e.textContent=u(this.strings.minSearchLength,{count:String(this.opts.minSearchLength)});break;case"error":e.className="forge-select__error",e.setAttribute("role","option"),e.setAttribute("aria-disabled","true"),e.setAttribute("aria-selected","false"),e.textContent=this.strings.errorLoading;break;case"loading":e.className="forge-select__loading",e.setAttribute("role","option"),e.setAttribute("aria-disabled","true"),e.setAttribute("aria-selected","false"),e.textContent=this.strings.loading;break;case"loading-more":e.className="forge-select__loading-more",e.setAttribute("aria-hidden","true"),e.textContent=this.strings.loadingMore;break;case"create":e.className="forge-select__option forge-select__option--create",e.setAttribute("role","option"),e.id=`${this.uid}-nav-${t.navIndex}`,e.dataset.navIndex=String(t.navIndex),e.textContent=u(this.strings.createOption,{query:this.query.trim()}),t.navIndex===this.highlightedIndex&&e.classList.add("forge-select__option--highlighted");break;case"option":{e.className="forge-select__option",e.dataset.optionValue=t.option.value,t.option.className&&e.classList.add(...t.option.className.trim().split(/\s+/).filter(Boolean)),e.setAttribute("role","option");let s=this.selected.includes(t.option.value);if(e.setAttribute("aria-selected",String(s)),s&&e.classList.add("forge-select__option--selected"),this.opts.multiple&&t.hasChildren&&y(t.option,this.selected,this.isOptionDisabled)==="some"&&e.classList.add("forge-select__option--indeterminate"),t.depth>0&&(e.style.paddingLeft=`calc(12px + ${t.depth} * var(--fs-tree-indent, 18px))`),this.isOptionDisabled(t.option)||this.hasReachedMaximum()&&!this.selected.includes(t.option.value)?(e.classList.add("forge-select__option--disabled"),e.setAttribute("aria-disabled","true")):(e.id=`${this.uid}-nav-${t.navIndex}`,e.dataset.navIndex=String(t.navIndex),t.navIndex===this.highlightedIndex&&e.classList.add("forge-select__option--highlighted")),t.hasChildren){let i=this.query!==""||this.expandedValues.has(t.option.value);e.setAttribute("aria-expanded",String(i));let r=document.createElement("span");r.className="forge-select__twisty",r.dataset.twisty=t.option.value,r.setAttribute("aria-hidden","true"),r.textContent=i?"\u25BC":"\u25B6",e.append(r)}e.append(this.optionContent(t.option));break}}return e}optionContent(t){let e=this.rowContentCache.get(t.value);if(!e){let s=document.createElement("span");if(s.className="forge-select__option-content",x(s,t,this.opts.templateResult),this.rowContentCache.size>=K){let i=this.rowContentCache.keys().next().value;this.rowContentCache.delete(i)}this.rowContentCache.set(t.value,s),e=s}return e.cloneNode(!0)}moveHighlight(t){if(this.navItems.length===0)return;let e=this.highlightedIndex===-1?t>0?0:this.navItems.length-1:(this.highlightedIndex+t+this.navItems.length)%this.navItems.length;this.focusNavIndex(e)}focusNavIndex(t){if(this.highlightedIndex=t,this.usesVirtualScroll()){let e=this.rows.findIndex(s=>(s.kind==="option"||s.kind==="create")&&s.navIndex===t);if(e>=0){let s=this.opts.itemHeight,i=e*s,r=this.list.clientHeight||s*8,n=this.list.scrollTop;i<n?n=i:i+s>n+r&&(n=i+s-r),n!==this.list.scrollTop&&(this.list.scrollTop=n)}this.renderRows()}else this.renderRows(),this.list.querySelector(".forge-select__option--highlighted")?.scrollIntoView?.({block:"nearest"})}navigateTree(t){let e=this.navItems[this.highlightedIndex];if(!e||e.kind!=="option")return!1;let{option:s,parentValue:i}=e,r=!!s.children?.length,n=this.query!==""||this.expandedValues.has(s.value);if(t==="right"){if(r&&!n)return this.expandedValues.add(s.value),this.renderList(),!0;if(r){let a=this.navItems.findIndex(l=>l.kind==="option"&&l.parentValue===s.value);if(a>=0)return this.focusNavIndex(a),!0}return!1}if(r&&n&&this.query==="")return this.expandedValues.delete(s.value),this.renderList(),!0;if(i){let a=this.navItems.findIndex(l=>l.kind==="option"&&l.option.value===i);if(a>=0)return this.focusNavIndex(a),!0}return!1}updateActiveDescendant(){let t=this.searchInput??this.control;this.highlightedIndex>=0?t.setAttribute("aria-activedescendant",`${this.uid}-nav-${this.highlightedIndex}`):t.removeAttribute("aria-activedescendant")}scheduleRemoteLoad(t,e){this.ajaxTimer&&clearTimeout(this.ajaxTimer);let s=++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(t,{requestId:s})},e)}maybeLoadNextPage(){if(!this.opts.ajax?.pagination||!this.hasMore||this.loading||this.loadingMore)return;let{scrollHeight:e,scrollTop:s,clientHeight:i}=this.list,r=this.opts.itemHeight*2;e-s-i>=r||(this.loadingMore=!0,this.renderList(),this.loadRemote(this.query,{append:!0}))}async loadRemote(t,{append:e=!1,requestId:s}={}){let i=this.opts.ajax,r=s??++this.ajaxRequestId;if(r!==this.ajaxRequestId)return;this.ajaxController?.abort();let n=new AbortController;this.ajaxController=n;let a=e?this.page+1:0;try{let l;if(i.request)l=await i.request(t,a,n.signal);else{let d=R(i,t,a),c=await fetch(d,{signal:n.signal});if(c.ok===!1)throw new Error(`ForgeSelect: remote request failed with HTTP ${c.status}`);l=await c.json()}if(r!==this.ajaxRequestId||this.destroyed)return;let{options:h,hasMore:m}=k(i,l);if(e){let d=I(this.data);this.data=[...this.data,...h.filter(c=>!d.has(c.value))]}else this.data=h,this.rowContentCache.clear();this.page=a,this.hasMore=m,this.remoteLoaded=!0,this.loadError=null}catch(l){if(r!==this.ajaxRequestId||this.destroyed||n.signal.aborted)return;let h=l instanceof Error?l:new Error(String(l));e||(this.data=[],this.rowContentCache.clear()),this.hasMore=!1,this.loadError=h,this.emitter.emit("error",h)}finally{r===this.ajaxRequestId&&!this.destroyed&&(this.ajaxController=null,this.loading=!1,this.loadingMore=!1,this.isOpen&&this.renderList())}}};return U(W);})();
|
|
901
2
|
//# sourceMappingURL=index.global.js.map
|