@selkit/dom 0.1.1 → 0.3.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 +84 -8
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +89 -9
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -155,6 +155,11 @@ var SelkitDom = class {
|
|
|
155
155
|
#dropdownParent;
|
|
156
156
|
#templateSelection;
|
|
157
157
|
#templateOption;
|
|
158
|
+
#templateArrow;
|
|
159
|
+
#templateClear;
|
|
160
|
+
#templateTagRemove;
|
|
161
|
+
#templateGroup;
|
|
162
|
+
#templateEmpty;
|
|
158
163
|
#sourceSelect;
|
|
159
164
|
#control;
|
|
160
165
|
#field;
|
|
@@ -165,6 +170,8 @@ var SelkitDom = class {
|
|
|
165
170
|
#hiddenContainer = null;
|
|
166
171
|
#selectPrevDisplay = "";
|
|
167
172
|
#dragFrom = -1;
|
|
173
|
+
/** 上次已捲入可視的 activeIndex 僅在變動時才捲 避免跟使用者手動捲動打架 */
|
|
174
|
+
#lastActive = -1;
|
|
168
175
|
#positioner = null;
|
|
169
176
|
#unsubscribe;
|
|
170
177
|
#offClose;
|
|
@@ -192,6 +199,11 @@ var SelkitDom = class {
|
|
|
192
199
|
this.#dropdownParent = resolveParent(cfg.dropdownParent);
|
|
193
200
|
this.#templateSelection = cfg.templateSelection;
|
|
194
201
|
this.#templateOption = cfg.templateOption;
|
|
202
|
+
this.#templateArrow = cfg.templateArrow;
|
|
203
|
+
this.#templateClear = cfg.templateClear;
|
|
204
|
+
this.#templateTagRemove = cfg.templateTagRemove;
|
|
205
|
+
this.#templateGroup = cfg.templateGroup;
|
|
206
|
+
this.#templateEmpty = cfg.templateEmpty;
|
|
195
207
|
this.controller = (0, import_core.createSelkit)(cfg);
|
|
196
208
|
this.element = document.createElement("div");
|
|
197
209
|
this.element.className = this.#prefix;
|
|
@@ -432,16 +444,50 @@ var SelkitDom = class {
|
|
|
432
444
|
this.#syncForm();
|
|
433
445
|
this.element.classList.toggle(this.#cls("", "open"), s.isOpen);
|
|
434
446
|
this.element.classList.toggle(this.#cls("", "disabled"), s.disabled);
|
|
447
|
+
this.#scrollActiveIntoView(s);
|
|
435
448
|
}
|
|
436
|
-
/**
|
|
449
|
+
/**
|
|
450
|
+
* 鍵盤導航/開啟時讓作用中選項保持可見(aria-activedescendant 完整度)
|
|
451
|
+
* 僅在 activeIndex 變動時動作 手動捲動觸發的重繪不會跟著捲
|
|
452
|
+
*/
|
|
453
|
+
#scrollActiveIntoView(s) {
|
|
454
|
+
if (!s.isOpen) {
|
|
455
|
+
this.#lastActive = -1;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (s.activeIndex === this.#lastActive || s.activeIndex < 0) return;
|
|
459
|
+
this.#lastActive = s.activeIndex;
|
|
460
|
+
const hasGroups = this.controller.getGroupedView().rows.some((r) => r.type === "group");
|
|
461
|
+
if (this.#virtual && !hasGroups) {
|
|
462
|
+
const next = (0, import_core.computeScrollIntoView)({
|
|
463
|
+
index: s.activeIndex,
|
|
464
|
+
scrollTop: this.#dropdown.scrollTop,
|
|
465
|
+
viewportHeight: this.#dropdown.clientHeight,
|
|
466
|
+
itemHeight: this.#itemHeight
|
|
467
|
+
});
|
|
468
|
+
if (next !== null) {
|
|
469
|
+
this.#dropdown.scrollTop = next;
|
|
470
|
+
this.#renderOptions(s);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const active = this.#dropdown.querySelector(
|
|
475
|
+
`.${this.#cls("option", "active")}`
|
|
476
|
+
);
|
|
477
|
+
active?.scrollIntoView?.({ block: "nearest" });
|
|
478
|
+
}
|
|
479
|
+
/** 套用模板輸出:字串走 textContent(防 XSS)Node 直接掛入 */
|
|
480
|
+
#applyTemplate(host, out) {
|
|
481
|
+
if (out instanceof Node) host.append(out);
|
|
482
|
+
else host.textContent = out;
|
|
483
|
+
}
|
|
484
|
+
/** 套用 templateSelection 到已選容器 無模板則用 label */
|
|
437
485
|
#fillSelection(host, option, meta) {
|
|
438
486
|
if (!this.#templateSelection) {
|
|
439
487
|
host.textContent = option.label;
|
|
440
488
|
return;
|
|
441
489
|
}
|
|
442
|
-
|
|
443
|
-
if (out instanceof Node) host.append(out);
|
|
444
|
-
else host.textContent = out;
|
|
490
|
+
this.#applyTemplate(host, this.#templateSelection(option, meta));
|
|
445
491
|
}
|
|
446
492
|
#renderField(s) {
|
|
447
493
|
for (const child of Array.from(this.#field.children)) {
|
|
@@ -462,7 +508,11 @@ var SelkitDom = class {
|
|
|
462
508
|
remove.className = this.#cls("tag-remove");
|
|
463
509
|
remove.dataset.index = String(i);
|
|
464
510
|
remove.setAttribute("aria-label", `Remove ${opt.label}`);
|
|
465
|
-
|
|
511
|
+
if (this.#templateTagRemove) {
|
|
512
|
+
this.#applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
|
|
513
|
+
} else {
|
|
514
|
+
remove.textContent = "\xD7";
|
|
515
|
+
}
|
|
466
516
|
tag.append(label, remove);
|
|
467
517
|
frag.append(tag);
|
|
468
518
|
});
|
|
@@ -492,12 +542,19 @@ var SelkitDom = class {
|
|
|
492
542
|
clear.type = "button";
|
|
493
543
|
clear.className = this.#cls("clear");
|
|
494
544
|
clear.setAttribute("aria-label", "Clear");
|
|
495
|
-
|
|
545
|
+
if (this.#templateClear) {
|
|
546
|
+
this.#applyTemplate(clear, this.#templateClear());
|
|
547
|
+
} else {
|
|
548
|
+
clear.textContent = "\xD7";
|
|
549
|
+
}
|
|
496
550
|
this.#indicators.append(clear);
|
|
497
551
|
}
|
|
498
552
|
const arrow = document.createElement("span");
|
|
499
553
|
arrow.className = this.#cls("arrow");
|
|
500
554
|
arrow.setAttribute("aria-hidden", "true");
|
|
555
|
+
if (this.#templateArrow) {
|
|
556
|
+
this.#applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
|
|
557
|
+
}
|
|
501
558
|
this.#indicators.append(arrow);
|
|
502
559
|
}
|
|
503
560
|
#renderOptions(s) {
|
|
@@ -507,7 +564,19 @@ var SelkitDom = class {
|
|
|
507
564
|
if (view.rows.length === 0) {
|
|
508
565
|
const empty = document.createElement("div");
|
|
509
566
|
empty.className = this.#cls("empty");
|
|
510
|
-
|
|
567
|
+
const message = this.controller.getEmptyMessage();
|
|
568
|
+
if (this.#templateEmpty) {
|
|
569
|
+
this.#applyTemplate(
|
|
570
|
+
empty,
|
|
571
|
+
this.#templateEmpty({
|
|
572
|
+
reason: this.controller.getEmptyReason(),
|
|
573
|
+
message,
|
|
574
|
+
query: s.query
|
|
575
|
+
})
|
|
576
|
+
);
|
|
577
|
+
} else {
|
|
578
|
+
empty.textContent = message;
|
|
579
|
+
}
|
|
511
580
|
this.#dropdown.append(empty);
|
|
512
581
|
return;
|
|
513
582
|
}
|
|
@@ -536,7 +605,14 @@ var SelkitDom = class {
|
|
|
536
605
|
const group = document.createElement("div");
|
|
537
606
|
group.className = this.#cls("group");
|
|
538
607
|
if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
|
|
539
|
-
|
|
608
|
+
if (this.#templateGroup) {
|
|
609
|
+
this.#applyTemplate(
|
|
610
|
+
group,
|
|
611
|
+
this.#templateGroup({ label: row.label, disabled: !!row.disabled })
|
|
612
|
+
);
|
|
613
|
+
} else {
|
|
614
|
+
group.textContent = row.label;
|
|
615
|
+
}
|
|
540
616
|
this.#dropdown.append(group);
|
|
541
617
|
continue;
|
|
542
618
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SelkitController, SelkitConfig, SelkitOption } from '@selkit/core';
|
|
1
|
+
import { SelkitController, SelkitConfig, SelkitOption, SelkitEmptyReason } from '@selkit/core';
|
|
2
2
|
|
|
3
3
|
interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
4
4
|
/** class 前綴 預設 "selkit" */
|
|
@@ -28,6 +28,27 @@ interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
|
28
28
|
active: boolean;
|
|
29
29
|
selected: boolean;
|
|
30
30
|
}) => string | Node;
|
|
31
|
+
/** 自訂下拉箭頭內容(▾)外殼保留 回傳 string 走 textContent Node 直接掛入 */
|
|
32
|
+
templateArrow?: (meta: {
|
|
33
|
+
open: boolean;
|
|
34
|
+
}) => string | Node;
|
|
35
|
+
/** 自訂清除鈕內容(×)按鈕外殼與 click 行為保留 */
|
|
36
|
+
templateClear?: () => string | Node;
|
|
37
|
+
/** 自訂多選標籤移除鈕內容(×)按鈕外殼與 click 行為保留 */
|
|
38
|
+
templateTagRemove?: (option: SelkitOption<T>, meta: {
|
|
39
|
+
index: number;
|
|
40
|
+
}) => string | Node;
|
|
41
|
+
/** 自訂分組標題內容 外殼保留 */
|
|
42
|
+
templateGroup?: (meta: {
|
|
43
|
+
label: string;
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
}) => string | Node;
|
|
46
|
+
/** 自訂下拉為空/載入中的整塊內容 reason 分流 message 為預設文字 */
|
|
47
|
+
templateEmpty?: (meta: {
|
|
48
|
+
reason: SelkitEmptyReason;
|
|
49
|
+
message: string;
|
|
50
|
+
query: string;
|
|
51
|
+
}) => string | Node;
|
|
31
52
|
}
|
|
32
53
|
interface SelkitDomInstance<T = unknown> {
|
|
33
54
|
readonly controller: SelkitController<T>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SelkitController, SelkitConfig, SelkitOption } from '@selkit/core';
|
|
1
|
+
import { SelkitController, SelkitConfig, SelkitOption, SelkitEmptyReason } from '@selkit/core';
|
|
2
2
|
|
|
3
3
|
interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
4
4
|
/** class 前綴 預設 "selkit" */
|
|
@@ -28,6 +28,27 @@ interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
|
28
28
|
active: boolean;
|
|
29
29
|
selected: boolean;
|
|
30
30
|
}) => string | Node;
|
|
31
|
+
/** 自訂下拉箭頭內容(▾)外殼保留 回傳 string 走 textContent Node 直接掛入 */
|
|
32
|
+
templateArrow?: (meta: {
|
|
33
|
+
open: boolean;
|
|
34
|
+
}) => string | Node;
|
|
35
|
+
/** 自訂清除鈕內容(×)按鈕外殼與 click 行為保留 */
|
|
36
|
+
templateClear?: () => string | Node;
|
|
37
|
+
/** 自訂多選標籤移除鈕內容(×)按鈕外殼與 click 行為保留 */
|
|
38
|
+
templateTagRemove?: (option: SelkitOption<T>, meta: {
|
|
39
|
+
index: number;
|
|
40
|
+
}) => string | Node;
|
|
41
|
+
/** 自訂分組標題內容 外殼保留 */
|
|
42
|
+
templateGroup?: (meta: {
|
|
43
|
+
label: string;
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
}) => string | Node;
|
|
46
|
+
/** 自訂下拉為空/載入中的整塊內容 reason 分流 message 為預設文字 */
|
|
47
|
+
templateEmpty?: (meta: {
|
|
48
|
+
reason: SelkitEmptyReason;
|
|
49
|
+
message: string;
|
|
50
|
+
query: string;
|
|
51
|
+
}) => string | Node;
|
|
31
52
|
}
|
|
32
53
|
interface SelkitDomInstance<T = unknown> {
|
|
33
54
|
readonly controller: SelkitController<T>;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// src/dom.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
computeScrollIntoView,
|
|
4
|
+
computeVirtualRange,
|
|
5
|
+
createSelkit
|
|
6
|
+
} from "@selkit/core";
|
|
3
7
|
|
|
4
8
|
// src/positioner.ts
|
|
5
9
|
function computePosition(triggerRect, dropdownHeight, viewportHeight, gap = 4) {
|
|
@@ -124,6 +128,11 @@ var SelkitDom = class {
|
|
|
124
128
|
#dropdownParent;
|
|
125
129
|
#templateSelection;
|
|
126
130
|
#templateOption;
|
|
131
|
+
#templateArrow;
|
|
132
|
+
#templateClear;
|
|
133
|
+
#templateTagRemove;
|
|
134
|
+
#templateGroup;
|
|
135
|
+
#templateEmpty;
|
|
127
136
|
#sourceSelect;
|
|
128
137
|
#control;
|
|
129
138
|
#field;
|
|
@@ -134,6 +143,8 @@ var SelkitDom = class {
|
|
|
134
143
|
#hiddenContainer = null;
|
|
135
144
|
#selectPrevDisplay = "";
|
|
136
145
|
#dragFrom = -1;
|
|
146
|
+
/** 上次已捲入可視的 activeIndex 僅在變動時才捲 避免跟使用者手動捲動打架 */
|
|
147
|
+
#lastActive = -1;
|
|
137
148
|
#positioner = null;
|
|
138
149
|
#unsubscribe;
|
|
139
150
|
#offClose;
|
|
@@ -161,6 +172,11 @@ var SelkitDom = class {
|
|
|
161
172
|
this.#dropdownParent = resolveParent(cfg.dropdownParent);
|
|
162
173
|
this.#templateSelection = cfg.templateSelection;
|
|
163
174
|
this.#templateOption = cfg.templateOption;
|
|
175
|
+
this.#templateArrow = cfg.templateArrow;
|
|
176
|
+
this.#templateClear = cfg.templateClear;
|
|
177
|
+
this.#templateTagRemove = cfg.templateTagRemove;
|
|
178
|
+
this.#templateGroup = cfg.templateGroup;
|
|
179
|
+
this.#templateEmpty = cfg.templateEmpty;
|
|
164
180
|
this.controller = createSelkit(cfg);
|
|
165
181
|
this.element = document.createElement("div");
|
|
166
182
|
this.element.className = this.#prefix;
|
|
@@ -401,16 +417,50 @@ var SelkitDom = class {
|
|
|
401
417
|
this.#syncForm();
|
|
402
418
|
this.element.classList.toggle(this.#cls("", "open"), s.isOpen);
|
|
403
419
|
this.element.classList.toggle(this.#cls("", "disabled"), s.disabled);
|
|
420
|
+
this.#scrollActiveIntoView(s);
|
|
404
421
|
}
|
|
405
|
-
/**
|
|
422
|
+
/**
|
|
423
|
+
* 鍵盤導航/開啟時讓作用中選項保持可見(aria-activedescendant 完整度)
|
|
424
|
+
* 僅在 activeIndex 變動時動作 手動捲動觸發的重繪不會跟著捲
|
|
425
|
+
*/
|
|
426
|
+
#scrollActiveIntoView(s) {
|
|
427
|
+
if (!s.isOpen) {
|
|
428
|
+
this.#lastActive = -1;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (s.activeIndex === this.#lastActive || s.activeIndex < 0) return;
|
|
432
|
+
this.#lastActive = s.activeIndex;
|
|
433
|
+
const hasGroups = this.controller.getGroupedView().rows.some((r) => r.type === "group");
|
|
434
|
+
if (this.#virtual && !hasGroups) {
|
|
435
|
+
const next = computeScrollIntoView({
|
|
436
|
+
index: s.activeIndex,
|
|
437
|
+
scrollTop: this.#dropdown.scrollTop,
|
|
438
|
+
viewportHeight: this.#dropdown.clientHeight,
|
|
439
|
+
itemHeight: this.#itemHeight
|
|
440
|
+
});
|
|
441
|
+
if (next !== null) {
|
|
442
|
+
this.#dropdown.scrollTop = next;
|
|
443
|
+
this.#renderOptions(s);
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const active = this.#dropdown.querySelector(
|
|
448
|
+
`.${this.#cls("option", "active")}`
|
|
449
|
+
);
|
|
450
|
+
active?.scrollIntoView?.({ block: "nearest" });
|
|
451
|
+
}
|
|
452
|
+
/** 套用模板輸出:字串走 textContent(防 XSS)Node 直接掛入 */
|
|
453
|
+
#applyTemplate(host, out) {
|
|
454
|
+
if (out instanceof Node) host.append(out);
|
|
455
|
+
else host.textContent = out;
|
|
456
|
+
}
|
|
457
|
+
/** 套用 templateSelection 到已選容器 無模板則用 label */
|
|
406
458
|
#fillSelection(host, option, meta) {
|
|
407
459
|
if (!this.#templateSelection) {
|
|
408
460
|
host.textContent = option.label;
|
|
409
461
|
return;
|
|
410
462
|
}
|
|
411
|
-
|
|
412
|
-
if (out instanceof Node) host.append(out);
|
|
413
|
-
else host.textContent = out;
|
|
463
|
+
this.#applyTemplate(host, this.#templateSelection(option, meta));
|
|
414
464
|
}
|
|
415
465
|
#renderField(s) {
|
|
416
466
|
for (const child of Array.from(this.#field.children)) {
|
|
@@ -431,7 +481,11 @@ var SelkitDom = class {
|
|
|
431
481
|
remove.className = this.#cls("tag-remove");
|
|
432
482
|
remove.dataset.index = String(i);
|
|
433
483
|
remove.setAttribute("aria-label", `Remove ${opt.label}`);
|
|
434
|
-
|
|
484
|
+
if (this.#templateTagRemove) {
|
|
485
|
+
this.#applyTemplate(remove, this.#templateTagRemove(opt, { index: i }));
|
|
486
|
+
} else {
|
|
487
|
+
remove.textContent = "\xD7";
|
|
488
|
+
}
|
|
435
489
|
tag.append(label, remove);
|
|
436
490
|
frag.append(tag);
|
|
437
491
|
});
|
|
@@ -461,12 +515,19 @@ var SelkitDom = class {
|
|
|
461
515
|
clear.type = "button";
|
|
462
516
|
clear.className = this.#cls("clear");
|
|
463
517
|
clear.setAttribute("aria-label", "Clear");
|
|
464
|
-
|
|
518
|
+
if (this.#templateClear) {
|
|
519
|
+
this.#applyTemplate(clear, this.#templateClear());
|
|
520
|
+
} else {
|
|
521
|
+
clear.textContent = "\xD7";
|
|
522
|
+
}
|
|
465
523
|
this.#indicators.append(clear);
|
|
466
524
|
}
|
|
467
525
|
const arrow = document.createElement("span");
|
|
468
526
|
arrow.className = this.#cls("arrow");
|
|
469
527
|
arrow.setAttribute("aria-hidden", "true");
|
|
528
|
+
if (this.#templateArrow) {
|
|
529
|
+
this.#applyTemplate(arrow, this.#templateArrow({ open: s.isOpen }));
|
|
530
|
+
}
|
|
470
531
|
this.#indicators.append(arrow);
|
|
471
532
|
}
|
|
472
533
|
#renderOptions(s) {
|
|
@@ -476,7 +537,19 @@ var SelkitDom = class {
|
|
|
476
537
|
if (view.rows.length === 0) {
|
|
477
538
|
const empty = document.createElement("div");
|
|
478
539
|
empty.className = this.#cls("empty");
|
|
479
|
-
|
|
540
|
+
const message = this.controller.getEmptyMessage();
|
|
541
|
+
if (this.#templateEmpty) {
|
|
542
|
+
this.#applyTemplate(
|
|
543
|
+
empty,
|
|
544
|
+
this.#templateEmpty({
|
|
545
|
+
reason: this.controller.getEmptyReason(),
|
|
546
|
+
message,
|
|
547
|
+
query: s.query
|
|
548
|
+
})
|
|
549
|
+
);
|
|
550
|
+
} else {
|
|
551
|
+
empty.textContent = message;
|
|
552
|
+
}
|
|
480
553
|
this.#dropdown.append(empty);
|
|
481
554
|
return;
|
|
482
555
|
}
|
|
@@ -505,7 +578,14 @@ var SelkitDom = class {
|
|
|
505
578
|
const group = document.createElement("div");
|
|
506
579
|
group.className = this.#cls("group");
|
|
507
580
|
if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
|
|
508
|
-
|
|
581
|
+
if (this.#templateGroup) {
|
|
582
|
+
this.#applyTemplate(
|
|
583
|
+
group,
|
|
584
|
+
this.#templateGroup({ label: row.label, disabled: !!row.disabled })
|
|
585
|
+
);
|
|
586
|
+
} else {
|
|
587
|
+
group.textContent = row.label;
|
|
588
|
+
}
|
|
509
589
|
this.#dropdown.append(group);
|
|
510
590
|
continue;
|
|
511
591
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selkit/dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"axe-core": "^4.12.1",
|