@selkit/dom 0.3.0 → 0.5.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 +79 -37
- package/dist/index.d.cts +48 -32
- package/dist/index.d.ts +48 -32
- package/dist/index.js +81 -37
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -72,7 +72,9 @@ function attachPositioner(trigger, dropdown, autoWidth = false) {
|
|
|
72
72
|
// src/dom.ts
|
|
73
73
|
var LOAD_MORE_THRESHOLD = 32;
|
|
74
74
|
var DEFAULT_ITEM_HEIGHT = 36;
|
|
75
|
+
var DEFAULT_GROUP_HEIGHT = 28;
|
|
75
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);
|
|
76
78
|
function resolveParent(parent) {
|
|
77
79
|
if (!parent) return null;
|
|
78
80
|
if (typeof parent !== "string") return parent;
|
|
@@ -152,7 +154,9 @@ var SelkitDom = class {
|
|
|
152
154
|
#name;
|
|
153
155
|
#virtual;
|
|
154
156
|
#itemHeight;
|
|
157
|
+
#groupHeight;
|
|
155
158
|
#dropdownParent;
|
|
159
|
+
#positionerFactory;
|
|
156
160
|
#templateSelection;
|
|
157
161
|
#templateOption;
|
|
158
162
|
#templateArrow;
|
|
@@ -196,7 +200,9 @@ var SelkitDom = class {
|
|
|
196
200
|
this.#name = cfg.name;
|
|
197
201
|
this.#virtual = cfg.virtualScroll ?? false;
|
|
198
202
|
this.#itemHeight = cfg.itemHeight ?? DEFAULT_ITEM_HEIGHT;
|
|
203
|
+
this.#groupHeight = cfg.groupHeight ?? DEFAULT_GROUP_HEIGHT;
|
|
199
204
|
this.#dropdownParent = resolveParent(cfg.dropdownParent);
|
|
205
|
+
this.#positionerFactory = cfg.positioner ?? builtinPositioner;
|
|
200
206
|
this.#templateSelection = cfg.templateSelection;
|
|
201
207
|
this.#templateOption = cfg.templateOption;
|
|
202
208
|
this.#templateArrow = cfg.templateArrow;
|
|
@@ -457,7 +463,8 @@ var SelkitDom = class {
|
|
|
457
463
|
}
|
|
458
464
|
if (s.activeIndex === this.#lastActive || s.activeIndex < 0) return;
|
|
459
465
|
this.#lastActive = s.activeIndex;
|
|
460
|
-
const
|
|
466
|
+
const rows = this.controller.getGroupedView().rows;
|
|
467
|
+
const hasGroups = rows.some((r) => r.type === "group");
|
|
461
468
|
if (this.#virtual && !hasGroups) {
|
|
462
469
|
const next = (0, import_core.computeScrollIntoView)({
|
|
463
470
|
index: s.activeIndex,
|
|
@@ -471,6 +478,22 @@ var SelkitDom = class {
|
|
|
471
478
|
}
|
|
472
479
|
return;
|
|
473
480
|
}
|
|
481
|
+
if (this.#virtual && hasGroups) {
|
|
482
|
+
const rowIndex = rows.findIndex(
|
|
483
|
+
(r) => r.type !== "group" && r.index === s.activeIndex
|
|
484
|
+
);
|
|
485
|
+
const next = (0, import_core.computeScrollIntoViewVariable)({
|
|
486
|
+
heights: this.#rowHeights(rows),
|
|
487
|
+
rowIndex,
|
|
488
|
+
scrollTop: this.#dropdown.scrollTop,
|
|
489
|
+
viewportHeight: this.#dropdown.clientHeight
|
|
490
|
+
});
|
|
491
|
+
if (next !== null) {
|
|
492
|
+
this.#dropdown.scrollTop = next;
|
|
493
|
+
this.#renderOptions(s);
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
474
497
|
const active = this.#dropdown.querySelector(
|
|
475
498
|
`.${this.#cls("option", "active")}`
|
|
476
499
|
);
|
|
@@ -588,40 +611,61 @@ var SelkitDom = class {
|
|
|
588
611
|
itemHeight: this.#itemHeight,
|
|
589
612
|
itemCount: view.rows.length
|
|
590
613
|
});
|
|
591
|
-
this.#
|
|
592
|
-
for (let i = range.startIndex; i < range.endIndex; i++) {
|
|
593
|
-
const row = view.rows[i];
|
|
594
|
-
if (row?.type === "option") {
|
|
595
|
-
this.#dropdown.append(this.#buildOption(row, a11y, s.activeIndex));
|
|
596
|
-
} else if (row?.type === "create") {
|
|
597
|
-
this.#dropdown.append(this.#buildCreateRow(row, a11y, s.activeIndex));
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
this.#dropdown.append(this.#spacer(range.paddingBottom));
|
|
614
|
+
this.#renderVirtualSlice(view.rows, range, a11y, s.activeIndex);
|
|
601
615
|
return;
|
|
602
616
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
617
|
+
if (this.#virtual && hasGroups) {
|
|
618
|
+
const win = (0, import_core.computeVirtualWindow)({
|
|
619
|
+
heights: this.#rowHeights(view.rows),
|
|
620
|
+
scrollTop: this.#dropdown.scrollTop,
|
|
621
|
+
viewportHeight: this.#dropdown.clientHeight
|
|
622
|
+
});
|
|
623
|
+
this.#renderVirtualSlice(view.rows, win, a11y, s.activeIndex);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
for (const row of view.rows) this.#renderRow(row, a11y, s.activeIndex);
|
|
627
|
+
}
|
|
628
|
+
/** 每列高度:分組標題用 groupHeight 其餘(option/create)用 itemHeight */
|
|
629
|
+
#rowHeights(rows) {
|
|
630
|
+
return rows.map(
|
|
631
|
+
(r) => r.type === "group" ? this.#groupHeight : this.#itemHeight
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
/** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
|
|
635
|
+
#renderVirtualSlice(rows, range, a11y, activeIndex) {
|
|
636
|
+
this.#dropdown.append(this.#spacer(range.paddingTop));
|
|
637
|
+
for (let i = range.startIndex; i < range.endIndex; i++) {
|
|
638
|
+
const row = rows[i];
|
|
639
|
+
if (row) this.#renderRow(row, a11y, activeIndex);
|
|
640
|
+
}
|
|
641
|
+
this.#dropdown.append(this.#spacer(range.paddingBottom));
|
|
642
|
+
}
|
|
643
|
+
/** 依列型別渲染並掛入下拉 group / create / option */
|
|
644
|
+
#renderRow(row, a11y, activeIndex) {
|
|
645
|
+
if (row.type === "group") {
|
|
646
|
+
this.#dropdown.append(this.#buildGroupRow(row));
|
|
647
|
+
return;
|
|
624
648
|
}
|
|
649
|
+
if (row.type === "create") {
|
|
650
|
+
this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
|
|
654
|
+
}
|
|
655
|
+
/** 分組標題列 套用 templateGroup(無則用 label)*/
|
|
656
|
+
#buildGroupRow(row) {
|
|
657
|
+
const group = document.createElement("div");
|
|
658
|
+
group.className = this.#cls("group");
|
|
659
|
+
if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
|
|
660
|
+
if (this.#templateGroup) {
|
|
661
|
+
this.#applyTemplate(
|
|
662
|
+
group,
|
|
663
|
+
this.#templateGroup({ label: row.label, disabled: !!row.disabled })
|
|
664
|
+
);
|
|
665
|
+
} else {
|
|
666
|
+
group.textContent = row.label;
|
|
667
|
+
}
|
|
668
|
+
return group;
|
|
625
669
|
}
|
|
626
670
|
/** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
|
|
627
671
|
#buildCreateRow(row, a11y, activeIndex) {
|
|
@@ -699,11 +743,9 @@ var SelkitDom = class {
|
|
|
699
743
|
this.#dropdown.hidden = false;
|
|
700
744
|
if (this.#positioner) this.#positioner.update();
|
|
701
745
|
else
|
|
702
|
-
this.#positioner =
|
|
703
|
-
this.#
|
|
704
|
-
|
|
705
|
-
this.#dropdownAutoWidth
|
|
706
|
-
);
|
|
746
|
+
this.#positioner = this.#positionerFactory(this.#control, this.#dropdown, {
|
|
747
|
+
autoWidth: this.#dropdownAutoWidth
|
|
748
|
+
});
|
|
707
749
|
} else {
|
|
708
750
|
this.#dropdown.hidden = true;
|
|
709
751
|
this.#positioner?.destroy();
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
import { SelkitController, SelkitConfig, SelkitOption, SelkitEmptyReason } from '@selkit/core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @selkit/dom — 預設輕量定位器(零依賴)
|
|
5
|
+
*
|
|
6
|
+
* 計算邏輯與 DOM 套用分離:computePosition 為純函式(可單元測)
|
|
7
|
+
* attachPositioner 負責讀取 rect、套 style、監聽 scroll/resize
|
|
8
|
+
*/
|
|
9
|
+
type Placement = 'bottom' | 'top';
|
|
10
|
+
interface Rect {
|
|
11
|
+
top: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
left: number;
|
|
14
|
+
width: number;
|
|
15
|
+
}
|
|
16
|
+
interface PositionResult {
|
|
17
|
+
placement: Placement;
|
|
18
|
+
top: number;
|
|
19
|
+
left: number;
|
|
20
|
+
width: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 依觸發元件位置與下拉高度 決定放上方或下方(空間不足才翻轉)
|
|
24
|
+
* 並回傳套用座標 不做水平翻轉(select 下拉慣例對齊左緣、同寬)
|
|
25
|
+
*/
|
|
26
|
+
declare function computePosition(triggerRect: Rect, dropdownHeight: number, viewportHeight: number, gap?: number): PositionResult;
|
|
27
|
+
interface Positioner {
|
|
28
|
+
update(): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
}
|
|
31
|
+
/** 定位器工廠收到的選項 由 @selkit/dom 在開啟下拉時傳入 */
|
|
32
|
+
interface PositionerOptions {
|
|
33
|
+
/** 下拉寬度貼齊內容(至少與控制項同寬)而非固定等寬 */
|
|
34
|
+
autoWidth?: boolean;
|
|
35
|
+
/** trigger 與下拉之間的間距 px 預設 4 */
|
|
36
|
+
gap?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 定位器工廠 可插拔點:@selkit/dom 預設用內建 attachPositioner
|
|
40
|
+
* 傳入自訂工廠(如 @selkit/floating 的 createFloatingPositioner)即換成進階定位
|
|
41
|
+
*/
|
|
42
|
+
type PositionerFactory = (trigger: HTMLElement, dropdown: HTMLElement, opts?: PositionerOptions) => Positioner;
|
|
43
|
+
/** 將 dropdown 定位到 trigger 旁 並隨 scroll/resize 更新 */
|
|
44
|
+
declare function attachPositioner(trigger: HTMLElement, dropdown: HTMLElement, autoWidth?: boolean): Positioner;
|
|
45
|
+
|
|
3
46
|
interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
4
47
|
/** class 前綴 預設 "selkit" */
|
|
5
48
|
classPrefix?: string;
|
|
@@ -15,8 +58,12 @@ interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
|
15
58
|
virtualScroll?: boolean;
|
|
16
59
|
/** 虛擬捲動的單列固定高度 px 預設 36 須與實際樣式高度一致 */
|
|
17
60
|
itemHeight?: number;
|
|
61
|
+
/** 虛擬捲動下分組標題列的固定高度 px 預設 28 須與實際樣式高度一致 */
|
|
62
|
+
groupHeight?: number;
|
|
18
63
|
/** 把下拉浮層掛到指定容器(元素或選擇器)逃離 overflow/transform 祖先的裁切 常用 document.body */
|
|
19
64
|
dropdownParent?: HTMLElement | string;
|
|
65
|
+
/** 自訂定位器工廠 預設為內建零依賴定位器(垂直翻轉)傳入 @selkit/floating 的 createFloatingPositioner 即啟用 flip/shift/size 進階定位 */
|
|
66
|
+
positioner?: PositionerFactory;
|
|
20
67
|
/** 自訂已選顯示內容(tag 或單值) 回傳字串走 textContent 防 XSS 需 markup(icon 等)請回傳 Node */
|
|
21
68
|
templateSelection?: (option: SelkitOption<T>, meta: {
|
|
22
69
|
index: number;
|
|
@@ -65,35 +112,4 @@ declare class SelkitDom<T> implements SelkitDomInstance<T> {
|
|
|
65
112
|
/** 在指定元素或 selector 對應的元素內建立並掛載一個 Selkit 下拉 */
|
|
66
113
|
declare function createSelkitDom<T = unknown>(host: HTMLElement | string, config?: SelkitDomConfig<T>): SelkitDomInstance<T>;
|
|
67
114
|
|
|
68
|
-
|
|
69
|
-
* @selkit/dom — 預設輕量定位器(零依賴)
|
|
70
|
-
*
|
|
71
|
-
* 計算邏輯與 DOM 套用分離:computePosition 為純函式(可單元測)
|
|
72
|
-
* attachPositioner 負責讀取 rect、套 style、監聽 scroll/resize
|
|
73
|
-
*/
|
|
74
|
-
type Placement = 'bottom' | 'top';
|
|
75
|
-
interface Rect {
|
|
76
|
-
top: number;
|
|
77
|
-
bottom: number;
|
|
78
|
-
left: number;
|
|
79
|
-
width: number;
|
|
80
|
-
}
|
|
81
|
-
interface PositionResult {
|
|
82
|
-
placement: Placement;
|
|
83
|
-
top: number;
|
|
84
|
-
left: number;
|
|
85
|
-
width: number;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* 依觸發元件位置與下拉高度 決定放上方或下方(空間不足才翻轉)
|
|
89
|
-
* 並回傳套用座標 不做水平翻轉(select 下拉慣例對齊左緣、同寬)
|
|
90
|
-
*/
|
|
91
|
-
declare function computePosition(triggerRect: Rect, dropdownHeight: number, viewportHeight: number, gap?: number): PositionResult;
|
|
92
|
-
interface Positioner {
|
|
93
|
-
update(): void;
|
|
94
|
-
destroy(): void;
|
|
95
|
-
}
|
|
96
|
-
/** 將 dropdown 定位到 trigger 旁 並隨 scroll/resize 更新 */
|
|
97
|
-
declare function attachPositioner(trigger: HTMLElement, dropdown: HTMLElement, autoWidth?: boolean): Positioner;
|
|
98
|
-
|
|
99
|
-
export { type Placement, type PositionResult, type Positioner, type Rect, SelkitDom as Selkit, SelkitDom, type SelkitDomConfig, type SelkitDomInstance, attachPositioner, computePosition, createSelkitDom, createSelkitDom as sk };
|
|
115
|
+
export { type Placement, type PositionResult, type Positioner, type PositionerFactory, type PositionerOptions, type Rect, SelkitDom as Selkit, SelkitDom, type SelkitDomConfig, type SelkitDomInstance, attachPositioner, computePosition, createSelkitDom, createSelkitDom as sk };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
import { SelkitController, SelkitConfig, SelkitOption, SelkitEmptyReason } from '@selkit/core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @selkit/dom — 預設輕量定位器(零依賴)
|
|
5
|
+
*
|
|
6
|
+
* 計算邏輯與 DOM 套用分離:computePosition 為純函式(可單元測)
|
|
7
|
+
* attachPositioner 負責讀取 rect、套 style、監聽 scroll/resize
|
|
8
|
+
*/
|
|
9
|
+
type Placement = 'bottom' | 'top';
|
|
10
|
+
interface Rect {
|
|
11
|
+
top: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
left: number;
|
|
14
|
+
width: number;
|
|
15
|
+
}
|
|
16
|
+
interface PositionResult {
|
|
17
|
+
placement: Placement;
|
|
18
|
+
top: number;
|
|
19
|
+
left: number;
|
|
20
|
+
width: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 依觸發元件位置與下拉高度 決定放上方或下方(空間不足才翻轉)
|
|
24
|
+
* 並回傳套用座標 不做水平翻轉(select 下拉慣例對齊左緣、同寬)
|
|
25
|
+
*/
|
|
26
|
+
declare function computePosition(triggerRect: Rect, dropdownHeight: number, viewportHeight: number, gap?: number): PositionResult;
|
|
27
|
+
interface Positioner {
|
|
28
|
+
update(): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
}
|
|
31
|
+
/** 定位器工廠收到的選項 由 @selkit/dom 在開啟下拉時傳入 */
|
|
32
|
+
interface PositionerOptions {
|
|
33
|
+
/** 下拉寬度貼齊內容(至少與控制項同寬)而非固定等寬 */
|
|
34
|
+
autoWidth?: boolean;
|
|
35
|
+
/** trigger 與下拉之間的間距 px 預設 4 */
|
|
36
|
+
gap?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 定位器工廠 可插拔點:@selkit/dom 預設用內建 attachPositioner
|
|
40
|
+
* 傳入自訂工廠(如 @selkit/floating 的 createFloatingPositioner)即換成進階定位
|
|
41
|
+
*/
|
|
42
|
+
type PositionerFactory = (trigger: HTMLElement, dropdown: HTMLElement, opts?: PositionerOptions) => Positioner;
|
|
43
|
+
/** 將 dropdown 定位到 trigger 旁 並隨 scroll/resize 更新 */
|
|
44
|
+
declare function attachPositioner(trigger: HTMLElement, dropdown: HTMLElement, autoWidth?: boolean): Positioner;
|
|
45
|
+
|
|
3
46
|
interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
4
47
|
/** class 前綴 預設 "selkit" */
|
|
5
48
|
classPrefix?: string;
|
|
@@ -15,8 +58,12 @@ interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
|
|
|
15
58
|
virtualScroll?: boolean;
|
|
16
59
|
/** 虛擬捲動的單列固定高度 px 預設 36 須與實際樣式高度一致 */
|
|
17
60
|
itemHeight?: number;
|
|
61
|
+
/** 虛擬捲動下分組標題列的固定高度 px 預設 28 須與實際樣式高度一致 */
|
|
62
|
+
groupHeight?: number;
|
|
18
63
|
/** 把下拉浮層掛到指定容器(元素或選擇器)逃離 overflow/transform 祖先的裁切 常用 document.body */
|
|
19
64
|
dropdownParent?: HTMLElement | string;
|
|
65
|
+
/** 自訂定位器工廠 預設為內建零依賴定位器(垂直翻轉)傳入 @selkit/floating 的 createFloatingPositioner 即啟用 flip/shift/size 進階定位 */
|
|
66
|
+
positioner?: PositionerFactory;
|
|
20
67
|
/** 自訂已選顯示內容(tag 或單值) 回傳字串走 textContent 防 XSS 需 markup(icon 等)請回傳 Node */
|
|
21
68
|
templateSelection?: (option: SelkitOption<T>, meta: {
|
|
22
69
|
index: number;
|
|
@@ -65,35 +112,4 @@ declare class SelkitDom<T> implements SelkitDomInstance<T> {
|
|
|
65
112
|
/** 在指定元素或 selector 對應的元素內建立並掛載一個 Selkit 下拉 */
|
|
66
113
|
declare function createSelkitDom<T = unknown>(host: HTMLElement | string, config?: SelkitDomConfig<T>): SelkitDomInstance<T>;
|
|
67
114
|
|
|
68
|
-
|
|
69
|
-
* @selkit/dom — 預設輕量定位器(零依賴)
|
|
70
|
-
*
|
|
71
|
-
* 計算邏輯與 DOM 套用分離:computePosition 為純函式(可單元測)
|
|
72
|
-
* attachPositioner 負責讀取 rect、套 style、監聽 scroll/resize
|
|
73
|
-
*/
|
|
74
|
-
type Placement = 'bottom' | 'top';
|
|
75
|
-
interface Rect {
|
|
76
|
-
top: number;
|
|
77
|
-
bottom: number;
|
|
78
|
-
left: number;
|
|
79
|
-
width: number;
|
|
80
|
-
}
|
|
81
|
-
interface PositionResult {
|
|
82
|
-
placement: Placement;
|
|
83
|
-
top: number;
|
|
84
|
-
left: number;
|
|
85
|
-
width: number;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* 依觸發元件位置與下拉高度 決定放上方或下方(空間不足才翻轉)
|
|
89
|
-
* 並回傳套用座標 不做水平翻轉(select 下拉慣例對齊左緣、同寬)
|
|
90
|
-
*/
|
|
91
|
-
declare function computePosition(triggerRect: Rect, dropdownHeight: number, viewportHeight: number, gap?: number): PositionResult;
|
|
92
|
-
interface Positioner {
|
|
93
|
-
update(): void;
|
|
94
|
-
destroy(): void;
|
|
95
|
-
}
|
|
96
|
-
/** 將 dropdown 定位到 trigger 旁 並隨 scroll/resize 更新 */
|
|
97
|
-
declare function attachPositioner(trigger: HTMLElement, dropdown: HTMLElement, autoWidth?: boolean): Positioner;
|
|
98
|
-
|
|
99
|
-
export { type Placement, type PositionResult, type Positioner, type Rect, SelkitDom as Selkit, SelkitDom, type SelkitDomConfig, type SelkitDomInstance, attachPositioner, computePosition, createSelkitDom, createSelkitDom as sk };
|
|
115
|
+
export { type Placement, type PositionResult, type Positioner, type PositionerFactory, type PositionerOptions, type Rect, SelkitDom as Selkit, SelkitDom, type SelkitDomConfig, type SelkitDomInstance, attachPositioner, computePosition, createSelkitDom, createSelkitDom as sk };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/dom.ts
|
|
2
2
|
import {
|
|
3
3
|
computeScrollIntoView,
|
|
4
|
+
computeScrollIntoViewVariable,
|
|
4
5
|
computeVirtualRange,
|
|
6
|
+
computeVirtualWindow,
|
|
5
7
|
createSelkit
|
|
6
8
|
} from "@selkit/core";
|
|
7
9
|
|
|
@@ -45,7 +47,9 @@ function attachPositioner(trigger, dropdown, autoWidth = false) {
|
|
|
45
47
|
// src/dom.ts
|
|
46
48
|
var LOAD_MORE_THRESHOLD = 32;
|
|
47
49
|
var DEFAULT_ITEM_HEIGHT = 36;
|
|
50
|
+
var DEFAULT_GROUP_HEIGHT = 28;
|
|
48
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);
|
|
49
53
|
function resolveParent(parent) {
|
|
50
54
|
if (!parent) return null;
|
|
51
55
|
if (typeof parent !== "string") return parent;
|
|
@@ -125,7 +129,9 @@ var SelkitDom = class {
|
|
|
125
129
|
#name;
|
|
126
130
|
#virtual;
|
|
127
131
|
#itemHeight;
|
|
132
|
+
#groupHeight;
|
|
128
133
|
#dropdownParent;
|
|
134
|
+
#positionerFactory;
|
|
129
135
|
#templateSelection;
|
|
130
136
|
#templateOption;
|
|
131
137
|
#templateArrow;
|
|
@@ -169,7 +175,9 @@ var SelkitDom = class {
|
|
|
169
175
|
this.#name = cfg.name;
|
|
170
176
|
this.#virtual = cfg.virtualScroll ?? false;
|
|
171
177
|
this.#itemHeight = cfg.itemHeight ?? DEFAULT_ITEM_HEIGHT;
|
|
178
|
+
this.#groupHeight = cfg.groupHeight ?? DEFAULT_GROUP_HEIGHT;
|
|
172
179
|
this.#dropdownParent = resolveParent(cfg.dropdownParent);
|
|
180
|
+
this.#positionerFactory = cfg.positioner ?? builtinPositioner;
|
|
173
181
|
this.#templateSelection = cfg.templateSelection;
|
|
174
182
|
this.#templateOption = cfg.templateOption;
|
|
175
183
|
this.#templateArrow = cfg.templateArrow;
|
|
@@ -430,7 +438,8 @@ var SelkitDom = class {
|
|
|
430
438
|
}
|
|
431
439
|
if (s.activeIndex === this.#lastActive || s.activeIndex < 0) return;
|
|
432
440
|
this.#lastActive = s.activeIndex;
|
|
433
|
-
const
|
|
441
|
+
const rows = this.controller.getGroupedView().rows;
|
|
442
|
+
const hasGroups = rows.some((r) => r.type === "group");
|
|
434
443
|
if (this.#virtual && !hasGroups) {
|
|
435
444
|
const next = computeScrollIntoView({
|
|
436
445
|
index: s.activeIndex,
|
|
@@ -444,6 +453,22 @@ var SelkitDom = class {
|
|
|
444
453
|
}
|
|
445
454
|
return;
|
|
446
455
|
}
|
|
456
|
+
if (this.#virtual && hasGroups) {
|
|
457
|
+
const rowIndex = rows.findIndex(
|
|
458
|
+
(r) => r.type !== "group" && r.index === s.activeIndex
|
|
459
|
+
);
|
|
460
|
+
const next = computeScrollIntoViewVariable({
|
|
461
|
+
heights: this.#rowHeights(rows),
|
|
462
|
+
rowIndex,
|
|
463
|
+
scrollTop: this.#dropdown.scrollTop,
|
|
464
|
+
viewportHeight: this.#dropdown.clientHeight
|
|
465
|
+
});
|
|
466
|
+
if (next !== null) {
|
|
467
|
+
this.#dropdown.scrollTop = next;
|
|
468
|
+
this.#renderOptions(s);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
447
472
|
const active = this.#dropdown.querySelector(
|
|
448
473
|
`.${this.#cls("option", "active")}`
|
|
449
474
|
);
|
|
@@ -561,40 +586,61 @@ var SelkitDom = class {
|
|
|
561
586
|
itemHeight: this.#itemHeight,
|
|
562
587
|
itemCount: view.rows.length
|
|
563
588
|
});
|
|
564
|
-
this.#
|
|
565
|
-
for (let i = range.startIndex; i < range.endIndex; i++) {
|
|
566
|
-
const row = view.rows[i];
|
|
567
|
-
if (row?.type === "option") {
|
|
568
|
-
this.#dropdown.append(this.#buildOption(row, a11y, s.activeIndex));
|
|
569
|
-
} else if (row?.type === "create") {
|
|
570
|
-
this.#dropdown.append(this.#buildCreateRow(row, a11y, s.activeIndex));
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
this.#dropdown.append(this.#spacer(range.paddingBottom));
|
|
589
|
+
this.#renderVirtualSlice(view.rows, range, a11y, s.activeIndex);
|
|
574
590
|
return;
|
|
575
591
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
592
|
+
if (this.#virtual && hasGroups) {
|
|
593
|
+
const win = computeVirtualWindow({
|
|
594
|
+
heights: this.#rowHeights(view.rows),
|
|
595
|
+
scrollTop: this.#dropdown.scrollTop,
|
|
596
|
+
viewportHeight: this.#dropdown.clientHeight
|
|
597
|
+
});
|
|
598
|
+
this.#renderVirtualSlice(view.rows, win, a11y, s.activeIndex);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
for (const row of view.rows) this.#renderRow(row, a11y, s.activeIndex);
|
|
602
|
+
}
|
|
603
|
+
/** 每列高度:分組標題用 groupHeight 其餘(option/create)用 itemHeight */
|
|
604
|
+
#rowHeights(rows) {
|
|
605
|
+
return rows.map(
|
|
606
|
+
(r) => r.type === "group" ? this.#groupHeight : this.#itemHeight
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
/** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
|
|
610
|
+
#renderVirtualSlice(rows, range, a11y, activeIndex) {
|
|
611
|
+
this.#dropdown.append(this.#spacer(range.paddingTop));
|
|
612
|
+
for (let i = range.startIndex; i < range.endIndex; i++) {
|
|
613
|
+
const row = rows[i];
|
|
614
|
+
if (row) this.#renderRow(row, a11y, activeIndex);
|
|
615
|
+
}
|
|
616
|
+
this.#dropdown.append(this.#spacer(range.paddingBottom));
|
|
617
|
+
}
|
|
618
|
+
/** 依列型別渲染並掛入下拉 group / create / option */
|
|
619
|
+
#renderRow(row, a11y, activeIndex) {
|
|
620
|
+
if (row.type === "group") {
|
|
621
|
+
this.#dropdown.append(this.#buildGroupRow(row));
|
|
622
|
+
return;
|
|
597
623
|
}
|
|
624
|
+
if (row.type === "create") {
|
|
625
|
+
this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
|
|
629
|
+
}
|
|
630
|
+
/** 分組標題列 套用 templateGroup(無則用 label)*/
|
|
631
|
+
#buildGroupRow(row) {
|
|
632
|
+
const group = document.createElement("div");
|
|
633
|
+
group.className = this.#cls("group");
|
|
634
|
+
if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
|
|
635
|
+
if (this.#templateGroup) {
|
|
636
|
+
this.#applyTemplate(
|
|
637
|
+
group,
|
|
638
|
+
this.#templateGroup({ label: row.label, disabled: !!row.disabled })
|
|
639
|
+
);
|
|
640
|
+
} else {
|
|
641
|
+
group.textContent = row.label;
|
|
642
|
+
}
|
|
643
|
+
return group;
|
|
598
644
|
}
|
|
599
645
|
/** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
|
|
600
646
|
#buildCreateRow(row, a11y, activeIndex) {
|
|
@@ -672,11 +718,9 @@ var SelkitDom = class {
|
|
|
672
718
|
this.#dropdown.hidden = false;
|
|
673
719
|
if (this.#positioner) this.#positioner.update();
|
|
674
720
|
else
|
|
675
|
-
this.#positioner =
|
|
676
|
-
this.#
|
|
677
|
-
|
|
678
|
-
this.#dropdownAutoWidth
|
|
679
|
-
);
|
|
721
|
+
this.#positioner = this.#positionerFactory(this.#control, this.#dropdown, {
|
|
722
|
+
autoWidth: this.#dropdownAutoWidth
|
|
723
|
+
});
|
|
680
724
|
} else {
|
|
681
725
|
this.#dropdown.hidden = true;
|
|
682
726
|
this.#positioner?.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selkit/dom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"axe-core": "^4.12.1",
|