@vueye-table/headless 3.0.0-beta.6 → 3.0.0-beta.8

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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useTableLocale, useVirtualColumns, useVirtualRows } from "@vueye-table/vue";
2
- import { Fragment, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, onScopeDispose, provide, ref, shallowRef, useId, watch } from "vue";
3
- import { formatValue, getRowItemKey, gridCommand } from "@vueye-table/core";
2
+ import { Fragment, cloneVNode, computed, defineComponent, getCurrentInstance, h, inject, isVNode, mergeProps, nextTick, onMounted, onScopeDispose, onUpdated, provide, ref, shallowRef, useId, watch } from "vue";
3
+ import { clampColumnWidth, formatValue, getRowItemKey, gridCommand } from "@vueye-table/core";
4
4
 
5
5
  //#region src/shared.ts
6
6
  /** Leading native utility cells, independent of data-column coordinates. */
@@ -370,173 +370,447 @@ const DataTableLoadMore = defineComponent({
370
370
  });
371
371
 
372
372
  //#endregion
373
- //#region src/editor.ts
374
- function editorText(value, kind) {
375
- if (value === null || value === void 0) return "";
376
- if (kind === "date" && value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString().slice(0, 10) : "";
377
- return formatValue(value);
373
+ //#region src/row-layout.ts
374
+ const groupingProps = { aggregatePosition: {
375
+ type: String,
376
+ default: "group"
377
+ } };
378
+ const layoutKey$1 = Symbol("vt-row-layout");
379
+ const pinnedKey = Symbol("vt-pinned-row");
380
+ function injectRowLayout() {
381
+ return inject(layoutKey$1, void 0);
378
382
  }
379
- const cellEditorProps = {
380
- editor: {
381
- type: Object,
382
- required: true
383
- },
384
- spec: {
385
- type: Object,
386
- default: () => ({ kind: "text" })
387
- },
388
- /** Keyboard movement is owned by the surrounding cell. */
389
- finish: {
390
- type: Function,
391
- default: void 0
392
- },
393
- autofocus: {
394
- type: Boolean,
395
- default: true
383
+ function injectPinnedRow() {
384
+ return inject(pinnedKey, void 0);
385
+ }
386
+ /** Presentation indices include group headers and optional footers; grid positions stay on leaves. */
387
+ function provideRowLayout(table, options, element) {
388
+ const measured = shallowRef(/* @__PURE__ */ new Map());
389
+ const headerHeight = shallowRef(0);
390
+ const targets = /* @__PURE__ */ new Map();
391
+ let head = null;
392
+ let observer;
393
+ const plan = computed(() => {
394
+ if (!table.grouped && table.topRows.length === 0 && table.bottomRows.length === 0) return {
395
+ items: table.renderItems,
396
+ footers: /* @__PURE__ */ new Map(),
397
+ indices: /* @__PURE__ */ new Map(),
398
+ levels: /* @__PURE__ */ new Map(),
399
+ structured: false
400
+ };
401
+ const items = [];
402
+ const footers = /* @__PURE__ */ new Map();
403
+ const stack = [];
404
+ const levels = /* @__PURE__ */ new Map();
405
+ let depth = 0;
406
+ const appendFooter = () => {
407
+ const group = stack.pop();
408
+ const key = JSON.stringify(["aggregate-footer", group.key]);
409
+ footers.set(key, group);
410
+ items.push(Object.freeze({
411
+ ...group,
412
+ key
413
+ }));
414
+ };
415
+ const footerMode = options.aggregatePosition === "footer" && table.columns.some((column) => column.aggregate);
416
+ for (const item of table.renderItems) {
417
+ if (item.kind === "group") depth = item.depth;
418
+ else levels.set(item.row.key, depth + 2);
419
+ if (footerMode && item.kind === "group") {
420
+ while (stack.length && stack.at(-1).depth >= item.depth) appendFooter();
421
+ if (item.isExpanded) stack.push(item);
422
+ }
423
+ items.push(item);
424
+ }
425
+ while (stack.length) appendFooter();
426
+ const structured = table.grouped || table.topRows.length > 0 || table.bottomRows.length > 0;
427
+ const indices = /* @__PURE__ */ new Map();
428
+ table.topRows.forEach((row, index) => indices.set(getRowItemKey(row.key), index + 2));
429
+ items.forEach((item, index) => indices.set(item.key, table.topRows.length + index + 2));
430
+ table.bottomRows.forEach((row, index) => indices.set(getRowItemKey(row.key), table.topRows.length + items.length + index + 2));
431
+ return {
432
+ items: Object.freeze(items),
433
+ footers,
434
+ indices,
435
+ structured,
436
+ levels
437
+ };
438
+ });
439
+ const size = (key) => measured.value.get(key) ?? (options.rowHeight > 0 ? options.rowHeight : 40);
440
+ const region = (side) => side === "top" ? table.topRows : table.bottomRows;
441
+ const height = (side) => region(side).reduce((sum, row) => sum + size(row.key), 0);
442
+ const measure = (target, key) => {
443
+ const value = target.getBoundingClientRect().height;
444
+ if (key === void 0) {
445
+ if (Number.isFinite(value)) headerHeight.value = Math.max(0, value);
446
+ } else if (value > 0 && Number.isFinite(value) && measured.value.get(key) !== value) measured.value = new Map(measured.value).set(key, value);
447
+ };
448
+ const refresh = () => {
449
+ const next = element()?.querySelector("thead") ?? null;
450
+ if (head !== next) {
451
+ if (head) observer?.unobserve(head);
452
+ head = next;
453
+ if (head) observer?.observe(head);
454
+ }
455
+ if (head) measure(head);
456
+ for (const [key, target] of targets) measure(target, key);
457
+ };
458
+ onMounted(() => {
459
+ const Observer = element()?.ownerDocument.defaultView?.ResizeObserver;
460
+ if (Observer) observer = new Observer((entries) => {
461
+ for (const { target } of entries) if (target === head) measure(target);
462
+ else for (const [key, value] of targets) if (value === target) measure(target, key);
463
+ });
464
+ refresh();
465
+ for (const [key, target] of targets) {
466
+ observer?.observe(target);
467
+ measure(target, key);
468
+ }
469
+ });
470
+ onUpdated(refresh);
471
+ onScopeDispose(() => {
472
+ observer?.disconnect();
473
+ targets.clear();
474
+ });
475
+ const layout = {
476
+ get aggregatePosition() {
477
+ return options.aggregatePosition;
478
+ },
479
+ get items() {
480
+ return plan.value.items;
481
+ },
482
+ get headerHeight() {
483
+ return headerHeight.value;
484
+ },
485
+ get topHeight() {
486
+ return height("top");
487
+ },
488
+ get bottomHeight() {
489
+ return height("bottom");
490
+ },
491
+ get count() {
492
+ return plan.value.structured ? table.topRows.length + plan.value.items.length + table.bottomRows.length + 1 : void 0;
493
+ },
494
+ index(key, kind = "row") {
495
+ return plan.value.structured ? plan.value.indices.get(kind === "group" ? key : getRowItemKey(key, kind)) : void 0;
496
+ },
497
+ level(key) {
498
+ return table.grouped ? plan.value.levels.get(key) ?? 1 : void 0;
499
+ },
500
+ footer: (key) => plan.value.footers.get(key),
501
+ register(key, target, owner) {
502
+ const previous = targets.get(key);
503
+ if (!target && owner && previous !== owner) return;
504
+ if (previous === target) return;
505
+ if (previous) observer?.unobserve(previous);
506
+ if (target) {
507
+ targets.set(key, target);
508
+ observer?.observe(target);
509
+ measure(target, key);
510
+ } else {
511
+ targets.delete(key);
512
+ if (measured.value.has(key)) {
513
+ const next = new Map(measured.value);
514
+ next.delete(key);
515
+ measured.value = next;
516
+ }
517
+ }
518
+ },
519
+ pinStyle(side, key, columnPinned) {
520
+ const rows = region(side);
521
+ const index = rows.findIndex((row) => row.key === key);
522
+ const offset = (side === "top" ? rows.slice(0, index) : rows.slice(index + 1)).reduce((sum, row) => sum + size(row.key), 0);
523
+ return {
524
+ position: "sticky",
525
+ [side === "top" ? "insetBlockStart" : "insetBlockEnd"]: `${offset + (side === "top" ? headerHeight.value : 0)}px`,
526
+ zIndex: columnPinned ? "5" : "3"
527
+ };
528
+ },
529
+ headerStyle(columnPinned) {
530
+ return table.topRows.length || table.bottomRows.length ? {
531
+ position: "sticky",
532
+ insetBlockStart: "0px",
533
+ zIndex: columnPinned ? "6" : "4"
534
+ } : void 0;
535
+ }
536
+ };
537
+ provide(layoutKey$1, layout);
538
+ return layout;
539
+ }
540
+ /** Apply region context to an ordinary data row without changing its cell/grid coordinates. */
541
+ const DataTablePinnedRow = defineComponent({
542
+ name: "DataTablePinnedRow",
543
+ props: {
544
+ row: rowProp,
545
+ side: {
546
+ type: String,
547
+ required: true
548
+ }
396
549
  },
397
- blurCommit: {
550
+ slots: Object,
551
+ setup(props, { slots }) {
552
+ const layout = injectRowLayout();
553
+ const instance = getCurrentInstance();
554
+ let current;
555
+ const register = () => {
556
+ const target = instance?.subTree.el;
557
+ if (target && "getBoundingClientRect" in target) {
558
+ current = target;
559
+ layout?.register(props.row.key, target);
560
+ }
561
+ };
562
+ onMounted(register);
563
+ onUpdated(register);
564
+ onScopeDispose(() => layout?.register(props.row.key, null, current));
565
+ provide(pinnedKey, {
566
+ get side() {
567
+ return props.side;
568
+ },
569
+ attrs: (columnPinned = false) => ({
570
+ "data-row-pinned": props.side,
571
+ style: layout?.pinStyle(props.side, props.row.key, columnPinned)
572
+ })
573
+ });
574
+ return () => {
575
+ const child = slots.default?.()[0];
576
+ return child ? cloneVNode(child, { "data-row-pinned": props.side }, true) : h("tr");
577
+ };
578
+ }
579
+ });
580
+
581
+ //#endregion
582
+ //#region src/column-layout.ts
583
+ const resizingProps = {
584
+ resizableColumns: {
398
585
  type: Boolean,
399
- default: true
586
+ default: false
400
587
  },
401
- tabCommit: {
402
- type: Boolean,
403
- default: true
588
+ /** Pixel widths of leading utility columns; unspecified widths default to 48. */
589
+ leadingColumnWidths: {
590
+ type: Array,
591
+ default: void 0
404
592
  }
405
593
  };
406
- /** Plain native fields. Select results are searched and capped at 50 mounted options. */
407
- const DataCellEditor = defineComponent({
408
- name: "DataCellEditor",
409
- props: cellEditorProps,
594
+ const layoutKey = Symbol("vt-column-layout");
595
+ const headerKey = Symbol("vt-header-column");
596
+ function provideHeaderColumn(column) {
597
+ provide(headerKey, column);
598
+ }
599
+ function injectColumnLayout() {
600
+ return inject(layoutKey, void 0);
601
+ }
602
+ function effectiveColumnWidth(column, fallback = 120) {
603
+ return Math.max(1, clampColumnWidth(column, column.width ?? fallback));
604
+ }
605
+ /** CSS-only preview keeps one gesture to one engine operation. */
606
+ function provideColumnLayout(table, options, element) {
607
+ const columns = computed(() => table.columns);
608
+ const previewVariables = /* @__PURE__ */ new Set();
609
+ const variable = (column) => `--vt-column-${columns.value.findIndex((item) => item.id === column.id)}-width`;
610
+ const sum = (values) => values.length ? `calc(${values.join(" + ")})` : "0px";
611
+ const layout = {
612
+ enabled: () => options.resizableColumns && columns.value.some((column) => column.resizable) || columns.value.some((column) => column.pinned),
613
+ resizable: () => options.resizableColumns,
614
+ effectiveWidth: (column) => effectiveColumnWidth(column, options.columnWidth ?? 120),
615
+ width: (column, fallback = layout.effectiveWidth(column)) => `var(${variable(column)}, ${fallback}px)`,
616
+ leadingWidths: () => Array.from({ length: options.leadingColumns }, (_, index) => {
617
+ const width = options.leadingColumnWidths?.[index] ?? (options.gutter ? options.gutter / Math.max(1, options.leadingColumns) : 48);
618
+ return Number.isFinite(width) && width > 0 ? width : 48;
619
+ }),
620
+ attrs(column, header = false) {
621
+ if (!layout.enabled()) return {};
622
+ const style = {};
623
+ if (column.width !== void 0) style["width"] = `${column.width}px`;
624
+ if (column.minWidth !== void 0) style["minWidth"] = `${column.minWidth}px`;
625
+ if (layout.enabled()) style["width"] = layout.width(column);
626
+ if (column.pinned) {
627
+ const pins = columns.value.filter((item) => item.pinned === column.pinned);
628
+ const index = pins.findIndex((item) => item.id === column.id);
629
+ const previous = column.pinned === "start" ? pins.slice(0, index) : pins.slice(index + 1);
630
+ style["position"] = "sticky";
631
+ style[column.pinned === "start" ? "insetInlineStart" : "insetInlineEnd"] = sum([...column.pinned === "start" ? layout.leadingWidths().map((width) => `${width}px`) : [], ...previous.map((item) => layout.width(item))]);
632
+ style["zIndex"] = header ? "4" : "2";
633
+ }
634
+ return {
635
+ "data-pinned": column.pinned,
636
+ "data-pinned-edge": flag(column.isPinnedEdge),
637
+ "data-resizable": header ? flag(options.resizableColumns && column.resizable) : void 0,
638
+ style
639
+ };
640
+ },
641
+ leading(index, header) {
642
+ const pinned = columns.value.some((column) => column.pinned);
643
+ return {
644
+ "data-leading-column": index,
645
+ "data-pinned": pinned ? "start" : void 0,
646
+ "data-pinned-edge": flag(pinned && !columns.value.some((column) => column.pinned === "start") && index === options.leadingColumns - 1),
647
+ style: pinned ? {
648
+ position: "sticky",
649
+ insetInlineStart: `${layout.leadingWidths().slice(0, index).reduce((a, b) => a + b, 0)}px`,
650
+ zIndex: header ? "4" : "2"
651
+ } : void 0
652
+ };
653
+ },
654
+ style(total) {
655
+ if (!layout.enabled() && total === void 0) return void 0;
656
+ return {
657
+ tableLayout: "fixed",
658
+ width: total === void 0 ? sum([...layout.leadingWidths().map((value) => `${value}px`), ...columns.value.map((column) => layout.width(column))]) : `calc(${total}px + var(--vt-resize-delta, 0px))`
659
+ };
660
+ },
661
+ preview(column, width) {
662
+ element()?.style.setProperty(variable(column), `${width}px`);
663
+ previewVariables.add(variable(column));
664
+ element()?.style.setProperty("--vt-resize-delta", `${width - layout.effectiveWidth(column)}px`);
665
+ },
666
+ clear() {
667
+ for (const name of previewVariables) element()?.style.removeProperty(name);
668
+ previewVariables.clear();
669
+ element()?.style.removeProperty("--vt-resize-delta");
670
+ },
671
+ colgroup() {
672
+ return layout.enabled() ? h("colgroup", [...layout.leadingWidths().map((width) => h("col", { style: { width: `${width}px` } })), ...columns.value.map((column) => h("col", {
673
+ key: column.id,
674
+ style: { width: layout.width(column) }
675
+ }))]) : null;
676
+ }
677
+ };
678
+ provide(layoutKey, layout);
679
+ return layout;
680
+ }
681
+ /** Leading selection, disclosure or row-number cells share start-region offsets. */
682
+ const DataTableLeadingCell = defineComponent({
683
+ name: "DataTableLeadingCell",
684
+ props: {
685
+ as: asProp("td"),
686
+ index: {
687
+ type: Number,
688
+ default: 0
689
+ }
690
+ },
691
+ slots: Object,
692
+ setup(props, { slots, attrs }) {
693
+ const layout = injectColumnLayout();
694
+ const rows = injectRowLayout();
695
+ const pin = injectPinnedRow();
696
+ return () => {
697
+ const header = props.as === "th" && attrs["scope"] !== "row";
698
+ const leading = layout?.leading(props.index, header) ?? {};
699
+ return h(props.as, mergeProps(leading, { style: header ? rows?.headerStyle(!!leading["data-pinned"]) : void 0 }, pin?.attrs(!!leading["data-pinned"]) ?? {}), slots.default?.());
700
+ };
701
+ }
702
+ });
703
+ /** A keyboard-operable separator; pointer movement previews and release commits once. */
704
+ const DataTableColumnResizer = defineComponent({
705
+ name: "DataTableColumnResizer",
706
+ props: { column: {
707
+ type: Object,
708
+ default: void 0
709
+ } },
410
710
  setup(props) {
711
+ const table = injectDataTable("<DataTableColumnResizer>");
712
+ const header = inject(headerKey, void 0);
713
+ const layout = injectColumnLayout();
411
714
  const locale = useTableLocale();
412
- const field = ref();
413
- const query = ref("");
414
- const options = props.spec.options ?? [];
415
- const matches = computed(() => {
416
- const search = query.value.trim().toLocaleLowerCase();
417
- return options.map((value, index) => ({
418
- value,
419
- index
420
- })).filter(({ value }) => String(value ?? "").toLocaleLowerCase().includes(search));
421
- });
422
- onMounted(() => {
423
- if (!props.autofocus) return;
424
- field.value?.focus();
425
- if (field.value && "setSelectionRange" in field.value && ["text", "number"].includes(props.spec.kind)) field.value.setSelectionRange(field.value.value.length, field.value.value.length);
426
- });
427
- const finish = (direction) => {
428
- if (props.finish) props.finish(direction);
429
- else props.editor.commit();
715
+ const preview = shallowRef();
716
+ let drag;
717
+ const column = () => props.column ?? header?.();
718
+ const currentWidth = (value) => layout?.effectiveWidth(value) ?? effectiveColumnWidth(value);
719
+ const stop = (commit = false) => {
720
+ const current = drag;
721
+ if (!current) return;
722
+ drag = void 0;
723
+ const width = preview.value;
724
+ if (commit && width !== void 0) table.resizeColumn(current.column.id, width);
725
+ layout?.clear(current.column);
726
+ preview.value = void 0;
727
+ if (current.target.hasPointerCapture?.(current.pointer)) current.target.releasePointerCapture(current.pointer);
430
728
  };
431
- const keydown = (event) => {
432
- if (event.isComposing) return;
433
- if (event.key === "Escape") {
434
- event.preventDefault();
435
- event.stopPropagation();
436
- if (!props.editor.pending) props.editor.cancel();
437
- } else if (event.key === "Enter" || event.key === "Tab") {
438
- if (event.key === "Tab" && !props.tabCommit) return;
439
- event.preventDefault();
440
- event.stopPropagation();
441
- if (!props.editor.pending) finish(event.key === "Tab" ? event.shiftKey ? "left" : "right" : event.shiftKey ? "up" : "down");
442
- }
443
- };
444
- const paste = (event) => {
445
- const text = event.clipboardData?.getData("text/plain");
446
- if (text === void 0 || props.editor.pending) return;
447
- event.preventDefault();
448
- const target = event.target;
449
- const start = target.selectionStart ?? 0;
450
- const end = target.selectionEnd ?? target.value.length;
451
- props.editor.input(target.value.slice(0, start) + text + target.value.slice(end));
729
+ const move = (event) => {
730
+ if (!drag || drag.pointer !== event.pointerId) return;
731
+ preview.value = Math.max(1, clampColumnWidth(drag.column, drag.width + (event.clientX - drag.x) * drag.sign));
732
+ layout?.preview(drag.column, preview.value);
452
733
  };
734
+ watch(() => table.columns, () => stop(), { flush: "sync" });
735
+ onScopeDispose(() => stop());
453
736
  return () => {
454
- const { editor, spec } = props;
455
- const { messages, count } = locale();
456
- const attrs = {
457
- ...editor.attrs,
458
- "aria-label": messages.editCell(editor.column.header),
459
- "data-editor": "",
460
- disabled: editor.pending,
461
- onKeydown: keydown
462
- };
463
- if (spec.kind === "select") {
464
- const shown = matches.value.slice(0, 50);
465
- return h("div", {
466
- "data-select-editor": "",
467
- onKeydown: (event) => {
468
- if (event.key === "Escape") keydown(event);
737
+ const current = column();
738
+ if (!current?.resizable) return null;
739
+ return h("span", {
740
+ role: "separator",
741
+ tabindex: 0,
742
+ "aria-orientation": "vertical",
743
+ "aria-label": locale().messages.resizeColumn(current.header),
744
+ "aria-description": locale().messages.resizeColumnHelp,
745
+ "aria-valuenow": preview.value ?? currentWidth(current),
746
+ "aria-valuemin": Math.max(1, current.minWidth ?? 1),
747
+ "aria-valuemax": current.maxWidth === void 0 ? void 0 : Math.max(1, current.minWidth ?? 1, current.maxWidth),
748
+ "data-column-resizer": "",
749
+ "data-resizing": flag(preview.value !== void 0),
750
+ style: {
751
+ touchAction: "none",
752
+ cursor: "col-resize",
753
+ userSelect: "none"
754
+ },
755
+ onClick: (event) => event.stopPropagation(),
756
+ onDblclick: (event) => {
757
+ event.stopPropagation();
758
+ stop();
759
+ table.resetColumnSize(current.id);
760
+ },
761
+ onPointerdown: (event) => {
762
+ if (event.button !== 0 || drag || !layout) return;
763
+ const target = event.currentTarget;
764
+ target.focus({ preventScroll: true });
765
+ target.setPointerCapture?.(event.pointerId);
766
+ event.preventDefault();
767
+ event.stopPropagation();
768
+ drag = {
769
+ target,
770
+ pointer: event.pointerId,
771
+ x: event.clientX,
772
+ width: currentWidth(current),
773
+ column: current,
774
+ sign: directionOf(target) === "rtl" ? -1 : 1
775
+ };
776
+ preview.value = drag.width;
777
+ },
778
+ onPointermove: move,
779
+ onPointerup: (event) => {
780
+ if (drag?.pointer === event.pointerId) {
781
+ move(event);
782
+ stop(true);
469
783
  }
470
- }, [
471
- h("input", {
472
- ref: field,
473
- type: "search",
474
- value: query.value,
475
- "aria-label": messages.searchOptions(editor.column.header),
476
- disabled: editor.pending,
477
- onInput: (event) => {
478
- query.value = event.target.value;
479
- },
480
- onKeydown: (event) => {
481
- if (event.key === "Enter" && shown[0]) {
482
- event.preventDefault();
483
- editor.commit(shown[0].value);
484
- } else keydown(event);
485
- }
486
- }),
487
- h("select", {
488
- ...attrs,
489
- value: options.findIndex((value) => Object.is(value, editor.value)),
490
- size: Math.max(2, Math.min(5, shown.length)),
491
- onChange: (event) => {
492
- const index = Number(event.target.value);
493
- if (index >= 0) editor.commit(options[index]);
494
- }
495
- }, shown.map(({ value, index }) => h("option", {
496
- key: index,
497
- value: index
498
- }, value === null || value === void 0 ? messages.emptyOption : String(value)))),
499
- h("span", {
500
- role: "status",
501
- "data-editor-hint": ""
502
- }, matches.value.length === 0 ? messages.noMatchingOptions : matches.value.length > 50 ? messages.firstMatchesOnly(count(50)) : messages.optionCount(count(matches.value.length)))
503
- ]);
504
- }
505
- return h("input", {
506
- ...attrs,
507
- ref: field,
508
- type: spec.kind === "date" ? "date" : spec.kind === "checkbox" ? "checkbox" : "text",
509
- inputmode: spec.kind === "number" ? "decimal" : void 0,
510
- value: editor.draft,
511
- checked: spec.kind === "checkbox" ? editor.value === true : void 0,
512
- onInput: spec.kind === "checkbox" ? void 0 : (event) => editor.input(event.target.value),
513
- onChange: spec.kind === "checkbox" ? (event) => editor.commit(event.target.checked) : void 0,
514
- onBlur: () => {
515
- if (props.blurCommit && !editor.pending) finish();
516
784
  },
517
- onPaste: spec.kind === "checkbox" ? void 0 : paste
785
+ onPointercancel: (event) => {
786
+ if (drag?.pointer === event.pointerId) stop();
787
+ },
788
+ onLostpointercapture: (event) => {
789
+ if (drag?.pointer === event.pointerId) stop();
790
+ },
791
+ onKeydown: (event) => {
792
+ if (event.key === "Escape" && drag) {
793
+ event.preventDefault();
794
+ event.stopPropagation();
795
+ stop();
796
+ return;
797
+ }
798
+ if (drag) return;
799
+ let width;
800
+ const sign = directionOf(event.currentTarget) === "rtl" ? -1 : 1;
801
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") width = currentWidth(current) + (event.key === "ArrowRight" ? 1 : -1) * sign * (event.shiftKey ? 50 : 10);
802
+ else if (event.key === "Home") width = Math.max(1, current.minWidth ?? 1);
803
+ else if (event.key === "End") width = current.maxWidth;
804
+ else if (event.key === "Enter") table.resetColumnSize(current.id);
805
+ else return;
806
+ event.preventDefault();
807
+ event.stopPropagation();
808
+ if (width !== void 0) table.resizeColumn(current.id, Math.max(1, width));
809
+ }
518
810
  });
519
811
  };
520
812
  }
521
813
  });
522
- /** The cell owns the id so it remains stable while its editor mounts and unmounts. */
523
- function issueAttrs(id, issues, pending) {
524
- return {
525
- "aria-invalid": issues.length ? "true" : void 0,
526
- "aria-describedby": issues.length ? id : void 0,
527
- "aria-busy": pending ? "true" : void 0
528
- };
529
- }
530
- function editorIssues(id, issues, pending = false) {
531
- return issues.length || pending ? h("span", {
532
- id,
533
- "data-edit-issues": "",
534
- role: "status"
535
- }, pending ? "Checking changes…" : issues.map((problem) => problem.message).join(" ")) : null;
536
- }
537
- function useEditorId() {
538
- return `vt-edit-${useId()}`;
539
- }
540
814
 
541
815
  //#endregion
542
816
  //#region src/virtual.ts
@@ -608,21 +882,27 @@ const DataTableViewport = defineComponent({
608
882
  });
609
883
  function createComponentVirtual(table, options, tableElement, grid, virtualColumns = false, gutter = 0) {
610
884
  const scrollElement = inject(viewportKey, shallowRef(null));
885
+ const layout = injectRowLayout();
611
886
  const rows = useVirtualRows(table, {
612
887
  scrollElement,
613
888
  estimateRowHeight: options.rowHeight,
614
889
  overscan: options.overscan,
615
890
  initialCount: options.initialCount,
616
891
  grid,
617
- scrollMargin: () => tableElement()?.querySelector("thead")?.getBoundingClientRect().height ?? 0
892
+ renderItems: () => layout?.items ?? table.renderItems,
893
+ pinnedRowHeights: () => ({
894
+ top: layout?.topHeight ?? 0,
895
+ bottom: layout?.bottomHeight ?? 0
896
+ }),
897
+ scrollMargin: () => layout?.headerHeight ?? tableElement()?.querySelector("thead")?.getBoundingClientRect().height ?? 0
618
898
  });
619
899
  onMounted(() => {
620
900
  watch(() => [
621
901
  rows.endIndex,
622
- table.renderItems.length,
902
+ layout?.items.length ?? table.renderItems.length,
623
903
  table.canLoadMore
624
904
  ], () => {
625
- if (table.loadingMode === "cursor" && table.canLoadMore && rows.endIndex >= table.renderItems.length - 1 - table.endThreshold) table.loadNext();
905
+ if (table.loadingMode === "cursor" && table.canLoadMore && rows.endIndex >= (layout?.items.length ?? table.renderItems.length) - 1 - table.endThreshold) table.loadNext();
626
906
  }, { immediate: true });
627
907
  });
628
908
  const columns = grid && virtualColumns ? useVirtualColumns(grid, {
@@ -632,22 +912,27 @@ function createComponentVirtual(table, options, tableElement, grid, virtualColum
632
912
  initialCount: options.initialColumnCount ?? 5,
633
913
  scrollMargin: gutter
634
914
  }) : void 0;
635
- const pinnedRows = computed(() => {
915
+ const retainedRows = computed(() => {
636
916
  const result = [...rows.items];
637
917
  const positions = /* @__PURE__ */ new Set([grid?.selection?.focus.row, grid?.editor?.position.row]);
638
918
  for (const position of positions) {
639
919
  if (position === void 0 || result.some((item) => item.renderItem.kind === "row" && item.renderItem.rowIndex === position)) continue;
640
- const direct = table.renderItems[position];
641
- const index = direct?.kind === "row" && direct.rowIndex === position ? position : table.renderItems.findIndex((item) => item.kind === "row" && item.rowIndex === position);
920
+ const items = layout?.items ?? table.renderItems;
921
+ const direct = items[position];
922
+ const index = direct?.kind === "row" && direct.rowIndex === position ? position : items.findIndex((item) => item.kind === "row" && item.rowIndex === position);
642
923
  const item = rows.getItem(index);
643
924
  if (item) result.push(item);
644
925
  }
645
926
  return result.sort((a, b) => a.index - b.index);
646
927
  });
647
- const pinnedColumns = computed(() => {
928
+ const retainedColumns = computed(() => {
648
929
  if (!columns) return void 0;
649
930
  const result = [...columns.items];
650
- const positions = /* @__PURE__ */ new Set([grid?.selection?.focus.column, grid?.editor?.position.column]);
931
+ const positions = /* @__PURE__ */ new Set([
932
+ grid?.selection?.focus.column,
933
+ grid?.editor?.position.column,
934
+ ...table.grouped ? [0] : []
935
+ ]);
651
936
  for (const position of positions) {
652
937
  if (position === void 0 || result.some((item) => item.index === position)) continue;
653
938
  const item = columns.getItem(position);
@@ -660,10 +945,10 @@ function createComponentVirtual(table, options, tableElement, grid, virtualColum
660
945
  columns,
661
946
  gutter,
662
947
  get rowItems() {
663
- return pinnedRows.value;
948
+ return retainedRows.value;
664
949
  },
665
950
  get columnItems() {
666
- return pinnedColumns.value;
951
+ return retainedColumns.value;
667
952
  }
668
953
  };
669
954
  provide(virtualKey, binding);
@@ -724,55 +1009,357 @@ function rowSpacer(size, colspan, key) {
724
1009
  }
725
1010
  })]) : null;
726
1011
  }
727
- function columnSpacer(size, tag, key) {
728
- return size > 0 ? h(tag, {
729
- key,
730
- "aria-hidden": "true",
731
- role: "presentation",
732
- "data-virtual-spacer": "",
733
- style: {
734
- width: `${size}px`,
735
- minWidth: `${size}px`,
736
- padding: "0",
737
- border: "0"
738
- }
739
- }) : null;
1012
+ function columnSpacer(size, tag, key) {
1013
+ return size > 0 ? h(tag, {
1014
+ key,
1015
+ "aria-hidden": "true",
1016
+ role: "presentation",
1017
+ "data-virtual-spacer": "",
1018
+ style: {
1019
+ width: `${size}px`,
1020
+ minWidth: `${size}px`,
1021
+ padding: "0",
1022
+ border: "0"
1023
+ }
1024
+ }) : null;
1025
+ }
1026
+ function renderColumns(tableColumns, binding, render, tag) {
1027
+ if (!binding?.columns || !binding.columnItems) return tableColumns.map(render);
1028
+ const result = [];
1029
+ let end = 0;
1030
+ for (const item of binding.columnItems) {
1031
+ result.push(columnSpacer(item.start - end, tag, `gap-${item.index}`), render(item.column, item.index));
1032
+ end = item.start + item.size;
1033
+ }
1034
+ result.push(columnSpacer(binding.columns.totalSize - end, tag, "gap-end"));
1035
+ return result;
1036
+ }
1037
+ /** Native colspans count rendered cells and spacers, rather than logical omitted columns. */
1038
+ function physicalColumnCount(binding, count, leading = 0) {
1039
+ if (!binding?.columns || !binding.columnItems) return Math.max(1, count + leading);
1040
+ let end = 0;
1041
+ let size = leading;
1042
+ for (const item of binding.columnItems) {
1043
+ if (item.start > end) size++;
1044
+ size++;
1045
+ end = item.start + item.size;
1046
+ }
1047
+ if (binding.columns.totalSize > end) size++;
1048
+ return Math.max(1, size);
1049
+ }
1050
+ function renderRows(binding, colspan, render) {
1051
+ const result = [];
1052
+ let end = 0;
1053
+ for (const item of binding.rowItems) {
1054
+ result.push(rowSpacer(item.start - end, colspan, `gap-${item.index}`), render(item));
1055
+ end = item.start + item.size;
1056
+ }
1057
+ result.push(rowSpacer(binding.rows.totalSize - end, colspan, "gap-end"));
1058
+ return result;
1059
+ }
1060
+ function virtualColgroup(tableColumns, binding, width, leadingWidths) {
1061
+ if (!binding.columns || !binding.columnItems) return null;
1062
+ const cols = leadingWidths?.length ? leadingWidths.map((size) => h("col", { style: { width: `${size}px` } })) : binding.gutter ? [h("col", { style: { width: `${binding.gutter}px` } })] : [];
1063
+ let end = 0;
1064
+ for (const item of binding.columnItems) {
1065
+ if (item.start > end) cols.push(h("col", { style: { width: `${item.start - end}px` } }));
1066
+ cols.push(h("col", { style: { width: width?.(item.column, item.size) ?? `${item.size}px` } }));
1067
+ end = item.start + item.size;
1068
+ }
1069
+ if (binding.columns.totalSize > end) cols.push(h("col", { style: { width: `${binding.columns.totalSize - end}px` } }));
1070
+ return h("colgroup", cols);
1071
+ }
1072
+ function virtualRow(binding, row) {
1073
+ return binding.rowItems.find((item) => item.renderItem.kind === "row" && item.renderItem.row.key === row.key);
1074
+ }
1075
+
1076
+ //#endregion
1077
+ //#region src/grouped-rows.ts
1078
+ const groupProp = {
1079
+ type: Object,
1080
+ required: true
1081
+ };
1082
+ const DataTableAggregateCell = defineComponent({
1083
+ name: "DataTableAggregateCell",
1084
+ props: {
1085
+ group: groupProp,
1086
+ column: columnProp,
1087
+ as: asProp("td")
1088
+ },
1089
+ slots: Object,
1090
+ setup(props, { slots }) {
1091
+ const layout = injectColumnLayout();
1092
+ const table = injectDataTable("<DataTableAggregateCell>");
1093
+ const offset = injectColumnOffset();
1094
+ return () => h(props.as, {
1095
+ "data-aggregate": props.column.id,
1096
+ "aria-readonly": props.as === "td" ? "true" : void 0,
1097
+ "data-align": props.column.align,
1098
+ "aria-colindex": props.as === "td" ? table.columns.indexOf(props.column) + offset() + 1 : void 0,
1099
+ ...props.as === "td" ? layout?.attrs(props.column) : {}
1100
+ }, props.column.aggregate ? slots.default?.({
1101
+ group: props.group,
1102
+ column: props.column,
1103
+ value: props.group.getAggregate(props.column.id),
1104
+ display: props.group.getAggregateDisplay(props.column.id)
1105
+ }) ?? props.group.getAggregateDisplay(props.column.id) : void 0);
1106
+ }
1107
+ });
1108
+ /** A read-only structural row; aggregate cells use the same native column layout as data cells. */
1109
+ const DataTableGroupRow = defineComponent({
1110
+ name: "DataTableGroupRow",
1111
+ props: {
1112
+ group: groupProp,
1113
+ footer: {
1114
+ type: Boolean,
1115
+ default: false
1116
+ },
1117
+ itemKey: {
1118
+ type: String,
1119
+ default: void 0
1120
+ },
1121
+ aggregates: {
1122
+ type: Boolean,
1123
+ default: true
1124
+ }
1125
+ },
1126
+ slots: Object,
1127
+ setup(props, { slots }) {
1128
+ const table = injectDataTable("<DataTableGroupRow>");
1129
+ const locale = useTableLocale();
1130
+ const offset = injectColumnOffset();
1131
+ const virtual = injectVirtual();
1132
+ const layout = injectRowLayout();
1133
+ return () => {
1134
+ const { group } = props;
1135
+ const label = locale().messages.groupLabel(table.getColumn(group.columnId)?.header ?? group.columnId, String(group.value), locale().count(group.leafCount));
1136
+ const toggle = () => table.toggleGroup(group.key);
1137
+ const content = props.footer ? locale().messages.groupTotals(label) : [table.selectionMode !== "none" ? h("input", {
1138
+ type: "checkbox",
1139
+ checked: group.selection === "all",
1140
+ indeterminate: group.selection === "some",
1141
+ "aria-label": locale().messages.selectGroup(label),
1142
+ onChange: (event) => table.toggleGroupSelection(group.key, event.target.checked)
1143
+ }) : null, h("button", {
1144
+ type: "button",
1145
+ "aria-expanded": String(group.isExpanded),
1146
+ "aria-label": (group.isExpanded ? locale().messages.collapseGroup : locale().messages.expandGroup)(label),
1147
+ onClick: toggle,
1148
+ onKeydown: (event) => event.stopPropagation()
1149
+ }, slots.group?.({
1150
+ group,
1151
+ label,
1152
+ toggle
1153
+ }) ?? label)];
1154
+ const aggregate = (column, as = "td") => h(DataTableAggregateCell, {
1155
+ key: column.id,
1156
+ group,
1157
+ column,
1158
+ as
1159
+ }, { default: slots[`aggregate.${column.id}`] });
1160
+ const hasAggregates = props.aggregates && table.columns.some((column) => column.aggregate);
1161
+ const labelCell = h("td", {
1162
+ "data-group-label": "",
1163
+ "aria-readonly": "true",
1164
+ colspan: hasAggregates ? offset() + 1 : physicalColumnCount(virtual, table.columns.length, offset()),
1165
+ style: { "--vt-group-depth": group.depth }
1166
+ }, [h("div", { class: "vt-group-label-content" }, content), hasAggregates && table.columns[0]?.aggregate ? aggregate(table.columns[0], "span") : null]);
1167
+ const cells = hasAggregates ? [labelCell, ...renderColumns(table.columns, virtual, (column, index) => index === 0 ? null : aggregate(column), "td")] : [labelCell];
1168
+ const key = props.itemKey ?? group.key;
1169
+ return h("tr", {
1170
+ role: "row",
1171
+ "data-group": group.key,
1172
+ "data-group-depth": group.depth,
1173
+ "data-aggregate-footer": props.footer ? "" : void 0,
1174
+ "aria-level": group.depth + 1,
1175
+ "aria-expanded": props.footer ? void 0 : String(group.isExpanded),
1176
+ "aria-rowindex": layout?.index(key, "group"),
1177
+ ...virtual ? {
1178
+ "data-virtual-row": "",
1179
+ ref: (target) => virtual.rows.measureElement(target, key)
1180
+ } : {}
1181
+ }, cells);
1182
+ };
1183
+ }
1184
+ });
1185
+ function groupRow(item, slots, aggregatePosition, footer) {
1186
+ return h(DataTableGroupRow, {
1187
+ key: item.key,
1188
+ itemKey: item.key,
1189
+ group: footer ?? item,
1190
+ footer: !!footer,
1191
+ aggregates: !!footer || aggregatePosition === "group",
1192
+ class: "vt-group-row"
1193
+ }, slots);
1194
+ }
1195
+
1196
+ //#endregion
1197
+ //#region src/editor.ts
1198
+ function editorText(value, kind) {
1199
+ if (value === null || value === void 0) return "";
1200
+ if (kind === "date" && value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString().slice(0, 10) : "";
1201
+ return formatValue(value);
740
1202
  }
741
- function renderColumns(tableColumns, binding, render, tag) {
742
- if (!binding?.columns || !binding.columnItems) return tableColumns.map(render);
743
- const result = [];
744
- let end = 0;
745
- for (const item of binding.columnItems) {
746
- result.push(columnSpacer(item.start - end, tag, `gap-${item.index}`), render(item.column, item.index));
747
- end = item.start + item.size;
1203
+ const cellEditorProps = {
1204
+ editor: {
1205
+ type: Object,
1206
+ required: true
1207
+ },
1208
+ spec: {
1209
+ type: Object,
1210
+ default: () => ({ kind: "text" })
1211
+ },
1212
+ /** Keyboard movement is owned by the surrounding cell. */
1213
+ finish: {
1214
+ type: Function,
1215
+ default: void 0
1216
+ },
1217
+ autofocus: {
1218
+ type: Boolean,
1219
+ default: true
1220
+ },
1221
+ blurCommit: {
1222
+ type: Boolean,
1223
+ default: true
1224
+ },
1225
+ tabCommit: {
1226
+ type: Boolean,
1227
+ default: true
748
1228
  }
749
- result.push(columnSpacer(binding.columns.totalSize - end, tag, "gap-end"));
750
- return result;
751
- }
752
- function renderRows(binding, colspan, render) {
753
- const result = [];
754
- let end = 0;
755
- for (const item of binding.rowItems) {
756
- result.push(rowSpacer(item.start - end, colspan, `gap-${item.index}`), render(item));
757
- end = item.start + item.size;
1229
+ };
1230
+ /** Plain native fields. Select results are searched and capped at 50 mounted options. */
1231
+ const DataCellEditor = defineComponent({
1232
+ name: "DataCellEditor",
1233
+ props: cellEditorProps,
1234
+ setup(props) {
1235
+ const locale = useTableLocale();
1236
+ const field = ref();
1237
+ const query = ref("");
1238
+ const options = props.spec.options ?? [];
1239
+ const matches = computed(() => {
1240
+ const search = query.value.trim().toLocaleLowerCase();
1241
+ return options.map((value, index) => ({
1242
+ value,
1243
+ index
1244
+ })).filter(({ value }) => String(value ?? "").toLocaleLowerCase().includes(search));
1245
+ });
1246
+ onMounted(() => {
1247
+ if (!props.autofocus) return;
1248
+ field.value?.focus();
1249
+ if (field.value && "setSelectionRange" in field.value && ["text", "number"].includes(props.spec.kind)) field.value.setSelectionRange(field.value.value.length, field.value.value.length);
1250
+ });
1251
+ const finish = (direction) => {
1252
+ if (props.finish) props.finish(direction);
1253
+ else props.editor.commit();
1254
+ };
1255
+ const keydown = (event) => {
1256
+ if (event.isComposing) return;
1257
+ if (event.key === "Escape") {
1258
+ event.preventDefault();
1259
+ event.stopPropagation();
1260
+ if (!props.editor.pending) props.editor.cancel();
1261
+ } else if (event.key === "Enter" || event.key === "Tab") {
1262
+ if (event.key === "Tab" && !props.tabCommit) return;
1263
+ event.preventDefault();
1264
+ event.stopPropagation();
1265
+ if (!props.editor.pending) finish(event.key === "Tab" ? event.shiftKey ? "left" : "right" : event.shiftKey ? "up" : "down");
1266
+ }
1267
+ };
1268
+ const paste = (event) => {
1269
+ const text = event.clipboardData?.getData("text/plain");
1270
+ if (text === void 0 || props.editor.pending) return;
1271
+ event.preventDefault();
1272
+ const target = event.target;
1273
+ const start = target.selectionStart ?? 0;
1274
+ const end = target.selectionEnd ?? target.value.length;
1275
+ props.editor.input(target.value.slice(0, start) + text + target.value.slice(end));
1276
+ };
1277
+ return () => {
1278
+ const { editor, spec } = props;
1279
+ const { messages, count } = locale();
1280
+ const attrs = {
1281
+ ...editor.attrs,
1282
+ "aria-label": messages.editCell(editor.column.header),
1283
+ "data-editor": "",
1284
+ disabled: editor.pending,
1285
+ onKeydown: keydown
1286
+ };
1287
+ if (spec.kind === "select") {
1288
+ const shown = matches.value.slice(0, 50);
1289
+ return h("div", {
1290
+ "data-select-editor": "",
1291
+ onKeydown: (event) => {
1292
+ if (event.key === "Escape") keydown(event);
1293
+ }
1294
+ }, [
1295
+ h("input", {
1296
+ ref: field,
1297
+ type: "search",
1298
+ value: query.value,
1299
+ "aria-label": messages.searchOptions(editor.column.header),
1300
+ disabled: editor.pending,
1301
+ onInput: (event) => {
1302
+ query.value = event.target.value;
1303
+ },
1304
+ onKeydown: (event) => {
1305
+ if (event.key === "Enter" && shown[0]) {
1306
+ event.preventDefault();
1307
+ editor.commit(shown[0].value);
1308
+ } else keydown(event);
1309
+ }
1310
+ }),
1311
+ h("select", {
1312
+ ...attrs,
1313
+ value: options.findIndex((value) => Object.is(value, editor.value)),
1314
+ size: Math.max(2, Math.min(5, shown.length)),
1315
+ onChange: (event) => {
1316
+ const index = Number(event.target.value);
1317
+ if (index >= 0) editor.commit(options[index]);
1318
+ }
1319
+ }, shown.map(({ value, index }) => h("option", {
1320
+ key: index,
1321
+ value: index
1322
+ }, value === null || value === void 0 ? messages.emptyOption : String(value)))),
1323
+ h("span", {
1324
+ role: "status",
1325
+ "data-editor-hint": ""
1326
+ }, matches.value.length === 0 ? messages.noMatchingOptions : matches.value.length > 50 ? messages.firstMatchesOnly(count(50)) : messages.optionCount(count(matches.value.length)))
1327
+ ]);
1328
+ }
1329
+ return h("input", {
1330
+ ...attrs,
1331
+ ref: field,
1332
+ type: spec.kind === "date" ? "date" : spec.kind === "checkbox" ? "checkbox" : "text",
1333
+ inputmode: spec.kind === "number" ? "decimal" : void 0,
1334
+ value: editor.draft,
1335
+ checked: spec.kind === "checkbox" ? editor.value === true : void 0,
1336
+ onInput: spec.kind === "checkbox" ? void 0 : (event) => editor.input(event.target.value),
1337
+ onChange: spec.kind === "checkbox" ? (event) => editor.commit(event.target.checked) : void 0,
1338
+ onBlur: () => {
1339
+ if (props.blurCommit && !editor.pending) finish();
1340
+ },
1341
+ onPaste: spec.kind === "checkbox" ? void 0 : paste
1342
+ });
1343
+ };
758
1344
  }
759
- result.push(rowSpacer(binding.rows.totalSize - end, colspan, "gap-end"));
760
- return result;
1345
+ });
1346
+ /** The cell owns the id so it remains stable while its editor mounts and unmounts. */
1347
+ function issueAttrs(id, issues, pending) {
1348
+ return {
1349
+ "aria-invalid": issues.length ? "true" : void 0,
1350
+ "aria-describedby": issues.length ? id : void 0,
1351
+ "aria-busy": pending ? "true" : void 0
1352
+ };
761
1353
  }
762
- function virtualColgroup(tableColumns, binding) {
763
- if (!binding.columns || !binding.columnItems) return null;
764
- const cols = binding.gutter ? [h("col", { style: { width: `${binding.gutter}px` } })] : [];
765
- let end = 0;
766
- for (const item of binding.columnItems) {
767
- if (item.start > end) cols.push(h("col", { style: { width: `${item.start - end}px` } }));
768
- cols.push(h("col", { style: { width: `${item.size}px` } }));
769
- end = item.start + item.size;
770
- }
771
- if (binding.columns.totalSize > end) cols.push(h("col", { style: { width: `${binding.columns.totalSize - end}px` } }));
772
- return h("colgroup", cols);
1354
+ function editorIssues(id, issues, pending = false) {
1355
+ return issues.length || pending ? h("span", {
1356
+ id,
1357
+ "data-edit-issues": "",
1358
+ role: "status"
1359
+ }, pending ? "Checking changes…" : issues.map((problem) => problem.message).join(" ")) : null;
773
1360
  }
774
- function virtualRow(binding, row) {
775
- return binding.rowItems.find((item) => item.renderItem.kind === "row" && item.renderItem.row.key === row.key);
1361
+ function useEditorId() {
1362
+ return `vt-edit-${useId()}`;
776
1363
  }
777
1364
 
778
1365
  //#endregion
@@ -857,8 +1444,9 @@ function treeKeydown(event, context, virtual) {
857
1444
  if (!table.tree || !target?.closest) return;
858
1445
  if (target.closest("input, textarea, select, [contenteditable=true]") || target.closest("button") && !target.closest("[data-expand-toggle]")) return;
859
1446
  const key = target.closest("tr")?.getAttribute("data-tree-key");
860
- const index = table.rows.findIndex((row) => getRowItemKey(row.key) === key);
861
- const row = table.rows[index];
1447
+ const visible = table.gridRows;
1448
+ const index = visible.findIndex((row) => getRowItemKey(row.key) === key);
1449
+ const row = visible[index];
862
1450
  if (!row) return;
863
1451
  const command = gridCommand({
864
1452
  key: event.key,
@@ -874,11 +1462,15 @@ function treeKeydown(event, context, virtual) {
874
1462
  let next = row;
875
1463
  if (command?.type === "tree") {
876
1464
  if (command.action === "expand" || command.action === "collapse") table.toggleExpanded(row.key, command.action === "expand");
877
- else if (command.action === "siblings") for (const sibling of table.processedRows.filter((candidate) => candidate.parentKey === row.parentKey && candidate.canExpand)) table.toggleExpanded(sibling.key, true);
878
- else if (command.action === "child") next = table.rows.find((candidate) => candidate.parentKey === row.key) ?? row;
879
- else next = table.rows.find((candidate) => candidate.key === row.parentKey) ?? row;
880
- } else if (event.key === "ArrowDown" || event.key === "ArrowUp") next = table.rows[index + (event.key === "ArrowDown" ? 1 : -1)] ?? row;
881
- else if (event.key === "Home" || event.key === "End") next = table.rows[event.key === "Home" ? 0 : table.rows.length - 1] ?? row;
1465
+ else if (command.action === "siblings") for (const sibling of [
1466
+ ...table.topRows,
1467
+ ...table.processedRows,
1468
+ ...table.bottomRows
1469
+ ].filter((candidate) => candidate.parentKey === row.parentKey && candidate.canExpand)) table.toggleExpanded(sibling.key, true);
1470
+ else if (command.action === "child") next = visible.find((candidate) => candidate.parentKey === row.key) ?? row;
1471
+ else next = visible.find((candidate) => candidate.key === row.parentKey) ?? row;
1472
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") next = visible[index + (event.key === "ArrowDown" ? 1 : -1)] ?? row;
1473
+ else if (event.key === "Home" || event.key === "End") next = visible[event.key === "Home" ? 0 : visible.length - 1] ?? row;
882
1474
  else return;
883
1475
  event.preventDefault();
884
1476
  const root = target.closest("table");
@@ -912,11 +1504,12 @@ const DataTableExpandToggle = defineComponent({
912
1504
  slots: Object,
913
1505
  setup(props, { slots }) {
914
1506
  const table = injectDataTable("<DataTableExpandToggle>");
1507
+ const pin = injectPinnedRow();
915
1508
  const context = injectHierarchy();
916
1509
  const grid = injectDataGrid();
917
1510
  const locale = useTableLocale();
918
1511
  const label = () => props.row.isExpanded ? props.collapseLabel ?? locale().messages.collapseRow : props.expandLabel ?? locale().messages.expandRow;
919
- return () => props.row.canExpand ? h("button", {
1512
+ return () => props.row.canExpand && !(pin && !table.tree) ? h("button", {
920
1513
  type: "button",
921
1514
  "data-expand-toggle": "",
922
1515
  tabindex: table.tree ? -1 : void 0,
@@ -928,7 +1521,7 @@ const DataTableExpandToggle = defineComponent({
928
1521
  onClick: (event) => {
929
1522
  event.stopPropagation();
930
1523
  if (grid?.table === table) grid.focusCell({
931
- row: table.rows.indexOf(props.row),
1524
+ row: table.gridRows.indexOf(props.row),
932
1525
  column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
933
1526
  });
934
1527
  table.toggleExpanded(props.row.key);
@@ -965,7 +1558,7 @@ const DataTableTreeCell = defineComponent({
965
1558
  event.stopPropagation();
966
1559
  const element = event.currentTarget.closest("table");
967
1560
  if (grid?.table === table) grid.focusCell({
968
- row: table.rows.indexOf(props.row),
1561
+ row: table.gridRows.indexOf(props.row),
969
1562
  column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
970
1563
  });
971
1564
  table.toggleExpanded(props.row.key, true);
@@ -999,8 +1592,10 @@ const DataTableDetailRow = defineComponent({
999
1592
  slots: Object,
1000
1593
  setup(props, { slots }) {
1001
1594
  const table = injectDataTable("<DataTableDetailRow>");
1595
+ const columnOffset = injectColumnOffset();
1002
1596
  const context = injectHierarchy();
1003
1597
  const virtual = injectVirtual();
1598
+ const rowLayout = injectRowLayout();
1004
1599
  let opened = false;
1005
1600
  return () => {
1006
1601
  if (props.row.isExpanded && !props.hidden) opened = true;
@@ -1009,10 +1604,11 @@ const DataTableDetailRow = defineComponent({
1009
1604
  return h("tr", {
1010
1605
  id: itemId(context, props.row, true),
1011
1606
  "data-detail": "",
1607
+ "aria-rowindex": hidden ? void 0 : rowLayout?.index(props.row.key, "detail"),
1012
1608
  hidden,
1013
1609
  ...virtual && !hidden ? { ref: (target) => virtual.rows.measureElement(target, getRowItemKey(props.row.key, "detail")) } : {}
1014
1610
  }, [h("td", {
1015
- colspan: Math.max(1, props.colspan ?? table.columns.length),
1611
+ colspan: virtual ? physicalColumnCount(virtual, table.columns.length, columnOffset()) : Math.max(1, props.colspan ?? table.columns.length),
1016
1612
  class: "vt-detail-cell"
1017
1613
  }, [slots.default?.({ row: props.row })])]);
1018
1614
  };
@@ -1043,7 +1639,7 @@ function createDetails(table, slot, colspan) {
1043
1639
  visited.delete(key);
1044
1640
  return [];
1045
1641
  }
1046
- return visible.has(key) ? [] : [render(table.rows[index], index, true)];
1642
+ return visible.has(key) ? [] : [render(table.rows[index], index + table.topRows.length, true)];
1047
1643
  });
1048
1644
  }
1049
1645
  };
@@ -1066,29 +1662,36 @@ const DataTableRoot = defineComponent({
1066
1662
  as: asProp("table"),
1067
1663
  ...virtualProps,
1068
1664
  ...hierarchyProps,
1069
- ...columnLayoutProps
1665
+ ...columnLayoutProps,
1666
+ ...resizingProps,
1667
+ ...groupingProps
1070
1668
  },
1071
1669
  setup(props, { slots }) {
1072
1670
  provideDataTable(props.table);
1073
1671
  provideColumnOffset(() => props.leadingColumns);
1074
1672
  const hierarchy = provideHierarchy(props.table, props);
1075
1673
  const element = shallowRef();
1674
+ const layout = provideColumnLayout(props.table, props, () => element.value);
1675
+ const rowLayout = provideRowLayout(props.table, props, () => element.value);
1076
1676
  const editingGrid = injectEditingGrid();
1677
+ const virtualGrid = props.virtualColumns ? editingGrid ?? useDataGrid(props.table) : editingGrid;
1077
1678
  const render = (virtual) => h(props.as, {
1078
- role: props.table.tree ? "treegrid" : void 0,
1079
- "aria-rowcount": (props.table.tree ? props.table.processedRows.length : props.table.rowCount) + 1,
1679
+ ref: element,
1680
+ style: layout.style(virtual?.columns ? virtual.columns.totalSize + virtual.gutter : void 0),
1681
+ "data-column-layout": flag(layout.enabled()),
1682
+ role: props.table.tree || props.table.grouped ? "treegrid" : void 0,
1683
+ "aria-rowcount": rowLayout.count ?? (props.table.tree ? props.table.processedRows.length : props.table.rowCount) + 1,
1080
1684
  "aria-colcount": props.table.columns.length + props.leadingColumns,
1081
1685
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
1082
- "data-empty": flag(props.table.rowCount === 0),
1686
+ "data-empty": flag(props.table.renderItems.length === 0 && props.table.topRows.length === 0 && props.table.bottomRows.length === 0),
1083
1687
  onKeydown: (event) => treeKeydown(event, hierarchy, virtual),
1084
- ...virtual ? {
1085
- ref: element,
1086
- "data-virtual": ""
1087
- } : {}
1088
- }, slots.default?.({ table: props.table }) ?? [h(DataTableHeader), h(DataTableBody)]);
1688
+ ...virtual ? { "data-virtual": "" } : {}
1689
+ }, [virtual?.columns ? virtualColgroup(props.table.columns, virtual, layout.width, layout.leadingWidths()) : layout.colgroup(), slots.default?.({ table: props.table }) ?? [h(DataTableHeader), h(DataTableBody)]]);
1089
1690
  return () => props.virtual ? h(VirtualContent, {
1090
1691
  table: props.table,
1091
- ...editingGrid ? { grid: editingGrid } : {},
1692
+ ...virtualGrid ? { grid: virtualGrid } : {},
1693
+ virtualColumns: props.virtualColumns,
1694
+ gutter: layout.leadingWidths().reduce((sum, width) => sum + width, 0),
1092
1695
  options: virtualOptions(props),
1093
1696
  tableElement: () => element.value,
1094
1697
  render
@@ -1159,6 +1762,9 @@ const DataTableHeaderCell = defineComponent({
1159
1762
  setup(props, { slots }) {
1160
1763
  const table = injectDataTable("<DataTableHeaderCell>");
1161
1764
  const columnOffset = injectColumnOffset();
1765
+ const layout = injectColumnLayout();
1766
+ const rows = injectRowLayout();
1767
+ provideHeaderColumn(() => props.column);
1162
1768
  return () => {
1163
1769
  const { column } = props;
1164
1770
  const sort = table.getSort(column.id);
@@ -1175,9 +1781,14 @@ const DataTableHeaderCell = defineComponent({
1175
1781
  "data-align": column.align,
1176
1782
  "data-sortable": flag(column.sortable),
1177
1783
  "data-sort": sort?.direction,
1178
- style: columnStyle(column),
1784
+ ...layout?.attrs(column, true),
1785
+ style: [
1786
+ columnStyle(column),
1787
+ layout?.attrs(column, true)["style"],
1788
+ rows?.headerStyle(!!column.pinned)
1789
+ ],
1179
1790
  "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
1180
- }, content);
1791
+ }, [content, layout?.resizable() && column.resizable ? h(DataTableColumnResizer) : null]);
1181
1792
  };
1182
1793
  }
1183
1794
  });
@@ -1219,6 +1830,7 @@ const DataTableSortButton = defineComponent({
1219
1830
  */
1220
1831
  const DataTableBody = defineComponent({
1221
1832
  name: "DataTableBody",
1833
+ inheritAttrs: false,
1222
1834
  slots: Object,
1223
1835
  props: {
1224
1836
  as: asProp("tbody"),
@@ -1228,36 +1840,74 @@ const DataTableBody = defineComponent({
1228
1840
  default: void 0
1229
1841
  }
1230
1842
  },
1231
- setup(props, { slots }) {
1843
+ setup(props, { slots, attrs }) {
1232
1844
  const table = injectDataTable("<DataTableBody>");
1233
1845
  const locale = useTableLocale();
1234
1846
  const virtual = injectVirtual();
1847
+ const rowLayout = injectRowLayout();
1235
1848
  const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
1236
1849
  return () => {
1237
- if (props.virtual && !virtual) return h(VirtualDataTableBody, props, slots);
1238
- if (virtual) return renderTableBody(table, virtual, props.as, props.colspan, slots, details);
1850
+ if (props.virtual && !virtual) return h(VirtualDataTableBody, mergeProps(props, attrs), slots);
1851
+ if (virtual) return pinnedTableSections(table, renderTableBody(table, virtual, props.as, props.colspan, slots, details, void 0, rowLayout), props.as, slots, attrs);
1239
1852
  let content = slots.default?.({ rows: table.rows });
1240
- if (!content) content = table.rows.length === 0 && slots.empty ? h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, table.columns.length) }, slots.empty())]) : table.renderItems.map((item) => item.kind === "row" ? slots.row ? h(Fragment, { key: item.key }, [slots.row(item)]) : h(DataTableRow, {
1853
+ if (!content) content = table.renderItems.length === 0 && slots.empty ? h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, table.columns.length) }, slots.empty())]) : (rowLayout?.items ?? table.renderItems).map((item) => item.kind === "row" ? slots.row ? h(Fragment, { key: item.key }, [slots.row(item)]) : h(DataTableRow, {
1241
1854
  key: item.key,
1242
1855
  row: item.row,
1243
1856
  rowIndex: item.rowIndex
1244
- }) : item.kind === "detail" ? details.render(item.row, item.rowIndex) : null);
1857
+ }) : item.kind === "detail" ? details.render(item.row, item.rowIndex) : groupRow(item, slots, rowLayout?.aggregatePosition ?? "group", rowLayout?.footer(item.key)));
1245
1858
  const visibleDetails = new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key));
1246
1859
  const retained = slots.default ? [] : details.retained(visibleDetails);
1247
1860
  const sentinel = loadSentinel(table, props.colspan ?? table.columns.length, locale());
1248
- return h(props.as, [
1861
+ return pinnedTableSections(table, h(props.as, { "data-row-region": "centre" }, [
1249
1862
  ...Array.isArray(content) ? content : content ? [content] : [],
1250
1863
  ...retained,
1251
1864
  ...sentinel ? [sentinel] : []
1252
- ]);
1865
+ ]), props.as, slots, attrs);
1253
1866
  };
1254
1867
  }
1255
1868
  });
1256
- function renderTableBody(table, virtual, as, colspan, slots, details, element) {
1257
- const width = colspan ?? table.columns.length;
1258
- const attrs = element ? { ref: element } : {};
1869
+ function pinnedTableSections(table, centre, as, slots, attrs = {}) {
1870
+ const body = cloneVNode(centre, {
1871
+ ...attrs,
1872
+ "data-row-region": "centre"
1873
+ });
1874
+ const region = (side) => {
1875
+ const rows = side === "top" ? table.topRows : table.bottomRows;
1876
+ return rows.length ? h(as, {
1877
+ "data-row-region": side,
1878
+ class: attrs["class"]
1879
+ }, rows.map((row, index) => {
1880
+ const rowIndex = side === "top" ? index : table.topRows.length + table.rows.length + index;
1881
+ return h(DataTablePinnedRow, {
1882
+ key: getRowItemKey(row.key),
1883
+ row,
1884
+ side
1885
+ }, { default: () => {
1886
+ const child = slots.row?.({
1887
+ row,
1888
+ rowIndex
1889
+ }) ?? h(DataTableRow, {
1890
+ row,
1891
+ rowIndex
1892
+ });
1893
+ return [isVNode(child) ? child : h(Fragment, null, [child])];
1894
+ } });
1895
+ })) : null;
1896
+ };
1897
+ return table.topRows.length || table.bottomRows.length ? h(Fragment, [
1898
+ region("top"),
1899
+ body,
1900
+ region("bottom")
1901
+ ]) : body;
1902
+ }
1903
+ function renderTableBody(table, virtual, as, colspan, slots, details, element, rowLayout) {
1904
+ const width = physicalColumnCount(virtual, table.columns.length, Math.max(0, (colspan ?? table.columns.length) - table.columns.length));
1905
+ const attrs = {
1906
+ "data-row-region": "centre",
1907
+ ...element ? { ref: element } : {}
1908
+ };
1259
1909
  const content = slots.default?.({ rows: virtual.rowItems.flatMap((item) => item.renderItem.kind === "row" ? [item.renderItem.row] : []) });
1260
- if (table.rows.length === 0 && slots.empty) return h(as, attrs, h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, width) }, slots.empty())]));
1910
+ if (table.renderItems.length === 0 && slots.empty) return h(as, attrs, h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, width) }, slots.empty())]));
1261
1911
  if (content) return h(as, attrs, [
1262
1912
  rowSpacer(virtual.rows.paddingStart, width, "gap-start"),
1263
1913
  ...content,
@@ -1269,13 +1919,15 @@ function renderTableBody(table, virtual, as, colspan, slots, details, element) {
1269
1919
  key: item.key,
1270
1920
  row: item.renderItem.row,
1271
1921
  rowIndex: item.renderItem.rowIndex
1272
- }) : item.renderItem.kind === "detail" ? details.render(item.renderItem.row, item.renderItem.rowIndex) : null),
1922
+ }) : item.renderItem.kind === "detail" ? details.render(item.renderItem.row, item.renderItem.rowIndex) : groupRow(item.renderItem, slots, rowLayout?.aggregatePosition ?? "group", rowLayout?.footer(item.key))),
1923
+ ...table.bottomRows.length ? [rowSpacer(Math.max(0, virtual.rows.viewportSize - virtual.rows.totalSize), width, "pin-fill")] : [],
1273
1924
  ...details.retained(new Set(virtual.rowItems.flatMap((item) => item.renderItem.kind === "detail" ? [item.renderItem.row.key] : []))),
1274
1925
  ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []
1275
1926
  ]);
1276
1927
  }
1277
1928
  const VirtualDataTableBody = defineComponent({
1278
1929
  name: "DataTableVirtualBody",
1930
+ inheritAttrs: false,
1279
1931
  props: {
1280
1932
  as: asProp("tbody"),
1281
1933
  ...virtualProps,
@@ -1285,12 +1937,13 @@ const VirtualDataTableBody = defineComponent({
1285
1937
  }
1286
1938
  },
1287
1939
  slots: Object,
1288
- setup(props, { slots }) {
1940
+ setup(props, { slots, attrs }) {
1289
1941
  const table = injectDataTable("<DataTableBody>");
1942
+ const rowLayout = injectRowLayout();
1290
1943
  const element = shallowRef();
1291
1944
  const virtual = createComponentVirtual(table, virtualOptions(props), () => element.value?.closest("table"));
1292
1945
  const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
1293
- return () => renderTableBody(table, virtual, props.as, props.colspan, slots, details, element);
1946
+ return () => pinnedTableSections(table, renderTableBody(table, virtual, props.as, props.colspan, slots, details, element, rowLayout), props.as, slots, attrs);
1294
1947
  }
1295
1948
  });
1296
1949
  /** A body row; `aria-selected` and `data-selected` follow the selection. */
@@ -1317,19 +1970,21 @@ const DataTableRow = defineComponent({
1317
1970
  const table = injectDataTable("<DataTableRow>");
1318
1971
  const virtual = injectVirtual();
1319
1972
  const hierarchy = injectHierarchy();
1973
+ const rowLayout = injectRowLayout();
1320
1974
  return () => {
1321
1975
  const { row } = props;
1322
1976
  const selected = table.isSelected(row.key);
1323
1977
  const item = virtual ? virtualRow(virtual, row) : void 0;
1324
- const position = props.rowIndex ?? (item?.renderItem.kind === "row" ? item.renderItem.rowIndex : table.rows.indexOf(row));
1978
+ const position = props.rowIndex ?? (item?.renderItem.kind === "row" ? item.renderItem.rowIndex : table.gridRows.indexOf(row));
1325
1979
  return h(props.as, mergeProps(props.attributes ?? {}, {
1326
- "aria-rowindex": position === -1 ? void 0 : table.pageStart + position + 1,
1980
+ "aria-rowindex": rowLayout?.index(row.key) ?? (position === -1 ? void 0 : table.pageStart + position + 1),
1327
1981
  "aria-selected": table.selectionMode === "none" ? void 0 : String(selected),
1982
+ "aria-level": rowLayout?.level(row.key),
1328
1983
  "data-selected": flag(selected),
1329
1984
  "data-key": String(row.key),
1330
1985
  "data-dirty": flag(row.isDirty),
1331
1986
  ...hierarchyRowAttrs(table, row, hierarchy),
1332
- tabindex: table.tree ? (hierarchy?.focusKey ?? table.rows[0]?.key) === row.key ? 0 : -1 : void 0,
1987
+ tabindex: table.tree ? (hierarchy?.focusKey ?? table.gridRows[0]?.key) === row.key ? 0 : -1 : void 0,
1333
1988
  onFocus: () => {
1334
1989
  if (hierarchy) hierarchy.focus.value = row.key;
1335
1990
  },
@@ -1341,11 +1996,11 @@ const DataTableRow = defineComponent({
1341
1996
  row,
1342
1997
  selected,
1343
1998
  columns: table.columns
1344
- }) ?? table.columns.map((column) => h(DataTableCell, {
1999
+ }) ?? renderColumns(table.columns, virtual, (column) => h(DataTableCell, {
1345
2000
  key: column.id,
1346
2001
  row,
1347
2002
  column
1348
- })));
2003
+ }), "td"));
1349
2004
  };
1350
2005
  }
1351
2006
  });
@@ -1366,6 +2021,8 @@ const DataTableCell = defineComponent({
1366
2021
  const table = injectDataTable("<DataTableCell>");
1367
2022
  const columnOffset = injectColumnOffset();
1368
2023
  const hierarchy = injectHierarchy();
2024
+ const layout = injectColumnLayout();
2025
+ const pin = injectPinnedRow();
1369
2026
  return () => {
1370
2027
  const { row, column } = props;
1371
2028
  const value = row.getValue(column.id);
@@ -1379,6 +2036,8 @@ const DataTableCell = defineComponent({
1379
2036
  return h(props.as, {
1380
2037
  "data-column": column.id,
1381
2038
  "data-align": column.align,
2039
+ ...layout?.attrs(column),
2040
+ ...pin ? mergeProps(layout?.attrs(column) ?? {}, pin.attrs(!!column.pinned)) : {},
1382
2041
  "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
1383
2042
  }, props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: content }) : content());
1384
2043
  };
@@ -1499,7 +2158,7 @@ const DataTableEditCell = defineComponent({
1499
2158
  const id = useEditorId();
1500
2159
  const element = shallowRef();
1501
2160
  const position = () => ({
1502
- row: props.rowIndex ?? table.rows.indexOf(props.row),
2161
+ row: props.rowIndex ?? table.gridRows.indexOf(props.row),
1503
2162
  column: table.columns.indexOf(props.column)
1504
2163
  });
1505
2164
  const active = computed(() => context.mode === "row" ? context.draft?.rowKey === props.row.key : grid.isEditing(position()));
@@ -1652,6 +2311,22 @@ const DataTableRowActions = defineComponent({
1652
2311
  const context = inject(editingKey, void 0);
1653
2312
  const id = useEditorId();
1654
2313
  const element = shallowRef();
2314
+ const virtual = injectVirtual();
2315
+ const pin = (side) => {
2316
+ const root = element.value?.closest("table");
2317
+ const key = props.row.key;
2318
+ table.pinRow(key, side);
2319
+ if (side === false) virtual?.rows.scrollToKey(key);
2320
+ nextTick(() => {
2321
+ const control = [...root?.querySelectorAll("[data-row-actions-key]") ?? []].find((target) => target.getAttribute("data-row-actions-key") === getRowItemKey(key))?.querySelector("summary");
2322
+ control?.focus({ preventScroll: true });
2323
+ if (side === false) control?.scrollIntoView?.({
2324
+ block: "nearest",
2325
+ inline: "nearest"
2326
+ });
2327
+ if (!control) root?.querySelector("thead button")?.focus();
2328
+ });
2329
+ };
1655
2330
  watch(() => context?.draft?.rowKey === props.row.key, (active, previous) => {
1656
2331
  const target = element.value;
1657
2332
  if (previous && !active && !context?.draft && target?.closest("tr")?.contains(target.ownerDocument.activeElement)) nextTick(() => target.querySelector("button[data-edit-row]")?.focus());
@@ -1672,6 +2347,7 @@ const DataTableRowActions = defineComponent({
1672
2347
  return h("div", {
1673
2348
  ref: element,
1674
2349
  "data-row-actions": "",
2350
+ "data-row-actions-key": getRowItemKey(props.row.key),
1675
2351
  role: "group",
1676
2352
  "aria-label": messages.rowActionsFor(rowName),
1677
2353
  "aria-busy": draft?.pending ? "true" : void 0
@@ -1683,7 +2359,13 @@ const DataTableRowActions = defineComponent({
1683
2359
  block: "nearest",
1684
2360
  inline: "nearest"
1685
2361
  }));
1686
- } }, [h("summary", { "aria-label": messages.actionsForRow(rowName) }, messages.actions), h("div", { "data-row-menu": "" }, [button(messages.revertRow, () => table.revert([props.row.key]), !props.row.isDirty || !!draft), props.removable ? button(messages.removeRow, () => table.removeRows([props.row.key]), !!draft) : null])]),
2362
+ } }, [h("summary", { "aria-label": messages.actionsForRow(rowName) }, messages.actions), h("div", { "data-row-menu": "" }, [
2363
+ button(messages.pinRowTop, () => pin("top"), !!draft || table.rowPinning.top.includes(props.row.key)),
2364
+ button(messages.pinRowBottom, () => pin("bottom"), !!draft || table.rowPinning.bottom.includes(props.row.key)),
2365
+ table.rowPinning.top.includes(props.row.key) || table.rowPinning.bottom.includes(props.row.key) ? button(messages.unpinRow, () => pin(false), !!draft) : null,
2366
+ button(messages.revertRow, () => table.revert([props.row.key]), !props.row.isDirty || !!draft),
2367
+ props.removable ? button(messages.removeRow, () => table.removeRows([props.row.key]), !!draft) : null
2368
+ ])]),
1687
2369
  editorIssues(id, issues, !!draft?.pending)
1688
2370
  ]);
1689
2371
  };
@@ -1743,6 +2425,8 @@ const DataGridRoot = defineComponent({
1743
2425
  ...virtualProps,
1744
2426
  ...hierarchyProps,
1745
2427
  ...columnLayoutProps,
2428
+ ...resizingProps,
2429
+ ...groupingProps,
1746
2430
  /** Width reserved before data columns, for a row-number/header column. */
1747
2431
  gutter: {
1748
2432
  type: Number,
@@ -1760,6 +2444,8 @@ const DataGridRoot = defineComponent({
1760
2444
  provideDataGrid(grid);
1761
2445
  provideHierarchy(props.table, props);
1762
2446
  const element = ref();
2447
+ const layout = provideColumnLayout(props.table, props, () => element.value);
2448
+ const rowLayout = provideRowLayout(props.table, props, () => element.value);
1763
2449
  const id = `vt-grid-${useId()}`;
1764
2450
  const focus = () => {
1765
2451
  nextTick(() => {
@@ -1794,7 +2480,7 @@ const DataGridRoot = defineComponent({
1794
2480
  row: 0,
1795
2481
  column: 0
1796
2482
  } : {
1797
- row: props.table.rows.length - 1,
2483
+ row: props.table.gridRows.length - 1,
1798
2484
  column: props.table.columns.length - 1
1799
2485
  }, { extend: event.shiftKey });
1800
2486
  event.preventDefault();
@@ -1842,11 +2528,11 @@ const DataGridRoot = defineComponent({
1842
2528
  return h(props.as, {
1843
2529
  ref: element,
1844
2530
  id,
1845
- role: props.table.tree ? "treegrid" : "grid",
2531
+ role: props.table.tree || props.table.grouped ? "treegrid" : "grid",
1846
2532
  tabindex: 0,
1847
2533
  "aria-label": props.label,
1848
2534
  "aria-multiselectable": "true",
1849
- "aria-rowcount": (props.table.tree ? props.table.processedRows.length : binding ? props.table.rowCount : props.table.rows.length) + 1,
2535
+ "aria-rowcount": rowLayout.count ?? (props.table.tree ? props.table.processedRows.length : binding ? props.table.rowCount : props.table.rows.length) + props.table.topRows.length + props.table.bottomRows.length + 1,
1850
2536
  "aria-colcount": props.table.columns.length + props.leadingColumns,
1851
2537
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
1852
2538
  "aria-activedescendant": focusPosition && !grid.editor ? cellId(id, focusPosition.row, focusPosition.column) : void 0,
@@ -1854,20 +2540,16 @@ const DataGridRoot = defineComponent({
1854
2540
  onCopy: (event) => onCopy(event),
1855
2541
  onCut: (event) => onCopy(event, true),
1856
2542
  onPaste,
1857
- ...binding ? {
1858
- "data-virtual": "",
1859
- style: binding.columns ? {
1860
- tableLayout: "fixed",
1861
- width: `${binding.columns.totalSize + binding.gutter}px`
1862
- } : void 0
1863
- } : {}
1864
- }, binding ? [virtualColgroup(props.table.columns, binding), slots.default?.({
2543
+ "data-column-layout": flag(layout.enabled()),
2544
+ style: layout.style(binding?.columns ? binding.columns.totalSize + binding.gutter : void 0),
2545
+ ...binding ? { "data-virtual": "" } : {}
2546
+ }, binding ? [binding.columns ? virtualColgroup(props.table.columns, binding, layout.width, layout.leadingWidths()) : layout.colgroup(), slots.default?.({
1865
2547
  table: props.table,
1866
2548
  grid
1867
- }) ?? [h(DataTableHeader), h(DataGridBody)]] : slots.default?.({
2549
+ }) ?? [h(DataTableHeader), h(DataGridBody)]] : [layout.colgroup(), slots.default?.({
1868
2550
  table: props.table,
1869
2551
  grid
1870
- }) ?? [h(DataTableHeader), h(DataGridBody)]);
2552
+ }) ?? [h(DataTableHeader), h(DataGridBody)]]);
1871
2553
  };
1872
2554
  return () => props.virtual ? h(VirtualContent, {
1873
2555
  table: props.table,
@@ -1886,6 +2568,7 @@ const DataGridRoot = defineComponent({
1886
2568
  */
1887
2569
  const DataGridBody = defineComponent({
1888
2570
  name: "DataGridBody",
2571
+ inheritAttrs: false,
1889
2572
  slots: Object,
1890
2573
  props: {
1891
2574
  as: asProp("tbody"),
@@ -1904,22 +2587,76 @@ const DataGridBody = defineComponent({
1904
2587
  default: void 0
1905
2588
  }
1906
2589
  },
1907
- setup(props, { slots }) {
2590
+ setup(props, { slots, attrs }) {
1908
2591
  const table = injectDataTable("<DataGridBody>");
1909
2592
  const { grid } = useGrid("<DataGridBody>");
1910
2593
  const virtual = injectVirtual();
1911
2594
  const hierarchy = injectHierarchy();
2595
+ const rowLayout = injectRowLayout();
2596
+ const columnOffset = injectColumnOffset();
1912
2597
  const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1913
2598
  const element = ref();
2599
+ const pinnedSections = (centre, binding) => {
2600
+ const body = cloneVNode(centre, {
2601
+ ...attrs,
2602
+ "data-row-region": "centre"
2603
+ });
2604
+ const region = (side) => {
2605
+ const rows = side === "top" ? table.topRows : table.bottomRows;
2606
+ return rows.length ? h(props.as, {
2607
+ "data-row-region": side,
2608
+ class: attrs["class"]
2609
+ }, rows.map((row, index) => {
2610
+ const rowIndex = side === "top" ? index : table.topRows.length + table.rows.length + index;
2611
+ return h(DataTablePinnedRow, {
2612
+ key: row.key,
2613
+ row,
2614
+ side
2615
+ }, { default: () => [h("tr", mergeProps(rowAttributes(props.rowAttrs, row), {
2616
+ role: "row",
2617
+ "data-key": String(row.key),
2618
+ "data-dirty": flag(row.isDirty),
2619
+ "aria-rowindex": rowLayout?.index(row.key),
2620
+ "aria-level": rowLayout?.level(row.key),
2621
+ ...hierarchyRowAttrs(table, row, hierarchy)
2622
+ }), [slots.rowHeader?.({
2623
+ row,
2624
+ index: rowIndex
2625
+ }), ...renderColumns(table.columns, binding, (column, columnIndex) => h(DataGridCell, {
2626
+ key: column.id,
2627
+ row,
2628
+ column,
2629
+ rowIndex,
2630
+ columnIndex,
2631
+ tree: props.treeCell
2632
+ }, {
2633
+ default: slots.cell,
2634
+ editor: slots.editor,
2635
+ $stable: true
2636
+ }), "td")])] });
2637
+ })) : null;
2638
+ };
2639
+ return table.topRows.length || table.bottomRows.length ? h(Fragment, [
2640
+ region("top"),
2641
+ body,
2642
+ region("bottom")
2643
+ ]) : body;
2644
+ };
1914
2645
  const render = (binding) => {
1915
2646
  if (binding && slots.default) {
1916
2647
  const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1917
2648
  const content = slots.default({ rows: binding.rowItems.flatMap((item) => item.renderItem.kind === "row" ? [item.renderItem.row] : []) });
1918
- return h(props.as, { ref: element }, sentinel ? [...content, sentinel] : content);
2649
+ return pinnedSections(h(props.as, {
2650
+ ref: element,
2651
+ "data-row-region": "centre"
2652
+ }, sentinel ? [...content, sentinel] : content), binding);
1919
2653
  }
1920
- if (binding) return h(props.as, { ref: element }, [
1921
- ...renderRows(binding, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0), (item) => {
1922
- if (item.renderItem.kind === "group") return null;
2654
+ if (binding) return pinnedSections(h(props.as, {
2655
+ ref: element,
2656
+ "data-row-region": "centre"
2657
+ }, [
2658
+ ...renderRows(binding, physicalColumnCount(binding, table.columns.length, columnOffset()), (item) => {
2659
+ if (item.renderItem.kind === "group") return groupRow(item.renderItem, slots, rowLayout?.aggregatePosition ?? "group", rowLayout?.footer(item.key));
1923
2660
  if (item.renderItem.kind !== "row") return details.render(item.renderItem.row, item.renderItem.rowIndex);
1924
2661
  const { row, rowIndex } = item.renderItem;
1925
2662
  const columns = renderColumns(table.columns, binding, (column, columnIndex) => h(DataGridCell, {
@@ -1937,7 +2674,8 @@ const DataGridBody = defineComponent({
1937
2674
  return h("tr", mergeProps(rowAttributes(props.rowAttrs, row), {
1938
2675
  key: item.key,
1939
2676
  role: "row",
1940
- "aria-rowindex": table.pageStart + rowIndex + 1,
2677
+ "aria-rowindex": rowLayout?.index(row.key) ?? table.pageStart + rowIndex + 1,
2678
+ "aria-level": rowLayout?.level(row.key),
1941
2679
  "data-key": String(row.key),
1942
2680
  "data-dirty": flag(row.isDirty),
1943
2681
  ...hierarchyRowAttrs(table, row, hierarchy),
@@ -1948,15 +2686,17 @@ const DataGridBody = defineComponent({
1948
2686
  index: rowIndex
1949
2687
  }), ...columns]);
1950
2688
  }),
2689
+ ...table.bottomRows.length ? [rowSpacer(Math.max(0, binding.rows.viewportSize - binding.rows.totalSize), physicalColumnCount(binding, table.columns.length, columnOffset()), "pin-fill")] : [],
1951
2690
  ...details.retained(new Set(binding.rowItems.flatMap((item) => item.renderItem.kind === "detail" ? [item.renderItem.row.key] : []))),
1952
2691
  ...loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0)) ? [loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0))] : []
1953
- ]);
1954
- const content = slots.default?.({ rows: table.rows }) ?? table.renderItems.map((item) => item.kind === "group" ? null : item.kind === "detail" ? details.render(item.row, item.rowIndex) : (() => {
2692
+ ]), binding);
2693
+ const content = slots.default?.({ rows: table.rows }) ?? (rowLayout?.items ?? table.renderItems).map((item) => item.kind === "group" ? groupRow(item, slots, rowLayout?.aggregatePosition ?? "group", rowLayout?.footer(item.key)) : item.kind === "detail" ? details.render(item.row, item.rowIndex) : (() => {
1955
2694
  const { row, rowIndex } = item;
1956
2695
  return h("tr", mergeProps(rowAttributes(props.rowAttrs, row), {
1957
2696
  key: item.key,
1958
2697
  role: "row",
1959
- "aria-rowindex": rowIndex + 2,
2698
+ "aria-rowindex": rowLayout?.index(row.key) ?? rowIndex + 2,
2699
+ "aria-level": rowLayout?.level(row.key),
1960
2700
  "data-key": String(row.key),
1961
2701
  "data-dirty": flag(row.isDirty),
1962
2702
  ...hierarchyRowAttrs(table, row, hierarchy)
@@ -1977,11 +2717,11 @@ const DataGridBody = defineComponent({
1977
2717
  }))]);
1978
2718
  })());
1979
2719
  const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1980
- return h(props.as, [
2720
+ return pinnedSections(h(props.as, { "data-row-region": "centre" }, [
1981
2721
  ...content,
1982
2722
  ...slots.default ? [] : details.retained(new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key))),
1983
2723
  sentinel
1984
- ]);
2724
+ ]));
1985
2725
  };
1986
2726
  return () => props.virtual && !virtual ? h(VirtualContent, {
1987
2727
  table,
@@ -2022,12 +2762,14 @@ const DataGridCell = defineComponent({
2022
2762
  const table = injectDataTable("<DataGridCell>");
2023
2763
  const columnOffset = injectColumnOffset();
2024
2764
  const { grid, context } = useGrid("<DataGridCell>");
2765
+ const layout = injectColumnLayout();
2766
+ const pin = injectPinnedRow();
2025
2767
  const virtual = injectVirtual();
2026
2768
  const hierarchy = injectHierarchy();
2027
2769
  const element = ref();
2028
2770
  const issueId = useEditorId();
2029
2771
  const position = () => ({
2030
- row: props.rowIndex ?? table.rows.indexOf(props.row),
2772
+ row: props.rowIndex ?? table.gridRows.indexOf(props.row),
2031
2773
  column: props.columnIndex ?? table.columns.indexOf(props.column)
2032
2774
  });
2033
2775
  watch(() => grid.isFocused(position()), (focused) => {
@@ -2110,6 +2852,8 @@ const DataGridCell = defineComponent({
2110
2852
  "data-dirty": flag(row.isDirty),
2111
2853
  ...issueAttrs(issueId, issues, pending),
2112
2854
  style: columnStyle(column),
2855
+ ...layout?.attrs(column),
2856
+ ...pin ? mergeProps(layout?.attrs(column) ?? {}, pin.attrs(!!column.pinned)) : {},
2113
2857
  onMousedown: (event) => {
2114
2858
  if (editing || event.button !== 0) return;
2115
2859
  grid.focusCell(at, { extend: event.shiftKey });
@@ -2128,5 +2872,5 @@ const DataGridCell = defineComponent({
2128
2872
  });
2129
2873
 
2130
2874
  //#endregion
2131
- export { DataCellEditor, DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableDetailRow, DataTableEditCell, DataTableEditRoot, DataTableEmpty, DataTableExpandToggle, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTableLoadMore, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableRowActions, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, DataTableTreeCell, DataTableViewport, DataTableVirtualColumns, cellEditorProps, cellId, columnStyle, expandToggleProps, hierarchyProps, injectVirtual as injectVirtualRenderer, resolveTreeColumn, rowAttributes, treeCellProps, virtualProps };
2875
+ export { DataCellEditor, DataGridBody, DataGridCell, DataGridRoot, DataTableAggregateCell, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnResizer, DataTableColumnVisibility, DataTableDetailRow, DataTableEditCell, DataTableEditRoot, DataTableEmpty, DataTableExpandToggle, DataTableGroupRow, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTableLeadingCell, DataTableLoadMore, DataTablePageSize, DataTablePagination, DataTablePinnedRow, DataTableRoot, DataTableRow, DataTableRowActions, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, DataTableTreeCell, DataTableViewport, DataTableVirtualColumns, cellEditorProps, cellId, columnStyle, expandToggleProps, groupingProps, hierarchyProps, injectVirtual as injectVirtualRenderer, resolveTreeColumn, rowAttributes, treeCellProps, virtualProps };
2132
2876
  //# sourceMappingURL=index.js.map