@selkit/dom 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -72,6 +72,7 @@ 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";
76
77
  function resolveParent(parent) {
77
78
  if (!parent) return null;
@@ -152,6 +153,7 @@ var SelkitDom = class {
152
153
  #name;
153
154
  #virtual;
154
155
  #itemHeight;
156
+ #groupHeight;
155
157
  #dropdownParent;
156
158
  #templateSelection;
157
159
  #templateOption;
@@ -196,6 +198,7 @@ var SelkitDom = class {
196
198
  this.#name = cfg.name;
197
199
  this.#virtual = cfg.virtualScroll ?? false;
198
200
  this.#itemHeight = cfg.itemHeight ?? DEFAULT_ITEM_HEIGHT;
201
+ this.#groupHeight = cfg.groupHeight ?? DEFAULT_GROUP_HEIGHT;
199
202
  this.#dropdownParent = resolveParent(cfg.dropdownParent);
200
203
  this.#templateSelection = cfg.templateSelection;
201
204
  this.#templateOption = cfg.templateOption;
@@ -457,7 +460,8 @@ var SelkitDom = class {
457
460
  }
458
461
  if (s.activeIndex === this.#lastActive || s.activeIndex < 0) return;
459
462
  this.#lastActive = s.activeIndex;
460
- const hasGroups = this.controller.getGroupedView().rows.some((r) => r.type === "group");
463
+ const rows = this.controller.getGroupedView().rows;
464
+ const hasGroups = rows.some((r) => r.type === "group");
461
465
  if (this.#virtual && !hasGroups) {
462
466
  const next = (0, import_core.computeScrollIntoView)({
463
467
  index: s.activeIndex,
@@ -471,6 +475,22 @@ var SelkitDom = class {
471
475
  }
472
476
  return;
473
477
  }
478
+ if (this.#virtual && hasGroups) {
479
+ const rowIndex = rows.findIndex(
480
+ (r) => r.type !== "group" && r.index === s.activeIndex
481
+ );
482
+ const next = (0, import_core.computeScrollIntoViewVariable)({
483
+ heights: this.#rowHeights(rows),
484
+ rowIndex,
485
+ scrollTop: this.#dropdown.scrollTop,
486
+ viewportHeight: this.#dropdown.clientHeight
487
+ });
488
+ if (next !== null) {
489
+ this.#dropdown.scrollTop = next;
490
+ this.#renderOptions(s);
491
+ }
492
+ return;
493
+ }
474
494
  const active = this.#dropdown.querySelector(
475
495
  `.${this.#cls("option", "active")}`
476
496
  );
@@ -588,40 +608,61 @@ var SelkitDom = class {
588
608
  itemHeight: this.#itemHeight,
589
609
  itemCount: view.rows.length
590
610
  });
591
- this.#dropdown.append(this.#spacer(range.paddingTop));
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));
611
+ this.#renderVirtualSlice(view.rows, range, a11y, s.activeIndex);
601
612
  return;
602
613
  }
603
- for (const row of view.rows) {
604
- if (row.type === "group") {
605
- const group = document.createElement("div");
606
- group.className = this.#cls("group");
607
- if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
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
- }
616
- this.#dropdown.append(group);
617
- continue;
618
- }
619
- if (row.type === "create") {
620
- this.#dropdown.append(this.#buildCreateRow(row, a11y, s.activeIndex));
621
- continue;
622
- }
623
- this.#dropdown.append(this.#buildOption(row, a11y, s.activeIndex));
614
+ if (this.#virtual && hasGroups) {
615
+ const win = (0, import_core.computeVirtualWindow)({
616
+ heights: this.#rowHeights(view.rows),
617
+ scrollTop: this.#dropdown.scrollTop,
618
+ viewportHeight: this.#dropdown.clientHeight
619
+ });
620
+ this.#renderVirtualSlice(view.rows, win, a11y, s.activeIndex);
621
+ return;
622
+ }
623
+ for (const row of view.rows) this.#renderRow(row, a11y, s.activeIndex);
624
+ }
625
+ /** 每列高度:分組標題用 groupHeight 其餘(option/create)用 itemHeight */
626
+ #rowHeights(rows) {
627
+ return rows.map(
628
+ (r) => r.type === "group" ? this.#groupHeight : this.#itemHeight
629
+ );
630
+ }
631
+ /** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
632
+ #renderVirtualSlice(rows, range, a11y, activeIndex) {
633
+ this.#dropdown.append(this.#spacer(range.paddingTop));
634
+ for (let i = range.startIndex; i < range.endIndex; i++) {
635
+ const row = rows[i];
636
+ if (row) this.#renderRow(row, a11y, activeIndex);
637
+ }
638
+ this.#dropdown.append(this.#spacer(range.paddingBottom));
639
+ }
640
+ /** 依列型別渲染並掛入下拉 group / create / option */
641
+ #renderRow(row, a11y, activeIndex) {
642
+ if (row.type === "group") {
643
+ this.#dropdown.append(this.#buildGroupRow(row));
644
+ return;
645
+ }
646
+ if (row.type === "create") {
647
+ this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
648
+ return;
649
+ }
650
+ this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
651
+ }
652
+ /** 分組標題列 套用 templateGroup(無則用 label)*/
653
+ #buildGroupRow(row) {
654
+ const group = document.createElement("div");
655
+ group.className = this.#cls("group");
656
+ if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
657
+ if (this.#templateGroup) {
658
+ this.#applyTemplate(
659
+ group,
660
+ this.#templateGroup({ label: row.label, disabled: !!row.disabled })
661
+ );
662
+ } else {
663
+ group.textContent = row.label;
624
664
  }
665
+ return group;
625
666
  }
626
667
  /** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
627
668
  #buildCreateRow(row, a11y, activeIndex) {
package/dist/index.d.cts CHANGED
@@ -15,6 +15,8 @@ interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
15
15
  virtualScroll?: boolean;
16
16
  /** 虛擬捲動的單列固定高度 px 預設 36 須與實際樣式高度一致 */
17
17
  itemHeight?: number;
18
+ /** 虛擬捲動下分組標題列的固定高度 px 預設 28 須與實際樣式高度一致 */
19
+ groupHeight?: number;
18
20
  /** 把下拉浮層掛到指定容器(元素或選擇器)逃離 overflow/transform 祖先的裁切 常用 document.body */
19
21
  dropdownParent?: HTMLElement | string;
20
22
  /** 自訂已選顯示內容(tag 或單值) 回傳字串走 textContent 防 XSS 需 markup(icon 等)請回傳 Node */
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ interface SelkitDomConfig<T = unknown> extends SelkitConfig<T> {
15
15
  virtualScroll?: boolean;
16
16
  /** 虛擬捲動的單列固定高度 px 預設 36 須與實際樣式高度一致 */
17
17
  itemHeight?: number;
18
+ /** 虛擬捲動下分組標題列的固定高度 px 預設 28 須與實際樣式高度一致 */
19
+ groupHeight?: number;
18
20
  /** 把下拉浮層掛到指定容器(元素或選擇器)逃離 overflow/transform 祖先的裁切 常用 document.body */
19
21
  dropdownParent?: HTMLElement | string;
20
22
  /** 自訂已選顯示內容(tag 或單值) 回傳字串走 textContent 防 XSS 需 markup(icon 等)請回傳 Node */
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,6 +47,7 @@ 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";
49
52
  function resolveParent(parent) {
50
53
  if (!parent) return null;
@@ -125,6 +128,7 @@ var SelkitDom = class {
125
128
  #name;
126
129
  #virtual;
127
130
  #itemHeight;
131
+ #groupHeight;
128
132
  #dropdownParent;
129
133
  #templateSelection;
130
134
  #templateOption;
@@ -169,6 +173,7 @@ var SelkitDom = class {
169
173
  this.#name = cfg.name;
170
174
  this.#virtual = cfg.virtualScroll ?? false;
171
175
  this.#itemHeight = cfg.itemHeight ?? DEFAULT_ITEM_HEIGHT;
176
+ this.#groupHeight = cfg.groupHeight ?? DEFAULT_GROUP_HEIGHT;
172
177
  this.#dropdownParent = resolveParent(cfg.dropdownParent);
173
178
  this.#templateSelection = cfg.templateSelection;
174
179
  this.#templateOption = cfg.templateOption;
@@ -430,7 +435,8 @@ var SelkitDom = class {
430
435
  }
431
436
  if (s.activeIndex === this.#lastActive || s.activeIndex < 0) return;
432
437
  this.#lastActive = s.activeIndex;
433
- const hasGroups = this.controller.getGroupedView().rows.some((r) => r.type === "group");
438
+ const rows = this.controller.getGroupedView().rows;
439
+ const hasGroups = rows.some((r) => r.type === "group");
434
440
  if (this.#virtual && !hasGroups) {
435
441
  const next = computeScrollIntoView({
436
442
  index: s.activeIndex,
@@ -444,6 +450,22 @@ var SelkitDom = class {
444
450
  }
445
451
  return;
446
452
  }
453
+ if (this.#virtual && hasGroups) {
454
+ const rowIndex = rows.findIndex(
455
+ (r) => r.type !== "group" && r.index === s.activeIndex
456
+ );
457
+ const next = computeScrollIntoViewVariable({
458
+ heights: this.#rowHeights(rows),
459
+ rowIndex,
460
+ scrollTop: this.#dropdown.scrollTop,
461
+ viewportHeight: this.#dropdown.clientHeight
462
+ });
463
+ if (next !== null) {
464
+ this.#dropdown.scrollTop = next;
465
+ this.#renderOptions(s);
466
+ }
467
+ return;
468
+ }
447
469
  const active = this.#dropdown.querySelector(
448
470
  `.${this.#cls("option", "active")}`
449
471
  );
@@ -561,40 +583,61 @@ var SelkitDom = class {
561
583
  itemHeight: this.#itemHeight,
562
584
  itemCount: view.rows.length
563
585
  });
564
- this.#dropdown.append(this.#spacer(range.paddingTop));
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));
586
+ this.#renderVirtualSlice(view.rows, range, a11y, s.activeIndex);
574
587
  return;
575
588
  }
576
- for (const row of view.rows) {
577
- if (row.type === "group") {
578
- const group = document.createElement("div");
579
- group.className = this.#cls("group");
580
- if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
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
- }
589
- this.#dropdown.append(group);
590
- continue;
591
- }
592
- if (row.type === "create") {
593
- this.#dropdown.append(this.#buildCreateRow(row, a11y, s.activeIndex));
594
- continue;
595
- }
596
- this.#dropdown.append(this.#buildOption(row, a11y, s.activeIndex));
589
+ if (this.#virtual && hasGroups) {
590
+ const win = computeVirtualWindow({
591
+ heights: this.#rowHeights(view.rows),
592
+ scrollTop: this.#dropdown.scrollTop,
593
+ viewportHeight: this.#dropdown.clientHeight
594
+ });
595
+ this.#renderVirtualSlice(view.rows, win, a11y, s.activeIndex);
596
+ return;
597
+ }
598
+ for (const row of view.rows) this.#renderRow(row, a11y, s.activeIndex);
599
+ }
600
+ /** 每列高度:分組標題用 groupHeight 其餘(option/create)用 itemHeight */
601
+ #rowHeights(rows) {
602
+ return rows.map(
603
+ (r) => r.type === "group" ? this.#groupHeight : this.#itemHeight
604
+ );
605
+ }
606
+ /** 渲染虛擬切片:上下佔位 + range 內各列 共用扁平與分組兩路徑 */
607
+ #renderVirtualSlice(rows, range, a11y, activeIndex) {
608
+ this.#dropdown.append(this.#spacer(range.paddingTop));
609
+ for (let i = range.startIndex; i < range.endIndex; i++) {
610
+ const row = rows[i];
611
+ if (row) this.#renderRow(row, a11y, activeIndex);
612
+ }
613
+ this.#dropdown.append(this.#spacer(range.paddingBottom));
614
+ }
615
+ /** 依列型別渲染並掛入下拉 group / create / option */
616
+ #renderRow(row, a11y, activeIndex) {
617
+ if (row.type === "group") {
618
+ this.#dropdown.append(this.#buildGroupRow(row));
619
+ return;
620
+ }
621
+ if (row.type === "create") {
622
+ this.#dropdown.append(this.#buildCreateRow(row, a11y, activeIndex));
623
+ return;
624
+ }
625
+ this.#dropdown.append(this.#buildOption(row, a11y, activeIndex));
626
+ }
627
+ /** 分組標題列 套用 templateGroup(無則用 label)*/
628
+ #buildGroupRow(row) {
629
+ const group = document.createElement("div");
630
+ group.className = this.#cls("group");
631
+ if (row.disabled) group.classList.add(this.#cls("group", "disabled"));
632
+ if (this.#templateGroup) {
633
+ this.#applyTemplate(
634
+ group,
635
+ this.#templateGroup({ label: row.label, disabled: !!row.disabled })
636
+ );
637
+ } else {
638
+ group.textContent = row.label;
597
639
  }
640
+ return group;
598
641
  }
599
642
  /** 「建立新項」列 共用 option 樣式與 a11y 但點擊走 createTag */
600
643
  #buildCreateRow(row, a11y, activeIndex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selkit/dom",
3
- "version": "0.3.0",
3
+ "version": "0.4.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.3.0"
22
+ "@selkit/core": "0.4.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "axe-core": "^4.12.1",