@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 +134 -125
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +134 -125
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -69,19 +69,7 @@ function attachPositioner(trigger, dropdown, autoWidth = false) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// src/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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.
|
|
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.
|
|
22
|
+
"@selkit/core": "0.8.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"axe-core": "^4.12.1",
|