@vueye-table/headless 3.0.0-alpha.1 → 3.0.0-alpha.11

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,7 +1,20 @@
1
- import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid } from "@vueye-table/vue";
2
- import { defineComponent, h, inject, nextTick, onMounted, onScopeDispose, provide, ref, useId, watch } from "vue";
1
+ import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useVirtualColumns, useVirtualRows } from "@vueye-table/vue";
2
+ import { Fragment, computed, defineComponent, h, inject, nextTick, onMounted, onScopeDispose, provide, ref, shallowRef, useId, watch } from "vue";
3
+ import { formatValue, getRowItemKey, gridCommand } from "@vueye-table/core";
3
4
 
4
5
  //#region src/shared.ts
6
+ /** Leading native utility cells, independent of data-column coordinates. */
7
+ const columnLayoutProps = { leadingColumns: {
8
+ type: Number,
9
+ default: 0
10
+ } };
11
+ const columnOffsetKey = Symbol("vt-leading-columns");
12
+ function provideColumnOffset(offset) {
13
+ provide(columnOffsetKey, offset);
14
+ }
15
+ function injectColumnOffset() {
16
+ return inject(columnOffsetKey, () => 0);
17
+ }
5
18
  /** The element a component renders. Every headless component accepts `as`. */
6
19
  const asProp = (tag) => ({
7
20
  type: String,
@@ -76,14 +89,16 @@ const DataTableSelectRow = defineComponent({
76
89
  setup(props, { slots }) {
77
90
  const table = injectDataTable("<DataTableSelectRow>");
78
91
  return () => {
79
- const selected = table.isSelected(props.row.key);
92
+ const selected = table.tree ? props.row.selection === "all" : table.isSelected(props.row.key);
93
+ const indeterminate = table.tree && props.row.selection === "some";
80
94
  const toggle = () => table.toggleRow(props.row.key);
81
95
  return slots.default?.({
82
96
  selected,
97
+ indeterminate,
83
98
  toggle
84
99
  }) ?? checkbox({
85
100
  checked: selected,
86
- indeterminate: false,
101
+ indeterminate,
87
102
  label: props.label,
88
103
  disabled: table.selectionMode === "none",
89
104
  onChange: toggle
@@ -244,10 +259,6 @@ const DataTableColumnVisibility = defineComponent({
244
259
  };
245
260
  }
246
261
  });
247
- /**
248
- * A polite live region describing what is shown, such as "1–10 of 57 rows". Screen readers
249
- * announce it after a search, filter, or page change.
250
- */
251
262
  const DataTableStatus = defineComponent({
252
263
  name: "DataTableStatus",
253
264
  slots: Object,
@@ -260,16 +271,737 @@ const DataTableStatus = defineComponent({
260
271
  end: table.pageEnd,
261
272
  rowCount: table.rowCount,
262
273
  totalRowCount: table.totalRowCount,
263
- selectedCount: table.selectedCount
274
+ selectedCount: table.selectedCount,
275
+ loadState: table.loadState,
276
+ loadedRowCount: table.loadedRowCount,
277
+ loadError: table.loadError,
278
+ canLoadMore: table.canLoadMore,
279
+ retry: () => table.retry(),
280
+ loadNext: () => table.loadNext(),
281
+ loadingChildrenCount: table.tree ? table.processedRows.filter((row) => row.childStatus === "loading").length : 0,
282
+ childErrorCount: table.tree ? table.processedRows.filter((row) => row.childStatus === "error").length : 0
264
283
  };
265
- const text = table.rowCount === 0 ? "No rows" : `${table.pageStart}–${table.pageEnd} of ${table.rowCount} rows` + (table.selectedCount > 0 ? `, ${table.selectedCount} selected` : "");
284
+ const count = `Loaded ${table.loadedRowCount.toLocaleString()} ${table.loadedRowCount === 1 ? "row" : "rows"}`;
285
+ const text = table.loadingMode ? table.loadState === "error" ? `${count}. Could not load rows. Retry loading.` : table.loadState === "aborted" ? `${count}. Loading stopped.` : table.loadState === "loading" || table.loadState === "streaming" ? `${count}…` : `${count}${table.loadState === "done" ? ". All rows loaded." : "."}` : table.rowCount === 0 ? "No rows" : `${table.pageStart}–${table.pageEnd} of ${table.rowCount} rows` + (table.selectedCount > 0 ? `, ${table.selectedCount} selected` : "");
266
286
  return h(props.as, {
267
287
  role: "status",
268
288
  "aria-live": "polite"
269
- }, slots.default?.(slotProps) ?? text);
289
+ }, slots.default?.(slotProps) ?? text + (slotProps.loadingChildrenCount ? " Loading children…" : "") + (slotProps.childErrorCount ? " Could not load children. Retry children in the affected row." : ""));
290
+ };
291
+ }
292
+ });
293
+
294
+ //#endregion
295
+ //#region src/loading.ts
296
+ /** Keep this visual sentinel out of the live region and logical data-row count. */
297
+ function loadSentinel(table, colspan) {
298
+ return table.loadingMode === "cursor" && table.loadedRowCount > 0 && (table.loadState === "loading" || table.loadState === "streaming") ? h("tr", {
299
+ key: "vt-load-sentinel",
300
+ "data-load-sentinel": "",
301
+ "aria-hidden": "true"
302
+ }, [h("td", {
303
+ colspan: Math.max(1, colspan),
304
+ class: "vt-load-sentinel"
305
+ }, "Loading more rows…")]) : void 0;
306
+ }
307
+ /** A keyboard-accessible alternative to automatic virtual-end loading, with error recovery. */
308
+ const DataTableLoadMore = defineComponent({
309
+ name: "DataTableLoadMore",
310
+ props: {
311
+ label: {
312
+ type: String,
313
+ default: "Load more rows"
314
+ },
315
+ retryLabel: {
316
+ type: String,
317
+ default: "Retry loading"
318
+ },
319
+ loadingLabel: {
320
+ type: String,
321
+ default: "Loading rows…"
322
+ }
323
+ },
324
+ setup(props) {
325
+ const table = injectDataTable("<DataTableLoadMore>");
326
+ return () => table.loadState === "error" ? h("button", {
327
+ type: "button",
328
+ onClick: () => table.retry()
329
+ }, props.retryLabel) : table.loadingMode === "cursor" && table.loadState !== "done" ? h("button", {
330
+ type: "button",
331
+ disabled: !table.canLoadMore,
332
+ onClick: () => {
333
+ table.loadNext();
334
+ }
335
+ }, table.loadState === "loading" || table.loadState === "streaming" ? props.loadingLabel : props.label) : null;
336
+ }
337
+ });
338
+
339
+ //#endregion
340
+ //#region src/editor.ts
341
+ function editorText(value, kind) {
342
+ if (value === null || value === void 0) return "";
343
+ if (kind === "date" && value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString().slice(0, 10) : "";
344
+ return formatValue(value);
345
+ }
346
+ const cellEditorProps = {
347
+ editor: {
348
+ type: Object,
349
+ required: true
350
+ },
351
+ spec: {
352
+ type: Object,
353
+ default: () => ({ kind: "text" })
354
+ },
355
+ /** Keyboard movement is owned by the surrounding cell. */
356
+ finish: {
357
+ type: Function,
358
+ default: void 0
359
+ },
360
+ autofocus: {
361
+ type: Boolean,
362
+ default: true
363
+ },
364
+ blurCommit: {
365
+ type: Boolean,
366
+ default: true
367
+ },
368
+ tabCommit: {
369
+ type: Boolean,
370
+ default: true
371
+ }
372
+ };
373
+ /** Plain native fields. Select results are searched and capped at 50 mounted options. */
374
+ const DataCellEditor = defineComponent({
375
+ name: "DataCellEditor",
376
+ props: cellEditorProps,
377
+ setup(props) {
378
+ const field = ref();
379
+ const query = ref("");
380
+ const options = props.spec.options ?? [];
381
+ const matches = computed(() => {
382
+ const search = query.value.trim().toLocaleLowerCase();
383
+ return options.map((value, index) => ({
384
+ value,
385
+ index
386
+ })).filter(({ value }) => String(value ?? "").toLocaleLowerCase().includes(search));
387
+ });
388
+ onMounted(() => {
389
+ if (!props.autofocus) return;
390
+ field.value?.focus();
391
+ if (field.value && "setSelectionRange" in field.value && ["text", "number"].includes(props.spec.kind)) field.value.setSelectionRange(field.value.value.length, field.value.value.length);
392
+ });
393
+ const finish = (direction) => {
394
+ if (props.finish) props.finish(direction);
395
+ else props.editor.commit();
396
+ };
397
+ const keydown = (event) => {
398
+ if (event.isComposing) return;
399
+ if (event.key === "Escape") {
400
+ event.preventDefault();
401
+ event.stopPropagation();
402
+ if (!props.editor.pending) props.editor.cancel();
403
+ } else if (event.key === "Enter" || event.key === "Tab") {
404
+ if (event.key === "Tab" && !props.tabCommit) return;
405
+ event.preventDefault();
406
+ event.stopPropagation();
407
+ if (!props.editor.pending) finish(event.key === "Tab" ? event.shiftKey ? "left" : "right" : event.shiftKey ? "up" : "down");
408
+ }
409
+ };
410
+ const paste = (event) => {
411
+ const text = event.clipboardData?.getData("text/plain");
412
+ if (text === void 0 || props.editor.pending) return;
413
+ event.preventDefault();
414
+ const target = event.target;
415
+ const start = target.selectionStart ?? 0;
416
+ const end = target.selectionEnd ?? target.value.length;
417
+ props.editor.input(target.value.slice(0, start) + text + target.value.slice(end));
418
+ };
419
+ return () => {
420
+ const { editor, spec } = props;
421
+ const attrs = {
422
+ ...editor.attrs,
423
+ "aria-label": `Edit ${editor.column.header}`,
424
+ "data-editor": "",
425
+ disabled: editor.pending,
426
+ onKeydown: keydown
427
+ };
428
+ if (spec.kind === "select") {
429
+ const shown = matches.value.slice(0, 50);
430
+ return h("div", {
431
+ "data-select-editor": "",
432
+ onKeydown: (event) => {
433
+ if (event.key === "Escape") keydown(event);
434
+ }
435
+ }, [
436
+ h("input", {
437
+ ref: field,
438
+ type: "search",
439
+ value: query.value,
440
+ "aria-label": `Search ${editor.column.header} options`,
441
+ disabled: editor.pending,
442
+ onInput: (event) => {
443
+ query.value = event.target.value;
444
+ },
445
+ onKeydown: (event) => {
446
+ if (event.key === "Enter" && shown[0]) {
447
+ event.preventDefault();
448
+ editor.commit(shown[0].value);
449
+ } else keydown(event);
450
+ }
451
+ }),
452
+ h("select", {
453
+ ...attrs,
454
+ value: options.findIndex((value) => Object.is(value, editor.value)),
455
+ size: Math.max(2, Math.min(5, shown.length)),
456
+ onChange: (event) => {
457
+ const index = Number(event.target.value);
458
+ if (index >= 0) editor.commit(options[index]);
459
+ }
460
+ }, shown.map(({ value, index }) => h("option", {
461
+ key: index,
462
+ value: index
463
+ }, String(value ?? "(empty)")))),
464
+ h("span", {
465
+ role: "status",
466
+ "data-editor-hint": ""
467
+ }, matches.value.length === 0 ? "No matching options. Change your search." : matches.value.length > 50 ? "Showing the first 50 matches. Refine your search." : `${matches.value.length} options`)
468
+ ]);
469
+ }
470
+ return h("input", {
471
+ ...attrs,
472
+ ref: field,
473
+ type: spec.kind === "date" ? "date" : spec.kind === "checkbox" ? "checkbox" : "text",
474
+ inputmode: spec.kind === "number" ? "decimal" : void 0,
475
+ value: editor.draft,
476
+ checked: spec.kind === "checkbox" ? editor.value === true : void 0,
477
+ onInput: spec.kind === "checkbox" ? void 0 : (event) => editor.input(event.target.value),
478
+ onChange: spec.kind === "checkbox" ? (event) => editor.commit(event.target.checked) : void 0,
479
+ onBlur: () => {
480
+ if (props.blurCommit && !editor.pending) finish();
481
+ },
482
+ onPaste: spec.kind === "checkbox" ? void 0 : paste
483
+ });
270
484
  };
271
485
  }
272
486
  });
487
+ /** The cell owns the id so it remains stable while its editor mounts and unmounts. */
488
+ function issueAttrs(id, issues, pending) {
489
+ return {
490
+ "aria-invalid": issues.length ? "true" : void 0,
491
+ "aria-describedby": issues.length ? id : void 0,
492
+ "aria-busy": pending ? "true" : void 0
493
+ };
494
+ }
495
+ function editorIssues(id, issues, pending = false) {
496
+ return issues.length || pending ? h("span", {
497
+ id,
498
+ "data-edit-issues": "",
499
+ role: "status"
500
+ }, pending ? "Checking changes…" : issues.map((problem) => problem.message).join(" ")) : null;
501
+ }
502
+ function useEditorId() {
503
+ return `vt-edit-${useId()}`;
504
+ }
505
+
506
+ //#endregion
507
+ //#region src/virtual.ts
508
+ /** Shared opt-in renderer props. Options are applied when the virtual renderer is created. */
509
+ const virtualProps = {
510
+ virtual: {
511
+ type: [Boolean, Object],
512
+ default: false
513
+ },
514
+ height: {
515
+ type: String,
516
+ default: void 0
517
+ },
518
+ rowHeight: {
519
+ type: Number,
520
+ default: 40
521
+ },
522
+ overscan: {
523
+ type: Number,
524
+ default: 5
525
+ },
526
+ virtualColumns: {
527
+ type: Boolean,
528
+ default: false
529
+ },
530
+ columnWidth: {
531
+ type: Number,
532
+ default: 120
533
+ }
534
+ };
535
+ function virtualOptions(props) {
536
+ return {
537
+ rowHeight: props.rowHeight,
538
+ overscan: props.overscan,
539
+ columnWidth: props.columnWidth,
540
+ ...typeof props.virtual === "object" ? props.virtual : {}
541
+ };
542
+ }
543
+ const viewportKey = Symbol("vt-viewport");
544
+ const virtualKey = Symbol("vt-virtual-renderer");
545
+ function injectVirtual() {
546
+ return inject(virtualKey, void 0);
547
+ }
548
+ /** A scroll container for a headless table/grid. It supplies its element without global reads. */
549
+ const DataTableViewport = defineComponent({
550
+ name: "DataTableViewport",
551
+ slots: Object,
552
+ props: {
553
+ as: asProp("div"),
554
+ height: {
555
+ type: String,
556
+ default: "24rem"
557
+ }
558
+ },
559
+ setup(props, { slots, expose }) {
560
+ const element = shallowRef(null);
561
+ provide(viewportKey, element);
562
+ expose({ element });
563
+ return () => h(props.as, {
564
+ ref: element,
565
+ "data-virtual-viewport": "",
566
+ style: {
567
+ height: props.height,
568
+ overflow: "auto",
569
+ overflowAnchor: "none"
570
+ }
571
+ }, slots.default?.());
572
+ }
573
+ });
574
+ function createComponentVirtual(table, options, tableElement, grid, virtualColumns = false, gutter = 0) {
575
+ const scrollElement = inject(viewportKey, shallowRef(null));
576
+ const rows = useVirtualRows(table, {
577
+ scrollElement,
578
+ estimateRowHeight: options.rowHeight,
579
+ overscan: options.overscan,
580
+ initialCount: options.initialCount,
581
+ grid,
582
+ scrollMargin: () => tableElement()?.querySelector("thead")?.getBoundingClientRect().height ?? 0
583
+ });
584
+ onMounted(() => {
585
+ watch(() => [
586
+ rows.endIndex,
587
+ table.renderItems.length,
588
+ table.canLoadMore
589
+ ], () => {
590
+ if (table.loadingMode === "cursor" && table.canLoadMore && rows.endIndex >= table.renderItems.length - 1 - table.endThreshold) table.loadNext();
591
+ }, { immediate: true });
592
+ });
593
+ const columns = grid && virtualColumns ? useVirtualColumns(grid, {
594
+ scrollElement,
595
+ estimateColumnWidth: (index) => table.columns[index]?.width ?? options.columnWidth ?? 120,
596
+ overscan: options.columnOverscan ?? 1,
597
+ initialCount: options.initialColumnCount ?? 5,
598
+ scrollMargin: gutter
599
+ }) : void 0;
600
+ const pinnedRows = computed(() => {
601
+ const result = [...rows.items];
602
+ const positions = /* @__PURE__ */ new Set([grid?.selection?.focus.row, grid?.editor?.position.row]);
603
+ for (const position of positions) {
604
+ if (position === void 0 || result.some((item) => item.renderItem.kind === "row" && item.renderItem.rowIndex === position)) continue;
605
+ const direct = table.renderItems[position];
606
+ const index = direct?.kind === "row" && direct.rowIndex === position ? position : table.renderItems.findIndex((item) => item.kind === "row" && item.rowIndex === position);
607
+ const item = rows.getItem(index);
608
+ if (item) result.push(item);
609
+ }
610
+ return result.sort((a, b) => a.index - b.index);
611
+ });
612
+ const pinnedColumns = computed(() => {
613
+ if (!columns) return void 0;
614
+ const result = [...columns.items];
615
+ const positions = /* @__PURE__ */ new Set([grid?.selection?.focus.column, grid?.editor?.position.column]);
616
+ for (const position of positions) {
617
+ if (position === void 0 || result.some((item) => item.index === position)) continue;
618
+ const item = columns.getItem(position);
619
+ if (item) result.push(item);
620
+ }
621
+ return result.sort((a, b) => a.index - b.index);
622
+ });
623
+ const binding = {
624
+ rows,
625
+ columns,
626
+ gutter,
627
+ get rowItems() {
628
+ return pinnedRows.value;
629
+ },
630
+ get columnItems() {
631
+ return pinnedColumns.value;
632
+ }
633
+ };
634
+ provide(virtualKey, binding);
635
+ return binding;
636
+ }
637
+ /** Create composables only for an enabled renderer, preserving the ordinary component path. */
638
+ const VirtualContent = defineComponent({
639
+ name: "DataTableVirtualContent",
640
+ props: {
641
+ table: {
642
+ type: Object,
643
+ required: true
644
+ },
645
+ options: {
646
+ type: Object,
647
+ required: true
648
+ },
649
+ tableElement: {
650
+ type: Function,
651
+ required: true
652
+ },
653
+ grid: {
654
+ type: Object,
655
+ default: void 0
656
+ },
657
+ virtualColumns: {
658
+ type: Boolean,
659
+ default: false
660
+ },
661
+ gutter: {
662
+ type: Number,
663
+ default: 0
664
+ },
665
+ render: {
666
+ type: Function,
667
+ required: true
668
+ }
669
+ },
670
+ setup(props) {
671
+ const binding = createComponentVirtual(props.table, props.options, props.tableElement, props.grid, props.virtualColumns, props.gutter);
672
+ return () => props.render(binding);
673
+ }
674
+ });
675
+ function rowSpacer(size, colspan, key) {
676
+ return size > 0 ? h("tr", {
677
+ key,
678
+ "aria-hidden": "true",
679
+ role: "presentation",
680
+ "data-virtual-spacer": "",
681
+ style: { height: `${size}px` }
682
+ }, [h("td", {
683
+ colspan: Math.max(1, colspan),
684
+ style: {
685
+ height: `${size}px`,
686
+ padding: "0",
687
+ border: "0",
688
+ lineHeight: "0"
689
+ }
690
+ })]) : null;
691
+ }
692
+ function columnSpacer(size, tag, key) {
693
+ return size > 0 ? h(tag, {
694
+ key,
695
+ "aria-hidden": "true",
696
+ role: "presentation",
697
+ "data-virtual-spacer": "",
698
+ style: {
699
+ width: `${size}px`,
700
+ minWidth: `${size}px`,
701
+ padding: "0",
702
+ border: "0"
703
+ }
704
+ }) : null;
705
+ }
706
+ function renderColumns(tableColumns, binding, render, tag) {
707
+ if (!binding?.columns || !binding.columnItems) return tableColumns.map(render);
708
+ const result = [];
709
+ let end = 0;
710
+ for (const item of binding.columnItems) {
711
+ result.push(columnSpacer(item.start - end, tag, `gap-${item.index}`), render(item.column, item.index));
712
+ end = item.start + item.size;
713
+ }
714
+ result.push(columnSpacer(binding.columns.totalSize - end, tag, "gap-end"));
715
+ return result;
716
+ }
717
+ function renderRows(binding, colspan, render) {
718
+ const result = [];
719
+ let end = 0;
720
+ for (const item of binding.rowItems) {
721
+ result.push(rowSpacer(item.start - end, colspan, `gap-${item.index}`), render(item));
722
+ end = item.start + item.size;
723
+ }
724
+ result.push(rowSpacer(binding.rows.totalSize - end, colspan, "gap-end"));
725
+ return result;
726
+ }
727
+ function virtualColgroup(tableColumns, binding) {
728
+ if (!binding.columns || !binding.columnItems) return null;
729
+ const cols = binding.gutter ? [h("col", { style: { width: `${binding.gutter}px` } })] : [];
730
+ let end = 0;
731
+ for (const item of binding.columnItems) {
732
+ if (item.start > end) cols.push(h("col", { style: { width: `${item.start - end}px` } }));
733
+ cols.push(h("col", { style: { width: `${item.size}px` } }));
734
+ end = item.start + item.size;
735
+ }
736
+ if (binding.columns.totalSize > end) cols.push(h("col", { style: { width: `${binding.columns.totalSize - end}px` } }));
737
+ return h("colgroup", cols);
738
+ }
739
+ function virtualRow(binding, row) {
740
+ return binding.rowItems.find((item) => item.renderItem.kind === "row" && item.renderItem.row.key === row.key);
741
+ }
742
+
743
+ //#endregion
744
+ //#region src/hierarchy.ts
745
+ const hierarchyProps = {
746
+ treeColumn: {
747
+ type: String,
748
+ default: void 0
749
+ },
750
+ keepAliveDetail: {
751
+ type: Boolean,
752
+ default: false
753
+ }
754
+ };
755
+ const hierarchyKey = Symbol("vueye-table-hierarchy");
756
+ function provideHierarchy(table, props) {
757
+ const focus = shallowRef();
758
+ const focusKey = computed(() => table.rows.some((row) => row.key === focus.value) ? focus.value : table.rows[0]?.key);
759
+ const context = {
760
+ id: `vt-tree-${useId()}`,
761
+ table,
762
+ get treeColumn() {
763
+ return props.treeColumn;
764
+ },
765
+ get keepAliveDetail() {
766
+ return props.keepAliveDetail;
767
+ },
768
+ focus,
769
+ get focusKey() {
770
+ return focusKey.value;
771
+ }
772
+ };
773
+ provide(hierarchyKey, context);
774
+ return context;
775
+ }
776
+ function injectHierarchy() {
777
+ return inject(hierarchyKey, void 0);
778
+ }
779
+ function resolveTreeColumn(table, column) {
780
+ return table.columns.find((candidate) => candidate.id === column)?.id ?? table.columns[0]?.id;
781
+ }
782
+ function itemId(context, row, detail = false) {
783
+ return context ? `${context.id}-${encodeURIComponent(getRowItemKey(row.key, detail ? "detail" : "row"))}` : void 0;
784
+ }
785
+ const siblingCache = /* @__PURE__ */ new WeakMap();
786
+ function hierarchyRowAttrs(table, row, context) {
787
+ if (!table.tree) return {};
788
+ let positions = siblingCache.get(table.processedRows);
789
+ if (!positions) {
790
+ const groups = /* @__PURE__ */ new Map();
791
+ for (const candidate of table.processedRows) {
792
+ const group = groups.get(candidate.parentKey) ?? [];
793
+ group.push(candidate);
794
+ groups.set(candidate.parentKey, group);
795
+ }
796
+ const indices = new Map(table.processedRows.map((candidate, index) => [candidate.key, index]));
797
+ const result = /* @__PURE__ */ new Map();
798
+ for (const group of groups.values()) group.forEach((candidate, index) => result.set(candidate.key, {
799
+ position: index + 1,
800
+ size: group.length,
801
+ index: indices.get(candidate.key)
802
+ }));
803
+ positions = result;
804
+ siblingCache.set(table.processedRows, result);
805
+ }
806
+ const position = positions.get(row.key);
807
+ return {
808
+ id: itemId(context, row),
809
+ "data-tree-key": getRowItemKey(row.key),
810
+ "aria-level": row.depth + 1,
811
+ "aria-expanded": row.canExpand ? String(row.isExpanded) : void 0,
812
+ "aria-setsize": position?.size,
813
+ "aria-posinset": position?.position,
814
+ "aria-rowindex": position ? position.index + 2 : void 0,
815
+ "aria-busy": row.childStatus === "loading" ? "true" : void 0
816
+ };
817
+ }
818
+ /** Row-focused table navigation; cell-focused grids use the same pure core commands. */
819
+ function treeKeydown(event, context, virtual) {
820
+ const table = context.table;
821
+ const target = event.target;
822
+ if (!table.tree || !target?.closest) return;
823
+ if (target.closest("input, textarea, select, [contenteditable=true]") || target.closest("button") && !target.closest("[data-expand-toggle]")) return;
824
+ const key = target.closest("tr")?.getAttribute("data-tree-key");
825
+ const index = table.rows.findIndex((row) => getRowItemKey(row.key) === key);
826
+ const row = table.rows[index];
827
+ if (!row) return;
828
+ const command = gridCommand(event, {
829
+ canExpand: row.canExpand,
830
+ expanded: row.isExpanded
831
+ });
832
+ let next = row;
833
+ if (command?.type === "tree") {
834
+ if (command.action === "expand" || command.action === "collapse") table.toggleExpanded(row.key, command.action === "expand");
835
+ else if (command.action === "siblings") for (const sibling of table.processedRows.filter((candidate) => candidate.parentKey === row.parentKey && candidate.canExpand)) table.toggleExpanded(sibling.key, true);
836
+ else if (command.action === "child") next = table.rows.find((candidate) => candidate.parentKey === row.key) ?? row;
837
+ else next = table.rows.find((candidate) => candidate.key === row.parentKey) ?? row;
838
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") next = table.rows[index + (event.key === "ArrowDown" ? 1 : -1)] ?? row;
839
+ else if (event.key === "Home" || event.key === "End") next = table.rows[event.key === "Home" ? 0 : table.rows.length - 1] ?? row;
840
+ else return;
841
+ event.preventDefault();
842
+ context.focus.value = next.key;
843
+ virtual?.rows.scrollToKey(next.key);
844
+ const id = itemId(context, next);
845
+ nextTick(() => target.closest("table")?.ownerDocument.getElementById(id ?? "")?.focus());
846
+ }
847
+ const expandToggleProps = {
848
+ row: rowProp,
849
+ controls: {
850
+ type: String,
851
+ default: void 0
852
+ },
853
+ expandLabel: {
854
+ type: String,
855
+ default: "Expand row"
856
+ },
857
+ collapseLabel: {
858
+ type: String,
859
+ default: "Collapse row"
860
+ }
861
+ };
862
+ const treeCellProps = {
863
+ row: rowProp,
864
+ as: asProp("div")
865
+ };
866
+ const DataTableExpandToggle = defineComponent({
867
+ name: "DataTableExpandToggle",
868
+ props: expandToggleProps,
869
+ slots: Object,
870
+ setup(props, { slots }) {
871
+ const table = injectDataTable("<DataTableExpandToggle>");
872
+ const context = injectHierarchy();
873
+ const grid = injectDataGrid();
874
+ return () => props.row.canExpand ? h("button", {
875
+ type: "button",
876
+ "data-expand-toggle": "",
877
+ tabindex: table.tree ? -1 : void 0,
878
+ "aria-expanded": String(props.row.isExpanded),
879
+ "aria-controls": props.controls ?? (!table.tree ? itemId(context, props.row, true) : props.row.isExpanded ? table.rows.filter((row) => row.parentKey === props.row.key).map((row) => itemId(context, row)).filter(Boolean).join(" ") || void 0 : void 0),
880
+ "aria-label": props.row.isExpanded ? props.collapseLabel : props.expandLabel,
881
+ onMousedown: (event) => event.stopPropagation(),
882
+ onDblclick: (event) => event.stopPropagation(),
883
+ onClick: (event) => {
884
+ event.stopPropagation();
885
+ if (grid?.table === table) grid.focusCell({
886
+ row: table.rows.indexOf(props.row),
887
+ column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
888
+ });
889
+ table.toggleExpanded(props.row.key);
890
+ if (grid?.table === table) nextTick(() => event.target.closest("table")?.focus());
891
+ }
892
+ }, slots.default?.({
893
+ row: props.row,
894
+ expanded: props.row.isExpanded
895
+ }) ?? (props.row.isExpanded ? props.collapseLabel : props.expandLabel)) : null;
896
+ }
897
+ });
898
+ const DataTableTreeCell = defineComponent({
899
+ name: "DataTableTreeCell",
900
+ props: treeCellProps,
901
+ slots: Object,
902
+ setup(props, { slots }) {
903
+ const table = injectDataTable("<DataTableTreeCell>");
904
+ const context = injectHierarchy();
905
+ const grid = injectDataGrid();
906
+ return () => h(props.as, {
907
+ class: "vt-tree-cell",
908
+ "data-tree-cell": "",
909
+ style: { paddingInlineStart: `calc(var(--vt-tree-indent, 1.25rem) * ${props.row.depth})` }
910
+ }, [
911
+ h("span", { class: "vt-tree-toggle" }, slots.toggle?.({ row: props.row }) ?? h(DataTableExpandToggle, { row: props.row })),
912
+ h("span", { class: "vt-tree-label" }, [slots.default?.()]),
913
+ props.row.childStatus === "loading" ? h("span", { class: "vt-tree-loading" }, "Loading children…") : null,
914
+ props.row.childStatus === "error" ? h("span", { class: "vt-tree-error" }, ["Could not load children. ", h("button", {
915
+ type: "button",
916
+ class: "vt-tree-retry",
917
+ onMousedown: (event) => event.stopPropagation(),
918
+ onClick: (event) => {
919
+ event.stopPropagation();
920
+ const element = event.currentTarget.closest("table");
921
+ if (grid?.table === table) grid.focusCell({
922
+ row: table.rows.indexOf(props.row),
923
+ column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
924
+ });
925
+ table.toggleExpanded(props.row.key, true);
926
+ nextTick(() => {
927
+ if (grid?.table === table) element?.focus();
928
+ else element?.ownerDocument.getElementById(itemId(context, props.row) ?? "")?.focus();
929
+ });
930
+ }
931
+ }, "Retry children")]) : null
932
+ ]);
933
+ }
934
+ });
935
+ /** One full-width detail; retained collapsed content remains hidden and unmeasured. */
936
+ const DataTableDetailRow = defineComponent({
937
+ name: "DataTableDetailRow",
938
+ props: {
939
+ row: rowProp,
940
+ colspan: {
941
+ type: Number,
942
+ default: void 0
943
+ },
944
+ hidden: {
945
+ type: Boolean,
946
+ default: false
947
+ },
948
+ keepAlive: {
949
+ type: Boolean,
950
+ default: void 0
951
+ }
952
+ },
953
+ slots: Object,
954
+ setup(props, { slots }) {
955
+ const table = injectDataTable("<DataTableDetailRow>");
956
+ const context = injectHierarchy();
957
+ const virtual = injectVirtual();
958
+ let opened = false;
959
+ return () => {
960
+ if (props.row.isExpanded && !props.hidden) opened = true;
961
+ if (!opened || (!props.row.isExpanded || props.hidden) && !(props.keepAlive ?? context?.keepAliveDetail)) return null;
962
+ const hidden = props.hidden || !props.row.isExpanded;
963
+ return h("tr", {
964
+ id: itemId(context, props.row, true),
965
+ "data-detail": "",
966
+ hidden,
967
+ ...virtual && !hidden ? { ref: (target) => virtual.rows.measureElement(target, getRowItemKey(props.row.key, "detail")) } : {}
968
+ }, [h("td", {
969
+ colspan: Math.max(1, props.colspan ?? table.columns.length),
970
+ class: "vt-detail-cell"
971
+ }, [slots.default?.({ row: props.row })])]);
972
+ };
973
+ }
974
+ });
975
+ function createDetails(table, slot, colspan) {
976
+ const context = injectHierarchy();
977
+ const visited = /* @__PURE__ */ new Set();
978
+ const render = (row, rowIndex, hidden = false) => {
979
+ if (!hidden && context?.keepAliveDetail) visited.add(row.key);
980
+ return h(DataTableDetailRow, {
981
+ key: getRowItemKey(row.key, "detail"),
982
+ row,
983
+ colspan: colspan(),
984
+ hidden
985
+ }, { default: () => slot()?.({
986
+ row,
987
+ rowIndex
988
+ }) });
989
+ };
990
+ return {
991
+ render,
992
+ retained(visible) {
993
+ if (!context?.keepAliveDetail) return [];
994
+ return [...visited].flatMap((key) => {
995
+ const index = table.rows.findIndex((row) => row.key === key);
996
+ if (index < 0) {
997
+ visited.delete(key);
998
+ return [];
999
+ }
1000
+ return visible.has(key) ? [] : [render(table.rows[index], index, true)];
1001
+ });
1002
+ }
1003
+ };
1004
+ }
273
1005
 
274
1006
  //#endregion
275
1007
  //#region src/table.ts
@@ -285,15 +1017,36 @@ const DataTableRoot = defineComponent({
285
1017
  type: Object,
286
1018
  required: true
287
1019
  },
288
- as: asProp("table")
1020
+ as: asProp("table"),
1021
+ ...virtualProps,
1022
+ ...hierarchyProps,
1023
+ ...columnLayoutProps
289
1024
  },
290
1025
  setup(props, { slots }) {
291
1026
  provideDataTable(props.table);
292
- return () => h(props.as, {
293
- "aria-rowcount": props.table.rowCount + 1,
294
- "aria-colcount": props.table.columns.length,
295
- "data-empty": flag(props.table.rowCount === 0)
1027
+ provideColumnOffset(() => props.leadingColumns);
1028
+ const hierarchy = provideHierarchy(props.table, props);
1029
+ const element = shallowRef();
1030
+ const editingGrid = injectEditingGrid();
1031
+ const render = (virtual) => h(props.as, {
1032
+ role: props.table.tree ? "treegrid" : void 0,
1033
+ "aria-rowcount": (props.table.tree ? props.table.processedRows.length : props.table.rowCount) + 1,
1034
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
1035
+ "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
1036
+ "data-empty": flag(props.table.rowCount === 0),
1037
+ onKeydown: (event) => treeKeydown(event, hierarchy, virtual),
1038
+ ...virtual ? {
1039
+ ref: element,
1040
+ "data-virtual": ""
1041
+ } : {}
296
1042
  }, slots.default?.({ table: props.table }) ?? [h(DataTableHeader), h(DataTableBody)]);
1043
+ return () => props.virtual ? h(VirtualContent, {
1044
+ table: props.table,
1045
+ ...editingGrid ? { grid: editingGrid } : {},
1046
+ options: virtualOptions(props),
1047
+ tableElement: () => element.value,
1048
+ render
1049
+ }) : render();
297
1050
  }
298
1051
  });
299
1052
  const DataTableCaption = defineComponent({
@@ -311,7 +1064,11 @@ const DataTableHeader = defineComponent({
311
1064
  props: { as: asProp("thead") },
312
1065
  setup(props, { slots }) {
313
1066
  const table = injectDataTable("<DataTableHeader>");
314
- return () => h(props.as, slots.default?.({ columns: table.columns }) ?? h(DataTableHeaderRow, () => table.columns.map((column) => h(DataTableHeaderCell, {
1067
+ const virtual = injectVirtual();
1068
+ return () => h(props.as, slots.default?.({ columns: table.columns }) ?? h(DataTableHeaderRow, () => virtual?.columns ? h(DataTableVirtualColumns, null, { default: ({ column }) => [h(DataTableHeaderCell, {
1069
+ key: column.id,
1070
+ column
1071
+ })] }) : table.columns.map((column) => h(DataTableHeaderCell, {
315
1072
  key: column.id,
316
1073
  column
317
1074
  }))));
@@ -325,6 +1082,23 @@ const DataTableHeaderRow = defineComponent({
325
1082
  return () => h(props.as, { "aria-rowindex": 1 }, slots.default?.());
326
1083
  }
327
1084
  });
1085
+ /** Shared header/body column slice, with native spacer cells for omitted columns. */
1086
+ const DataTableVirtualColumns = defineComponent({
1087
+ name: "DataTableVirtualColumns",
1088
+ slots: Object,
1089
+ props: { as: {
1090
+ type: String,
1091
+ default: "th"
1092
+ } },
1093
+ setup(props, { slots }) {
1094
+ const table = injectDataTable("<DataTableVirtualColumns>");
1095
+ const virtual = injectVirtual();
1096
+ return () => h(Fragment, renderColumns(table.columns, virtual, (column, index) => slots.default?.({
1097
+ column,
1098
+ index
1099
+ }), props.as));
1100
+ }
1101
+ });
328
1102
  /**
329
1103
  * A column header with `aria-sort`. Without a slot it shows the header text inside a sort button
330
1104
  * for sortable columns.
@@ -338,6 +1112,7 @@ const DataTableHeaderCell = defineComponent({
338
1112
  },
339
1113
  setup(props, { slots }) {
340
1114
  const table = injectDataTable("<DataTableHeaderCell>");
1115
+ const columnOffset = injectColumnOffset();
341
1116
  return () => {
342
1117
  const { column } = props;
343
1118
  const sort = table.getSort(column.id);
@@ -354,7 +1129,8 @@ const DataTableHeaderCell = defineComponent({
354
1129
  "data-align": column.align,
355
1130
  "data-sortable": flag(column.sortable),
356
1131
  "data-sort": sort?.direction,
357
- style: columnStyle(column)
1132
+ style: columnStyle(column),
1133
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
358
1134
  }, content);
359
1135
  };
360
1136
  }
@@ -397,38 +1173,114 @@ const DataTableSortButton = defineComponent({
397
1173
  const DataTableBody = defineComponent({
398
1174
  name: "DataTableBody",
399
1175
  slots: Object,
400
- props: { as: asProp("tbody") },
1176
+ props: {
1177
+ as: asProp("tbody"),
1178
+ ...virtualProps,
1179
+ colspan: {
1180
+ type: Number,
1181
+ default: void 0
1182
+ }
1183
+ },
401
1184
  setup(props, { slots }) {
402
1185
  const table = injectDataTable("<DataTableBody>");
1186
+ const virtual = injectVirtual();
1187
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
403
1188
  return () => {
1189
+ if (props.virtual && !virtual) return h(VirtualDataTableBody, props, slots);
1190
+ if (virtual) return renderTableBody(table, virtual, props.as, props.colspan, slots, details);
404
1191
  let content = slots.default?.({ rows: table.rows });
405
- 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.rows.map((row) => h(DataTableRow, {
406
- key: row.key,
407
- row
408
- }));
409
- return h(props.as, content);
1192
+ 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, {
1193
+ key: item.key,
1194
+ row: item.row,
1195
+ rowIndex: item.rowIndex
1196
+ }) : details.render(item.row, item.rowIndex));
1197
+ const visibleDetails = new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key));
1198
+ const retained = slots.default ? [] : details.retained(visibleDetails);
1199
+ const sentinel = loadSentinel(table, props.colspan ?? table.columns.length);
1200
+ return h(props.as, [
1201
+ ...Array.isArray(content) ? content : content ? [content] : [],
1202
+ ...retained,
1203
+ ...sentinel ? [sentinel] : []
1204
+ ]);
410
1205
  };
411
1206
  }
412
1207
  });
1208
+ function renderTableBody(table, virtual, as, colspan, slots, details, element) {
1209
+ const width = colspan ?? table.columns.length;
1210
+ const attrs = element ? { ref: element } : {};
1211
+ const content = slots.default?.({ rows: virtual.rowItems.filter((item) => item.renderItem.kind === "row").map((item) => item.renderItem.row) });
1212
+ if (table.rows.length === 0 && slots.empty) return h(as, attrs, h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, width) }, slots.empty())]));
1213
+ if (content) return h(as, attrs, [
1214
+ rowSpacer(virtual.rows.paddingStart, width, "gap-start"),
1215
+ ...content,
1216
+ rowSpacer(virtual.rows.paddingEnd, width, "gap-end"),
1217
+ ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []
1218
+ ]);
1219
+ return h(as, attrs, [
1220
+ ...renderRows(virtual, width, (item) => item.renderItem.kind === "row" ? slots.row ? h(Fragment, { key: item.key }, [slots.row(item.renderItem)]) : h(DataTableRow, {
1221
+ key: item.key,
1222
+ row: item.renderItem.row,
1223
+ rowIndex: item.renderItem.rowIndex
1224
+ }) : details.render(item.renderItem.row, item.renderItem.rowIndex)),
1225
+ ...details.retained(new Set(virtual.rowItems.filter((item) => item.renderItem.kind === "detail").map((item) => item.renderItem.row.key))),
1226
+ ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []
1227
+ ]);
1228
+ }
1229
+ const VirtualDataTableBody = defineComponent({
1230
+ name: "DataTableVirtualBody",
1231
+ props: {
1232
+ as: asProp("tbody"),
1233
+ ...virtualProps,
1234
+ colspan: {
1235
+ type: Number,
1236
+ default: void 0
1237
+ }
1238
+ },
1239
+ slots: Object,
1240
+ setup(props, { slots }) {
1241
+ const table = injectDataTable("<DataTableBody>");
1242
+ const element = shallowRef();
1243
+ const virtual = createComponentVirtual(table, virtualOptions(props), () => element.value?.closest("table"));
1244
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
1245
+ return () => renderTableBody(table, virtual, props.as, props.colspan, slots, details, element);
1246
+ }
1247
+ });
413
1248
  /** A body row; `aria-selected` and `data-selected` follow the selection. */
414
1249
  const DataTableRow = defineComponent({
415
1250
  name: "DataTableRow",
416
1251
  slots: Object,
417
1252
  props: {
418
1253
  row: rowProp,
419
- as: asProp("tr")
1254
+ as: asProp("tr"),
1255
+ rowIndex: {
1256
+ type: Number,
1257
+ default: void 0
1258
+ }
420
1259
  },
421
1260
  setup(props, { slots }) {
422
1261
  const table = injectDataTable("<DataTableRow>");
1262
+ const virtual = injectVirtual();
1263
+ const hierarchy = injectHierarchy();
423
1264
  return () => {
424
1265
  const { row } = props;
425
1266
  const selected = table.isSelected(row.key);
426
- const position = table.rows.indexOf(row);
1267
+ const item = virtual ? virtualRow(virtual, row) : void 0;
1268
+ const position = props.rowIndex ?? item?.renderItem.rowIndex ?? table.rows.indexOf(row);
427
1269
  return h(props.as, {
428
1270
  "aria-rowindex": position === -1 ? void 0 : table.pageStart + position + 1,
429
1271
  "aria-selected": table.selectionMode === "none" ? void 0 : String(selected),
430
1272
  "data-selected": flag(selected),
431
- "data-key": String(row.key)
1273
+ "data-key": String(row.key),
1274
+ "data-dirty": flag(row.isDirty),
1275
+ ...hierarchyRowAttrs(table, row, hierarchy),
1276
+ tabindex: table.tree ? (hierarchy?.focusKey ?? table.rows[0]?.key) === row.key ? 0 : -1 : void 0,
1277
+ onFocus: () => {
1278
+ if (hierarchy) hierarchy.focus.value = row.key;
1279
+ },
1280
+ ...virtual && item ? {
1281
+ ref: (element) => virtual.rows.measureElement(element, item.key),
1282
+ "data-virtual-row": ""
1283
+ } : {}
432
1284
  }, slots.default?.({
433
1285
  row,
434
1286
  selected,
@@ -448,22 +1300,31 @@ const DataTableCell = defineComponent({
448
1300
  props: {
449
1301
  row: rowProp,
450
1302
  column: columnProp,
451
- as: asProp("td")
1303
+ as: asProp("td"),
1304
+ tree: {
1305
+ type: Boolean,
1306
+ default: true
1307
+ }
452
1308
  },
453
1309
  setup(props, { slots }) {
1310
+ const table = injectDataTable("<DataTableCell>");
1311
+ const columnOffset = injectColumnOffset();
1312
+ const hierarchy = injectHierarchy();
454
1313
  return () => {
455
1314
  const { row, column } = props;
456
1315
  const value = row.getValue(column.id);
457
1316
  const display = row.getDisplay(column.id);
458
- return h(props.as, {
459
- "data-column": column.id,
460
- "data-align": column.align
461
- }, slots.default?.({
1317
+ const content = () => slots.default?.({
462
1318
  row,
463
1319
  column,
464
1320
  value,
465
1321
  display
466
- }) ?? display);
1322
+ }) ?? display;
1323
+ return h(props.as, {
1324
+ "data-column": column.id,
1325
+ "data-align": column.align,
1326
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
1327
+ }, props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: content }) : content());
467
1328
  };
468
1329
  }
469
1330
  });
@@ -478,6 +1339,298 @@ const DataTableEmpty = defineComponent({
478
1339
  }
479
1340
  });
480
1341
 
1342
+ //#endregion
1343
+ //#region src/editing.ts
1344
+ const editingKey = Symbol("vueye-table-editing");
1345
+ function injectEditingGrid() {
1346
+ return inject(editingKey, void 0)?.grid;
1347
+ }
1348
+ /** Renderless provider for editable data-table cells and row forms. */
1349
+ const DataTableEditRoot = defineComponent({
1350
+ name: "DataTableEditRoot",
1351
+ slots: Object,
1352
+ props: {
1353
+ table: {
1354
+ type: Object,
1355
+ required: true
1356
+ },
1357
+ mode: {
1358
+ type: String,
1359
+ default: "cell"
1360
+ }
1361
+ },
1362
+ emits: {
1363
+ save: (_result) => true,
1364
+ cancel: (_key) => true
1365
+ },
1366
+ setup(props, { slots, emit, expose }) {
1367
+ const grid = useDataGrid(props.table);
1368
+ const draft = shallowRef();
1369
+ const context = {
1370
+ grid,
1371
+ get mode() {
1372
+ return props.mode;
1373
+ },
1374
+ get draft() {
1375
+ return draft.value;
1376
+ },
1377
+ start(row) {
1378
+ if (draft.value?.pending) return;
1379
+ if (draft.value) context.cancel(draft.value.rowKey);
1380
+ grid.cancelEdit();
1381
+ const column = props.table.columns.findIndex((candidate) => candidate.isEditable(row.original));
1382
+ grid.focusCell({
1383
+ row: props.table.rows.findIndex((candidate) => candidate.key === row.key),
1384
+ column
1385
+ });
1386
+ grid.startEdit();
1387
+ draft.value = props.table.editRow(row.key);
1388
+ },
1389
+ async save() {
1390
+ const active = draft.value;
1391
+ if (!active || active.pending) return void 0;
1392
+ const result = await active.save();
1393
+ if (draft.value === active && active.status !== "editing" && !active.pending) {
1394
+ draft.value = void 0;
1395
+ grid.cancelEdit();
1396
+ }
1397
+ if (result.status !== "rejected") emit("save", result);
1398
+ return result;
1399
+ },
1400
+ cancel(key) {
1401
+ if (draft.value?.rowKey === key && !draft.value.pending) {
1402
+ draft.value.cancel();
1403
+ draft.value = void 0;
1404
+ grid.cancelEdit();
1405
+ emit("cancel", key);
1406
+ }
1407
+ }
1408
+ };
1409
+ provide(editingKey, context);
1410
+ expose({ editing: context });
1411
+ watch(() => props.table.rows, (rows) => {
1412
+ if (draft.value && !rows.some((row) => row.key === draft.value?.rowKey)) context.cancel(draft.value.rowKey);
1413
+ });
1414
+ onScopeDispose(() => draft.value?.cancel());
1415
+ return () => slots.default?.({ editing: context });
1416
+ }
1417
+ });
1418
+ /** A cell with on-demand editing; row mode shares one draft and one save operation. */
1419
+ const DataTableEditCell = defineComponent({
1420
+ name: "DataTableEditCell",
1421
+ props: {
1422
+ row: rowProp,
1423
+ column: columnProp,
1424
+ rowIndex: {
1425
+ type: Number,
1426
+ default: void 0
1427
+ },
1428
+ tree: {
1429
+ type: Boolean,
1430
+ default: true
1431
+ }
1432
+ },
1433
+ slots: Object,
1434
+ emits: {
1435
+ save: (_result) => true,
1436
+ cancel: (_key) => true
1437
+ },
1438
+ setup(props, { slots, emit }) {
1439
+ const table = injectDataTable("<DataTableEditCell>");
1440
+ const context = inject(editingKey);
1441
+ if (!context) throw new Error("<DataTableEditCell> requires <DataTableEditRoot>.");
1442
+ const { grid } = context;
1443
+ const id = useEditorId();
1444
+ const element = shallowRef();
1445
+ const position = () => ({
1446
+ row: props.rowIndex ?? table.rows.indexOf(props.row),
1447
+ column: table.columns.indexOf(props.column)
1448
+ });
1449
+ const active = computed(() => context.mode === "row" ? context.draft?.rowKey === props.row.key : grid.isEditing(position()));
1450
+ const pending = computed(() => context.mode === "row" ? !!context.draft?.pending : active.value && grid.lastResult?.status === "pending");
1451
+ const localText = shallowRef("");
1452
+ watch(active, (value) => {
1453
+ if (value) localText.value = editorText(props.row.getValue(props.column.id), grid.editorFor(position())?.kind ?? "text");
1454
+ });
1455
+ const focus = (direction) => {
1456
+ nextTick(() => {
1457
+ if (!direction) {
1458
+ element.value?.focus();
1459
+ return;
1460
+ }
1461
+ const all = [...element.value?.closest("table")?.querySelectorAll("[data-edit-cell][data-editable]") ?? []];
1462
+ all[all.indexOf(element.value) + (direction === "left" ? -1 : 1)]?.focus();
1463
+ });
1464
+ };
1465
+ const submit = (value, direction) => {
1466
+ if (context.mode === "row") {
1467
+ if (value !== void 0) {
1468
+ if (typeof value === "string") {
1469
+ localText.value = value;
1470
+ context.draft?.setValue(props.column.id, value);
1471
+ context.draft?.setInput(props.column.id, value);
1472
+ } else context.draft?.setValue(props.column.id, value);
1473
+ }
1474
+ return;
1475
+ }
1476
+ const result = grid.submitEdit(value === void 0 ? void 0 : typeof value === "string" ? { input: value } : { value });
1477
+ const finish = (final) => {
1478
+ if (final.status !== "rejected") {
1479
+ emit("save", final);
1480
+ focus(direction);
1481
+ }
1482
+ };
1483
+ if (result?.completion) result.completion.then(finish);
1484
+ else if (result) finish(result);
1485
+ return result;
1486
+ };
1487
+ return () => {
1488
+ const { row, column } = props;
1489
+ const editable = column.isEditable(row.original);
1490
+ const editing = editable && active.value;
1491
+ const draft = context.draft?.rowKey === row.key ? context.draft : void 0;
1492
+ const issues = draft?.issues.filter((problem) => problem.column === column.id) ?? (row.cellIssues?.get(column.id) ? [row.cellIssues.get(column.id)] : active.value ? grid.lastResult?.issues.filter((problem) => problem.rowKey === row.key && (problem.column === column.id || !problem.column)) ?? [] : []);
1493
+ const spec = editing ? grid.editorFor(position()) ?? { kind: "text" } : void 0;
1494
+ const input = (text) => {
1495
+ localText.value = text;
1496
+ if (context.mode === "row") {
1497
+ draft?.setValue(column.id, text);
1498
+ draft?.setInput(column.id, text);
1499
+ } else grid.updateDraft(text);
1500
+ };
1501
+ const cancel = () => {
1502
+ if (pending.value) return;
1503
+ if (context.mode === "row") context.cancel(row.key);
1504
+ else {
1505
+ grid.cancelEdit();
1506
+ emit("cancel", row.key);
1507
+ }
1508
+ focus();
1509
+ };
1510
+ const editor = {
1511
+ row,
1512
+ column,
1513
+ value: context.mode === "row" ? Reflect.get(draft?.values ?? {}, column.id) : row.getValue(column.id),
1514
+ draft: context.mode === "row" ? localText.value : grid.editor?.draft ?? "",
1515
+ spec: spec ?? { kind: "text" },
1516
+ finish: (direction) => {
1517
+ if (context.mode === "row") context.save();
1518
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1519
+ },
1520
+ input,
1521
+ update: input,
1522
+ commit: submit,
1523
+ cancel,
1524
+ issues,
1525
+ pending: pending.value,
1526
+ attrs: issueAttrs(id, issues, pending.value)
1527
+ };
1528
+ return h(DataTableCell, {
1529
+ ref: (target) => {
1530
+ element.value = target && typeof target === "object" && "$el" in target ? target.$el : void 0;
1531
+ },
1532
+ row,
1533
+ column,
1534
+ tree: props.tree,
1535
+ "data-edit-cell": "",
1536
+ "data-editable": editable ? "" : void 0,
1537
+ "data-editing": editing ? "" : void 0,
1538
+ "data-dirty": row.isDirty ? "" : void 0,
1539
+ ...issueAttrs(id, issues, pending.value),
1540
+ tabindex: editable ? 0 : void 0,
1541
+ onFocus: (event) => {
1542
+ if (event.target === element.value && !grid.editor) grid.focusCell(position());
1543
+ },
1544
+ onDblclick: () => {
1545
+ if (editable && context.mode === "cell") {
1546
+ grid.focusCell(position());
1547
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1548
+ }
1549
+ },
1550
+ onKeydown: (event) => {
1551
+ if (event.target === element.value && event.key === "Enter" && editable) {
1552
+ event.preventDefault();
1553
+ event.stopPropagation();
1554
+ if (context.mode === "row") context.start(row);
1555
+ else {
1556
+ grid.focusCell(position());
1557
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1558
+ }
1559
+ }
1560
+ }
1561
+ }, { default: ({ value, display }) => [editing ? slots.editor?.(editor) ?? h(DataCellEditor, {
1562
+ editor,
1563
+ spec: spec ?? { kind: "text" },
1564
+ autofocus: context.mode === "cell" || table.columns.find((candidate) => candidate.isEditable(row.original))?.id === column.id,
1565
+ blurCommit: context.mode === "cell",
1566
+ tabCommit: context.mode === "cell",
1567
+ finish: (direction) => {
1568
+ if (context.mode === "row") context.save();
1569
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1570
+ }
1571
+ }) : slots.default?.({
1572
+ row,
1573
+ value,
1574
+ display
1575
+ }) ?? display, editorIssues(id, issues, pending.value)] });
1576
+ };
1577
+ }
1578
+ });
1579
+ /** One active row form. Controls share the provider's draft and core save/undo path. */
1580
+ const DataTableRowActions = defineComponent({
1581
+ name: "DataTableRowActions",
1582
+ props: {
1583
+ row: rowProp,
1584
+ editable: {
1585
+ type: Boolean,
1586
+ default: true
1587
+ },
1588
+ removable: {
1589
+ type: Boolean,
1590
+ default: false
1591
+ }
1592
+ },
1593
+ setup(props) {
1594
+ const table = injectDataTable("<DataTableRowActions>");
1595
+ const context = inject(editingKey, void 0);
1596
+ const id = useEditorId();
1597
+ const element = shallowRef();
1598
+ watch(() => context?.draft?.rowKey === props.row.key, (active, previous) => {
1599
+ const target = element.value;
1600
+ if (previous && !active && !context?.draft && target?.closest("tr")?.contains(target.ownerDocument.activeElement)) nextTick(() => target.querySelector("button[data-edit-row]")?.focus());
1601
+ });
1602
+ return () => {
1603
+ const draft = context?.draft?.rowKey === props.row.key ? context.draft : void 0;
1604
+ const issues = [...new Map(draft?.issues.map((problem) => [problem.message, problem])).values()];
1605
+ const button = (label, action, disabled = false) => h("button", {
1606
+ type: "button",
1607
+ disabled,
1608
+ "aria-label": `${label}, row ${String(props.row.key)}`,
1609
+ "data-edit-row": label === "Edit row" ? "" : void 0,
1610
+ ...label === "Save row" ? issueAttrs(id, issues, !!draft?.pending) : {},
1611
+ onClick: action
1612
+ }, label);
1613
+ return h("div", {
1614
+ ref: element,
1615
+ "data-row-actions": "",
1616
+ role: "group",
1617
+ "aria-label": `Row ${String(props.row.key)} actions`,
1618
+ "aria-busy": draft?.pending ? "true" : void 0
1619
+ }, [
1620
+ ...context?.mode === "row" && props.editable ? draft ? [button("Save row", () => context.save(), draft.pending), button("Cancel", () => context.cancel(props.row.key), draft.pending)] : [button("Edit row", () => context.start(props.row))] : [],
1621
+ h("details", { onToggle: (event) => {
1622
+ const menu = event.currentTarget;
1623
+ if (menu.open) nextTick(() => menu.scrollIntoView?.({
1624
+ block: "nearest",
1625
+ inline: "nearest"
1626
+ }));
1627
+ } }, [h("summary", { "aria-label": `Actions for row ${String(props.row.key)}` }, "Actions"), h("div", { "data-row-menu": "" }, [button("Revert row", () => table.revert([props.row.key]), !props.row.isDirty || !!draft), props.removable ? button("Remove row", () => table.removeRows([props.row.key]), !!draft) : null])]),
1628
+ editorIssues(id, issues, !!draft?.pending)
1629
+ ]);
1630
+ };
1631
+ }
1632
+ });
1633
+
481
1634
  //#endregion
482
1635
  //#region src/grid.ts
483
1636
  const gridContextKey = Symbol("vueye-table-grid-context");
@@ -510,12 +1663,26 @@ const DataGridRoot = defineComponent({
510
1663
  label: {
511
1664
  type: String,
512
1665
  default: void 0
1666
+ },
1667
+ ...virtualProps,
1668
+ ...hierarchyProps,
1669
+ ...columnLayoutProps,
1670
+ /** Width reserved before data columns, for a row-number/header column. */
1671
+ gutter: {
1672
+ type: Number,
1673
+ default: 0
513
1674
  }
514
1675
  },
515
- setup(props, { slots, expose }) {
1676
+ emits: {
1677
+ save: (_result) => true,
1678
+ cancel: (_key) => true
1679
+ },
1680
+ setup(props, { slots, expose, emit }) {
516
1681
  provideDataTable(props.table);
517
- const grid = useDataGrid(props.table);
1682
+ provideColumnOffset(() => props.leadingColumns);
1683
+ const grid = useDataGrid(props.table, { treeColumn: () => props.treeColumn });
518
1684
  provideDataGrid(grid);
1685
+ provideHierarchy(props.table, props);
519
1686
  const element = ref();
520
1687
  const id = `vt-grid-${useId()}`;
521
1688
  const focus = () => {
@@ -523,14 +1690,52 @@ const DataGridRoot = defineComponent({
523
1690
  };
524
1691
  provide(gridContextKey, {
525
1692
  id,
526
- focus
1693
+ focus,
1694
+ cancel: (key) => emit("cancel", key)
1695
+ });
1696
+ watch(() => grid.lastResult, (result) => {
1697
+ if (result && [
1698
+ "applied",
1699
+ "partial",
1700
+ "unchanged"
1701
+ ].includes(result.status)) emit("save", result);
527
1702
  });
528
1703
  expose({
529
1704
  grid,
530
1705
  focus
531
1706
  });
1707
+ let virtual;
532
1708
  const onKeydown = (event) => {
533
1709
  if (event.target !== element.value) return;
1710
+ if (event.key === " " && !grid.editor && !event.ctrlKey && !event.metaKey && !event.altKey && grid.toggleCheckbox()) {
1711
+ event.preventDefault();
1712
+ return;
1713
+ }
1714
+ if (virtual && !grid.editor && (event.ctrlKey || event.metaKey) && (event.key === "Home" || event.key === "End")) {
1715
+ grid.focusCell(event.key === "Home" ? {
1716
+ row: 0,
1717
+ column: 0
1718
+ } : {
1719
+ row: props.table.rows.length - 1,
1720
+ column: props.table.columns.length - 1
1721
+ }, { extend: event.shiftKey });
1722
+ event.preventDefault();
1723
+ return;
1724
+ }
1725
+ if (virtual && !grid.editor && (event.key === "PageUp" || event.key === "PageDown")) {
1726
+ const current = grid.selection?.focus ?? {
1727
+ row: 0,
1728
+ column: 0
1729
+ };
1730
+ const item = virtual.rowItems.find((entry) => entry.renderItem.rowIndex === current.row);
1731
+ const step = Math.max(1, Math.floor(virtual.rows.viewportSize / (item?.size ?? props.rowHeight)));
1732
+ grid.focusCell({
1733
+ row: current.row + (event.key === "PageDown" ? step : -step),
1734
+ column: current.column
1735
+ }, { extend: event.shiftKey });
1736
+ event.preventDefault();
1737
+ return;
1738
+ }
534
1739
  if (grid.handleKey(event)) event.preventDefault();
535
1740
  };
536
1741
  const onCopy = (event, cut = false) => {
@@ -540,32 +1745,54 @@ const DataGridRoot = defineComponent({
540
1745
  if (cut) grid.clear();
541
1746
  };
542
1747
  const onPaste = (event) => {
1748
+ if (grid.editor || event.defaultPrevented) return;
543
1749
  const text = event.clipboardData?.getData("text/plain");
544
- if (grid.editor || text === void 0 || text === "") return;
1750
+ if (text === void 0 || text === "") return;
545
1751
  event.preventDefault();
546
1752
  grid.paste(text);
547
1753
  };
548
- return () => {
1754
+ const render = (binding) => {
1755
+ virtual = binding;
549
1756
  const focusPosition = grid.selection?.focus;
550
1757
  return h(props.as, {
551
1758
  ref: element,
552
1759
  id,
553
- role: "grid",
1760
+ role: props.table.tree ? "treegrid" : "grid",
554
1761
  tabindex: 0,
555
1762
  "aria-label": props.label,
556
1763
  "aria-multiselectable": "true",
557
- "aria-rowcount": props.table.rows.length + 1,
558
- "aria-colcount": props.table.columns.length,
1764
+ "aria-rowcount": (props.table.tree ? props.table.processedRows.length : binding ? props.table.rowCount : props.table.rows.length) + 1,
1765
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
1766
+ "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
559
1767
  "aria-activedescendant": focusPosition && !grid.editor ? cellId(id, focusPosition.row, focusPosition.column) : void 0,
560
1768
  onKeydown,
561
1769
  onCopy: (event) => onCopy(event),
562
1770
  onCut: (event) => onCopy(event, true),
563
- onPaste
564
- }, slots.default?.({
1771
+ onPaste,
1772
+ ...binding ? {
1773
+ "data-virtual": "",
1774
+ style: binding.columns ? {
1775
+ tableLayout: "fixed",
1776
+ width: `${binding.columns.totalSize + binding.gutter}px`
1777
+ } : void 0
1778
+ } : {}
1779
+ }, binding ? [virtualColgroup(props.table.columns, binding), slots.default?.({
1780
+ table: props.table,
1781
+ grid
1782
+ }) ?? [h(DataTableHeader), h(DataGridBody)]] : slots.default?.({
565
1783
  table: props.table,
566
1784
  grid
567
1785
  }) ?? [h(DataTableHeader), h(DataGridBody)]);
568
1786
  };
1787
+ return () => props.virtual ? h(VirtualContent, {
1788
+ table: props.table,
1789
+ grid,
1790
+ options: virtualOptions(props),
1791
+ virtualColumns: props.virtualColumns,
1792
+ gutter: props.gutter,
1793
+ tableElement: () => element.value,
1794
+ render
1795
+ }) : render();
569
1796
  }
570
1797
  });
571
1798
  /**
@@ -575,70 +1802,103 @@ const DataGridRoot = defineComponent({
575
1802
  const DataGridBody = defineComponent({
576
1803
  name: "DataGridBody",
577
1804
  slots: Object,
578
- props: { as: asProp("tbody") },
1805
+ props: {
1806
+ as: asProp("tbody"),
1807
+ ...virtualProps,
1808
+ colspan: {
1809
+ type: Number,
1810
+ default: void 0
1811
+ },
1812
+ treeCell: {
1813
+ type: Boolean,
1814
+ default: true
1815
+ }
1816
+ },
579
1817
  setup(props, { slots }) {
580
1818
  const table = injectDataTable("<DataGridBody>");
581
- return () => h(props.as, slots.default?.({ rows: table.rows }) ?? table.rows.map((row, rowIndex) => h("tr", {
582
- key: row.key,
583
- role: "row",
584
- "aria-rowindex": rowIndex + 2,
585
- "data-key": String(row.key)
586
- }, [slots.rowHeader?.({
587
- row,
588
- index: rowIndex
589
- }), ...table.columns.map((column, columnIndex) => h(DataGridCell, {
590
- key: column.id,
591
- row,
592
- column,
593
- rowIndex,
594
- columnIndex
595
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0))])));
596
- }
597
- });
598
- /** The text box shown in a cell while it is edited. */
599
- const DataGridEditor = defineComponent({
600
- name: "DataGridEditor",
601
- props: { label: {
602
- type: String,
603
- required: true
604
- } },
605
- setup(props) {
606
- const { grid, context } = useGrid("<DataGridEditor>");
607
- const input = ref();
608
- onMounted(() => {
609
- const element = input.value;
610
- if (element) {
611
- element.focus();
612
- element.setSelectionRange(element.value.length, element.value.length);
1819
+ const { grid } = useGrid("<DataGridBody>");
1820
+ const virtual = injectVirtual();
1821
+ const hierarchy = injectHierarchy();
1822
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1823
+ const element = ref();
1824
+ const render = (binding) => {
1825
+ if (binding && slots.default) {
1826
+ const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1827
+ const content = slots.default({ rows: binding.rowItems.filter((item) => item.renderItem.kind === "row").map((item) => item.renderItem.row) });
1828
+ return h(props.as, { ref: element }, sentinel ? [...content, sentinel] : content);
613
1829
  }
614
- });
615
- const finish = (then) => {
616
- grid.commitEdit(then);
617
- context.focus();
1830
+ if (binding) return h(props.as, { ref: element }, [
1831
+ ...renderRows(binding, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0), (item) => {
1832
+ if (item.renderItem.kind !== "row") return details.render(item.renderItem.row, item.renderItem.rowIndex);
1833
+ const { row, rowIndex } = item.renderItem;
1834
+ const columns = renderColumns(table.columns, binding, (column, columnIndex) => h(DataGridCell, {
1835
+ key: column.id,
1836
+ row,
1837
+ column,
1838
+ rowIndex,
1839
+ columnIndex,
1840
+ tree: props.treeCell
1841
+ }, {
1842
+ default: slots.cell,
1843
+ editor: slots.editor,
1844
+ $stable: true
1845
+ }), "td");
1846
+ return h("tr", {
1847
+ key: item.key,
1848
+ role: "row",
1849
+ "aria-rowindex": table.pageStart + rowIndex + 1,
1850
+ "data-key": String(row.key),
1851
+ "data-dirty": flag(row.isDirty),
1852
+ ...hierarchyRowAttrs(table, row, hierarchy),
1853
+ "data-virtual-row": "",
1854
+ ref: (target) => binding.rows.measureElement(target, item.key)
1855
+ }, [slots.rowHeader?.({
1856
+ row,
1857
+ index: rowIndex
1858
+ }), ...columns]);
1859
+ }),
1860
+ ...details.retained(new Set(binding.rowItems.filter((item) => item.renderItem.kind === "detail").map((item) => item.renderItem.row.key))),
1861
+ ...loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0)) ? [loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0))] : []
1862
+ ]);
1863
+ const content = slots.default?.({ rows: table.rows }) ?? table.renderItems.map((item) => item.kind === "detail" ? details.render(item.row, item.rowIndex) : (() => {
1864
+ const { row, rowIndex } = item;
1865
+ return h("tr", {
1866
+ key: item.key,
1867
+ role: "row",
1868
+ "aria-rowindex": rowIndex + 2,
1869
+ "data-key": String(row.key),
1870
+ "data-dirty": flag(row.isDirty),
1871
+ ...hierarchyRowAttrs(table, row, hierarchy)
1872
+ }, [slots.rowHeader?.({
1873
+ row,
1874
+ index: rowIndex
1875
+ }), ...table.columns.map((column, columnIndex) => h(DataGridCell, {
1876
+ key: column.id,
1877
+ row,
1878
+ column,
1879
+ rowIndex,
1880
+ columnIndex,
1881
+ tree: props.treeCell
1882
+ }, {
1883
+ default: slots.cell,
1884
+ editor: slots.editor,
1885
+ $stable: true
1886
+ }))]);
1887
+ })());
1888
+ const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1889
+ return h(props.as, [
1890
+ ...content,
1891
+ ...slots.default ? [] : details.retained(new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key))),
1892
+ sentinel
1893
+ ]);
618
1894
  };
619
- return () => h("input", {
620
- ref: input,
621
- value: grid.editor?.draft ?? "",
622
- "aria-label": props.label,
623
- "data-editor": "",
624
- onInput: (event) => grid.updateDraft(event.target.value),
625
- onBlur: () => {
626
- if (grid.editor) grid.commitEdit();
627
- },
628
- onKeydown: (event) => {
629
- if (event.key === "Enter") {
630
- event.preventDefault();
631
- finish(event.shiftKey ? "up" : "down");
632
- } else if (event.key === "Tab") {
633
- event.preventDefault();
634
- finish(event.shiftKey ? "left" : "right");
635
- } else if (event.key === "Escape") {
636
- event.preventDefault();
637
- grid.cancelEdit();
638
- context.focus();
639
- }
640
- }
641
- });
1895
+ return () => props.virtual && !virtual ? h(VirtualContent, {
1896
+ table,
1897
+ grid,
1898
+ options: virtualOptions(props),
1899
+ tableElement: () => element.value?.closest("table"),
1900
+ render
1901
+ }) : render(virtual);
642
1902
  }
643
1903
  });
644
1904
  /**
@@ -652,6 +1912,10 @@ const DataGridCell = defineComponent({
652
1912
  row: rowProp,
653
1913
  column: columnProp,
654
1914
  as: asProp("td"),
1915
+ tree: {
1916
+ type: Boolean,
1917
+ default: true
1918
+ },
655
1919
  /** The row's position on the page. Found by searching the rows when left out. */
656
1920
  rowIndex: {
657
1921
  type: Number,
@@ -665,14 +1929,18 @@ const DataGridCell = defineComponent({
665
1929
  },
666
1930
  setup(props, { slots }) {
667
1931
  const table = injectDataTable("<DataGridCell>");
1932
+ const columnOffset = injectColumnOffset();
668
1933
  const { grid, context } = useGrid("<DataGridCell>");
1934
+ const virtual = injectVirtual();
1935
+ const hierarchy = injectHierarchy();
669
1936
  const element = ref();
1937
+ const issueId = useEditorId();
670
1938
  const position = () => ({
671
1939
  row: props.rowIndex ?? table.rows.indexOf(props.row),
672
1940
  column: props.columnIndex ?? table.columns.indexOf(props.column)
673
1941
  });
674
1942
  watch(() => grid.isFocused(position()), (focused) => {
675
- if (focused) element.value?.scrollIntoView?.({
1943
+ if (focused && !virtual) element.value?.scrollIntoView?.({
676
1944
  block: "nearest",
677
1945
  inline: "nearest"
678
1946
  });
@@ -684,33 +1952,72 @@ const DataGridCell = defineComponent({
684
1952
  const editable = column.isEditable(row.original);
685
1953
  const value = row.getValue(column.id);
686
1954
  const display = row.getDisplay(column.id);
1955
+ const issues = row.cellIssues?.get(column.id) ? [row.cellIssues.get(column.id)] : grid.lastResult?.issues.filter((problem) => problem.rowKey === row.key && (problem.column === column.id || !problem.column)) ?? [];
1956
+ const pending = table.pendingCells.some((cell) => cell.rowKey === row.key && cell.column === column.id);
1957
+ const finish = (edit, direction) => {
1958
+ const result = grid.submitEdit(edit, direction);
1959
+ const focus = () => {
1960
+ if (!grid.editor) context.focus();
1961
+ };
1962
+ if (result?.completion) result.completion.then(focus);
1963
+ else focus();
1964
+ return result;
1965
+ };
687
1966
  let content;
688
- if (editing) content = slots.editor?.({
689
- draft: grid.editor?.draft ?? "",
690
- update: (draft) => grid.updateDraft(draft),
691
- commit: (then) => grid.commitEdit(then),
692
- cancel: () => grid.cancelEdit()
693
- }) ?? h(DataGridEditor, { label: `Edit ${column.header}` });
694
- else content = slots.default?.({
695
- row,
696
- column,
697
- value,
698
- display,
699
- editable
700
- }) ?? display;
1967
+ if (editing) {
1968
+ const editor = {
1969
+ row,
1970
+ column,
1971
+ value,
1972
+ draft: grid.editor?.draft ?? "",
1973
+ spec: grid.editorFor(at) ?? { kind: "text" },
1974
+ finish: (direction) => {
1975
+ finish(void 0, direction);
1976
+ },
1977
+ input: (text) => grid.updateDraft(text),
1978
+ update: (text) => grid.updateDraft(text),
1979
+ commit: (next) => finish(next === void 0 ? void 0 : typeof next === "string" ? { input: next } : { value: next }),
1980
+ cancel: () => {
1981
+ if (!pending) {
1982
+ grid.cancelEdit();
1983
+ context.cancel(row.key);
1984
+ context.focus();
1985
+ }
1986
+ },
1987
+ issues,
1988
+ pending,
1989
+ attrs: issueAttrs(issueId, issues, pending)
1990
+ };
1991
+ content = slots.editor?.(editor) ?? h(DataCellEditor, {
1992
+ editor,
1993
+ spec: grid.editorFor(at) ?? { kind: "text" },
1994
+ finish: (direction) => finish(void 0, direction)
1995
+ });
1996
+ } else {
1997
+ const children = () => slots.default?.({
1998
+ row,
1999
+ column,
2000
+ value,
2001
+ display,
2002
+ editable
2003
+ }) ?? display;
2004
+ content = props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: children }) : children();
2005
+ }
701
2006
  return h(props.as, {
702
2007
  ref: element,
703
2008
  id: cellId(context.id, at.row, at.column),
704
2009
  role: "gridcell",
705
2010
  "aria-selected": String(grid.isSelected(at)),
706
2011
  "aria-readonly": editable ? void 0 : "true",
707
- "aria-colindex": at.column + 1,
2012
+ "aria-colindex": at.column + columnOffset() + 1,
708
2013
  "data-column": column.id,
709
2014
  "data-align": column.align,
710
2015
  "data-focused": flag(grid.isFocused(at)),
711
2016
  "data-selected": flag(grid.isSelected(at)),
712
2017
  "data-editing": flag(editing),
713
2018
  "data-readonly": flag(!editable),
2019
+ "data-dirty": flag(row.isDirty),
2020
+ ...issueAttrs(issueId, issues, pending),
714
2021
  style: columnStyle(column),
715
2022
  onMousedown: (event) => {
716
2023
  if (editing || event.button !== 0) return;
@@ -722,13 +2029,13 @@ const DataGridCell = defineComponent({
722
2029
  },
723
2030
  onDblclick: () => {
724
2031
  grid.focusCell(at);
725
- grid.startEdit();
2032
+ grid.startEdit(editorText(value, grid.editorFor(at)?.kind ?? "text"));
726
2033
  }
727
- }, content);
2034
+ }, [content, editorIssues(issueId, issues, pending)]);
728
2035
  };
729
2036
  }
730
2037
  });
731
2038
 
732
2039
  //#endregion
733
- export { DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableEmpty, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, cellId, columnStyle };
2040
+ 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, treeCellProps, virtualProps };
734
2041
  //# sourceMappingURL=index.js.map