@selkit/dom 0.6.0 → 0.8.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/dist/index.cjs CHANGED
@@ -69,19 +69,7 @@ function attachPositioner(trigger, dropdown, autoWidth = false) {
69
69
  };
70
70
  }
71
71
 
72
- // src/dom.ts
73
- var LOAD_MORE_THRESHOLD = 32;
74
- var DEFAULT_ITEM_HEIGHT = 36;
75
- var DEFAULT_GROUP_HEIGHT = 28;
76
- 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";
77
- var builtinPositioner = (trigger, dropdown, opts) => attachPositioner(trigger, dropdown, opts?.autoWidth ?? false);
78
- function resolveParent(parent) {
79
- if (!parent) return null;
80
- if (typeof parent !== "string") return parent;
81
- const el = document.querySelector(parent);
82
- if (!el) throw new Error(`[selkit] \u627E\u4E0D\u5230 dropdownParent ${parent}`);
83
- return el;
84
- }
72
+ // src/select-form.ts
85
73
  function parseSelectElement(select) {
86
74
  const options = [];
87
75
  const selectedValues = [];
@@ -141,6 +129,115 @@ function mergeSelectConfig(config, select) {
141
129
  name: config.name ?? parsed.name
142
130
  };
143
131
  }
132
+ function syncToSelect(select, selected) {
133
+ const selectedSet = new Set(selected.map((o) => String(o.value)));
134
+ for (const opt of selected) {
135
+ const value = String(opt.value);
136
+ if (!Array.from(select.options).some((o) => o.value === value)) {
137
+ const el = document.createElement("option");
138
+ el.value = value;
139
+ el.textContent = opt.label;
140
+ select.append(el);
141
+ }
142
+ }
143
+ for (const o of Array.from(select.options)) {
144
+ o.selected = selectedSet.has(o.value);
145
+ }
146
+ select.dispatchEvent(new Event("change", { bubbles: true }));
147
+ }
148
+ function syncHiddenInputs(container, opts) {
149
+ container.replaceChildren();
150
+ const inputName = opts.multiple ? `${opts.name}[]` : opts.name;
151
+ const values = opts.multiple ? opts.selected : opts.selected.slice(0, 1);
152
+ for (const opt of values) {
153
+ const input = document.createElement("input");
154
+ input.type = "hidden";
155
+ input.name = inputName;
156
+ input.value = String(opt.value);
157
+ container.append(input);
158
+ }
159
+ }
160
+
161
+ // src/templates.ts
162
+ function applyTemplate(host, out) {
163
+ if (out instanceof Node) host.append(out);
164
+ else host.textContent = out;
165
+ }
166
+ function spacer(height) {
167
+ const el = document.createElement("div");
168
+ el.style.height = `${height}px`;
169
+ el.setAttribute("aria-hidden", "true");
170
+ return el;
171
+ }
172
+ function buildGroupRow(row, prefix, templateGroup) {
173
+ const group = document.createElement("div");
174
+ group.className = `${prefix}__group`;
175
+ if (row.disabled) group.classList.add(`${prefix}__group--disabled`);
176
+ if (templateGroup) {
177
+ applyTemplate(
178
+ group,
179
+ templateGroup({ label: row.label, disabled: !!row.disabled })
180
+ );
181
+ } else {
182
+ group.textContent = row.label;
183
+ }
184
+ return group;
185
+ }
186
+ function buildCreateRow(row, prefix, a11y, activeIndex) {
187
+ const attrs = a11y.option(row.index);
188
+ const el = document.createElement("div");
189
+ el.className = `${prefix}__option ${prefix}__create`;
190
+ el.id = attrs.id;
191
+ el.dataset.index = String(row.index);
192
+ el.dataset.create = "true";
193
+ el.setAttribute("role", "option");
194
+ el.setAttribute("aria-selected", "false");
195
+ if (row.index === activeIndex) el.classList.add(`${prefix}__option--active`);
196
+ el.textContent = row.label;
197
+ return el;
198
+ }
199
+ function buildOption(row, prefix, a11y, activeIndex, templateOption) {
200
+ const attrs = a11y.option(row.index);
201
+ const option = document.createElement("div");
202
+ option.className = `${prefix}__option`;
203
+ option.id = attrs.id;
204
+ option.dataset.index = String(row.index);
205
+ option.setAttribute("role", "option");
206
+ option.setAttribute("aria-selected", String(attrs["aria-selected"]));
207
+ if (attrs["aria-disabled"]) option.setAttribute("aria-disabled", "true");
208
+ if (row.index === activeIndex) {
209
+ option.classList.add(`${prefix}__option--active`);
210
+ }
211
+ if (attrs["aria-selected"]) {
212
+ option.classList.add(`${prefix}__option--selected`);
213
+ }
214
+ if (templateOption) {
215
+ const out = templateOption(row.option, {
216
+ index: row.index,
217
+ active: row.index === activeIndex,
218
+ selected: attrs["aria-selected"]
219
+ });
220
+ if (out instanceof Node) option.append(out);
221
+ else option.textContent = out;
222
+ } else {
223
+ option.textContent = row.option.label;
224
+ }
225
+ return option;
226
+ }
227
+
228
+ // src/dom.ts
229
+ var LOAD_MORE_THRESHOLD = 32;
230
+ var DEFAULT_ITEM_HEIGHT = 36;
231
+ var DEFAULT_GROUP_HEIGHT = 28;
232
+ 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";
233
+ var builtinPositioner = (trigger, dropdown, opts) => attachPositioner(trigger, dropdown, opts?.autoWidth ?? false);
234
+ function resolveParent(parent) {
235
+ if (!parent) return null;
236
+ if (typeof parent !== "string") return parent;
237
+ const el = document.querySelector(parent);
238
+ if (!el) throw new Error(`[selkit] \u627E\u4E0D\u5230 dropdownParent ${parent}`);
239
+ return el;
240
+ }
144
241
  var SelkitDom = class {
145
242
  controller;
146
243
  element;
@@ -450,6 +547,7 @@ var SelkitDom = class {
450
547
  this.#syncForm();
451
548
  this.element.classList.toggle(this.#cls("", "open"), s.isOpen);
452
549
  this.element.classList.toggle(this.#cls("", "disabled"), s.disabled);
550
+ this.element.classList.toggle(this.#cls("", "resolving"), s.resolving);
453
551
  this.#scrollActiveIntoView(s);
454
552
  }
455
553
  /**
@@ -499,18 +597,13 @@ var SelkitDom = class {
499
597
  );
500
598
  active?.scrollIntoView?.({ block: "nearest" });
501
599
  }
502
- /** 套用模板輸出:字串走 textContent(防 XSS)Node 直接掛入 */
503
- #applyTemplate(host, out) {
504
- if (out instanceof Node) host.append(out);
505
- else host.textContent = out;
506
- }
507
600
  /** 套用 templateSelection 到已選容器 無模板則用 label */
508
601
  #fillSelection(host, option, meta) {
509
602
  if (!this.#templateSelection) {
510
603
  host.textContent = option.label;
511
604
  return;
512
605
  }
513
- this.#applyTemplate(host, this.#templateSelection(option, meta));
606
+ applyTemplate(host, this.#templateSelection(option, meta));
514
607
  }
515
608
  #renderField(s) {
516
609
  for (const child of Array.from(this.#field.children)) {
@@ -532,7 +625,7 @@ var SelkitDom = class {
532
625
  remove.dataset.index = String(i);
533
626
  remove.setAttribute("aria-label", `Remove ${opt.label}`);
534
627
  if (this.#templateTagRemove) {
535
- this.#applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
628
+ applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
536
629
  } else {
537
630
  remove.textContent = "\xD7";
538
631
  }
@@ -566,7 +659,7 @@ var SelkitDom = class {
566
659
  clear.className = this.#cls("clear");
567
660
  clear.setAttribute("aria-label", "Clear");
568
661
  if (this.#templateClear) {
569
- this.#applyTemplate(clear, this.#templateClear());
662
+ applyTemplate(clear, this.#templateClear());
570
663
  } else {
571
664
  clear.textContent = "\xD7";
572
665
  }
@@ -576,7 +669,7 @@ var SelkitDom = class {
576
669
  arrow.className = this.#cls("arrow");
577
670
  arrow.setAttribute("aria-hidden", "true");
578
671
  if (this.#templateArrow) {
579
- this.#applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
672
+ applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
580
673
  }
581
674
  this.#indicators.append(arrow);
582
675
  }
@@ -589,7 +682,7 @@ var SelkitDom = class {
589
682
  empty.className = this.#cls("empty");
590
683
  const message = this.controller.getEmptyMessage();
591
684
  if (this.#templateEmpty) {
592
- this.#applyTemplate(
685
+ applyTemplate(
593
686
  empty,
594
687
  this.#templateEmpty({
595
688
  reason: this.controller.getEmptyReason(),
@@ -633,90 +726,30 @@ var SelkitDom = class {
633
726
  }
634
727
  /** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
635
728
  #renderVirtualSlice(rows, range, a11y, activeIndex) {
636
- this.#dropdown.append(this.#spacer(range.paddingTop));
729
+ this.#dropdown.append(spacer(range.paddingTop));
637
730
  for (let i = range.startIndex; i < range.endIndex; i++) {
638
731
  const row = rows[i];
639
732
  if (row) this.#renderRow(row, a11y, activeIndex);
640
733
  }
641
- this.#dropdown.append(this.#spacer(range.paddingBottom));
734
+ this.#dropdown.append(spacer(range.paddingBottom));
642
735
  }
643
736
  /** 依列型別渲染並掛入下拉 group / create / option */
644
737
  #renderRow(row, a11y, activeIndex) {
645
738
  if (row.type === "group") {
646
- this.#dropdown.append(this.#buildGroupRow(row));
739
+ this.#dropdown.append(
740
+ buildGroupRow(row, this.#prefix, this.#templateGroup)
741
+ );
647
742
  return;
648
743
  }
649
744
  if (row.type === "create") {
650
- this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
651
- return;
652
- }
653
- this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
654
- }
655
- /** 分組標題列 套用 templateGroup(無則用 label)*/
656
- #buildGroupRow(row) {
657
- const group = document.createElement("div");
658
- group.className = this.#cls("group");
659
- if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
660
- if (this.#templateGroup) {
661
- this.#applyTemplate(
662
- group,
663
- this.#templateGroup({ label: row.label, disabled: !!row.disabled })
745
+ this.#dropdown.append(
746
+ buildCreateRow(row, this.#prefix, a11y, activeIndex)
664
747
  );
665
- } else {
666
- group.textContent = row.label;
667
- }
668
- return group;
669
- }
670
- /** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
671
- #buildCreateRow(row, a11y, activeIndex) {
672
- const attrs = a11y.option(row.index);
673
- const el = document.createElement("div");
674
- el.className = `${this.#cls("option")} ${this.#cls("create")}`;
675
- el.id = attrs.id;
676
- el.dataset.index = String(row.index);
677
- el.dataset.create = "true";
678
- el.setAttribute("role", "option");
679
- el.setAttribute("aria-selected", "false");
680
- if (row.index === activeIndex) {
681
- el.classList.add(this.#cls("option", "active"));
682
- }
683
- el.textContent = row.label;
684
- return el;
685
- }
686
- /** 撐高佔位節點 維持虛擬捲動時的捲動總高度 */
687
- #spacer(height) {
688
- const el = document.createElement("div");
689
- el.style.height = `${height}px`;
690
- el.setAttribute("aria-hidden", "true");
691
- return el;
692
- }
693
- #buildOption(row, a11y, activeIndex) {
694
- const attrs = a11y.option(row.index);
695
- const option = document.createElement("div");
696
- option.className = this.#cls("option");
697
- option.id = attrs.id;
698
- option.dataset.index = String(row.index);
699
- option.setAttribute("role", "option");
700
- option.setAttribute("aria-selected", String(attrs["aria-selected"]));
701
- if (attrs["aria-disabled"]) option.setAttribute("aria-disabled", "true");
702
- if (row.index === activeIndex) {
703
- option.classList.add(this.#cls("option", "active"));
704
- }
705
- if (attrs["aria-selected"]) {
706
- option.classList.add(this.#cls("option", "selected"));
707
- }
708
- if (this.#templateOption) {
709
- const out = this.#templateOption(row.option, {
710
- index: row.index,
711
- active: row.index === activeIndex,
712
- selected: attrs["aria-selected"]
713
- });
714
- if (out instanceof Node) option.append(out);
715
- else option.textContent = out;
716
- } else {
717
- option.textContent = row.option.label;
748
+ return;
718
749
  }
719
- return option;
750
+ this.#dropdown.append(
751
+ buildOption(row, this.#prefix, a11y, activeIndex, this.#templateOption)
752
+ );
720
753
  }
721
754
  #syncA11y(s) {
722
755
  const a = this.controller.a11y();
@@ -754,38 +787,15 @@ var SelkitDom = class {
754
787
  }
755
788
  // ── 表單同步 ──────────────────────────────────────────────
756
789
  #syncForm() {
757
- if (this.#sourceSelect) this.#syncToSelect(this.#sourceSelect);
758
- else if (this.#hiddenContainer) this.#syncHiddenInputs(this.#hiddenContainer);
759
- }
760
- #syncToSelect(select) {
761
790
  const selected = this.controller.getState().selected;
762
- const selectedSet = new Set(selected.map((o) => String(o.value)));
763
- for (const opt of selected) {
764
- const value = String(opt.value);
765
- if (!Array.from(select.options).some((o) => o.value === value)) {
766
- const el = document.createElement("option");
767
- el.value = value;
768
- el.textContent = opt.label;
769
- select.append(el);
770
- }
771
- }
772
- for (const o of Array.from(select.options)) {
773
- o.selected = selectedSet.has(o.value);
774
- }
775
- select.dispatchEvent(new Event("change", { bubbles: true }));
776
- }
777
- #syncHiddenInputs(container) {
778
- container.replaceChildren();
779
- const name = this.#name;
780
- const inputName = this.#multiple ? `${name}[]` : name;
781
- const selected = this.controller.getState().selected;
782
- const values = this.#multiple ? selected : selected.slice(0, 1);
783
- for (const opt of values) {
784
- const input = document.createElement("input");
785
- input.type = "hidden";
786
- input.name = inputName;
787
- input.value = String(opt.value);
788
- container.append(input);
791
+ if (this.#sourceSelect) {
792
+ syncToSelect(this.#sourceSelect, selected);
793
+ } else if (this.#hiddenContainer) {
794
+ syncHiddenInputs(this.#hiddenContainer, {
795
+ name: this.#name,
796
+ multiple: this.#multiple,
797
+ selected
798
+ });
789
799
  }
790
800
  }
791
801
  };
package/dist/index.d.cts CHANGED
@@ -43,6 +43,12 @@ type PositionerFactory = (trigger: HTMLElement, dropdown: HTMLElement, opts?: Po
43
43
  /** 將 dropdown 定位到 trigger 旁 並隨 scroll/resize 更新 */
44
44
  declare function attachPositioner(trigger: HTMLElement, dropdown: HTMLElement, autoWidth?: boolean): Positioner;
45
45
 
46
+ /**
47
+ * @selkit/dom — 公開型別契約
48
+ *
49
+ * 從 dom.ts 抽出:config / instance / row 型別,供 dom.ts 與 select-form.ts 共用
50
+ */
51
+
46
52
  interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
47
53
  /** class 前綴 預設 "selkit" */
48
54
  classPrefix?: string;
@@ -102,6 +108,7 @@ interface SelkitDomInstance<T = unknown> {
102
108
  readonly element: HTMLElement;
103
109
  destroy(): void;
104
110
  }
111
+
105
112
  declare class SelkitDom<T> implements SelkitDomInstance<T> {
106
113
  #private;
107
114
  readonly controller: SelkitController<T>;
package/dist/index.d.ts CHANGED
@@ -43,6 +43,12 @@ type PositionerFactory = (trigger: HTMLElement, dropdown: HTMLElement, opts?: Po
43
43
  /** 將 dropdown 定位到 trigger 旁 並隨 scroll/resize 更新 */
44
44
  declare function attachPositioner(trigger: HTMLElement, dropdown: HTMLElement, autoWidth?: boolean): Positioner;
45
45
 
46
+ /**
47
+ * @selkit/dom — 公開型別契約
48
+ *
49
+ * 從 dom.ts 抽出:config / instance / row 型別,供 dom.ts 與 select-form.ts 共用
50
+ */
51
+
46
52
  interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
47
53
  /** class 前綴 預設 "selkit" */
48
54
  classPrefix?: string;
@@ -102,6 +108,7 @@ interface SelkitDomInstance<T = unknown> {
102
108
  readonly element: HTMLElement;
103
109
  destroy(): void;
104
110
  }
111
+
105
112
  declare class SelkitDom<T> implements SelkitDomInstance<T> {
106
113
  #private;
107
114
  readonly controller: SelkitController<T>;
package/dist/index.js CHANGED
@@ -44,19 +44,7 @@ function attachPositioner(trigger, dropdown, autoWidth = false) {
44
44
  };
45
45
  }
46
46
 
47
- // src/dom.ts
48
- var LOAD_MORE_THRESHOLD = 32;
49
- var DEFAULT_ITEM_HEIGHT = 36;
50
- var DEFAULT_GROUP_HEIGHT = 28;
51
- 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";
52
- var builtinPositioner = (trigger, dropdown, opts) => attachPositioner(trigger, dropdown, opts?.autoWidth ?? false);
53
- function resolveParent(parent) {
54
- if (!parent) return null;
55
- if (typeof parent !== "string") return parent;
56
- const el = document.querySelector(parent);
57
- if (!el) throw new Error(`[selkit] \u627E\u4E0D\u5230 dropdownParent ${parent}`);
58
- return el;
59
- }
47
+ // src/select-form.ts
60
48
  function parseSelectElement(select) {
61
49
  const options = [];
62
50
  const selectedValues = [];
@@ -116,6 +104,115 @@ function mergeSelectConfig(config, select) {
116
104
  name: config.name ?? parsed.name
117
105
  };
118
106
  }
107
+ function syncToSelect(select, selected) {
108
+ const selectedSet = new Set(selected.map((o) => String(o.value)));
109
+ for (const opt of selected) {
110
+ const value = String(opt.value);
111
+ if (!Array.from(select.options).some((o) => o.value === value)) {
112
+ const el = document.createElement("option");
113
+ el.value = value;
114
+ el.textContent = opt.label;
115
+ select.append(el);
116
+ }
117
+ }
118
+ for (const o of Array.from(select.options)) {
119
+ o.selected = selectedSet.has(o.value);
120
+ }
121
+ select.dispatchEvent(new Event("change", { bubbles: true }));
122
+ }
123
+ function syncHiddenInputs(container, opts) {
124
+ container.replaceChildren();
125
+ const inputName = opts.multiple ? `${opts.name}[]` : opts.name;
126
+ const values = opts.multiple ? opts.selected : opts.selected.slice(0, 1);
127
+ for (const opt of values) {
128
+ const input = document.createElement("input");
129
+ input.type = "hidden";
130
+ input.name = inputName;
131
+ input.value = String(opt.value);
132
+ container.append(input);
133
+ }
134
+ }
135
+
136
+ // src/templates.ts
137
+ function applyTemplate(host, out) {
138
+ if (out instanceof Node) host.append(out);
139
+ else host.textContent = out;
140
+ }
141
+ function spacer(height) {
142
+ const el = document.createElement("div");
143
+ el.style.height = `${height}px`;
144
+ el.setAttribute("aria-hidden", "true");
145
+ return el;
146
+ }
147
+ function buildGroupRow(row, prefix, templateGroup) {
148
+ const group = document.createElement("div");
149
+ group.className = `${prefix}__group`;
150
+ if (row.disabled) group.classList.add(`${prefix}__group--disabled`);
151
+ if (templateGroup) {
152
+ applyTemplate(
153
+ group,
154
+ templateGroup({ label: row.label, disabled: !!row.disabled })
155
+ );
156
+ } else {
157
+ group.textContent = row.label;
158
+ }
159
+ return group;
160
+ }
161
+ function buildCreateRow(row, prefix, a11y, activeIndex) {
162
+ const attrs = a11y.option(row.index);
163
+ const el = document.createElement("div");
164
+ el.className = `${prefix}__option ${prefix}__create`;
165
+ el.id = attrs.id;
166
+ el.dataset.index = String(row.index);
167
+ el.dataset.create = "true";
168
+ el.setAttribute("role", "option");
169
+ el.setAttribute("aria-selected", "false");
170
+ if (row.index === activeIndex) el.classList.add(`${prefix}__option--active`);
171
+ el.textContent = row.label;
172
+ return el;
173
+ }
174
+ function buildOption(row, prefix, a11y, activeIndex, templateOption) {
175
+ const attrs = a11y.option(row.index);
176
+ const option = document.createElement("div");
177
+ option.className = `${prefix}__option`;
178
+ option.id = attrs.id;
179
+ option.dataset.index = String(row.index);
180
+ option.setAttribute("role", "option");
181
+ option.setAttribute("aria-selected", String(attrs["aria-selected"]));
182
+ if (attrs["aria-disabled"]) option.setAttribute("aria-disabled", "true");
183
+ if (row.index === activeIndex) {
184
+ option.classList.add(`${prefix}__option--active`);
185
+ }
186
+ if (attrs["aria-selected"]) {
187
+ option.classList.add(`${prefix}__option--selected`);
188
+ }
189
+ if (templateOption) {
190
+ const out = templateOption(row.option, {
191
+ index: row.index,
192
+ active: row.index === activeIndex,
193
+ selected: attrs["aria-selected"]
194
+ });
195
+ if (out instanceof Node) option.append(out);
196
+ else option.textContent = out;
197
+ } else {
198
+ option.textContent = row.option.label;
199
+ }
200
+ return option;
201
+ }
202
+
203
+ // src/dom.ts
204
+ var LOAD_MORE_THRESHOLD = 32;
205
+ var DEFAULT_ITEM_HEIGHT = 36;
206
+ var DEFAULT_GROUP_HEIGHT = 28;
207
+ 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";
208
+ var builtinPositioner = (trigger, dropdown, opts) => attachPositioner(trigger, dropdown, opts?.autoWidth ?? false);
209
+ function resolveParent(parent) {
210
+ if (!parent) return null;
211
+ if (typeof parent !== "string") return parent;
212
+ const el = document.querySelector(parent);
213
+ if (!el) throw new Error(`[selkit] \u627E\u4E0D\u5230 dropdownParent ${parent}`);
214
+ return el;
215
+ }
119
216
  var SelkitDom = class {
120
217
  controller;
121
218
  element;
@@ -425,6 +522,7 @@ var SelkitDom = class {
425
522
  this.#syncForm();
426
523
  this.element.classList.toggle(this.#cls("", "open"), s.isOpen);
427
524
  this.element.classList.toggle(this.#cls("", "disabled"), s.disabled);
525
+ this.element.classList.toggle(this.#cls("", "resolving"), s.resolving);
428
526
  this.#scrollActiveIntoView(s);
429
527
  }
430
528
  /**
@@ -474,18 +572,13 @@ var SelkitDom = class {
474
572
  );
475
573
  active?.scrollIntoView?.({ block: "nearest" });
476
574
  }
477
- /** 套用模板輸出:字串走 textContent(防 XSS)Node 直接掛入 */
478
- #applyTemplate(host, out) {
479
- if (out instanceof Node) host.append(out);
480
- else host.textContent = out;
481
- }
482
575
  /** 套用 templateSelection 到已選容器 無模板則用 label */
483
576
  #fillSelection(host, option, meta) {
484
577
  if (!this.#templateSelection) {
485
578
  host.textContent = option.label;
486
579
  return;
487
580
  }
488
- this.#applyTemplate(host, this.#templateSelection(option, meta));
581
+ applyTemplate(host, this.#templateSelection(option, meta));
489
582
  }
490
583
  #renderField(s) {
491
584
  for (const child of Array.from(this.#field.children)) {
@@ -507,7 +600,7 @@ var SelkitDom = class {
507
600
  remove.dataset.index = String(i);
508
601
  remove.setAttribute("aria-label", `Remove ${opt.label}`);
509
602
  if (this.#templateTagRemove) {
510
- this.#applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
603
+ applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
511
604
  } else {
512
605
  remove.textContent = "\xD7";
513
606
  }
@@ -541,7 +634,7 @@ var SelkitDom = class {
541
634
  clear.className = this.#cls("clear");
542
635
  clear.setAttribute("aria-label", "Clear");
543
636
  if (this.#templateClear) {
544
- this.#applyTemplate(clear, this.#templateClear());
637
+ applyTemplate(clear, this.#templateClear());
545
638
  } else {
546
639
  clear.textContent = "\xD7";
547
640
  }
@@ -551,7 +644,7 @@ var SelkitDom = class {
551
644
  arrow.className = this.#cls("arrow");
552
645
  arrow.setAttribute("aria-hidden", "true");
553
646
  if (this.#templateArrow) {
554
- this.#applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
647
+ applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
555
648
  }
556
649
  this.#indicators.append(arrow);
557
650
  }
@@ -564,7 +657,7 @@ var SelkitDom = class {
564
657
  empty.className = this.#cls("empty");
565
658
  const message = this.controller.getEmptyMessage();
566
659
  if (this.#templateEmpty) {
567
- this.#applyTemplate(
660
+ applyTemplate(
568
661
  empty,
569
662
  this.#templateEmpty({
570
663
  reason: this.controller.getEmptyReason(),
@@ -608,90 +701,30 @@ var SelkitDom = class {
608
701
  }
609
702
  /** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
610
703
  #renderVirtualSlice(rows, range, a11y, activeIndex) {
611
- this.#dropdown.append(this.#spacer(range.paddingTop));
704
+ this.#dropdown.append(spacer(range.paddingTop));
612
705
  for (let i = range.startIndex; i < range.endIndex; i++) {
613
706
  const row = rows[i];
614
707
  if (row) this.#renderRow(row, a11y, activeIndex);
615
708
  }
616
- this.#dropdown.append(this.#spacer(range.paddingBottom));
709
+ this.#dropdown.append(spacer(range.paddingBottom));
617
710
  }
618
711
  /** 依列型別渲染並掛入下拉 group / create / option */
619
712
  #renderRow(row, a11y, activeIndex) {
620
713
  if (row.type === "group") {
621
- this.#dropdown.append(this.#buildGroupRow(row));
714
+ this.#dropdown.append(
715
+ buildGroupRow(row, this.#prefix, this.#templateGroup)
716
+ );
622
717
  return;
623
718
  }
624
719
  if (row.type === "create") {
625
- this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
626
- return;
627
- }
628
- this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
629
- }
630
- /** 分組標題列 套用 templateGroup(無則用 label)*/
631
- #buildGroupRow(row) {
632
- const group = document.createElement("div");
633
- group.className = this.#cls("group");
634
- if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
635
- if (this.#templateGroup) {
636
- this.#applyTemplate(
637
- group,
638
- this.#templateGroup({ label: row.label, disabled: !!row.disabled })
720
+ this.#dropdown.append(
721
+ buildCreateRow(row, this.#prefix, a11y, activeIndex)
639
722
  );
640
- } else {
641
- group.textContent = row.label;
642
- }
643
- return group;
644
- }
645
- /** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
646
- #buildCreateRow(row, a11y, activeIndex) {
647
- const attrs = a11y.option(row.index);
648
- const el = document.createElement("div");
649
- el.className = `${this.#cls("option")} ${this.#cls("create")}`;
650
- el.id = attrs.id;
651
- el.dataset.index = String(row.index);
652
- el.dataset.create = "true";
653
- el.setAttribute("role", "option");
654
- el.setAttribute("aria-selected", "false");
655
- if (row.index === activeIndex) {
656
- el.classList.add(this.#cls("option", "active"));
657
- }
658
- el.textContent = row.label;
659
- return el;
660
- }
661
- /** 撐高佔位節點 維持虛擬捲動時的捲動總高度 */
662
- #spacer(height) {
663
- const el = document.createElement("div");
664
- el.style.height = `${height}px`;
665
- el.setAttribute("aria-hidden", "true");
666
- return el;
667
- }
668
- #buildOption(row, a11y, activeIndex) {
669
- const attrs = a11y.option(row.index);
670
- const option = document.createElement("div");
671
- option.className = this.#cls("option");
672
- option.id = attrs.id;
673
- option.dataset.index = String(row.index);
674
- option.setAttribute("role", "option");
675
- option.setAttribute("aria-selected", String(attrs["aria-selected"]));
676
- if (attrs["aria-disabled"]) option.setAttribute("aria-disabled", "true");
677
- if (row.index === activeIndex) {
678
- option.classList.add(this.#cls("option", "active"));
679
- }
680
- if (attrs["aria-selected"]) {
681
- option.classList.add(this.#cls("option", "selected"));
682
- }
683
- if (this.#templateOption) {
684
- const out = this.#templateOption(row.option, {
685
- index: row.index,
686
- active: row.index === activeIndex,
687
- selected: attrs["aria-selected"]
688
- });
689
- if (out instanceof Node) option.append(out);
690
- else option.textContent = out;
691
- } else {
692
- option.textContent = row.option.label;
723
+ return;
693
724
  }
694
- return option;
725
+ this.#dropdown.append(
726
+ buildOption(row, this.#prefix, a11y, activeIndex, this.#templateOption)
727
+ );
695
728
  }
696
729
  #syncA11y(s) {
697
730
  const a = this.controller.a11y();
@@ -729,38 +762,15 @@ var SelkitDom = class {
729
762
  }
730
763
  // ── 表單同步 ──────────────────────────────────────────────
731
764
  #syncForm() {
732
- if (this.#sourceSelect) this.#syncToSelect(this.#sourceSelect);
733
- else if (this.#hiddenContainer) this.#syncHiddenInputs(this.#hiddenContainer);
734
- }
735
- #syncToSelect(select) {
736
765
  const selected = this.controller.getState().selected;
737
- const selectedSet = new Set(selected.map((o) => String(o.value)));
738
- for (const opt of selected) {
739
- const value = String(opt.value);
740
- if (!Array.from(select.options).some((o) => o.value === value)) {
741
- const el = document.createElement("option");
742
- el.value = value;
743
- el.textContent = opt.label;
744
- select.append(el);
745
- }
746
- }
747
- for (const o of Array.from(select.options)) {
748
- o.selected = selectedSet.has(o.value);
749
- }
750
- select.dispatchEvent(new Event("change", { bubbles: true }));
751
- }
752
- #syncHiddenInputs(container) {
753
- container.replaceChildren();
754
- const name = this.#name;
755
- const inputName = this.#multiple ? `${name}[]` : name;
756
- const selected = this.controller.getState().selected;
757
- const values = this.#multiple ? selected : selected.slice(0, 1);
758
- for (const opt of values) {
759
- const input = document.createElement("input");
760
- input.type = "hidden";
761
- input.name = inputName;
762
- input.value = String(opt.value);
763
- container.append(input);
766
+ if (this.#sourceSelect) {
767
+ syncToSelect(this.#sourceSelect, selected);
768
+ } else if (this.#hiddenContainer) {
769
+ syncHiddenInputs(this.#hiddenContainer, {
770
+ name: this.#name,
771
+ multiple: this.#multiple,
772
+ selected
773
+ });
764
774
  }
765
775
  }
766
776
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selkit/dom",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Vanilla JS renderer for Selkit — DOM, events, a11y and default positioner.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "sideEffects": false,
21
21
  "dependencies": {
22
- "@selkit/core": "0.6.0"
22
+ "@selkit/core": "0.8.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "axe-core": "^4.12.1",