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.js
CHANGED
|
@@ -30,19 +30,23 @@ var locales = {
|
|
|
30
30
|
noResults: "No results found",
|
|
31
31
|
loading: "Loading\u2026",
|
|
32
32
|
loadingMore: "Loading more\u2026",
|
|
33
|
+
errorLoading: "Could not load options",
|
|
33
34
|
createOption: 'Create "{query}"',
|
|
34
35
|
clearSelection: "Clear selection",
|
|
35
36
|
removeItem: "Remove {label}",
|
|
36
|
-
search: "Search"
|
|
37
|
+
search: "Search",
|
|
38
|
+
reorderHint: "{label}. Press Alt+Left or Alt+Right to reorder."
|
|
37
39
|
},
|
|
38
40
|
vi: {
|
|
39
41
|
noResults: "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3",
|
|
40
42
|
loading: "\u0110ang t\u1EA3i\u2026",
|
|
41
43
|
loadingMore: "\u0110ang t\u1EA3i th\xEAm\u2026",
|
|
44
|
+
errorLoading: "Kh\xF4ng th\u1EC3 t\u1EA3i t\xF9y ch\u1ECDn",
|
|
42
45
|
createOption: 'T\u1EA1o "{query}"',
|
|
43
46
|
clearSelection: "X\xF3a l\u1EF1a ch\u1ECDn",
|
|
44
47
|
removeItem: "X\xF3a {label}",
|
|
45
|
-
search: "T\xECm ki\u1EBFm"
|
|
48
|
+
search: "T\xECm ki\u1EBFm",
|
|
49
|
+
reorderHint: "{label}. Nh\u1EA5n Alt+Tr\xE1i ho\u1EB7c Alt+Ph\u1EA3i \u0111\u1EC3 s\u1EAFp x\u1EBFp l\u1EA1i."
|
|
46
50
|
}
|
|
47
51
|
};
|
|
48
52
|
function getStrings(language) {
|
|
@@ -55,12 +59,88 @@ function format(template, vars) {
|
|
|
55
59
|
return template.replace(/\{(\w+)\}/g, (match, key) => vars[key] ?? match);
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
// src/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// src/native-select.ts
|
|
63
|
+
function parseNativeOptions(select) {
|
|
64
|
+
const data = [];
|
|
65
|
+
for (const child of Array.from(select.children)) {
|
|
66
|
+
if (child instanceof HTMLOptGroupElement) {
|
|
67
|
+
data.push({ label: child.label, options: Array.from(child.querySelectorAll("option")).map(parseOption) });
|
|
68
|
+
} else if (child instanceof HTMLOptionElement) {
|
|
69
|
+
data.push(parseOption(child));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
function parseOption(option) {
|
|
75
|
+
const groupDisabled = option.parentElement instanceof HTMLOptGroupElement && option.parentElement.disabled;
|
|
76
|
+
return {
|
|
77
|
+
value: option.value,
|
|
78
|
+
label: option.textContent?.trim() ?? option.value,
|
|
79
|
+
disabled: option.disabled || groupDisabled || void 0
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/option-renderer.ts
|
|
84
|
+
function renderOptionContent(container, option, template, variant = "row") {
|
|
85
|
+
if (template) {
|
|
86
|
+
const result = template(option);
|
|
87
|
+
if (typeof result === "string") container.innerHTML = result;
|
|
88
|
+
else container.append(result);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!option.avatar && !option.description) {
|
|
92
|
+
container.textContent = option.label;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (option.avatar) {
|
|
96
|
+
const avatar = document.createElement("img");
|
|
97
|
+
avatar.className = variant === "row" ? "forge-select__option-avatar" : "forge-select__inline-avatar";
|
|
98
|
+
avatar.src = option.avatar;
|
|
99
|
+
avatar.alt = "";
|
|
100
|
+
avatar.setAttribute("loading", "lazy");
|
|
101
|
+
avatar.setAttribute("decoding", "async");
|
|
102
|
+
container.append(avatar);
|
|
103
|
+
}
|
|
104
|
+
if (variant === "row" && option.description) {
|
|
105
|
+
const body = document.createElement("span");
|
|
106
|
+
body.className = "forge-select__option-body";
|
|
107
|
+
const label = document.createElement("span");
|
|
108
|
+
label.className = "forge-select__option-label";
|
|
109
|
+
label.textContent = option.label;
|
|
110
|
+
const description = document.createElement("span");
|
|
111
|
+
description.className = "forge-select__option-desc";
|
|
112
|
+
description.textContent = option.description;
|
|
113
|
+
body.append(label, description);
|
|
114
|
+
container.append(body);
|
|
115
|
+
} else {
|
|
116
|
+
const label = document.createElement("span");
|
|
117
|
+
label.className = "forge-select__option-label";
|
|
118
|
+
label.textContent = option.label;
|
|
119
|
+
container.append(label);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/remote.ts
|
|
124
|
+
function buildUrl(ajax, query, page) {
|
|
125
|
+
if (typeof ajax.url === "function") return ajax.url(query, page);
|
|
126
|
+
if (!ajax.params) return ajax.url;
|
|
127
|
+
const params = new URLSearchParams();
|
|
128
|
+
for (const [key, value] of Object.entries(ajax.params(query, page))) params.set(key, String(value));
|
|
129
|
+
const separator = ajax.url.includes("?") ? "&" : "?";
|
|
130
|
+
return `${ajax.url}${separator}${params.toString()}`;
|
|
131
|
+
}
|
|
132
|
+
function normalizeRemoteResult(ajax, response) {
|
|
133
|
+
const result = ajax.transform ? ajax.transform(response) : response;
|
|
134
|
+
if (Array.isArray(result)) return { options: result, hasMore: false };
|
|
135
|
+
if (!result || !Array.isArray(result.options)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"ForgeSelect: ajax.transform must return an array of options, or an object shaped like { options: Option[], hasMore?: boolean }."
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return { options: result.options, hasMore: ajax.pagination ? Boolean(result.hasMore) : false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/selection.ts
|
|
64
144
|
function isGroup(item) {
|
|
65
145
|
return item.options !== void 0;
|
|
66
146
|
}
|
|
@@ -68,23 +148,69 @@ function collectDescendantValues(option) {
|
|
|
68
148
|
if (!option.children) return [];
|
|
69
149
|
const values = [];
|
|
70
150
|
for (const child of option.children) {
|
|
71
|
-
values.push(child.value
|
|
151
|
+
if (!child.disabled) values.push(child.value);
|
|
152
|
+
values.push(...collectDescendantValues(child));
|
|
72
153
|
}
|
|
73
154
|
return values;
|
|
74
155
|
}
|
|
75
156
|
function computeCheckState(option, selected) {
|
|
76
|
-
if (!option.children
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (states.every((
|
|
81
|
-
if (states.every((s) => s === "none")) return "none";
|
|
157
|
+
if (!option.children?.length) return selected.includes(option.value) ? "all" : "none";
|
|
158
|
+
const states = option.children.filter((child) => !child.disabled).map((child) => computeCheckState(child, selected));
|
|
159
|
+
if (states.length === 0) return "none";
|
|
160
|
+
if (states.every((state) => state === "all")) return "all";
|
|
161
|
+
if (states.every((state) => state === "none")) return "none";
|
|
82
162
|
return "some";
|
|
83
163
|
}
|
|
164
|
+
function findOption(items, value) {
|
|
165
|
+
const search = (options) => {
|
|
166
|
+
for (const option of options) {
|
|
167
|
+
if (option.value === value) return option;
|
|
168
|
+
const found = option.children ? search(option.children) : void 0;
|
|
169
|
+
if (found) return found;
|
|
170
|
+
}
|
|
171
|
+
return void 0;
|
|
172
|
+
};
|
|
173
|
+
for (const item of items) {
|
|
174
|
+
const found = search(isGroup(item) ? item.options : [item]);
|
|
175
|
+
if (found) return found;
|
|
176
|
+
}
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
function syncTreeAncestors(items, selected) {
|
|
180
|
+
const sync = (option) => {
|
|
181
|
+
if (!option.children?.length) return;
|
|
182
|
+
for (const child of option.children) sync(child);
|
|
183
|
+
const state = computeCheckState(option, selected);
|
|
184
|
+
const index = selected.indexOf(option.value);
|
|
185
|
+
if (state === "all" && index === -1) selected.push(option.value);
|
|
186
|
+
else if (state !== "all" && index !== -1) selected.splice(index, 1);
|
|
187
|
+
};
|
|
188
|
+
for (const item of items) (isGroup(item) ? item.options : [item]).forEach(sync);
|
|
189
|
+
}
|
|
190
|
+
function collectValues(items) {
|
|
191
|
+
const values = /* @__PURE__ */ new Set();
|
|
192
|
+
const visit = (option) => {
|
|
193
|
+
values.add(option.value);
|
|
194
|
+
option.children?.forEach(visit);
|
|
195
|
+
};
|
|
196
|
+
for (const item of items) (isGroup(item) ? item.options : [item]).forEach(visit);
|
|
197
|
+
return values;
|
|
198
|
+
}
|
|
199
|
+
function arraysEqual(a, b) {
|
|
200
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/ForgeSelect.ts
|
|
204
|
+
var DEFAULT_ITEM_HEIGHT = 36;
|
|
205
|
+
var VIRTUAL_BUFFER = 5;
|
|
206
|
+
var VIRTUAL_THRESHOLD = 100;
|
|
207
|
+
var ROW_CACHE_LIMIT = 2e3;
|
|
208
|
+
var uidCounter = 0;
|
|
84
209
|
var ForgeSelect = class {
|
|
85
210
|
constructor(target, options = {}) {
|
|
86
211
|
this.selected = [];
|
|
87
212
|
this.selectedOptions = /* @__PURE__ */ new Map();
|
|
213
|
+
this.suppressNextTagClick = false;
|
|
88
214
|
this.emitter = new Emitter();
|
|
89
215
|
this.uid = `forge-select-${++uidCounter}`;
|
|
90
216
|
this.searchInput = null;
|
|
@@ -103,24 +229,46 @@ var ForgeSelect = class {
|
|
|
103
229
|
this.hasMore = true;
|
|
104
230
|
this.ajaxTimer = null;
|
|
105
231
|
this.ajaxRequestId = 0;
|
|
232
|
+
this.ajaxController = null;
|
|
106
233
|
this.remoteLoaded = false;
|
|
234
|
+
this.loadError = null;
|
|
235
|
+
this.originalDisplay = "";
|
|
236
|
+
this.originalDisabled = false;
|
|
237
|
+
this.nativeSelect = null;
|
|
238
|
+
this.nativeForm = null;
|
|
239
|
+
this.syncingNative = false;
|
|
107
240
|
this.onDocumentMouseDown = (event) => {
|
|
108
241
|
if (!this.root.contains(event.target)) this.close();
|
|
109
242
|
};
|
|
243
|
+
this.onNativeChange = () => {
|
|
244
|
+
if (!this.nativeSelect || this.destroyed || this.syncingNative) return;
|
|
245
|
+
const values = Array.from(this.nativeSelect.selectedOptions, (option) => option.value);
|
|
246
|
+
this.applyNativeValues(values);
|
|
247
|
+
};
|
|
248
|
+
this.onFormReset = () => {
|
|
249
|
+
if (!this.nativeSelect || this.destroyed) return;
|
|
250
|
+
const defaults = Array.from(this.nativeSelect.options).filter((option) => option.defaultSelected).map((option) => option.value);
|
|
251
|
+
this.applyNativeValues(defaults);
|
|
252
|
+
};
|
|
110
253
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
111
254
|
if (!el) {
|
|
112
255
|
throw new Error(`ForgeSelect: target element not found: ${String(target)}`);
|
|
113
256
|
}
|
|
114
257
|
this.el = el;
|
|
115
258
|
const nativeSelect = el instanceof HTMLSelectElement ? el : null;
|
|
259
|
+
this.nativeSelect = nativeSelect;
|
|
260
|
+
this.nativeForm = nativeSelect?.form ?? null;
|
|
261
|
+
this.originalDisplay = el.style.display;
|
|
262
|
+
this.originalDisabled = nativeSelect?.disabled ?? false;
|
|
116
263
|
this.opts = {
|
|
117
264
|
placeholder: options.placeholder ?? "",
|
|
118
265
|
searchable: options.searchable ?? true,
|
|
119
266
|
multiple: options.multiple ?? nativeSelect?.multiple ?? false,
|
|
120
267
|
clearable: options.clearable ?? false,
|
|
121
268
|
allowCreate: options.allowCreate ?? false,
|
|
269
|
+
sortable: options.sortable ?? false,
|
|
122
270
|
theme: options.theme ?? "default",
|
|
123
|
-
disabled: options.disabled ?? false,
|
|
271
|
+
disabled: options.disabled ?? nativeSelect?.disabled ?? false,
|
|
124
272
|
data: options.data,
|
|
125
273
|
ajax: options.ajax,
|
|
126
274
|
templateResult: options.templateResult,
|
|
@@ -134,15 +282,26 @@ var ForgeSelect = class {
|
|
|
134
282
|
this.plugins = this.opts.plugins;
|
|
135
283
|
this.data = this.opts.data ?? (nativeSelect ? parseNativeOptions(nativeSelect) : []);
|
|
136
284
|
if (nativeSelect && !this.opts.data) {
|
|
137
|
-
|
|
138
|
-
|
|
285
|
+
const nativeOptions = Array.from(nativeSelect.options);
|
|
286
|
+
const hasIntentionalSelection = nativeSelect.multiple || nativeSelect.selectedIndex > 0 || nativeOptions.some((option) => option.defaultSelected);
|
|
287
|
+
for (const option of nativeOptions) {
|
|
288
|
+
if (hasIntentionalSelection && option.selected) this.selectValue(option.value, false);
|
|
139
289
|
}
|
|
140
290
|
}
|
|
141
291
|
this.buildDom();
|
|
142
292
|
this.renderValue();
|
|
143
293
|
if (this.opts.disabled) this.disable();
|
|
294
|
+
nativeSelect?.addEventListener("change", this.onNativeChange);
|
|
295
|
+
this.nativeForm?.addEventListener("reset", this.onFormReset);
|
|
144
296
|
for (const plugin of this.plugins) plugin.onInit?.(this);
|
|
145
297
|
}
|
|
298
|
+
applyNativeValues(values) {
|
|
299
|
+
this.selected = [];
|
|
300
|
+
for (const value of this.opts.multiple ? values : values.slice(0, 1)) this.selectValue(value, false);
|
|
301
|
+
this.renderValue();
|
|
302
|
+
if (this.isOpen) this.renderList();
|
|
303
|
+
this.emitter.emit("change", this.getValue());
|
|
304
|
+
}
|
|
146
305
|
// ---------------------------------------------------------------- public API
|
|
147
306
|
open() {
|
|
148
307
|
if (this.isOpen || this.isDisabled || this.destroyed) return;
|
|
@@ -180,28 +339,33 @@ var ForgeSelect = class {
|
|
|
180
339
|
for (const plugin of this.plugins) plugin.onDestroy?.(this);
|
|
181
340
|
this.destroyed = true;
|
|
182
341
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
342
|
+
this.ajaxController?.abort();
|
|
343
|
+
this.nativeSelect?.removeEventListener("change", this.onNativeChange);
|
|
344
|
+
this.nativeForm?.removeEventListener("reset", this.onFormReset);
|
|
183
345
|
this.rowContentCache.clear();
|
|
184
346
|
this.root.remove();
|
|
185
|
-
this.el.style.display =
|
|
347
|
+
this.el.style.display = this.originalDisplay;
|
|
348
|
+
if (this.nativeSelect) this.nativeSelect.disabled = this.originalDisabled;
|
|
186
349
|
this.emitter.clear();
|
|
187
350
|
}
|
|
188
351
|
getValue() {
|
|
189
352
|
if (this.opts.multiple) return [...this.selected];
|
|
190
353
|
return this.selected[0] ?? null;
|
|
191
354
|
}
|
|
192
|
-
setValue(value) {
|
|
355
|
+
setValue(value, options = {}) {
|
|
193
356
|
const values = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
194
357
|
const next = this.opts.multiple ? values : values.slice(0, 1);
|
|
195
358
|
if (arraysEqual(next, this.selected)) return;
|
|
196
359
|
this.selected = [];
|
|
197
360
|
for (const v of next) this.selectValue(v, false);
|
|
198
|
-
this.afterSelectionChange();
|
|
361
|
+
this.afterSelectionChange(options.emitChange ?? true);
|
|
199
362
|
}
|
|
200
363
|
enable() {
|
|
201
364
|
this.isDisabled = false;
|
|
202
365
|
this.root.classList.remove("forge-select--disabled");
|
|
203
366
|
this.control.tabIndex = 0;
|
|
204
367
|
this.control.setAttribute("aria-disabled", "false");
|
|
368
|
+
if (this.nativeSelect) this.nativeSelect.disabled = false;
|
|
205
369
|
}
|
|
206
370
|
disable() {
|
|
207
371
|
this.close();
|
|
@@ -209,6 +373,7 @@ var ForgeSelect = class {
|
|
|
209
373
|
this.root.classList.add("forge-select--disabled");
|
|
210
374
|
this.control.tabIndex = -1;
|
|
211
375
|
this.control.setAttribute("aria-disabled", "true");
|
|
376
|
+
if (this.nativeSelect) this.nativeSelect.disabled = true;
|
|
212
377
|
}
|
|
213
378
|
on(event, handler) {
|
|
214
379
|
this.emitter.on(event, handler);
|
|
@@ -217,11 +382,35 @@ var ForgeSelect = class {
|
|
|
217
382
|
this.emitter.off(event, handler);
|
|
218
383
|
}
|
|
219
384
|
// ---------------------------------------------------------------- DOM setup
|
|
385
|
+
/**
|
|
386
|
+
* The original target (a hidden native <select> or a plain mount div) can
|
|
387
|
+
* carry an accessible name via aria-label/aria-labelledby, or via a
|
|
388
|
+
* <label for> pointing at its id — but once `this.el` is display:none it
|
|
389
|
+
* drops out of the accessibility tree, so any such association silently
|
|
390
|
+
* stops reaching assistive tech unless we forward it onto the visible,
|
|
391
|
+
* interactive `this.control` ourselves.
|
|
392
|
+
*/
|
|
393
|
+
applyAccessibleName() {
|
|
394
|
+
const ariaLabelledby = this.el.getAttribute("aria-labelledby");
|
|
395
|
+
const ariaLabel = this.el.getAttribute("aria-label");
|
|
396
|
+
if (ariaLabelledby) {
|
|
397
|
+
this.control.setAttribute("aria-labelledby", ariaLabelledby);
|
|
398
|
+
} else if (ariaLabel) {
|
|
399
|
+
this.control.setAttribute("aria-label", ariaLabel);
|
|
400
|
+
} else if (this.el.id) {
|
|
401
|
+
const label = Array.from(document.getElementsByTagName("label")).find((el) => el.htmlFor === this.el.id);
|
|
402
|
+
if (label) {
|
|
403
|
+
if (!label.id) label.id = `${this.uid}-label`;
|
|
404
|
+
this.control.setAttribute("aria-labelledby", label.id);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
220
408
|
buildDom() {
|
|
221
409
|
this.root = document.createElement("div");
|
|
222
410
|
this.root.className = "forge-select";
|
|
223
411
|
this.root.dataset.theme = this.opts.theme;
|
|
224
412
|
this.root.style.setProperty("--fs-item-height", `${this.opts.itemHeight}px`);
|
|
413
|
+
if (this.opts.sortable && this.opts.multiple) this.root.classList.add("forge-select--sortable");
|
|
225
414
|
this.control = document.createElement("div");
|
|
226
415
|
this.control.className = "forge-select__control";
|
|
227
416
|
this.control.setAttribute("role", "combobox");
|
|
@@ -229,6 +418,7 @@ var ForgeSelect = class {
|
|
|
229
418
|
this.control.setAttribute("aria-expanded", "false");
|
|
230
419
|
this.control.setAttribute("aria-controls", `${this.uid}-list`);
|
|
231
420
|
this.control.tabIndex = 0;
|
|
421
|
+
this.applyAccessibleName();
|
|
232
422
|
this.valueEl = document.createElement("div");
|
|
233
423
|
this.valueEl.className = "forge-select__value";
|
|
234
424
|
this.clearBtn = document.createElement("button");
|
|
@@ -259,7 +449,11 @@ var ForgeSelect = class {
|
|
|
259
449
|
this.list.setAttribute("role", "listbox");
|
|
260
450
|
if (this.opts.multiple) this.list.setAttribute("aria-multiselectable", "true");
|
|
261
451
|
this.dropdown.append(this.list);
|
|
262
|
-
this.
|
|
452
|
+
this.liveRegion = document.createElement("div");
|
|
453
|
+
this.liveRegion.className = "forge-select__sr-only";
|
|
454
|
+
this.liveRegion.setAttribute("role", "status");
|
|
455
|
+
this.liveRegion.setAttribute("aria-live", "polite");
|
|
456
|
+
this.root.append(this.control, this.dropdown, this.liveRegion);
|
|
263
457
|
this.el.style.display = "none";
|
|
264
458
|
this.el.insertAdjacentElement("afterend", this.root);
|
|
265
459
|
this.bindEvents();
|
|
@@ -267,8 +461,13 @@ var ForgeSelect = class {
|
|
|
267
461
|
bindEvents() {
|
|
268
462
|
this.control.addEventListener("click", (event) => {
|
|
269
463
|
if (event.target === this.clearBtn) return;
|
|
464
|
+
if (this.suppressNextTagClick) {
|
|
465
|
+
this.suppressNextTagClick = false;
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
270
468
|
if (this.isDisabled) return;
|
|
271
|
-
this.isOpen
|
|
469
|
+
if (this.isOpen) this.close();
|
|
470
|
+
else this.open();
|
|
272
471
|
});
|
|
273
472
|
this.control.addEventListener("keydown", (event) => this.handleKeydown(event));
|
|
274
473
|
this.clearBtn.addEventListener("click", (event) => {
|
|
@@ -339,6 +538,12 @@ var ForgeSelect = class {
|
|
|
339
538
|
this.control.focus();
|
|
340
539
|
}
|
|
341
540
|
break;
|
|
541
|
+
case "ArrowRight":
|
|
542
|
+
if (this.isOpen && this.navigateTree("right")) event.preventDefault();
|
|
543
|
+
break;
|
|
544
|
+
case "ArrowLeft":
|
|
545
|
+
if (this.isOpen && this.navigateTree("left")) event.preventDefault();
|
|
546
|
+
break;
|
|
342
547
|
case "Tab":
|
|
343
548
|
this.close();
|
|
344
549
|
break;
|
|
@@ -383,18 +588,7 @@ var ForgeSelect = class {
|
|
|
383
588
|
* No-op for data with no `children` anywhere.
|
|
384
589
|
*/
|
|
385
590
|
syncTreeAncestors() {
|
|
386
|
-
|
|
387
|
-
if (!option.children || option.children.length === 0) return;
|
|
388
|
-
for (const child of option.children) sync(child);
|
|
389
|
-
const state = computeCheckState(option, this.selected);
|
|
390
|
-
const index = this.selected.indexOf(option.value);
|
|
391
|
-
if (state === "all" && index === -1) this.selected.push(option.value);
|
|
392
|
-
else if (state !== "all" && index !== -1) this.selected.splice(index, 1);
|
|
393
|
-
};
|
|
394
|
-
for (const item of this.data) {
|
|
395
|
-
const options = isGroup(item) ? item.options : [item];
|
|
396
|
-
options.forEach(sync);
|
|
397
|
-
}
|
|
591
|
+
syncTreeAncestors(this.data, this.selected);
|
|
398
592
|
}
|
|
399
593
|
clearSelection() {
|
|
400
594
|
if (this.selected.length === 0) return;
|
|
@@ -402,13 +596,13 @@ var ForgeSelect = class {
|
|
|
402
596
|
this.emitter.emit("clear");
|
|
403
597
|
this.afterSelectionChange();
|
|
404
598
|
}
|
|
405
|
-
afterSelectionChange() {
|
|
599
|
+
afterSelectionChange(emitChange = true) {
|
|
406
600
|
this.renderValue();
|
|
407
|
-
this.syncNativeSelect();
|
|
601
|
+
this.syncNativeSelect(emitChange);
|
|
408
602
|
if (this.isOpen) this.renderList();
|
|
409
|
-
this.emitter.emit("change", this.getValue());
|
|
603
|
+
if (emitChange) this.emitter.emit("change", this.getValue());
|
|
410
604
|
}
|
|
411
|
-
syncNativeSelect() {
|
|
605
|
+
syncNativeSelect(dispatchChange = true) {
|
|
412
606
|
if (!(this.el instanceof HTMLSelectElement)) return;
|
|
413
607
|
const existing = /* @__PURE__ */ new Set();
|
|
414
608
|
for (const option of Array.from(this.el.options)) {
|
|
@@ -423,24 +617,22 @@ var ForgeSelect = class {
|
|
|
423
617
|
option.selected = true;
|
|
424
618
|
this.el.append(option);
|
|
425
619
|
}
|
|
426
|
-
this.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
for (const option of options) {
|
|
431
|
-
if (option.value === value) return option;
|
|
432
|
-
if (option.children) {
|
|
433
|
-
const found = search(option.children);
|
|
434
|
-
if (found) return found;
|
|
435
|
-
}
|
|
620
|
+
if (this.opts.sortable && this.opts.multiple) {
|
|
621
|
+
for (const value of this.selected) {
|
|
622
|
+
const option = Array.from(this.el.options).find((o) => o.value === value);
|
|
623
|
+
if (option) this.el.append(option);
|
|
436
624
|
}
|
|
437
|
-
return void 0;
|
|
438
|
-
};
|
|
439
|
-
for (const item of this.data) {
|
|
440
|
-
const found = search(isGroup(item) ? item.options : [item]);
|
|
441
|
-
if (found) return found;
|
|
442
625
|
}
|
|
443
|
-
|
|
626
|
+
if (!dispatchChange) return;
|
|
627
|
+
this.syncingNative = true;
|
|
628
|
+
try {
|
|
629
|
+
this.el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
630
|
+
} finally {
|
|
631
|
+
this.syncingNative = false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
findOption(value) {
|
|
635
|
+
return findOption(this.data, value);
|
|
444
636
|
}
|
|
445
637
|
createFromQuery() {
|
|
446
638
|
const label = this.query.trim();
|
|
@@ -463,7 +655,8 @@ var ForgeSelect = class {
|
|
|
463
655
|
}
|
|
464
656
|
const { value } = item.option;
|
|
465
657
|
if (this.opts.multiple) {
|
|
466
|
-
this.selected.includes(value)
|
|
658
|
+
if (this.selected.includes(value)) this.deselectValue(value, true);
|
|
659
|
+
else this.selectValue(value, true);
|
|
467
660
|
} else {
|
|
468
661
|
this.selectValue(value, true);
|
|
469
662
|
this.close();
|
|
@@ -489,7 +682,7 @@ var ForgeSelect = class {
|
|
|
489
682
|
tag.className = "forge-select__tag";
|
|
490
683
|
const label = document.createElement("span");
|
|
491
684
|
label.className = "forge-select__tag-label";
|
|
492
|
-
|
|
685
|
+
renderOptionContent(label, option, this.opts.templateSelection, "inline");
|
|
493
686
|
const remove = document.createElement("button");
|
|
494
687
|
remove.type = "button";
|
|
495
688
|
remove.className = "forge-select__tag-remove";
|
|
@@ -500,6 +693,14 @@ var ForgeSelect = class {
|
|
|
500
693
|
if (!this.isDisabled) this.deselectValue(value, true);
|
|
501
694
|
});
|
|
502
695
|
tag.append(label, remove);
|
|
696
|
+
if (this.opts.sortable) {
|
|
697
|
+
tag.dataset.value = value;
|
|
698
|
+
tag.tabIndex = 0;
|
|
699
|
+
tag.setAttribute("aria-roledescription", "draggable item");
|
|
700
|
+
tag.setAttribute("aria-label", format(this.strings.reorderHint, { label: option.label }));
|
|
701
|
+
tag.addEventListener("keydown", (event) => this.handleTagKeydown(event, value));
|
|
702
|
+
this.bindTagDrag(tag, value);
|
|
703
|
+
}
|
|
503
704
|
this.valueEl.append(tag);
|
|
504
705
|
}
|
|
505
706
|
} else {
|
|
@@ -509,46 +710,99 @@ var ForgeSelect = class {
|
|
|
509
710
|
};
|
|
510
711
|
const span = document.createElement("span");
|
|
511
712
|
span.className = "forge-select__single-value";
|
|
512
|
-
|
|
713
|
+
renderOptionContent(span, option, this.opts.templateSelection, "inline");
|
|
513
714
|
this.valueEl.append(span);
|
|
514
715
|
}
|
|
515
716
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
717
|
+
/**
|
|
718
|
+
* Pointer-based (mouse/touch/pen) reorder for a single tag. Only the real
|
|
719
|
+
* dragged DOM node is moved during the gesture — a full renderValue()
|
|
720
|
+
* mid-drag would destroy it — so the reordered `this.selected` is only
|
|
721
|
+
* committed on release. The move/up listeners and pointer capture live on
|
|
722
|
+
* the stable `this.valueEl` container rather than the tag itself: `tag`
|
|
723
|
+
* gets repositioned via `insertBefore` during the drag, and browsers treat
|
|
724
|
+
* that reparenting as detaching the node, which silently drops pointer
|
|
725
|
+
* capture (and further move events) if it were captured on `tag`.
|
|
726
|
+
*/
|
|
727
|
+
bindTagDrag(tag, value) {
|
|
728
|
+
const DRAG_THRESHOLD = 4;
|
|
729
|
+
let startX = 0;
|
|
730
|
+
let dragging = false;
|
|
731
|
+
let order = [];
|
|
732
|
+
const onPointerMove = (event) => {
|
|
733
|
+
if (!dragging) {
|
|
734
|
+
if (Math.abs(event.clientX - startX) < DRAG_THRESHOLD) return;
|
|
735
|
+
dragging = true;
|
|
736
|
+
order = [...this.selected];
|
|
737
|
+
if (typeof this.valueEl.setPointerCapture === "function") {
|
|
738
|
+
this.valueEl.setPointerCapture(event.pointerId);
|
|
739
|
+
}
|
|
740
|
+
tag.classList.add("forge-select__tag--dragging");
|
|
741
|
+
}
|
|
742
|
+
event.preventDefault();
|
|
743
|
+
const draggedIndex = order.indexOf(value);
|
|
744
|
+
const siblings = Array.from(this.valueEl.querySelectorAll(".forge-select__tag"));
|
|
745
|
+
for (const sibling of siblings) {
|
|
746
|
+
if (sibling === tag) continue;
|
|
747
|
+
const siblingValue = sibling.dataset.value;
|
|
748
|
+
if (!siblingValue) continue;
|
|
749
|
+
const siblingIndex = order.indexOf(siblingValue);
|
|
750
|
+
if (siblingIndex === -1) continue;
|
|
751
|
+
const rect = sibling.getBoundingClientRect();
|
|
752
|
+
const midX = rect.left + rect.width / 2;
|
|
753
|
+
const movingRight = draggedIndex < siblingIndex;
|
|
754
|
+
const crossed = movingRight ? event.clientX > midX : event.clientX < midX;
|
|
755
|
+
if (!crossed) continue;
|
|
756
|
+
order.splice(draggedIndex, 1);
|
|
757
|
+
order.splice(siblingIndex, 0, value);
|
|
758
|
+
if (movingRight) this.valueEl.insertBefore(tag, sibling.nextSibling);
|
|
759
|
+
else this.valueEl.insertBefore(tag, sibling);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const finishDrag = (event) => {
|
|
764
|
+
this.valueEl.removeEventListener("pointermove", onPointerMove);
|
|
765
|
+
this.valueEl.removeEventListener("pointerup", finishDrag);
|
|
766
|
+
this.valueEl.removeEventListener("pointercancel", finishDrag);
|
|
767
|
+
if (!dragging) return;
|
|
768
|
+
if (typeof this.valueEl.releasePointerCapture === "function") {
|
|
769
|
+
this.valueEl.releasePointerCapture(event.pointerId);
|
|
770
|
+
}
|
|
771
|
+
tag.classList.remove("forge-select__tag--dragging");
|
|
772
|
+
this.selected = order;
|
|
773
|
+
this.suppressNextTagClick = true;
|
|
774
|
+
this.afterSelectionChange();
|
|
775
|
+
};
|
|
776
|
+
tag.addEventListener("pointerdown", (event) => {
|
|
777
|
+
if (this.isDisabled || event.button !== 0) return;
|
|
778
|
+
if (event.target.closest(".forge-select__tag-remove")) return;
|
|
779
|
+
startX = event.clientX;
|
|
780
|
+
dragging = false;
|
|
781
|
+
this.valueEl.addEventListener("pointermove", onPointerMove);
|
|
782
|
+
this.valueEl.addEventListener("pointerup", finishDrag);
|
|
783
|
+
this.valueEl.addEventListener("pointercancel", finishDrag);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/** Alt+Left/Alt+Right on a focused tag: the keyboard-operable equivalent of dragging. */
|
|
787
|
+
handleTagKeydown(event, value) {
|
|
788
|
+
if (!event.altKey || event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
789
|
+
const index = this.selected.indexOf(value);
|
|
790
|
+
const targetIndex = event.key === "ArrowLeft" ? index - 1 : index + 1;
|
|
791
|
+
if (index === -1 || targetIndex < 0 || targetIndex >= this.selected.length) return;
|
|
792
|
+
event.preventDefault();
|
|
793
|
+
event.stopPropagation();
|
|
794
|
+
const next = [...this.selected];
|
|
795
|
+
[next[index], next[targetIndex]] = [next[targetIndex], next[index]];
|
|
796
|
+
this.selected = next;
|
|
797
|
+
this.afterSelectionChange();
|
|
798
|
+
this.focusTagByValue(value);
|
|
799
|
+
}
|
|
800
|
+
focusTagByValue(value) {
|
|
801
|
+
for (const tag of Array.from(this.valueEl.querySelectorAll(".forge-select__tag"))) {
|
|
802
|
+
if (tag.dataset.value === value) {
|
|
803
|
+
tag.focus();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
552
806
|
}
|
|
553
807
|
}
|
|
554
808
|
buildRows() {
|
|
@@ -557,11 +811,11 @@ var ForgeSelect = class {
|
|
|
557
811
|
const query = this.query.trim().toLowerCase();
|
|
558
812
|
const matches = (option) => query === "" || option.label.toLowerCase().includes(query) || (option.description?.toLowerCase().includes(query) ?? false);
|
|
559
813
|
const subtreeMatches = (option) => query === "" || matches(option) || (option.children ?? []).some(subtreeMatches);
|
|
560
|
-
const pushOption = (option, depth) => {
|
|
814
|
+
const pushOption = (option, depth, parentValue) => {
|
|
561
815
|
let navIndex = -1;
|
|
562
816
|
if (!option.disabled) {
|
|
563
817
|
navIndex = this.navItems.length;
|
|
564
|
-
this.navItems.push({ kind: "option", option });
|
|
818
|
+
this.navItems.push({ kind: "option", option, parentValue });
|
|
565
819
|
}
|
|
566
820
|
const hasChildren = !!option.children && option.children.length > 0;
|
|
567
821
|
this.rows.push({ kind: "option", option, navIndex, depth, hasChildren });
|
|
@@ -569,7 +823,7 @@ var ForgeSelect = class {
|
|
|
569
823
|
const expanded = query !== "" || this.expandedValues.has(option.value);
|
|
570
824
|
if (expanded) {
|
|
571
825
|
for (const child of option.children) {
|
|
572
|
-
if (subtreeMatches(child)) pushOption(child, depth + 1);
|
|
826
|
+
if (subtreeMatches(child)) pushOption(child, depth + 1, option.value);
|
|
573
827
|
}
|
|
574
828
|
}
|
|
575
829
|
}
|
|
@@ -578,6 +832,10 @@ var ForgeSelect = class {
|
|
|
578
832
|
this.rows.push({ kind: "loading" });
|
|
579
833
|
return;
|
|
580
834
|
}
|
|
835
|
+
if (this.loadError) {
|
|
836
|
+
this.rows.push({ kind: "error" });
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
581
839
|
for (const item of this.data) {
|
|
582
840
|
if (isGroup(item)) {
|
|
583
841
|
const visible = item.options.filter(subtreeMatches);
|
|
@@ -610,6 +868,12 @@ var ForgeSelect = class {
|
|
|
610
868
|
renderList() {
|
|
611
869
|
this.buildRows();
|
|
612
870
|
this.renderRows();
|
|
871
|
+
this.announceStatus();
|
|
872
|
+
}
|
|
873
|
+
announceStatus() {
|
|
874
|
+
const first = this.rows[0];
|
|
875
|
+
const message = first?.kind === "loading" ? this.strings.loading : first?.kind === "error" ? this.strings.errorLoading : first?.kind === "empty" ? this.strings.noResults : "";
|
|
876
|
+
if (this.liveRegion.textContent !== message) this.liveRegion.textContent = message;
|
|
613
877
|
}
|
|
614
878
|
renderRows() {
|
|
615
879
|
const scrollTop = this.list.scrollTop;
|
|
@@ -654,10 +918,23 @@ var ForgeSelect = class {
|
|
|
654
918
|
break;
|
|
655
919
|
case "empty":
|
|
656
920
|
li.className = "forge-select__empty";
|
|
921
|
+
li.setAttribute("role", "option");
|
|
922
|
+
li.setAttribute("aria-disabled", "true");
|
|
923
|
+
li.setAttribute("aria-selected", "false");
|
|
657
924
|
li.textContent = this.strings.noResults;
|
|
658
925
|
break;
|
|
926
|
+
case "error":
|
|
927
|
+
li.className = "forge-select__error";
|
|
928
|
+
li.setAttribute("role", "option");
|
|
929
|
+
li.setAttribute("aria-disabled", "true");
|
|
930
|
+
li.setAttribute("aria-selected", "false");
|
|
931
|
+
li.textContent = this.strings.errorLoading;
|
|
932
|
+
break;
|
|
659
933
|
case "loading":
|
|
660
934
|
li.className = "forge-select__loading";
|
|
935
|
+
li.setAttribute("role", "option");
|
|
936
|
+
li.setAttribute("aria-disabled", "true");
|
|
937
|
+
li.setAttribute("aria-selected", "false");
|
|
661
938
|
li.textContent = this.strings.loading;
|
|
662
939
|
break;
|
|
663
940
|
case "loading-more":
|
|
@@ -694,11 +971,13 @@ var ForgeSelect = class {
|
|
|
694
971
|
if (row.navIndex === this.highlightedIndex) li.classList.add("forge-select__option--highlighted");
|
|
695
972
|
}
|
|
696
973
|
if (row.hasChildren) {
|
|
974
|
+
const expanded = this.query !== "" || this.expandedValues.has(row.option.value);
|
|
975
|
+
li.setAttribute("aria-expanded", String(expanded));
|
|
697
976
|
const twisty = document.createElement("span");
|
|
698
977
|
twisty.className = "forge-select__twisty";
|
|
699
978
|
twisty.dataset.twisty = row.option.value;
|
|
700
979
|
twisty.setAttribute("aria-hidden", "true");
|
|
701
|
-
twisty.textContent =
|
|
980
|
+
twisty.textContent = expanded ? "\u25BC" : "\u25B6";
|
|
702
981
|
li.append(twisty);
|
|
703
982
|
}
|
|
704
983
|
li.append(this.optionContent(row.option));
|
|
@@ -718,7 +997,7 @@ var ForgeSelect = class {
|
|
|
718
997
|
if (!cached) {
|
|
719
998
|
const holder = document.createElement("span");
|
|
720
999
|
holder.className = "forge-select__option-content";
|
|
721
|
-
|
|
1000
|
+
renderOptionContent(holder, option, this.opts.templateResult);
|
|
722
1001
|
if (this.rowContentCache.size >= ROW_CACHE_LIMIT) {
|
|
723
1002
|
const oldest = this.rowContentCache.keys().next().value;
|
|
724
1003
|
this.rowContentCache.delete(oldest);
|
|
@@ -730,7 +1009,10 @@ var ForgeSelect = class {
|
|
|
730
1009
|
}
|
|
731
1010
|
moveHighlight(delta) {
|
|
732
1011
|
if (this.navItems.length === 0) return;
|
|
733
|
-
const next = this.highlightedIndex === -1
|
|
1012
|
+
const next = this.highlightedIndex === -1 ? delta > 0 ? 0 : this.navItems.length - 1 : (this.highlightedIndex + delta + this.navItems.length) % this.navItems.length;
|
|
1013
|
+
this.focusNavIndex(next);
|
|
1014
|
+
}
|
|
1015
|
+
focusNavIndex(next) {
|
|
734
1016
|
this.highlightedIndex = next;
|
|
735
1017
|
if (this.usesVirtualScroll()) {
|
|
736
1018
|
const rowIndex = this.rows.findIndex(
|
|
@@ -752,6 +1034,41 @@ var ForgeSelect = class {
|
|
|
752
1034
|
highlighted?.scrollIntoView?.({ block: "nearest" });
|
|
753
1035
|
}
|
|
754
1036
|
}
|
|
1037
|
+
navigateTree(direction) {
|
|
1038
|
+
const item = this.navItems[this.highlightedIndex];
|
|
1039
|
+
if (!item || item.kind !== "option") return false;
|
|
1040
|
+
const { option, parentValue } = item;
|
|
1041
|
+
const hasChildren = !!option.children?.length;
|
|
1042
|
+
const expanded = this.query !== "" || this.expandedValues.has(option.value);
|
|
1043
|
+
if (direction === "right") {
|
|
1044
|
+
if (hasChildren && !expanded) {
|
|
1045
|
+
this.expandedValues.add(option.value);
|
|
1046
|
+
this.renderList();
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
if (hasChildren) {
|
|
1050
|
+
const childIndex = this.navItems.findIndex((nav) => nav.kind === "option" && nav.parentValue === option.value);
|
|
1051
|
+
if (childIndex >= 0) {
|
|
1052
|
+
this.focusNavIndex(childIndex);
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
if (hasChildren && expanded && this.query === "") {
|
|
1059
|
+
this.expandedValues.delete(option.value);
|
|
1060
|
+
this.renderList();
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
if (parentValue) {
|
|
1064
|
+
const parentIndex = this.navItems.findIndex((nav) => nav.kind === "option" && nav.option.value === parentValue);
|
|
1065
|
+
if (parentIndex >= 0) {
|
|
1066
|
+
this.focusNavIndex(parentIndex);
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
755
1072
|
updateActiveDescendant() {
|
|
756
1073
|
const target = this.searchInput ?? this.control;
|
|
757
1074
|
if (this.highlightedIndex >= 0) {
|
|
@@ -763,12 +1080,18 @@ var ForgeSelect = class {
|
|
|
763
1080
|
// ---------------------------------------------------------------- remote data
|
|
764
1081
|
scheduleRemoteLoad(query, delay) {
|
|
765
1082
|
if (this.ajaxTimer) clearTimeout(this.ajaxTimer);
|
|
1083
|
+
const requestId = ++this.ajaxRequestId;
|
|
1084
|
+
this.ajaxController?.abort();
|
|
1085
|
+
this.ajaxController = null;
|
|
766
1086
|
this.page = 0;
|
|
767
1087
|
this.hasMore = true;
|
|
768
1088
|
this.loading = true;
|
|
1089
|
+
this.loadingMore = false;
|
|
1090
|
+
this.loadError = null;
|
|
769
1091
|
this.renderList();
|
|
770
1092
|
this.ajaxTimer = setTimeout(() => {
|
|
771
|
-
|
|
1093
|
+
this.ajaxTimer = null;
|
|
1094
|
+
void this.loadRemote(query, { requestId });
|
|
772
1095
|
}, delay);
|
|
773
1096
|
}
|
|
774
1097
|
/**
|
|
@@ -786,18 +1109,21 @@ var ForgeSelect = class {
|
|
|
786
1109
|
this.renderList();
|
|
787
1110
|
void this.loadRemote(this.query, { append: true });
|
|
788
1111
|
}
|
|
789
|
-
async loadRemote(query, { append = false } = {}) {
|
|
1112
|
+
async loadRemote(query, { append = false, requestId } = {}) {
|
|
790
1113
|
const ajax = this.opts.ajax;
|
|
791
|
-
const
|
|
1114
|
+
const activeRequestId = requestId ?? ++this.ajaxRequestId;
|
|
1115
|
+
if (activeRequestId !== this.ajaxRequestId) return;
|
|
1116
|
+
this.ajaxController?.abort();
|
|
1117
|
+
const controller = new AbortController();
|
|
1118
|
+
this.ajaxController = controller;
|
|
792
1119
|
const page = append ? this.page + 1 : 0;
|
|
793
1120
|
try {
|
|
794
1121
|
const url = buildUrl(ajax, query, page);
|
|
795
|
-
const response = await fetch(url);
|
|
1122
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
1123
|
+
if (response.ok === false) throw new Error(`ForgeSelect: remote request failed with HTTP ${response.status}`);
|
|
796
1124
|
const json = await response.json();
|
|
797
|
-
if (
|
|
798
|
-
const
|
|
799
|
-
const options = Array.isArray(result) ? result : result.options;
|
|
800
|
-
const hasMore = ajax.pagination ? Array.isArray(result) ? false : result.hasMore : false;
|
|
1125
|
+
if (activeRequestId !== this.ajaxRequestId || this.destroyed) return;
|
|
1126
|
+
const { options, hasMore } = normalizeRemoteResult(ajax, json);
|
|
801
1127
|
if (append) {
|
|
802
1128
|
const existing = collectValues(this.data);
|
|
803
1129
|
this.data = [...this.data, ...options.filter((o) => !existing.has(o.value))];
|
|
@@ -808,15 +1134,20 @@ var ForgeSelect = class {
|
|
|
808
1134
|
this.page = page;
|
|
809
1135
|
this.hasMore = hasMore;
|
|
810
1136
|
this.remoteLoaded = true;
|
|
811
|
-
|
|
812
|
-
|
|
1137
|
+
this.loadError = null;
|
|
1138
|
+
} catch (cause) {
|
|
1139
|
+
if (activeRequestId !== this.ajaxRequestId || this.destroyed || controller.signal.aborted) return;
|
|
1140
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
813
1141
|
if (!append) {
|
|
814
1142
|
this.data = [];
|
|
815
1143
|
this.rowContentCache.clear();
|
|
816
1144
|
}
|
|
817
1145
|
this.hasMore = false;
|
|
1146
|
+
this.loadError = error;
|
|
1147
|
+
this.emitter.emit("error", error);
|
|
818
1148
|
} finally {
|
|
819
|
-
if (
|
|
1149
|
+
if (activeRequestId === this.ajaxRequestId && !this.destroyed) {
|
|
1150
|
+
this.ajaxController = null;
|
|
820
1151
|
this.loading = false;
|
|
821
1152
|
this.loadingMore = false;
|
|
822
1153
|
if (this.isOpen) this.renderList();
|
|
@@ -824,51 +1155,6 @@ var ForgeSelect = class {
|
|
|
824
1155
|
}
|
|
825
1156
|
}
|
|
826
1157
|
};
|
|
827
|
-
function parseNativeOptions(select) {
|
|
828
|
-
const data = [];
|
|
829
|
-
for (const child of Array.from(select.children)) {
|
|
830
|
-
if (child instanceof HTMLOptGroupElement) {
|
|
831
|
-
data.push({
|
|
832
|
-
label: child.label,
|
|
833
|
-
options: Array.from(child.querySelectorAll("option")).map(parseOption)
|
|
834
|
-
});
|
|
835
|
-
} else if (child instanceof HTMLOptionElement) {
|
|
836
|
-
data.push(parseOption(child));
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return data;
|
|
840
|
-
}
|
|
841
|
-
function parseOption(option) {
|
|
842
|
-
return {
|
|
843
|
-
value: option.value,
|
|
844
|
-
label: option.textContent?.trim() ?? option.value,
|
|
845
|
-
disabled: option.disabled || void 0
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
function buildUrl(ajax, query, page) {
|
|
849
|
-
if (typeof ajax.url === "function") return ajax.url(query);
|
|
850
|
-
if (!ajax.params) return ajax.url;
|
|
851
|
-
const params = new URLSearchParams();
|
|
852
|
-
for (const [key, value] of Object.entries(ajax.params(query, page))) {
|
|
853
|
-
params.set(key, String(value));
|
|
854
|
-
}
|
|
855
|
-
const separator = ajax.url.includes("?") ? "&" : "?";
|
|
856
|
-
return `${ajax.url}${separator}${params.toString()}`;
|
|
857
|
-
}
|
|
858
|
-
function collectValues(items) {
|
|
859
|
-
const values = /* @__PURE__ */ new Set();
|
|
860
|
-
for (const item of items) {
|
|
861
|
-
if (isGroup(item)) {
|
|
862
|
-
for (const option of item.options) values.add(option.value);
|
|
863
|
-
} else {
|
|
864
|
-
values.add(item.value);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
return values;
|
|
868
|
-
}
|
|
869
|
-
function arraysEqual(a, b) {
|
|
870
|
-
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
871
|
-
}
|
|
872
1158
|
export {
|
|
873
1159
|
ForgeSelect,
|
|
874
1160
|
ForgeSelect as default
|