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.cjs
CHANGED
|
@@ -57,19 +57,23 @@ var locales = {
|
|
|
57
57
|
noResults: "No results found",
|
|
58
58
|
loading: "Loading\u2026",
|
|
59
59
|
loadingMore: "Loading more\u2026",
|
|
60
|
+
errorLoading: "Could not load options",
|
|
60
61
|
createOption: 'Create "{query}"',
|
|
61
62
|
clearSelection: "Clear selection",
|
|
62
63
|
removeItem: "Remove {label}",
|
|
63
|
-
search: "Search"
|
|
64
|
+
search: "Search",
|
|
65
|
+
reorderHint: "{label}. Press Alt+Left or Alt+Right to reorder."
|
|
64
66
|
},
|
|
65
67
|
vi: {
|
|
66
68
|
noResults: "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3",
|
|
67
69
|
loading: "\u0110ang t\u1EA3i\u2026",
|
|
68
70
|
loadingMore: "\u0110ang t\u1EA3i th\xEAm\u2026",
|
|
71
|
+
errorLoading: "Kh\xF4ng th\u1EC3 t\u1EA3i t\xF9y ch\u1ECDn",
|
|
69
72
|
createOption: 'T\u1EA1o "{query}"',
|
|
70
73
|
clearSelection: "X\xF3a l\u1EF1a ch\u1ECDn",
|
|
71
74
|
removeItem: "X\xF3a {label}",
|
|
72
|
-
search: "T\xECm ki\u1EBFm"
|
|
75
|
+
search: "T\xECm ki\u1EBFm",
|
|
76
|
+
reorderHint: "{label}. Nh\u1EA5n Alt+Tr\xE1i ho\u1EB7c Alt+Ph\u1EA3i \u0111\u1EC3 s\u1EAFp x\u1EBFp l\u1EA1i."
|
|
73
77
|
}
|
|
74
78
|
};
|
|
75
79
|
function getStrings(language) {
|
|
@@ -82,12 +86,88 @@ function format(template, vars) {
|
|
|
82
86
|
return template.replace(/\{(\w+)\}/g, (match, key) => vars[key] ?? match);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
// src/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// src/native-select.ts
|
|
90
|
+
function parseNativeOptions(select) {
|
|
91
|
+
const data = [];
|
|
92
|
+
for (const child of Array.from(select.children)) {
|
|
93
|
+
if (child instanceof HTMLOptGroupElement) {
|
|
94
|
+
data.push({ label: child.label, options: Array.from(child.querySelectorAll("option")).map(parseOption) });
|
|
95
|
+
} else if (child instanceof HTMLOptionElement) {
|
|
96
|
+
data.push(parseOption(child));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
function parseOption(option) {
|
|
102
|
+
const groupDisabled = option.parentElement instanceof HTMLOptGroupElement && option.parentElement.disabled;
|
|
103
|
+
return {
|
|
104
|
+
value: option.value,
|
|
105
|
+
label: option.textContent?.trim() ?? option.value,
|
|
106
|
+
disabled: option.disabled || groupDisabled || void 0
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/option-renderer.ts
|
|
111
|
+
function renderOptionContent(container, option, template, variant = "row") {
|
|
112
|
+
if (template) {
|
|
113
|
+
const result = template(option);
|
|
114
|
+
if (typeof result === "string") container.innerHTML = result;
|
|
115
|
+
else container.append(result);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!option.avatar && !option.description) {
|
|
119
|
+
container.textContent = option.label;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (option.avatar) {
|
|
123
|
+
const avatar = document.createElement("img");
|
|
124
|
+
avatar.className = variant === "row" ? "forge-select__option-avatar" : "forge-select__inline-avatar";
|
|
125
|
+
avatar.src = option.avatar;
|
|
126
|
+
avatar.alt = "";
|
|
127
|
+
avatar.setAttribute("loading", "lazy");
|
|
128
|
+
avatar.setAttribute("decoding", "async");
|
|
129
|
+
container.append(avatar);
|
|
130
|
+
}
|
|
131
|
+
if (variant === "row" && option.description) {
|
|
132
|
+
const body = document.createElement("span");
|
|
133
|
+
body.className = "forge-select__option-body";
|
|
134
|
+
const label = document.createElement("span");
|
|
135
|
+
label.className = "forge-select__option-label";
|
|
136
|
+
label.textContent = option.label;
|
|
137
|
+
const description = document.createElement("span");
|
|
138
|
+
description.className = "forge-select__option-desc";
|
|
139
|
+
description.textContent = option.description;
|
|
140
|
+
body.append(label, description);
|
|
141
|
+
container.append(body);
|
|
142
|
+
} else {
|
|
143
|
+
const label = document.createElement("span");
|
|
144
|
+
label.className = "forge-select__option-label";
|
|
145
|
+
label.textContent = option.label;
|
|
146
|
+
container.append(label);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/remote.ts
|
|
151
|
+
function buildUrl(ajax, query, page) {
|
|
152
|
+
if (typeof ajax.url === "function") return ajax.url(query, page);
|
|
153
|
+
if (!ajax.params) return ajax.url;
|
|
154
|
+
const params = new URLSearchParams();
|
|
155
|
+
for (const [key, value] of Object.entries(ajax.params(query, page))) params.set(key, String(value));
|
|
156
|
+
const separator = ajax.url.includes("?") ? "&" : "?";
|
|
157
|
+
return `${ajax.url}${separator}${params.toString()}`;
|
|
158
|
+
}
|
|
159
|
+
function normalizeRemoteResult(ajax, response) {
|
|
160
|
+
const result = ajax.transform ? ajax.transform(response) : response;
|
|
161
|
+
if (Array.isArray(result)) return { options: result, hasMore: false };
|
|
162
|
+
if (!result || !Array.isArray(result.options)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"ForgeSelect: ajax.transform must return an array of options, or an object shaped like { options: Option[], hasMore?: boolean }."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return { options: result.options, hasMore: ajax.pagination ? Boolean(result.hasMore) : false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/selection.ts
|
|
91
171
|
function isGroup(item) {
|
|
92
172
|
return item.options !== void 0;
|
|
93
173
|
}
|
|
@@ -95,23 +175,69 @@ function collectDescendantValues(option) {
|
|
|
95
175
|
if (!option.children) return [];
|
|
96
176
|
const values = [];
|
|
97
177
|
for (const child of option.children) {
|
|
98
|
-
values.push(child.value
|
|
178
|
+
if (!child.disabled) values.push(child.value);
|
|
179
|
+
values.push(...collectDescendantValues(child));
|
|
99
180
|
}
|
|
100
181
|
return values;
|
|
101
182
|
}
|
|
102
183
|
function computeCheckState(option, selected) {
|
|
103
|
-
if (!option.children
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (states.every((
|
|
108
|
-
if (states.every((s) => s === "none")) return "none";
|
|
184
|
+
if (!option.children?.length) return selected.includes(option.value) ? "all" : "none";
|
|
185
|
+
const states = option.children.filter((child) => !child.disabled).map((child) => computeCheckState(child, selected));
|
|
186
|
+
if (states.length === 0) return "none";
|
|
187
|
+
if (states.every((state) => state === "all")) return "all";
|
|
188
|
+
if (states.every((state) => state === "none")) return "none";
|
|
109
189
|
return "some";
|
|
110
190
|
}
|
|
191
|
+
function findOption(items, value) {
|
|
192
|
+
const search = (options) => {
|
|
193
|
+
for (const option of options) {
|
|
194
|
+
if (option.value === value) return option;
|
|
195
|
+
const found = option.children ? search(option.children) : void 0;
|
|
196
|
+
if (found) return found;
|
|
197
|
+
}
|
|
198
|
+
return void 0;
|
|
199
|
+
};
|
|
200
|
+
for (const item of items) {
|
|
201
|
+
const found = search(isGroup(item) ? item.options : [item]);
|
|
202
|
+
if (found) return found;
|
|
203
|
+
}
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
function syncTreeAncestors(items, selected) {
|
|
207
|
+
const sync = (option) => {
|
|
208
|
+
if (!option.children?.length) return;
|
|
209
|
+
for (const child of option.children) sync(child);
|
|
210
|
+
const state = computeCheckState(option, selected);
|
|
211
|
+
const index = selected.indexOf(option.value);
|
|
212
|
+
if (state === "all" && index === -1) selected.push(option.value);
|
|
213
|
+
else if (state !== "all" && index !== -1) selected.splice(index, 1);
|
|
214
|
+
};
|
|
215
|
+
for (const item of items) (isGroup(item) ? item.options : [item]).forEach(sync);
|
|
216
|
+
}
|
|
217
|
+
function collectValues(items) {
|
|
218
|
+
const values = /* @__PURE__ */ new Set();
|
|
219
|
+
const visit = (option) => {
|
|
220
|
+
values.add(option.value);
|
|
221
|
+
option.children?.forEach(visit);
|
|
222
|
+
};
|
|
223
|
+
for (const item of items) (isGroup(item) ? item.options : [item]).forEach(visit);
|
|
224
|
+
return values;
|
|
225
|
+
}
|
|
226
|
+
function arraysEqual(a, b) {
|
|
227
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/ForgeSelect.ts
|
|
231
|
+
var DEFAULT_ITEM_HEIGHT = 36;
|
|
232
|
+
var VIRTUAL_BUFFER = 5;
|
|
233
|
+
var VIRTUAL_THRESHOLD = 100;
|
|
234
|
+
var ROW_CACHE_LIMIT = 2e3;
|
|
235
|
+
var uidCounter = 0;
|
|
111
236
|
var ForgeSelect = class {
|
|
112
237
|
constructor(target, options = {}) {
|
|
113
238
|
this.selected = [];
|
|
114
239
|
this.selectedOptions = /* @__PURE__ */ new Map();
|
|
240
|
+
this.suppressNextTagClick = false;
|
|
115
241
|
this.emitter = new Emitter();
|
|
116
242
|
this.uid = `forge-select-${++uidCounter}`;
|
|
117
243
|
this.searchInput = null;
|
|
@@ -130,24 +256,46 @@ var ForgeSelect = class {
|
|
|
130
256
|
this.hasMore = true;
|
|
131
257
|
this.ajaxTimer = null;
|
|
132
258
|
this.ajaxRequestId = 0;
|
|
259
|
+
this.ajaxController = null;
|
|
133
260
|
this.remoteLoaded = false;
|
|
261
|
+
this.loadError = null;
|
|
262
|
+
this.originalDisplay = "";
|
|
263
|
+
this.originalDisabled = false;
|
|
264
|
+
this.nativeSelect = null;
|
|
265
|
+
this.nativeForm = null;
|
|
266
|
+
this.syncingNative = false;
|
|
134
267
|
this.onDocumentMouseDown = (event) => {
|
|
135
268
|
if (!this.root.contains(event.target)) this.close();
|
|
136
269
|
};
|
|
270
|
+
this.onNativeChange = () => {
|
|
271
|
+
if (!this.nativeSelect || this.destroyed || this.syncingNative) return;
|
|
272
|
+
const values = Array.from(this.nativeSelect.selectedOptions, (option) => option.value);
|
|
273
|
+
this.applyNativeValues(values);
|
|
274
|
+
};
|
|
275
|
+
this.onFormReset = () => {
|
|
276
|
+
if (!this.nativeSelect || this.destroyed) return;
|
|
277
|
+
const defaults = Array.from(this.nativeSelect.options).filter((option) => option.defaultSelected).map((option) => option.value);
|
|
278
|
+
this.applyNativeValues(defaults);
|
|
279
|
+
};
|
|
137
280
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
138
281
|
if (!el) {
|
|
139
282
|
throw new Error(`ForgeSelect: target element not found: ${String(target)}`);
|
|
140
283
|
}
|
|
141
284
|
this.el = el;
|
|
142
285
|
const nativeSelect = el instanceof HTMLSelectElement ? el : null;
|
|
286
|
+
this.nativeSelect = nativeSelect;
|
|
287
|
+
this.nativeForm = nativeSelect?.form ?? null;
|
|
288
|
+
this.originalDisplay = el.style.display;
|
|
289
|
+
this.originalDisabled = nativeSelect?.disabled ?? false;
|
|
143
290
|
this.opts = {
|
|
144
291
|
placeholder: options.placeholder ?? "",
|
|
145
292
|
searchable: options.searchable ?? true,
|
|
146
293
|
multiple: options.multiple ?? nativeSelect?.multiple ?? false,
|
|
147
294
|
clearable: options.clearable ?? false,
|
|
148
295
|
allowCreate: options.allowCreate ?? false,
|
|
296
|
+
sortable: options.sortable ?? false,
|
|
149
297
|
theme: options.theme ?? "default",
|
|
150
|
-
disabled: options.disabled ?? false,
|
|
298
|
+
disabled: options.disabled ?? nativeSelect?.disabled ?? false,
|
|
151
299
|
data: options.data,
|
|
152
300
|
ajax: options.ajax,
|
|
153
301
|
templateResult: options.templateResult,
|
|
@@ -161,15 +309,26 @@ var ForgeSelect = class {
|
|
|
161
309
|
this.plugins = this.opts.plugins;
|
|
162
310
|
this.data = this.opts.data ?? (nativeSelect ? parseNativeOptions(nativeSelect) : []);
|
|
163
311
|
if (nativeSelect && !this.opts.data) {
|
|
164
|
-
|
|
165
|
-
|
|
312
|
+
const nativeOptions = Array.from(nativeSelect.options);
|
|
313
|
+
const hasIntentionalSelection = nativeSelect.multiple || nativeSelect.selectedIndex > 0 || nativeOptions.some((option) => option.defaultSelected);
|
|
314
|
+
for (const option of nativeOptions) {
|
|
315
|
+
if (hasIntentionalSelection && option.selected) this.selectValue(option.value, false);
|
|
166
316
|
}
|
|
167
317
|
}
|
|
168
318
|
this.buildDom();
|
|
169
319
|
this.renderValue();
|
|
170
320
|
if (this.opts.disabled) this.disable();
|
|
321
|
+
nativeSelect?.addEventListener("change", this.onNativeChange);
|
|
322
|
+
this.nativeForm?.addEventListener("reset", this.onFormReset);
|
|
171
323
|
for (const plugin of this.plugins) plugin.onInit?.(this);
|
|
172
324
|
}
|
|
325
|
+
applyNativeValues(values) {
|
|
326
|
+
this.selected = [];
|
|
327
|
+
for (const value of this.opts.multiple ? values : values.slice(0, 1)) this.selectValue(value, false);
|
|
328
|
+
this.renderValue();
|
|
329
|
+
if (this.isOpen) this.renderList();
|
|
330
|
+
this.emitter.emit("change", this.getValue());
|
|
331
|
+
}
|
|
173
332
|
// ---------------------------------------------------------------- public API
|
|
174
333
|
open() {
|
|
175
334
|
if (this.isOpen || this.isDisabled || this.destroyed) return;
|
|
@@ -207,28 +366,33 @@ var ForgeSelect = class {
|
|
|
207
366
|
for (const plugin of this.plugins) plugin.onDestroy?.(this);
|
|
208
367
|
this.destroyed = true;
|
|
209
368
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
369
|
+
this.ajaxController?.abort();
|
|
370
|
+
this.nativeSelect?.removeEventListener("change", this.onNativeChange);
|
|
371
|
+
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
210
372
|
this.rowContentCache.clear();
|
|
211
373
|
this.root.remove();
|
|
212
|
-
this.el.style.display =
|
|
374
|
+
this.el.style.display = this.originalDisplay;
|
|
375
|
+
if (this.nativeSelect) this.nativeSelect.disabled = this.originalDisabled;
|
|
213
376
|
this.emitter.clear();
|
|
214
377
|
}
|
|
215
378
|
getValue() {
|
|
216
379
|
if (this.opts.multiple) return [...this.selected];
|
|
217
380
|
return this.selected[0] ?? null;
|
|
218
381
|
}
|
|
219
|
-
setValue(value) {
|
|
382
|
+
setValue(value, options = {}) {
|
|
220
383
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
221
384
|
const next = this.opts.multiple ? values : values.slice(0, 1);
|
|
222
385
|
if (arraysEqual(next, this.selected)) return;
|
|
223
386
|
this.selected = [];
|
|
224
387
|
for (const v of next) this.selectValue(v, false);
|
|
225
|
-
this.afterSelectionChange();
|
|
388
|
+
this.afterSelectionChange(options.emitChange ?? true);
|
|
226
389
|
}
|
|
227
390
|
enable() {
|
|
228
391
|
this.isDisabled = false;
|
|
229
392
|
this.root.classList.remove("forge-select--disabled");
|
|
230
393
|
this.control.tabIndex = 0;
|
|
231
394
|
this.control.setAttribute("aria-disabled", "false");
|
|
395
|
+
if (this.nativeSelect) this.nativeSelect.disabled = false;
|
|
232
396
|
}
|
|
233
397
|
disable() {
|
|
234
398
|
this.close();
|
|
@@ -236,6 +400,7 @@ var ForgeSelect = class {
|
|
|
236
400
|
this.root.classList.add("forge-select--disabled");
|
|
237
401
|
this.control.tabIndex = -1;
|
|
238
402
|
this.control.setAttribute("aria-disabled", "true");
|
|
403
|
+
if (this.nativeSelect) this.nativeSelect.disabled = true;
|
|
239
404
|
}
|
|
240
405
|
on(event, handler) {
|
|
241
406
|
this.emitter.on(event, handler);
|
|
@@ -244,11 +409,35 @@ var ForgeSelect = class {
|
|
|
244
409
|
this.emitter.off(event, handler);
|
|
245
410
|
}
|
|
246
411
|
// ---------------------------------------------------------------- DOM setup
|
|
412
|
+
/**
|
|
413
|
+
* The original target (a hidden native <select> or a plain mount div) can
|
|
414
|
+
* carry an accessible name via aria-label/aria-labelledby, or via a
|
|
415
|
+
* <label for> pointing at its id — but once `this.el` is display:none it
|
|
416
|
+
* drops out of the accessibility tree, so any such association silently
|
|
417
|
+
* stops reaching assistive tech unless we forward it onto the visible,
|
|
418
|
+
* interactive `this.control` ourselves.
|
|
419
|
+
*/
|
|
420
|
+
applyAccessibleName() {
|
|
421
|
+
const ariaLabelledby = this.el.getAttribute("aria-labelledby");
|
|
422
|
+
const ariaLabel = this.el.getAttribute("aria-label");
|
|
423
|
+
if (ariaLabelledby) {
|
|
424
|
+
this.control.setAttribute("aria-labelledby", ariaLabelledby);
|
|
425
|
+
} else if (ariaLabel) {
|
|
426
|
+
this.control.setAttribute("aria-label", ariaLabel);
|
|
427
|
+
} else if (this.el.id) {
|
|
428
|
+
const label = Array.from(document.getElementsByTagName("label")).find((el) => el.htmlFor === this.el.id);
|
|
429
|
+
if (label) {
|
|
430
|
+
if (!label.id) label.id = `${this.uid}-label`;
|
|
431
|
+
this.control.setAttribute("aria-labelledby", label.id);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
247
435
|
buildDom() {
|
|
248
436
|
this.root = document.createElement("div");
|
|
249
437
|
this.root.className = "forge-select";
|
|
250
438
|
this.root.dataset.theme = this.opts.theme;
|
|
251
439
|
this.root.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
440
|
+
if (this.opts.sortable && this.opts.multiple) this.root.classList.add("forge-select--sortable");
|
|
252
441
|
this.control = document.createElement("div");
|
|
253
442
|
this.control.className = "forge-select__control";
|
|
254
443
|
this.control.setAttribute("role", "combobox");
|
|
@@ -256,6 +445,7 @@ var ForgeSelect = class {
|
|
|
256
445
|
this.control.setAttribute("aria-expanded", "false");
|
|
257
446
|
this.control.setAttribute("aria-controls", `${this.uid}-list`);
|
|
258
447
|
this.control.tabIndex = 0;
|
|
448
|
+
this.applyAccessibleName();
|
|
259
449
|
this.valueEl = document.createElement("div");
|
|
260
450
|
this.valueEl.className = "forge-select__value";
|
|
261
451
|
this.clearBtn = document.createElement("button");
|
|
@@ -286,7 +476,11 @@ var ForgeSelect = class {
|
|
|
286
476
|
this.list.setAttribute("role", "listbox");
|
|
287
477
|
if (this.opts.multiple) this.list.setAttribute("aria-multiselectable", "true");
|
|
288
478
|
this.dropdown.append(this.list);
|
|
289
|
-
this.
|
|
479
|
+
this.liveRegion = document.createElement("div");
|
|
480
|
+
this.liveRegion.className = "forge-select__sr-only";
|
|
481
|
+
this.liveRegion.setAttribute("role", "status");
|
|
482
|
+
this.liveRegion.setAttribute("aria-live", "polite");
|
|
483
|
+
this.root.append(this.control, this.dropdown, this.liveRegion);
|
|
290
484
|
this.el.style.display = "none";
|
|
291
485
|
this.el.insertAdjacentElement("afterend", this.root);
|
|
292
486
|
this.bindEvents();
|
|
@@ -294,8 +488,13 @@ var ForgeSelect = class {
|
|
|
294
488
|
bindEvents() {
|
|
295
489
|
this.control.addEventListener("click", (event) => {
|
|
296
490
|
if (event.target === this.clearBtn) return;
|
|
491
|
+
if (this.suppressNextTagClick) {
|
|
492
|
+
this.suppressNextTagClick = false;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
297
495
|
if (this.isDisabled) return;
|
|
298
|
-
this.isOpen
|
|
496
|
+
if (this.isOpen) this.close();
|
|
497
|
+
else this.open();
|
|
299
498
|
});
|
|
300
499
|
this.control.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
301
500
|
this.clearBtn.addEventListener("click", (event) => {
|
|
@@ -366,6 +565,12 @@ var ForgeSelect = class {
|
|
|
366
565
|
this.control.focus();
|
|
367
566
|
}
|
|
368
567
|
break;
|
|
568
|
+
case "ArrowRight":
|
|
569
|
+
if (this.isOpen && this.navigateTree("right")) event.preventDefault();
|
|
570
|
+
break;
|
|
571
|
+
case "ArrowLeft":
|
|
572
|
+
if (this.isOpen && this.navigateTree("left")) event.preventDefault();
|
|
573
|
+
break;
|
|
369
574
|
case "Tab":
|
|
370
575
|
this.close();
|
|
371
576
|
break;
|
|
@@ -410,18 +615,7 @@ var ForgeSelect = class {
|
|
|
410
615
|
* No-op for data with no `children` anywhere.
|
|
411
616
|
*/
|
|
412
617
|
syncTreeAncestors() {
|
|
413
|
-
|
|
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
|
-
}
|
|
618
|
+
syncTreeAncestors(this.data, this.selected);
|
|
425
619
|
}
|
|
426
620
|
clearSelection() {
|
|
427
621
|
if (this.selected.length === 0) return;
|
|
@@ -429,13 +623,13 @@ var ForgeSelect = class {
|
|
|
429
623
|
this.emitter.emit("clear");
|
|
430
624
|
this.afterSelectionChange();
|
|
431
625
|
}
|
|
432
|
-
afterSelectionChange() {
|
|
626
|
+
afterSelectionChange(emitChange = true) {
|
|
433
627
|
this.renderValue();
|
|
434
|
-
this.syncNativeSelect();
|
|
628
|
+
this.syncNativeSelect(emitChange);
|
|
435
629
|
if (this.isOpen) this.renderList();
|
|
436
|
-
this.emitter.emit("change", this.getValue());
|
|
630
|
+
if (emitChange) this.emitter.emit("change", this.getValue());
|
|
437
631
|
}
|
|
438
|
-
syncNativeSelect() {
|
|
632
|
+
syncNativeSelect(dispatchChange = true) {
|
|
439
633
|
if (!(this.el instanceof HTMLSelectElement)) return;
|
|
440
634
|
const existing = /* @__PURE__ */ new Set();
|
|
441
635
|
for (const option of Array.from(this.el.options)) {
|
|
@@ -450,24 +644,22 @@ var ForgeSelect = class {
|
|
|
450
644
|
option.selected = true;
|
|
451
645
|
this.el.append(option);
|
|
452
646
|
}
|
|
453
|
-
this.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
}
|
|
647
|
+
if (this.opts.sortable && this.opts.multiple) {
|
|
648
|
+
for (const value of this.selected) {
|
|
649
|
+
const option = Array.from(this.el.options).find((o) => o.value === value);
|
|
650
|
+
if (option) this.el.append(option);
|
|
463
651
|
}
|
|
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
652
|
}
|
|
470
|
-
|
|
653
|
+
if (!dispatchChange) return;
|
|
654
|
+
this.syncingNative = true;
|
|
655
|
+
try {
|
|
656
|
+
this.el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
657
|
+
} finally {
|
|
658
|
+
this.syncingNative = false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
findOption(value) {
|
|
662
|
+
return findOption(this.data, value);
|
|
471
663
|
}
|
|
472
664
|
createFromQuery() {
|
|
473
665
|
const label = this.query.trim();
|
|
@@ -490,7 +682,8 @@ var ForgeSelect = class {
|
|
|
490
682
|
}
|
|
491
683
|
const { value } = item.option;
|
|
492
684
|
if (this.opts.multiple) {
|
|
493
|
-
this.selected.includes(value)
|
|
685
|
+
if (this.selected.includes(value)) this.deselectValue(value, true);
|
|
686
|
+
else this.selectValue(value, true);
|
|
494
687
|
} else {
|
|
495
688
|
this.selectValue(value, true);
|
|
496
689
|
this.close();
|
|
@@ -516,7 +709,7 @@ var ForgeSelect = class {
|
|
|
516
709
|
tag.className = "forge-select__tag";
|
|
517
710
|
const label = document.createElement("span");
|
|
518
711
|
label.className = "forge-select__tag-label";
|
|
519
|
-
|
|
712
|
+
renderOptionContent(label, option, this.opts.templateSelection, "inline");
|
|
520
713
|
const remove = document.createElement("button");
|
|
521
714
|
remove.type = "button";
|
|
522
715
|
remove.className = "forge-select__tag-remove";
|
|
@@ -527,6 +720,14 @@ var ForgeSelect = class {
|
|
|
527
720
|
if (!this.isDisabled) this.deselectValue(value, true);
|
|
528
721
|
});
|
|
529
722
|
tag.append(label, remove);
|
|
723
|
+
if (this.opts.sortable) {
|
|
724
|
+
tag.dataset.value = value;
|
|
725
|
+
tag.tabIndex = 0;
|
|
726
|
+
tag.setAttribute("aria-roledescription", "draggable item");
|
|
727
|
+
tag.setAttribute("aria-label", format(this.strings.reorderHint, { label: option.label }));
|
|
728
|
+
tag.addEventListener("keydown", (event) => this.handleTagKeydown(event, value));
|
|
729
|
+
this.bindTagDrag(tag, value);
|
|
730
|
+
}
|
|
530
731
|
this.valueEl.append(tag);
|
|
531
732
|
}
|
|
532
733
|
} else {
|
|
@@ -536,46 +737,99 @@ var ForgeSelect = class {
|
|
|
536
737
|
};
|
|
537
738
|
const span = document.createElement("span");
|
|
538
739
|
span.className = "forge-select__single-value";
|
|
539
|
-
|
|
740
|
+
renderOptionContent(span, option, this.opts.templateSelection, "inline");
|
|
540
741
|
this.valueEl.append(span);
|
|
541
742
|
}
|
|
542
743
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Pointer-based (mouse/touch/pen) reorder for a single tag. Only the real
|
|
746
|
+
* dragged DOM node is moved during the gesture — a full renderValue()
|
|
747
|
+
* mid-drag would destroy it — so the reordered `this.selected` is only
|
|
748
|
+
* committed on release. The move/up listeners and pointer capture live on
|
|
749
|
+
* the stable `this.valueEl` container rather than the tag itself: `tag`
|
|
750
|
+
* gets repositioned via `insertBefore` during the drag, and browsers treat
|
|
751
|
+
* that reparenting as detaching the node, which silently drops pointer
|
|
752
|
+
* capture (and further move events) if it were captured on `tag`.
|
|
753
|
+
*/
|
|
754
|
+
bindTagDrag(tag, value) {
|
|
755
|
+
const DRAG_THRESHOLD = 4;
|
|
756
|
+
let startX = 0;
|
|
757
|
+
let dragging = false;
|
|
758
|
+
let order = [];
|
|
759
|
+
const onPointerMove = (event) => {
|
|
760
|
+
if (!dragging) {
|
|
761
|
+
if (Math.abs(event.clientX - startX) < DRAG_THRESHOLD) return;
|
|
762
|
+
dragging = true;
|
|
763
|
+
order = [...this.selected];
|
|
764
|
+
if (typeof this.valueEl.setPointerCapture === "function") {
|
|
765
|
+
this.valueEl.setPointerCapture(event.pointerId);
|
|
766
|
+
}
|
|
767
|
+
tag.classList.add("forge-select__tag--dragging");
|
|
768
|
+
}
|
|
769
|
+
event.preventDefault();
|
|
770
|
+
const draggedIndex = order.indexOf(value);
|
|
771
|
+
const siblings = Array.from(this.valueEl.querySelectorAll(".forge-select__tag"));
|
|
772
|
+
for (const sibling of siblings) {
|
|
773
|
+
if (sibling === tag) continue;
|
|
774
|
+
const siblingValue = sibling.dataset.value;
|
|
775
|
+
if (!siblingValue) continue;
|
|
776
|
+
const siblingIndex = order.indexOf(siblingValue);
|
|
777
|
+
if (siblingIndex === -1) continue;
|
|
778
|
+
const rect = sibling.getBoundingClientRect();
|
|
779
|
+
const midX = rect.left + rect.width / 2;
|
|
780
|
+
const movingRight = draggedIndex < siblingIndex;
|
|
781
|
+
const crossed = movingRight ? event.clientX > midX : event.clientX < midX;
|
|
782
|
+
if (!crossed) continue;
|
|
783
|
+
order.splice(draggedIndex, 1);
|
|
784
|
+
order.splice(siblingIndex, 0, value);
|
|
785
|
+
if (movingRight) this.valueEl.insertBefore(tag, sibling.nextSibling);
|
|
786
|
+
else this.valueEl.insertBefore(tag, sibling);
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
const finishDrag = (event) => {
|
|
791
|
+
this.valueEl.removeEventListener("pointermove", onPointerMove);
|
|
792
|
+
this.valueEl.removeEventListener("pointerup", finishDrag);
|
|
793
|
+
this.valueEl.removeEventListener("pointercancel", finishDrag);
|
|
794
|
+
if (!dragging) return;
|
|
795
|
+
if (typeof this.valueEl.releasePointerCapture === "function") {
|
|
796
|
+
this.valueEl.releasePointerCapture(event.pointerId);
|
|
797
|
+
}
|
|
798
|
+
tag.classList.remove("forge-select__tag--dragging");
|
|
799
|
+
this.selected = order;
|
|
800
|
+
this.suppressNextTagClick = true;
|
|
801
|
+
this.afterSelectionChange();
|
|
802
|
+
};
|
|
803
|
+
tag.addEventListener("pointerdown", (event) => {
|
|
804
|
+
if (this.isDisabled || event.button !== 0) return;
|
|
805
|
+
if (event.target.closest(".forge-select__tag-remove")) return;
|
|
806
|
+
startX = event.clientX;
|
|
807
|
+
dragging = false;
|
|
808
|
+
this.valueEl.addEventListener("pointermove", onPointerMove);
|
|
809
|
+
this.valueEl.addEventListener("pointerup", finishDrag);
|
|
810
|
+
this.valueEl.addEventListener("pointercancel", finishDrag);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/** Alt+Left/Alt+Right on a focused tag: the keyboard-operable equivalent of dragging. */
|
|
814
|
+
handleTagKeydown(event, value) {
|
|
815
|
+
if (!event.altKey || event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
816
|
+
const index = this.selected.indexOf(value);
|
|
817
|
+
const targetIndex = event.key === "ArrowLeft" ? index - 1 : index + 1;
|
|
818
|
+
if (index === -1 || targetIndex < 0 || targetIndex >= this.selected.length) return;
|
|
819
|
+
event.preventDefault();
|
|
820
|
+
event.stopPropagation();
|
|
821
|
+
const next = [...this.selected];
|
|
822
|
+
[next[index], next[targetIndex]] = [next[targetIndex], next[index]];
|
|
823
|
+
this.selected = next;
|
|
824
|
+
this.afterSelectionChange();
|
|
825
|
+
this.focusTagByValue(value);
|
|
826
|
+
}
|
|
827
|
+
focusTagByValue(value) {
|
|
828
|
+
for (const tag of Array.from(this.valueEl.querySelectorAll(".forge-select__tag"))) {
|
|
829
|
+
if (tag.dataset.value === value) {
|
|
830
|
+
tag.focus();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
579
833
|
}
|
|
580
834
|
}
|
|
581
835
|
buildRows() {
|
|
@@ -584,11 +838,11 @@ var ForgeSelect = class {
|
|
|
584
838
|
const query = this.query.trim().toLowerCase();
|
|
585
839
|
const matches = (option) => query === "" || option.label.toLowerCase().includes(query) || (option.description?.toLowerCase().includes(query) ?? false);
|
|
586
840
|
const subtreeMatches = (option) => query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
587
|
-
const pushOption = (option, depth) => {
|
|
841
|
+
const pushOption = (option, depth, parentValue) => {
|
|
588
842
|
let navIndex = -1;
|
|
589
843
|
if (!option.disabled) {
|
|
590
844
|
navIndex = this.navItems.length;
|
|
591
|
-
this.navItems.push({ kind: "option", option });
|
|
845
|
+
this.navItems.push({ kind: "option", option, parentValue });
|
|
592
846
|
}
|
|
593
847
|
const hasChildren = !!option.children && option.children.length > 0;
|
|
594
848
|
this.rows.push({ kind: "option", option, navIndex, depth, hasChildren });
|
|
@@ -596,7 +850,7 @@ var ForgeSelect = class {
|
|
|
596
850
|
const expanded = query !== "" || this.expandedValues.has(option.value);
|
|
597
851
|
if (expanded) {
|
|
598
852
|
for (const child of option.children) {
|
|
599
|
-
if (subtreeMatches(child)) pushOption(child, depth + 1);
|
|
853
|
+
if (subtreeMatches(child)) pushOption(child, depth + 1, option.value);
|
|
600
854
|
}
|
|
601
855
|
}
|
|
602
856
|
}
|
|
@@ -605,6 +859,10 @@ var ForgeSelect = class {
|
|
|
605
859
|
this.rows.push({ kind: "loading" });
|
|
606
860
|
return;
|
|
607
861
|
}
|
|
862
|
+
if (this.loadError) {
|
|
863
|
+
this.rows.push({ kind: "error" });
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
608
866
|
for (const item of this.data) {
|
|
609
867
|
if (isGroup(item)) {
|
|
610
868
|
const visible = item.options.filter(subtreeMatches);
|
|
@@ -637,6 +895,12 @@ var ForgeSelect = class {
|
|
|
637
895
|
renderList() {
|
|
638
896
|
this.buildRows();
|
|
639
897
|
this.renderRows();
|
|
898
|
+
this.announceStatus();
|
|
899
|
+
}
|
|
900
|
+
announceStatus() {
|
|
901
|
+
const first = this.rows[0];
|
|
902
|
+
const message = first?.kind === "loading" ? this.strings.loading : first?.kind === "error" ? this.strings.errorLoading : first?.kind === "empty" ? this.strings.noResults : "";
|
|
903
|
+
if (this.liveRegion.textContent !== message) this.liveRegion.textContent = message;
|
|
640
904
|
}
|
|
641
905
|
renderRows() {
|
|
642
906
|
const scrollTop = this.list.scrollTop;
|
|
@@ -681,10 +945,23 @@ var ForgeSelect = class {
|
|
|
681
945
|
break;
|
|
682
946
|
case "empty":
|
|
683
947
|
li.className = "forge-select__empty";
|
|
948
|
+
li.setAttribute("role", "option");
|
|
949
|
+
li.setAttribute("aria-disabled", "true");
|
|
950
|
+
li.setAttribute("aria-selected", "false");
|
|
684
951
|
li.textContent = this.strings.noResults;
|
|
685
952
|
break;
|
|
953
|
+
case "error":
|
|
954
|
+
li.className = "forge-select__error";
|
|
955
|
+
li.setAttribute("role", "option");
|
|
956
|
+
li.setAttribute("aria-disabled", "true");
|
|
957
|
+
li.setAttribute("aria-selected", "false");
|
|
958
|
+
li.textContent = this.strings.errorLoading;
|
|
959
|
+
break;
|
|
686
960
|
case "loading":
|
|
687
961
|
li.className = "forge-select__loading";
|
|
962
|
+
li.setAttribute("role", "option");
|
|
963
|
+
li.setAttribute("aria-disabled", "true");
|
|
964
|
+
li.setAttribute("aria-selected", "false");
|
|
688
965
|
li.textContent = this.strings.loading;
|
|
689
966
|
break;
|
|
690
967
|
case "loading-more":
|
|
@@ -721,11 +998,13 @@ var ForgeSelect = class {
|
|
|
721
998
|
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
722
999
|
}
|
|
723
1000
|
if (row.hasChildren) {
|
|
1001
|
+
const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
|
|
1002
|
+
li.setAttribute("aria-expanded", String(expanded));
|
|
724
1003
|
const twisty = document.createElement("span");
|
|
725
1004
|
twisty.className = "forge-select__twisty";
|
|
726
1005
|
twisty.dataset.twisty = row.option.value;
|
|
727
1006
|
twisty.setAttribute("aria-hidden", "true");
|
|
728
|
-
twisty.textContent =
|
|
1007
|
+
twisty.textContent = expanded ? "\u25BC" : "\u25B6";
|
|
729
1008
|
li.append(twisty);
|
|
730
1009
|
}
|
|
731
1010
|
li.append(this.optionContent(row.option));
|
|
@@ -745,7 +1024,7 @@ var ForgeSelect = class {
|
|
|
745
1024
|
if (!cached) {
|
|
746
1025
|
const holder = document.createElement("span");
|
|
747
1026
|
holder.className = "forge-select__option-content";
|
|
748
|
-
|
|
1027
|
+
renderOptionContent(holder, option, this.opts.templateResult);
|
|
749
1028
|
if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
|
|
750
1029
|
const oldest = this.rowContentCache.keys().next().value;
|
|
751
1030
|
this.rowContentCache.delete(oldest);
|
|
@@ -757,7 +1036,10 @@ var ForgeSelect = class {
|
|
|
757
1036
|
}
|
|
758
1037
|
moveHighlight(delta) {
|
|
759
1038
|
if (this.navItems.length === 0) return;
|
|
760
|
-
const next = this.highlightedIndex === -1
|
|
1039
|
+
const next = this.highlightedIndex === -1 ? delta > 0 ? 0 : this.navItems.length - 1 : (this.highlightedIndex + delta + this.navItems.length) % this.navItems.length;
|
|
1040
|
+
this.focusNavIndex(next);
|
|
1041
|
+
}
|
|
1042
|
+
focusNavIndex(next) {
|
|
761
1043
|
this.highlightedIndex = next;
|
|
762
1044
|
if (this.usesVirtualScroll()) {
|
|
763
1045
|
const rowIndex = this.rows.findIndex(
|
|
@@ -779,6 +1061,41 @@ var ForgeSelect = class {
|
|
|
779
1061
|
highlighted?.scrollIntoView?.({ block: "nearest" });
|
|
780
1062
|
}
|
|
781
1063
|
}
|
|
1064
|
+
navigateTree(direction) {
|
|
1065
|
+
const item = this.navItems[this.highlightedIndex];
|
|
1066
|
+
if (!item || item.kind !== "option") return false;
|
|
1067
|
+
const { option, parentValue } = item;
|
|
1068
|
+
const hasChildren = !!option.children?.length;
|
|
1069
|
+
const expanded = this.query !== "" || this.expandedValues.has(option.value);
|
|
1070
|
+
if (direction === "right") {
|
|
1071
|
+
if (hasChildren && !expanded) {
|
|
1072
|
+
this.expandedValues.add(option.value);
|
|
1073
|
+
this.renderList();
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
if (hasChildren) {
|
|
1077
|
+
const childIndex = this.navItems.findIndex((nav) => nav.kind === "option" && nav.parentValue === option.value);
|
|
1078
|
+
if (childIndex >= 0) {
|
|
1079
|
+
this.focusNavIndex(childIndex);
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
if (hasChildren && expanded && this.query === "") {
|
|
1086
|
+
this.expandedValues.delete(option.value);
|
|
1087
|
+
this.renderList();
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
if (parentValue) {
|
|
1091
|
+
const parentIndex = this.navItems.findIndex((nav) => nav.kind === "option" && nav.option.value === parentValue);
|
|
1092
|
+
if (parentIndex >= 0) {
|
|
1093
|
+
this.focusNavIndex(parentIndex);
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
782
1099
|
updateActiveDescendant() {
|
|
783
1100
|
const target = this.searchInput ?? this.control;
|
|
784
1101
|
if (this.highlightedIndex >= 0) {
|
|
@@ -790,12 +1107,18 @@ var ForgeSelect = class {
|
|
|
790
1107
|
// ---------------------------------------------------------------- remote data
|
|
791
1108
|
scheduleRemoteLoad(query, delay) {
|
|
792
1109
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
1110
|
+
const requestId = ++this.ajaxRequestId;
|
|
1111
|
+
this.ajaxController?.abort();
|
|
1112
|
+
this.ajaxController = null;
|
|
793
1113
|
this.page = 0;
|
|
794
1114
|
this.hasMore = true;
|
|
795
1115
|
this.loading = true;
|
|
1116
|
+
this.loadingMore = false;
|
|
1117
|
+
this.loadError = null;
|
|
796
1118
|
this.renderList();
|
|
797
1119
|
this.ajaxTimer = setTimeout(() => {
|
|
798
|
-
|
|
1120
|
+
this.ajaxTimer = null;
|
|
1121
|
+
void this.loadRemote(query, { requestId });
|
|
799
1122
|
}, delay);
|
|
800
1123
|
}
|
|
801
1124
|
/**
|
|
@@ -813,18 +1136,21 @@ var ForgeSelect = class {
|
|
|
813
1136
|
this.renderList();
|
|
814
1137
|
void this.loadRemote(this.query, { append: true });
|
|
815
1138
|
}
|
|
816
|
-
async loadRemote(query, { append = false } = {}) {
|
|
1139
|
+
async loadRemote(query, { append = false, requestId } = {}) {
|
|
817
1140
|
const ajax = this.opts.ajax;
|
|
818
|
-
const
|
|
1141
|
+
const activeRequestId = requestId ?? ++this.ajaxRequestId;
|
|
1142
|
+
if (activeRequestId !== this.ajaxRequestId) return;
|
|
1143
|
+
this.ajaxController?.abort();
|
|
1144
|
+
const controller = new AbortController();
|
|
1145
|
+
this.ajaxController = controller;
|
|
819
1146
|
const page = append ? this.page + 1 : 0;
|
|
820
1147
|
try {
|
|
821
1148
|
const url = buildUrl(ajax, query, page);
|
|
822
|
-
const response = await fetch(url);
|
|
1149
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
1150
|
+
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
823
1151
|
const json = await response.json();
|
|
824
|
-
if (
|
|
825
|
-
const
|
|
826
|
-
const options = Array.isArray(result) ? result : result.options;
|
|
827
|
-
const hasMore = ajax.pagination ? Array.isArray(result) ? false : result.hasMore : false;
|
|
1152
|
+
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
1153
|
+
const { options, hasMore } = normalizeRemoteResult(ajax, json);
|
|
828
1154
|
if (append) {
|
|
829
1155
|
const existing = collectValues(this.data);
|
|
830
1156
|
this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
|
|
@@ -835,15 +1161,20 @@ var ForgeSelect = class {
|
|
|
835
1161
|
this.page = page;
|
|
836
1162
|
this.hasMore = hasMore;
|
|
837
1163
|
this.remoteLoaded = true;
|
|
838
|
-
|
|
839
|
-
|
|
1164
|
+
this.loadError = null;
|
|
1165
|
+
} catch (cause) {
|
|
1166
|
+
if (activeRequestId !== this.ajaxRequestId || this.destroyed || controller.signal.aborted) return;
|
|
1167
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
840
1168
|
if (!append) {
|
|
841
1169
|
this.data = [];
|
|
842
1170
|
this.rowContentCache.clear();
|
|
843
1171
|
}
|
|
844
1172
|
this.hasMore = false;
|
|
1173
|
+
this.loadError = error;
|
|
1174
|
+
this.emitter.emit("error", error);
|
|
845
1175
|
} finally {
|
|
846
|
-
if (
|
|
1176
|
+
if (activeRequestId === this.ajaxRequestId && !this.destroyed) {
|
|
1177
|
+
this.ajaxController = null;
|
|
847
1178
|
this.loading = false;
|
|
848
1179
|
this.loadingMore = false;
|
|
849
1180
|
if (this.isOpen) this.renderList();
|
|
@@ -851,51 +1182,6 @@ var ForgeSelect = class {
|
|
|
851
1182
|
}
|
|
852
1183
|
}
|
|
853
1184
|
};
|
|
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
1185
|
// Annotate the CommonJS export names for ESM import in node:
|
|
900
1186
|
0 && (module.exports = {
|
|
901
1187
|
ForgeSelect
|