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

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