@selkit/dom 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cluion
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,685 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Selkit: () => SelkitDom,
24
+ SelkitDom: () => SelkitDom,
25
+ attachPositioner: () => attachPositioner,
26
+ computePosition: () => computePosition,
27
+ createSelkitDom: () => createSelkitDom,
28
+ sk: () => createSelkitDom
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/dom.ts
33
+ var import_core = require("@selkit/core");
34
+
35
+ // src/positioner.ts
36
+ function computePosition(triggerRect, dropdownHeight, viewportHeight, gap = 4) {
37
+ const spaceBelow = viewportHeight - triggerRect.bottom;
38
+ const spaceAbove = triggerRect.top;
39
+ const placement = spaceBelow < dropdownHeight && spaceAbove > spaceBelow ? "top" : "bottom";
40
+ const top = placement === "bottom" ? triggerRect.bottom + gap : triggerRect.top - gap - dropdownHeight;
41
+ return { placement, top, left: triggerRect.left, width: triggerRect.width };
42
+ }
43
+ function attachPositioner(trigger, dropdown, autoWidth = false) {
44
+ const update = () => {
45
+ const rect = trigger.getBoundingClientRect();
46
+ const pos = computePosition(rect, dropdown.offsetHeight, window.innerHeight);
47
+ dropdown.style.position = "fixed";
48
+ dropdown.style.top = `${pos.top}px`;
49
+ dropdown.style.left = `${pos.left}px`;
50
+ if (autoWidth) {
51
+ dropdown.style.minWidth = `${pos.width}px`;
52
+ dropdown.style.width = "max-content";
53
+ } else {
54
+ dropdown.style.width = `${pos.width}px`;
55
+ }
56
+ dropdown.dataset.placement = pos.placement;
57
+ };
58
+ const onScroll = () => update();
59
+ const onResize = () => update();
60
+ window.addEventListener("scroll", onScroll, true);
61
+ window.addEventListener("resize", onResize);
62
+ update();
63
+ return {
64
+ update,
65
+ destroy() {
66
+ window.removeEventListener("scroll", onScroll, true);
67
+ window.removeEventListener("resize", onResize);
68
+ }
69
+ };
70
+ }
71
+
72
+ // src/dom.ts
73
+ var LOAD_MORE_THRESHOLD = 32;
74
+ var DEFAULT_ITEM_HEIGHT = 36;
75
+ var SR_ONLY_CSS = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0";
76
+ function resolveParent(parent) {
77
+ if (!parent) return null;
78
+ if (typeof parent !== "string") return parent;
79
+ const el = document.querySelector(parent);
80
+ if (!el) throw new Error(`[selkit] \u627E\u4E0D\u5230 dropdownParent ${parent}`);
81
+ return el;
82
+ }
83
+ function parseSelectElement(select) {
84
+ const options = [];
85
+ const selectedValues = [];
86
+ let placeholder = select.dataset.placeholder;
87
+ const readOption = (o) => {
88
+ if (o.value === "") {
89
+ if (!placeholder) placeholder = o.textContent?.trim() || void 0;
90
+ return null;
91
+ }
92
+ if (o.selected) selectedValues.push(o.value);
93
+ return {
94
+ value: o.value,
95
+ label: (o.label || o.textContent || o.value).trim(),
96
+ ...o.disabled ? { disabled: true } : {}
97
+ };
98
+ };
99
+ for (const child of Array.from(select.children)) {
100
+ if (child instanceof HTMLOptGroupElement) {
101
+ const opts = [];
102
+ for (const o of Array.from(child.children)) {
103
+ if (o instanceof HTMLOptionElement) {
104
+ const parsed = readOption(o);
105
+ if (parsed) opts.push(parsed);
106
+ }
107
+ }
108
+ if (opts.length) {
109
+ options.push({
110
+ label: child.label,
111
+ ...child.disabled ? { disabled: true } : {},
112
+ options: opts
113
+ });
114
+ }
115
+ } else if (child instanceof HTMLOptionElement) {
116
+ const parsed = readOption(child);
117
+ if (parsed) options.push(parsed);
118
+ }
119
+ }
120
+ const value = select.multiple ? selectedValues : selectedValues[0] ?? null;
121
+ return {
122
+ options,
123
+ value,
124
+ multiple: select.multiple,
125
+ disabled: select.disabled,
126
+ placeholder,
127
+ name: select.name || void 0
128
+ };
129
+ }
130
+ function mergeSelectConfig(config, select) {
131
+ const parsed = parseSelectElement(select);
132
+ return {
133
+ ...config,
134
+ options: config.options ?? parsed.options,
135
+ multiple: config.multiple ?? parsed.multiple,
136
+ value: config.value ?? parsed.value,
137
+ disabled: config.disabled ?? parsed.disabled,
138
+ placeholder: config.placeholder ?? parsed.placeholder,
139
+ name: config.name ?? parsed.name
140
+ };
141
+ }
142
+ var SelkitDom = class {
143
+ controller;
144
+ element;
145
+ #prefix;
146
+ #multiple;
147
+ #checkboxes;
148
+ #autogrow;
149
+ #dropdownAutoWidth;
150
+ #clearable;
151
+ #placeholder;
152
+ #name;
153
+ #virtual;
154
+ #itemHeight;
155
+ #dropdownParent;
156
+ #templateSelection;
157
+ #templateOption;
158
+ #sourceSelect;
159
+ #control;
160
+ #field;
161
+ #input;
162
+ #indicators;
163
+ #dropdown;
164
+ #live;
165
+ #hiddenContainer = null;
166
+ #selectPrevDisplay = "";
167
+ #dragFrom = -1;
168
+ #positioner = null;
169
+ #unsubscribe;
170
+ #offClose;
171
+ #offCreate;
172
+ #offAnnounce;
173
+ #onDocPointer;
174
+ constructor(target, config) {
175
+ const host = typeof target === "string" ? document.querySelector(target) : target;
176
+ if (!host) {
177
+ throw new Error(`[selkit] \u627E\u4E0D\u5230\u639B\u8F09\u76EE\u6A19 ${String(target)}`);
178
+ }
179
+ const sourceSelect = host instanceof HTMLSelectElement ? host : null;
180
+ const cfg = sourceSelect ? mergeSelectConfig(config, sourceSelect) : config;
181
+ this.#sourceSelect = sourceSelect;
182
+ this.#prefix = cfg.classPrefix ?? "selkit";
183
+ this.#multiple = cfg.multiple ?? false;
184
+ this.#checkboxes = cfg.checkboxes ?? false;
185
+ this.#autogrow = cfg.autogrow ?? false;
186
+ this.#dropdownAutoWidth = cfg.dropdownAutoWidth ?? false;
187
+ this.#clearable = cfg.clearable ?? !this.#multiple;
188
+ this.#placeholder = cfg.placeholder ?? "";
189
+ this.#name = cfg.name;
190
+ this.#virtual = cfg.virtualScroll ?? false;
191
+ this.#itemHeight = cfg.itemHeight ?? DEFAULT_ITEM_HEIGHT;
192
+ this.#dropdownParent = resolveParent(cfg.dropdownParent);
193
+ this.#templateSelection = cfg.templateSelection;
194
+ this.#templateOption = cfg.templateOption;
195
+ this.controller = (0, import_core.createSelkit)(cfg);
196
+ this.element = document.createElement("div");
197
+ this.element.className = this.#prefix;
198
+ if (this.#multiple) this.element.classList.add(this.#cls("", "multiple"));
199
+ if (this.#multiple && this.#checkboxes) {
200
+ this.element.classList.add(this.#cls("", "checkboxes"));
201
+ }
202
+ if (this.#autogrow) this.element.classList.add(this.#cls("", "autogrow"));
203
+ if (this.#dropdownAutoWidth) {
204
+ this.element.classList.add(this.#cls("", "auto-width"));
205
+ }
206
+ this.#control = document.createElement("div");
207
+ this.#control.className = this.#cls("control");
208
+ this.#control.tabIndex = 0;
209
+ this.#field = document.createElement("div");
210
+ this.#field.className = this.#cls("field");
211
+ this.#input = document.createElement("input");
212
+ this.#input.className = this.#cls("input");
213
+ this.#input.type = "text";
214
+ this.#input.autocomplete = "off";
215
+ this.#input.setAttribute("aria-autocomplete", "list");
216
+ const ariaLabel = cfg.ariaLabel ?? this.#placeholder;
217
+ if (ariaLabel) this.#input.setAttribute("aria-label", ariaLabel);
218
+ this.#indicators = document.createElement("div");
219
+ this.#indicators.className = this.#cls("indicators");
220
+ this.#dropdown = document.createElement("div");
221
+ this.#dropdown.className = this.#cls("dropdown");
222
+ this.#dropdown.hidden = true;
223
+ this.#live = document.createElement("div");
224
+ this.#live.className = this.#cls("live");
225
+ this.#live.setAttribute("role", "status");
226
+ this.#live.setAttribute("aria-live", "polite");
227
+ this.#live.setAttribute("aria-atomic", "true");
228
+ this.#live.style.cssText = SR_ONLY_CSS;
229
+ this.#field.append(this.#input);
230
+ this.#control.append(this.#field, this.#indicators);
231
+ if (this.#dropdownParent) {
232
+ this.#dropdown.classList.add(this.#prefix);
233
+ this.element.append(this.#control);
234
+ this.#dropdownParent.append(this.#dropdown);
235
+ } else {
236
+ this.element.append(this.#control, this.#dropdown);
237
+ }
238
+ this.element.append(this.#live);
239
+ if (sourceSelect) {
240
+ sourceSelect.after(this.element);
241
+ this.#selectPrevDisplay = sourceSelect.style.display;
242
+ sourceSelect.style.display = "none";
243
+ sourceSelect.setAttribute("aria-hidden", "true");
244
+ sourceSelect.tabIndex = -1;
245
+ } else {
246
+ host.append(this.element);
247
+ if (this.#name) {
248
+ this.#hiddenContainer = document.createElement("div");
249
+ this.#hiddenContainer.style.display = "none";
250
+ this.element.append(this.#hiddenContainer);
251
+ }
252
+ }
253
+ this.#bindEvents();
254
+ this.#onDocPointer = (e) => {
255
+ const target2 = e.target;
256
+ if (!this.element.contains(target2) && !this.#dropdown.contains(target2)) {
257
+ this.controller.close();
258
+ }
259
+ };
260
+ document.addEventListener("pointerdown", this.#onDocPointer);
261
+ this.#offClose = this.controller.on("close", () => {
262
+ if (this.#input.value !== "") {
263
+ this.#input.value = "";
264
+ this.controller.setQuery("");
265
+ }
266
+ });
267
+ this.#offCreate = this.controller.on("create", () => {
268
+ this.#input.value = "";
269
+ });
270
+ this.#offAnnounce = this.controller.on("announce", ({ message }) => {
271
+ this.#live.textContent = message;
272
+ });
273
+ this.#unsubscribe = this.controller.subscribe((s) => this.#render(s));
274
+ this.#render(this.controller.getState());
275
+ }
276
+ destroy() {
277
+ this.#unsubscribe();
278
+ this.#offClose();
279
+ this.#offCreate();
280
+ this.#offAnnounce();
281
+ this.#positioner?.destroy();
282
+ document.removeEventListener("pointerdown", this.#onDocPointer);
283
+ this.controller.destroy();
284
+ this.#dropdown.remove();
285
+ this.element.remove();
286
+ if (this.#sourceSelect) {
287
+ this.#sourceSelect.style.display = this.#selectPrevDisplay;
288
+ this.#sourceSelect.removeAttribute("aria-hidden");
289
+ this.#sourceSelect.tabIndex = 0;
290
+ }
291
+ }
292
+ // ── class 命名 (BEM) ─────────────────────────────────────
293
+ #cls(name, modifier) {
294
+ const base = name ? `${this.#prefix}__${name}` : this.#prefix;
295
+ return modifier ? `${base}--${modifier}` : base;
296
+ }
297
+ // ── 事件綁定(轉呼叫 controller 不含邏輯)─────────────────
298
+ #bindEvents() {
299
+ this.#control.addEventListener("pointerdown", (e) => {
300
+ e.stopPropagation();
301
+ const target = e.target;
302
+ if (target.closest(`.${this.#cls("clear")}`)) {
303
+ e.preventDefault();
304
+ this.controller.clear();
305
+ return;
306
+ }
307
+ const tagRemove = target.closest(
308
+ `.${this.#cls("tag-remove")}`
309
+ );
310
+ if (tagRemove) {
311
+ e.preventDefault();
312
+ const idx = Number(tagRemove.dataset.index);
313
+ const sel = this.controller.getState().selected[idx];
314
+ if (sel) this.controller.deselect(sel.value);
315
+ return;
316
+ }
317
+ if (target !== this.#input) {
318
+ e.preventDefault();
319
+ this.#input.focus();
320
+ this.controller.toggle();
321
+ } else {
322
+ this.controller.open();
323
+ }
324
+ });
325
+ this.#input.addEventListener("input", () => {
326
+ this.controller.open();
327
+ this.controller.setQuery(this.#input.value);
328
+ const q = this.controller.getState().query;
329
+ if (q !== this.#input.value) this.#input.value = q;
330
+ });
331
+ this.#control.addEventListener("keydown", (e) => this.#onKeydown(e));
332
+ this.#dropdown.addEventListener("pointerdown", (e) => {
333
+ e.stopPropagation();
334
+ const optEl = e.target.closest(
335
+ `.${this.#cls("option")}`
336
+ );
337
+ if (!optEl || optEl.getAttribute("aria-disabled") === "true") return;
338
+ e.preventDefault();
339
+ if (optEl.dataset.create === "true") {
340
+ this.controller.createTag();
341
+ return;
342
+ }
343
+ const index = Number(optEl.dataset.index);
344
+ const opt = this.controller.getState().visibleOptions[index];
345
+ if (!opt) return;
346
+ if (this.#multiple) this.controller.toggleSelect(opt.value);
347
+ else this.controller.select(opt.value);
348
+ });
349
+ this.#dropdown.addEventListener("scroll", () => {
350
+ const el = this.#dropdown;
351
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - LOAD_MORE_THRESHOLD) {
352
+ this.controller.loadMore();
353
+ }
354
+ if (this.#virtual) this.#renderOptions(this.controller.getState());
355
+ });
356
+ this.#field.addEventListener("dragstart", (e) => {
357
+ const tag = e.target.closest(
358
+ `.${this.#cls("tag")}`
359
+ );
360
+ if (!tag) return;
361
+ this.#dragFrom = Number(tag.dataset.index);
362
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
363
+ });
364
+ this.#field.addEventListener("dragover", (e) => {
365
+ if (this.#dragFrom < 0) return;
366
+ e.preventDefault();
367
+ });
368
+ this.#field.addEventListener("drop", (e) => {
369
+ if (this.#dragFrom < 0) return;
370
+ e.preventDefault();
371
+ const tag = e.target.closest(
372
+ `.${this.#cls("tag")}`
373
+ );
374
+ const to = tag ? Number(tag.dataset.index) : this.controller.getState().selected.length - 1;
375
+ this.controller.moveSelected(this.#dragFrom, to);
376
+ this.#dragFrom = -1;
377
+ });
378
+ this.#field.addEventListener("dragend", () => {
379
+ this.#dragFrom = -1;
380
+ });
381
+ }
382
+ #onKeydown(e) {
383
+ const st = this.controller.getState();
384
+ switch (e.key) {
385
+ case "ArrowDown":
386
+ e.preventDefault();
387
+ st.isOpen ? this.controller.moveActive(1) : this.controller.open();
388
+ break;
389
+ case "ArrowUp":
390
+ e.preventDefault();
391
+ st.isOpen ? this.controller.moveActive(-1) : this.controller.open();
392
+ break;
393
+ case "Enter":
394
+ if (st.isOpen) {
395
+ e.preventDefault();
396
+ this.controller.selectActive();
397
+ }
398
+ break;
399
+ case "Escape":
400
+ if (st.isOpen) {
401
+ e.preventDefault();
402
+ this.controller.close();
403
+ }
404
+ break;
405
+ case "Home":
406
+ if (st.isOpen) {
407
+ e.preventDefault();
408
+ this.controller.moveActiveToStart();
409
+ }
410
+ break;
411
+ case "End":
412
+ if (st.isOpen) {
413
+ e.preventDefault();
414
+ this.controller.moveActiveToEnd();
415
+ }
416
+ break;
417
+ case "Backspace":
418
+ if (this.#multiple && this.#input.value === "" && st.selected.length) {
419
+ this.controller.backspace();
420
+ this.#input.value = this.controller.getState().query;
421
+ }
422
+ break;
423
+ }
424
+ }
425
+ // ── 渲染 ─────────────────────────────────────────────────
426
+ #render(s) {
427
+ this.#renderField(s);
428
+ this.#renderIndicators(s);
429
+ this.#renderOptions(s);
430
+ this.#syncA11y(s);
431
+ this.#syncOpen(s);
432
+ this.#syncForm();
433
+ this.element.classList.toggle(this.#cls("", "open"), s.isOpen);
434
+ this.element.classList.toggle(this.#cls("", "disabled"), s.disabled);
435
+ }
436
+ /** 套用 templateSelection 到已選容器 字串走 textContent Node 直接掛入 無模板則用 label */
437
+ #fillSelection(host, option, meta) {
438
+ if (!this.#templateSelection) {
439
+ host.textContent = option.label;
440
+ return;
441
+ }
442
+ const out = this.#templateSelection(option, meta);
443
+ if (out instanceof Node) host.append(out);
444
+ else host.textContent = out;
445
+ }
446
+ #renderField(s) {
447
+ for (const child of Array.from(this.#field.children)) {
448
+ if (child !== this.#input) child.remove();
449
+ }
450
+ const frag = document.createDocumentFragment();
451
+ if (this.#multiple) {
452
+ s.selected.forEach((opt, i) => {
453
+ const tag = document.createElement("span");
454
+ tag.className = this.#cls("tag");
455
+ tag.draggable = true;
456
+ tag.dataset.index = String(i);
457
+ const label = document.createElement("span");
458
+ label.className = this.#cls("tag-label");
459
+ this.#fillSelection(label, opt, { index: i, multiple: true });
460
+ const remove = document.createElement("button");
461
+ remove.type = "button";
462
+ remove.className = this.#cls("tag-remove");
463
+ remove.dataset.index = String(i);
464
+ remove.setAttribute("aria-label", `Remove ${opt.label}`);
465
+ remove.textContent = "\xD7";
466
+ tag.append(label, remove);
467
+ frag.append(tag);
468
+ });
469
+ } else {
470
+ const sel = s.selected[0];
471
+ if (sel && this.#input.value === "") {
472
+ const single = document.createElement("span");
473
+ single.className = this.#cls("single-value");
474
+ this.#fillSelection(single, sel, { index: 0, multiple: false });
475
+ frag.append(single);
476
+ }
477
+ }
478
+ this.#field.insertBefore(frag, this.#input);
479
+ this.#input.placeholder = s.selected.length === 0 ? this.#placeholder : "";
480
+ this.#syncInputSize();
481
+ }
482
+ /** autogrow:以 size 屬性讓輸入框寬度貼齊內容(值或佔位文字)*/
483
+ #syncInputSize() {
484
+ if (!this.#autogrow) return;
485
+ const basis = this.#input.value || this.#input.placeholder;
486
+ this.#input.size = Math.max(1, basis.length);
487
+ }
488
+ #renderIndicators(s) {
489
+ this.#indicators.replaceChildren();
490
+ if (this.#clearable && s.selected.length > 0) {
491
+ const clear = document.createElement("button");
492
+ clear.type = "button";
493
+ clear.className = this.#cls("clear");
494
+ clear.setAttribute("aria-label", "Clear");
495
+ clear.textContent = "\xD7";
496
+ this.#indicators.append(clear);
497
+ }
498
+ const arrow = document.createElement("span");
499
+ arrow.className = this.#cls("arrow");
500
+ arrow.setAttribute("aria-hidden", "true");
501
+ this.#indicators.append(arrow);
502
+ }
503
+ #renderOptions(s) {
504
+ this.#dropdown.replaceChildren();
505
+ const a11y = this.controller.a11y();
506
+ const view = this.controller.getGroupedView();
507
+ if (view.rows.length === 0) {
508
+ const empty = document.createElement("div");
509
+ empty.className = this.#cls("empty");
510
+ empty.textContent = this.controller.getEmptyMessage();
511
+ this.#dropdown.append(empty);
512
+ return;
513
+ }
514
+ const hasGroups = view.rows.some((r) => r.type === "group");
515
+ if (this.#virtual && !hasGroups) {
516
+ const range = (0, import_core.computeVirtualRange)({
517
+ scrollTop: this.#dropdown.scrollTop,
518
+ viewportHeight: this.#dropdown.clientHeight,
519
+ itemHeight: this.#itemHeight,
520
+ itemCount: view.rows.length
521
+ });
522
+ this.#dropdown.append(this.#spacer(range.paddingTop));
523
+ for (let i = range.startIndex; i < range.endIndex; i++) {
524
+ const row = view.rows[i];
525
+ if (row?.type === "option") {
526
+ this.#dropdown.append(this.#buildOption(row, a11y, s.activeIndex));
527
+ } else if (row?.type === "create") {
528
+ this.#dropdown.append(this.#buildCreateRow(row, a11y, s.activeIndex));
529
+ }
530
+ }
531
+ this.#dropdown.append(this.#spacer(range.paddingBottom));
532
+ return;
533
+ }
534
+ for (const row of view.rows) {
535
+ if (row.type === "group") {
536
+ const group = document.createElement("div");
537
+ group.className = this.#cls("group");
538
+ if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
539
+ group.textContent = row.label;
540
+ this.#dropdown.append(group);
541
+ continue;
542
+ }
543
+ if (row.type === "create") {
544
+ this.#dropdown.append(this.#buildCreateRow(row, a11y, s.activeIndex));
545
+ continue;
546
+ }
547
+ this.#dropdown.append(this.#buildOption(row, a11y, s.activeIndex));
548
+ }
549
+ }
550
+ /** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
551
+ #buildCreateRow(row, a11y, activeIndex) {
552
+ const attrs = a11y.option(row.index);
553
+ const el = document.createElement("div");
554
+ el.className = `${this.#cls("option")} ${this.#cls("create")}`;
555
+ el.id = attrs.id;
556
+ el.dataset.index = String(row.index);
557
+ el.dataset.create = "true";
558
+ el.setAttribute("role", "option");
559
+ el.setAttribute("aria-selected", "false");
560
+ if (row.index === activeIndex) {
561
+ el.classList.add(this.#cls("option", "active"));
562
+ }
563
+ el.textContent = row.label;
564
+ return el;
565
+ }
566
+ /** 撐高佔位節點 維持虛擬捲動時的捲動總高度 */
567
+ #spacer(height) {
568
+ const el = document.createElement("div");
569
+ el.style.height = `${height}px`;
570
+ el.setAttribute("aria-hidden", "true");
571
+ return el;
572
+ }
573
+ #buildOption(row, a11y, activeIndex) {
574
+ const attrs = a11y.option(row.index);
575
+ const option = document.createElement("div");
576
+ option.className = this.#cls("option");
577
+ option.id = attrs.id;
578
+ option.dataset.index = String(row.index);
579
+ option.setAttribute("role", "option");
580
+ option.setAttribute("aria-selected", String(attrs["aria-selected"]));
581
+ if (attrs["aria-disabled"]) option.setAttribute("aria-disabled", "true");
582
+ if (row.index === activeIndex) {
583
+ option.classList.add(this.#cls("option", "active"));
584
+ }
585
+ if (attrs["aria-selected"]) {
586
+ option.classList.add(this.#cls("option", "selected"));
587
+ }
588
+ if (this.#templateOption) {
589
+ const out = this.#templateOption(row.option, {
590
+ index: row.index,
591
+ active: row.index === activeIndex,
592
+ selected: attrs["aria-selected"]
593
+ });
594
+ if (out instanceof Node) option.append(out);
595
+ else option.textContent = out;
596
+ } else {
597
+ option.textContent = row.option.label;
598
+ }
599
+ return option;
600
+ }
601
+ #syncA11y(s) {
602
+ const a = this.controller.a11y();
603
+ const c = this.#control;
604
+ c.setAttribute("role", a.trigger.role);
605
+ c.setAttribute("aria-expanded", String(a.trigger["aria-expanded"]));
606
+ c.setAttribute("aria-haspopup", a.trigger["aria-haspopup"]);
607
+ c.setAttribute("aria-controls", a.trigger["aria-controls"]);
608
+ const active = a.trigger["aria-activedescendant"];
609
+ if (active) c.setAttribute("aria-activedescendant", active);
610
+ else c.removeAttribute("aria-activedescendant");
611
+ if (a.trigger["aria-disabled"]) c.setAttribute("aria-disabled", "true");
612
+ else c.removeAttribute("aria-disabled");
613
+ this.#dropdown.id = a.listbox.id;
614
+ this.#dropdown.setAttribute("role", a.listbox.role);
615
+ if (a.listbox["aria-multiselectable"]) {
616
+ this.#dropdown.setAttribute("aria-multiselectable", "true");
617
+ }
618
+ this.#input.disabled = s.disabled;
619
+ this.#input.readOnly = !this.controller.isSearchable();
620
+ }
621
+ #syncOpen(s) {
622
+ if (s.isOpen) {
623
+ this.#dropdown.hidden = false;
624
+ if (this.#positioner) this.#positioner.update();
625
+ else
626
+ this.#positioner = attachPositioner(
627
+ this.#control,
628
+ this.#dropdown,
629
+ this.#dropdownAutoWidth
630
+ );
631
+ } else {
632
+ this.#dropdown.hidden = true;
633
+ this.#positioner?.destroy();
634
+ this.#positioner = null;
635
+ }
636
+ }
637
+ // ── 表單同步 ──────────────────────────────────────────────
638
+ #syncForm() {
639
+ if (this.#sourceSelect) this.#syncToSelect(this.#sourceSelect);
640
+ else if (this.#hiddenContainer) this.#syncHiddenInputs(this.#hiddenContainer);
641
+ }
642
+ #syncToSelect(select) {
643
+ const selected = this.controller.getState().selected;
644
+ const selectedSet = new Set(selected.map((o) => String(o.value)));
645
+ for (const opt of selected) {
646
+ const value = String(opt.value);
647
+ if (!Array.from(select.options).some((o) => o.value === value)) {
648
+ const el = document.createElement("option");
649
+ el.value = value;
650
+ el.textContent = opt.label;
651
+ select.append(el);
652
+ }
653
+ }
654
+ for (const o of Array.from(select.options)) {
655
+ o.selected = selectedSet.has(o.value);
656
+ }
657
+ select.dispatchEvent(new Event("change", { bubbles: true }));
658
+ }
659
+ #syncHiddenInputs(container) {
660
+ container.replaceChildren();
661
+ const name = this.#name;
662
+ const inputName = this.#multiple ? `${name}[]` : name;
663
+ const selected = this.controller.getState().selected;
664
+ const values = this.#multiple ? selected : selected.slice(0, 1);
665
+ for (const opt of values) {
666
+ const input = document.createElement("input");
667
+ input.type = "hidden";
668
+ input.name = inputName;
669
+ input.value = String(opt.value);
670
+ container.append(input);
671
+ }
672
+ }
673
+ };
674
+ function createSelkitDom(host, config = {}) {
675
+ return new SelkitDom(host, config);
676
+ }
677
+ // Annotate the CommonJS export names for ESM import in node:
678
+ 0 && (module.exports = {
679
+ Selkit,
680
+ SelkitDom,
681
+ attachPositioner,
682
+ computePosition,
683
+ createSelkitDom,
684
+ sk
685
+ });