@selkit/dom 0.7.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;
@@ -500,18 +597,13 @@ var SelkitDom = class {
500
597
  );
501
598
  active?.scrollIntoView?.({ block: "nearest" });
502
599
  }
503
- /** 套用模板輸出:字串走 textContent(防 XSS)Node 直接掛入 */
504
- #applyTemplate(host, out) {
505
- if (out instanceof Node) host.append(out);
506
- else host.textContent = out;
507
- }
508
600
  /** 套用 templateSelection 到已選容器 無模板則用 label */
509
601
  #fillSelection(host, option, meta) {
510
602
  if (!this.#templateSelection) {
511
603
  host.textContent = option.label;
512
604
  return;
513
605
  }
514
- this.#applyTemplate(host, this.#templateSelection(option, meta));
606
+ applyTemplate(host, this.#templateSelection(option, meta));
515
607
  }
516
608
  #renderField(s) {
517
609
  for (const child of Array.from(this.#field.children)) {
@@ -533,7 +625,7 @@ var SelkitDom = class {
533
625
  remove.dataset.index = String(i);
534
626
  remove.setAttribute("aria-label", `Remove ${opt.label}`);
535
627
  if (this.#templateTagRemove) {
536
- this.#applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
628
+ applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
537
629
  } else {
538
630
  remove.textContent = "\xD7";
539
631
  }
@@ -567,7 +659,7 @@ var SelkitDom = class {
567
659
  clear.className = this.#cls("clear");
568
660
  clear.setAttribute("aria-label", "Clear");
569
661
  if (this.#templateClear) {
570
- this.#applyTemplate(clear, this.#templateClear());
662
+ applyTemplate(clear, this.#templateClear());
571
663
  } else {
572
664
  clear.textContent = "\xD7";
573
665
  }
@@ -577,7 +669,7 @@ var SelkitDom = class {
577
669
  arrow.className = this.#cls("arrow");
578
670
  arrow.setAttribute("aria-hidden", "true");
579
671
  if (this.#templateArrow) {
580
- this.#applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
672
+ applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
581
673
  }
582
674
  this.#indicators.append(arrow);
583
675
  }
@@ -590,7 +682,7 @@ var SelkitDom = class {
590
682
  empty.className = this.#cls("empty");
591
683
  const message = this.controller.getEmptyMessage();
592
684
  if (this.#templateEmpty) {
593
- this.#applyTemplate(
685
+ applyTemplate(
594
686
  empty,
595
687
  this.#templateEmpty({
596
688
  reason: this.controller.getEmptyReason(),
@@ -634,90 +726,30 @@ var SelkitDom = class {
634
726
  }
635
727
  /** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
636
728
  #renderVirtualSlice(rows, range, a11y, activeIndex) {
637
- this.#dropdown.append(this.#spacer(range.paddingTop));
729
+ this.#dropdown.append(spacer(range.paddingTop));
638
730
  for (let i = range.startIndex; i < range.endIndex; i++) {
639
731
  const row = rows[i];
640
732
  if (row) this.#renderRow(row, a11y, activeIndex);
641
733
  }
642
- this.#dropdown.append(this.#spacer(range.paddingBottom));
734
+ this.#dropdown.append(spacer(range.paddingBottom));
643
735
  }
644
736
  /** 依列型別渲染並掛入下拉 group / create / option */
645
737
  #renderRow(row, a11y, activeIndex) {
646
738
  if (row.type === "group") {
647
- this.#dropdown.append(this.#buildGroupRow(row));
739
+ this.#dropdown.append(
740
+ buildGroupRow(row, this.#prefix, this.#templateGroup)
741
+ );
648
742
  return;
649
743
  }
650
744
  if (row.type === "create") {
651
- this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
652
- return;
653
- }
654
- this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
655
- }
656
- /** 分組標題列 套用 templateGroup(無則用 label)*/
657
- #buildGroupRow(row) {
658
- const group = document.createElement("div");
659
- group.className = this.#cls("group");
660
- if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
661
- if (this.#templateGroup) {
662
- this.#applyTemplate(
663
- group,
664
- this.#templateGroup({ label: row.label, disabled: !!row.disabled })
745
+ this.#dropdown.append(
746
+ buildCreateRow(row, this.#prefix, a11y, activeIndex)
665
747
  );
666
- } else {
667
- group.textContent = row.label;
668
- }
669
- return group;
670
- }
671
- /** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
672
- #buildCreateRow(row, a11y, activeIndex) {
673
- const attrs = a11y.option(row.index);
674
- const el = document.createElement("div");
675
- el.className = `${this.#cls("option")} ${this.#cls("create")}`;
676
- el.id = attrs.id;
677
- el.dataset.index = String(row.index);
678
- el.dataset.create = "true";
679
- el.setAttribute("role", "option");
680
- el.setAttribute("aria-selected", "false");
681
- if (row.index === activeIndex) {
682
- el.classList.add(this.#cls("option", "active"));
683
- }
684
- el.textContent = row.label;
685
- return el;
686
- }
687
- /** 撐高佔位節點 維持虛擬捲動時的捲動總高度 */
688
- #spacer(height) {
689
- const el = document.createElement("div");
690
- el.style.height = `${height}px`;
691
- el.setAttribute("aria-hidden", "true");
692
- return el;
693
- }
694
- #buildOption(row, a11y, activeIndex) {
695
- const attrs = a11y.option(row.index);
696
- const option = document.createElement("div");
697
- option.className = this.#cls("option");
698
- option.id = attrs.id;
699
- option.dataset.index = String(row.index);
700
- option.setAttribute("role", "option");
701
- option.setAttribute("aria-selected", String(attrs["aria-selected"]));
702
- if (attrs["aria-disabled"]) option.setAttribute("aria-disabled", "true");
703
- if (row.index === activeIndex) {
704
- option.classList.add(this.#cls("option", "active"));
705
- }
706
- if (attrs["aria-selected"]) {
707
- option.classList.add(this.#cls("option", "selected"));
708
- }
709
- if (this.#templateOption) {
710
- const out = this.#templateOption(row.option, {
711
- index: row.index,
712
- active: row.index === activeIndex,
713
- selected: attrs["aria-selected"]
714
- });
715
- if (out instanceof Node) option.append(out);
716
- else option.textContent = out;
717
- } else {
718
- option.textContent = row.option.label;
748
+ return;
719
749
  }
720
- return option;
750
+ this.#dropdown.append(
751
+ buildOption(row, this.#prefix, a11y, activeIndex, this.#templateOption)
752
+ );
721
753
  }
722
754
  #syncA11y(s) {
723
755
  const a = this.controller.a11y();
@@ -755,38 +787,15 @@ var SelkitDom = class {
755
787
  }
756
788
  // ── 表單同步 ──────────────────────────────────────────────
757
789
  #syncForm() {
758
- if (this.#sourceSelect) this.#syncToSelect(this.#sourceSelect);
759
- else if (this.#hiddenContainer) this.#syncHiddenInputs(this.#hiddenContainer);
760
- }
761
- #syncToSelect(select) {
762
790
  const selected = this.controller.getState().selected;
763
- const selectedSet = new Set(selected.map((o) => String(o.value)));
764
- for (const opt of selected) {
765
- const value = String(opt.value);
766
- if (!Array.from(select.options).some((o) => o.value === value)) {
767
- const el = document.createElement("option");
768
- el.value = value;
769
- el.textContent = opt.label;
770
- select.append(el);
771
- }
772
- }
773
- for (const o of Array.from(select.options)) {
774
- o.selected = selectedSet.has(o.value);
775
- }
776
- select.dispatchEvent(new Event("change", { bubbles: true }));
777
- }
778
- #syncHiddenInputs(container) {
779
- container.replaceChildren();
780
- const name = this.#name;
781
- const inputName = this.#multiple ? `${name}[]` : name;
782
- const selected = this.controller.getState().selected;
783
- const values = this.#multiple ? selected : selected.slice(0, 1);
784
- for (const opt of values) {
785
- const input = document.createElement("input");
786
- input.type = "hidden";
787
- input.name = inputName;
788
- input.value = String(opt.value);
789
- 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
+ });
790
799
  }
791
800
  }
792
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;
@@ -475,18 +572,13 @@ var SelkitDom = class {
475
572
  );
476
573
  active?.scrollIntoView?.({ block: "nearest" });
477
574
  }
478
- /** 套用模板輸出:字串走 textContent(防 XSS)Node 直接掛入 */
479
- #applyTemplate(host, out) {
480
- if (out instanceof Node) host.append(out);
481
- else host.textContent = out;
482
- }
483
575
  /** 套用 templateSelection 到已選容器 無模板則用 label */
484
576
  #fillSelection(host, option, meta) {
485
577
  if (!this.#templateSelection) {
486
578
  host.textContent = option.label;
487
579
  return;
488
580
  }
489
- this.#applyTemplate(host, this.#templateSelection(option, meta));
581
+ applyTemplate(host, this.#templateSelection(option, meta));
490
582
  }
491
583
  #renderField(s) {
492
584
  for (const child of Array.from(this.#field.children)) {
@@ -508,7 +600,7 @@ var SelkitDom = class {
508
600
  remove.dataset.index = String(i);
509
601
  remove.setAttribute("aria-label", `Remove ${opt.label}`);
510
602
  if (this.#templateTagRemove) {
511
- this.#applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
603
+ applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
512
604
  } else {
513
605
  remove.textContent = "\xD7";
514
606
  }
@@ -542,7 +634,7 @@ var SelkitDom = class {
542
634
  clear.className = this.#cls("clear");
543
635
  clear.setAttribute("aria-label", "Clear");
544
636
  if (this.#templateClear) {
545
- this.#applyTemplate(clear, this.#templateClear());
637
+ applyTemplate(clear, this.#templateClear());
546
638
  } else {
547
639
  clear.textContent = "\xD7";
548
640
  }
@@ -552,7 +644,7 @@ var SelkitDom = class {
552
644
  arrow.className = this.#cls("arrow");
553
645
  arrow.setAttribute("aria-hidden", "true");
554
646
  if (this.#templateArrow) {
555
- this.#applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
647
+ applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
556
648
  }
557
649
  this.#indicators.append(arrow);
558
650
  }
@@ -565,7 +657,7 @@ var SelkitDom = class {
565
657
  empty.className = this.#cls("empty");
566
658
  const message = this.controller.getEmptyMessage();
567
659
  if (this.#templateEmpty) {
568
- this.#applyTemplate(
660
+ applyTemplate(
569
661
  empty,
570
662
  this.#templateEmpty({
571
663
  reason: this.controller.getEmptyReason(),
@@ -609,90 +701,30 @@ var SelkitDom = class {
609
701
  }
610
702
  /** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
611
703
  #renderVirtualSlice(rows, range, a11y, activeIndex) {
612
- this.#dropdown.append(this.#spacer(range.paddingTop));
704
+ this.#dropdown.append(spacer(range.paddingTop));
613
705
  for (let i = range.startIndex; i < range.endIndex; i++) {
614
706
  const row = rows[i];
615
707
  if (row) this.#renderRow(row, a11y, activeIndex);
616
708
  }
617
- this.#dropdown.append(this.#spacer(range.paddingBottom));
709
+ this.#dropdown.append(spacer(range.paddingBottom));
618
710
  }
619
711
  /** 依列型別渲染並掛入下拉 group / create / option */
620
712
  #renderRow(row, a11y, activeIndex) {
621
713
  if (row.type === "group") {
622
- this.#dropdown.append(this.#buildGroupRow(row));
714
+ this.#dropdown.append(
715
+ buildGroupRow(row, this.#prefix, this.#templateGroup)
716
+ );
623
717
  return;
624
718
  }
625
719
  if (row.type === "create") {
626
- this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
627
- return;
628
- }
629
- this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
630
- }
631
- /** 分組標題列 套用 templateGroup(無則用 label)*/
632
- #buildGroupRow(row) {
633
- const group = document.createElement("div");
634
- group.className = this.#cls("group");
635
- if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
636
- if (this.#templateGroup) {
637
- this.#applyTemplate(
638
- group,
639
- this.#templateGroup({ label: row.label, disabled: !!row.disabled })
720
+ this.#dropdown.append(
721
+ buildCreateRow(row, this.#prefix, a11y, activeIndex)
640
722
  );
641
- } else {
642
- group.textContent = row.label;
643
- }
644
- return group;
645
- }
646
- /** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
647
- #buildCreateRow(row, a11y, activeIndex) {
648
- const attrs = a11y.option(row.index);
649
- const el = document.createElement("div");
650
- el.className = `${this.#cls("option")} ${this.#cls("create")}`;
651
- el.id = attrs.id;
652
- el.dataset.index = String(row.index);
653
- el.dataset.create = "true";
654
- el.setAttribute("role", "option");
655
- el.setAttribute("aria-selected", "false");
656
- if (row.index === activeIndex) {
657
- el.classList.add(this.#cls("option", "active"));
658
- }
659
- el.textContent = row.label;
660
- return el;
661
- }
662
- /** 撐高佔位節點 維持虛擬捲動時的捲動總高度 */
663
- #spacer(height) {
664
- const el = document.createElement("div");
665
- el.style.height = `${height}px`;
666
- el.setAttribute("aria-hidden", "true");
667
- return el;
668
- }
669
- #buildOption(row, a11y, activeIndex) {
670
- const attrs = a11y.option(row.index);
671
- const option = document.createElement("div");
672
- option.className = this.#cls("option");
673
- option.id = attrs.id;
674
- option.dataset.index = String(row.index);
675
- option.setAttribute("role", "option");
676
- option.setAttribute("aria-selected", String(attrs["aria-selected"]));
677
- if (attrs["aria-disabled"]) option.setAttribute("aria-disabled", "true");
678
- if (row.index === activeIndex) {
679
- option.classList.add(this.#cls("option", "active"));
680
- }
681
- if (attrs["aria-selected"]) {
682
- option.classList.add(this.#cls("option", "selected"));
683
- }
684
- if (this.#templateOption) {
685
- const out = this.#templateOption(row.option, {
686
- index: row.index,
687
- active: row.index === activeIndex,
688
- selected: attrs["aria-selected"]
689
- });
690
- if (out instanceof Node) option.append(out);
691
- else option.textContent = out;
692
- } else {
693
- option.textContent = row.option.label;
723
+ return;
694
724
  }
695
- return option;
725
+ this.#dropdown.append(
726
+ buildOption(row, this.#prefix, a11y, activeIndex, this.#templateOption)
727
+ );
696
728
  }
697
729
  #syncA11y(s) {
698
730
  const a = this.controller.a11y();
@@ -730,38 +762,15 @@ var SelkitDom = class {
730
762
  }
731
763
  // ── 表單同步 ──────────────────────────────────────────────
732
764
  #syncForm() {
733
- if (this.#sourceSelect) this.#syncToSelect(this.#sourceSelect);
734
- else if (this.#hiddenContainer) this.#syncHiddenInputs(this.#hiddenContainer);
735
- }
736
- #syncToSelect(select) {
737
765
  const selected = this.controller.getState().selected;
738
- const selectedSet = new Set(selected.map((o) => String(o.value)));
739
- for (const opt of selected) {
740
- const value = String(opt.value);
741
- if (!Array.from(select.options).some((o) => o.value === value)) {
742
- const el = document.createElement("option");
743
- el.value = value;
744
- el.textContent = opt.label;
745
- select.append(el);
746
- }
747
- }
748
- for (const o of Array.from(select.options)) {
749
- o.selected = selectedSet.has(o.value);
750
- }
751
- select.dispatchEvent(new Event("change", { bubbles: true }));
752
- }
753
- #syncHiddenInputs(container) {
754
- container.replaceChildren();
755
- const name = this.#name;
756
- const inputName = this.#multiple ? `${name}[]` : name;
757
- const selected = this.controller.getState().selected;
758
- const values = this.#multiple ? selected : selected.slice(0, 1);
759
- for (const opt of values) {
760
- const input = document.createElement("input");
761
- input.type = "hidden";
762
- input.name = inputName;
763
- input.value = String(opt.value);
764
- 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
+ });
765
774
  }
766
775
  }
767
776
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selkit/dom",
3
- "version": "0.7.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.7.0"
22
+ "@selkit/core": "0.8.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "axe-core": "^4.12.1",