@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 +135 -125
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +135 -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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
651
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
626
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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.
|
|
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",
|