@vueye-table/headless 3.0.0-alpha.8 → 3.0.0-beta.1

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
1
  import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useVirtualColumns, useVirtualRows } from "@vueye-table/vue";
2
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
@@ -262,14 +277,16 @@ const DataTableStatus = defineComponent({
262
277
  loadError: table.loadError,
263
278
  canLoadMore: table.canLoadMore,
264
279
  retry: () => table.retry(),
265
- loadNext: () => table.loadNext()
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
266
283
  };
267
284
  const count = `Loaded ${table.loadedRowCount.toLocaleString()} ${table.loadedRowCount === 1 ? "row" : "rows"}`;
268
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` : "");
269
286
  return h(props.as, {
270
287
  role: "status",
271
288
  "aria-live": "polite"
272
- }, 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." : ""));
273
290
  };
274
291
  }
275
292
  });
@@ -319,6 +336,173 @@ const DataTableLoadMore = defineComponent({
319
336
  }
320
337
  });
321
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
+ });
484
+ };
485
+ }
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
+
322
506
  //#endregion
323
507
  //#region src/virtual.ts
324
508
  /** Shared opt-in renderer props. Options are applied when the virtual renderer is created. */
@@ -556,6 +740,270 @@ function virtualRow(binding, row) {
556
740
  return binding.rowItems.find((item) => item.renderItem.kind === "row" && item.renderItem.row.key === row.key);
557
741
  }
558
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
+ const root = target.closest("table");
843
+ context.focus.value = next.key;
844
+ virtual?.rows.scrollToKey(next.key);
845
+ const id = itemId(context, next);
846
+ nextTick(() => root?.ownerDocument.getElementById(id ?? "")?.focus());
847
+ }
848
+ const expandToggleProps = {
849
+ row: rowProp,
850
+ controls: {
851
+ type: String,
852
+ default: void 0
853
+ },
854
+ expandLabel: {
855
+ type: String,
856
+ default: "Expand row"
857
+ },
858
+ collapseLabel: {
859
+ type: String,
860
+ default: "Collapse row"
861
+ }
862
+ };
863
+ const treeCellProps = {
864
+ row: rowProp,
865
+ as: asProp("div")
866
+ };
867
+ const DataTableExpandToggle = defineComponent({
868
+ name: "DataTableExpandToggle",
869
+ props: expandToggleProps,
870
+ slots: Object,
871
+ setup(props, { slots }) {
872
+ const table = injectDataTable("<DataTableExpandToggle>");
873
+ const context = injectHierarchy();
874
+ const grid = injectDataGrid();
875
+ return () => props.row.canExpand ? h("button", {
876
+ type: "button",
877
+ "data-expand-toggle": "",
878
+ tabindex: table.tree ? -1 : void 0,
879
+ "aria-expanded": String(props.row.isExpanded),
880
+ "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),
881
+ "aria-label": props.row.isExpanded ? props.collapseLabel : props.expandLabel,
882
+ onMousedown: (event) => event.stopPropagation(),
883
+ onDblclick: (event) => event.stopPropagation(),
884
+ onClick: (event) => {
885
+ event.stopPropagation();
886
+ if (grid?.table === table) grid.focusCell({
887
+ row: table.rows.indexOf(props.row),
888
+ column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
889
+ });
890
+ table.toggleExpanded(props.row.key);
891
+ if (grid?.table === table) nextTick(() => event.target.closest("table")?.focus());
892
+ }
893
+ }, slots.default?.({
894
+ row: props.row,
895
+ expanded: props.row.isExpanded
896
+ }) ?? (props.row.isExpanded ? props.collapseLabel : props.expandLabel)) : null;
897
+ }
898
+ });
899
+ const DataTableTreeCell = defineComponent({
900
+ name: "DataTableTreeCell",
901
+ props: treeCellProps,
902
+ slots: Object,
903
+ setup(props, { slots }) {
904
+ const table = injectDataTable("<DataTableTreeCell>");
905
+ const context = injectHierarchy();
906
+ const grid = injectDataGrid();
907
+ return () => h(props.as, {
908
+ class: "vt-tree-cell",
909
+ "data-tree-cell": "",
910
+ style: { paddingInlineStart: `calc(var(--vt-tree-indent, 1.25rem) * ${props.row.depth})` }
911
+ }, [
912
+ h("span", { class: "vt-tree-toggle" }, slots.toggle?.({ row: props.row }) ?? h(DataTableExpandToggle, { row: props.row })),
913
+ h("span", { class: "vt-tree-label" }, [slots.default?.()]),
914
+ props.row.childStatus === "loading" ? h("span", { class: "vt-tree-loading" }, "Loading children…") : null,
915
+ props.row.childStatus === "error" ? h("span", { class: "vt-tree-error" }, ["Could not load children. ", h("button", {
916
+ type: "button",
917
+ class: "vt-tree-retry",
918
+ onMousedown: (event) => event.stopPropagation(),
919
+ onClick: (event) => {
920
+ event.stopPropagation();
921
+ const element = event.currentTarget.closest("table");
922
+ if (grid?.table === table) grid.focusCell({
923
+ row: table.rows.indexOf(props.row),
924
+ column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
925
+ });
926
+ table.toggleExpanded(props.row.key, true);
927
+ nextTick(() => {
928
+ if (grid?.table === table) element?.focus();
929
+ else element?.ownerDocument.getElementById(itemId(context, props.row) ?? "")?.focus();
930
+ });
931
+ }
932
+ }, "Retry children")]) : null
933
+ ]);
934
+ }
935
+ });
936
+ /** One full-width detail; retained collapsed content remains hidden and unmeasured. */
937
+ const DataTableDetailRow = defineComponent({
938
+ name: "DataTableDetailRow",
939
+ props: {
940
+ row: rowProp,
941
+ colspan: {
942
+ type: Number,
943
+ default: void 0
944
+ },
945
+ hidden: {
946
+ type: Boolean,
947
+ default: false
948
+ },
949
+ keepAlive: {
950
+ type: Boolean,
951
+ default: void 0
952
+ }
953
+ },
954
+ slots: Object,
955
+ setup(props, { slots }) {
956
+ const table = injectDataTable("<DataTableDetailRow>");
957
+ const context = injectHierarchy();
958
+ const virtual = injectVirtual();
959
+ let opened = false;
960
+ return () => {
961
+ if (props.row.isExpanded && !props.hidden) opened = true;
962
+ if (!opened || (!props.row.isExpanded || props.hidden) && !(props.keepAlive ?? context?.keepAliveDetail)) return null;
963
+ const hidden = props.hidden || !props.row.isExpanded;
964
+ return h("tr", {
965
+ id: itemId(context, props.row, true),
966
+ "data-detail": "",
967
+ hidden,
968
+ ...virtual && !hidden ? { ref: (target) => virtual.rows.measureElement(target, getRowItemKey(props.row.key, "detail")) } : {}
969
+ }, [h("td", {
970
+ colspan: Math.max(1, props.colspan ?? table.columns.length),
971
+ class: "vt-detail-cell"
972
+ }, [slots.default?.({ row: props.row })])]);
973
+ };
974
+ }
975
+ });
976
+ function createDetails(table, slot, colspan) {
977
+ const context = injectHierarchy();
978
+ const visited = /* @__PURE__ */ new Set();
979
+ const render = (row, rowIndex, hidden = false) => {
980
+ if (!hidden && context?.keepAliveDetail) visited.add(row.key);
981
+ return h(DataTableDetailRow, {
982
+ key: getRowItemKey(row.key, "detail"),
983
+ row,
984
+ colspan: colspan(),
985
+ hidden
986
+ }, { default: () => slot()?.({
987
+ row,
988
+ rowIndex
989
+ }) });
990
+ };
991
+ return {
992
+ render,
993
+ retained(visible) {
994
+ if (!context?.keepAliveDetail) return [];
995
+ return [...visited].flatMap((key) => {
996
+ const index = table.rows.findIndex((row) => row.key === key);
997
+ if (index < 0) {
998
+ visited.delete(key);
999
+ return [];
1000
+ }
1001
+ return visible.has(key) ? [] : [render(table.rows[index], index, true)];
1002
+ });
1003
+ }
1004
+ };
1005
+ }
1006
+
559
1007
  //#endregion
560
1008
  //#region src/table.ts
561
1009
  /**
@@ -571,16 +1019,23 @@ const DataTableRoot = defineComponent({
571
1019
  required: true
572
1020
  },
573
1021
  as: asProp("table"),
574
- ...virtualProps
1022
+ ...virtualProps,
1023
+ ...hierarchyProps,
1024
+ ...columnLayoutProps
575
1025
  },
576
1026
  setup(props, { slots }) {
577
1027
  provideDataTable(props.table);
1028
+ provideColumnOffset(() => props.leadingColumns);
1029
+ const hierarchy = provideHierarchy(props.table, props);
578
1030
  const element = shallowRef();
1031
+ const editingGrid = injectEditingGrid();
579
1032
  const render = (virtual) => h(props.as, {
580
- "aria-rowcount": props.table.rowCount + 1,
581
- "aria-colcount": props.table.columns.length,
1033
+ role: props.table.tree ? "treegrid" : void 0,
1034
+ "aria-rowcount": (props.table.tree ? props.table.processedRows.length : props.table.rowCount) + 1,
1035
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
582
1036
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
583
1037
  "data-empty": flag(props.table.rowCount === 0),
1038
+ onKeydown: (event) => treeKeydown(event, hierarchy, virtual),
584
1039
  ...virtual ? {
585
1040
  ref: element,
586
1041
  "data-virtual": ""
@@ -588,6 +1043,7 @@ const DataTableRoot = defineComponent({
588
1043
  }, slots.default?.({ table: props.table }) ?? [h(DataTableHeader), h(DataTableBody)]);
589
1044
  return () => props.virtual ? h(VirtualContent, {
590
1045
  table: props.table,
1046
+ ...editingGrid ? { grid: editingGrid } : {},
591
1047
  options: virtualOptions(props),
592
1048
  tableElement: () => element.value,
593
1049
  render
@@ -657,7 +1113,7 @@ const DataTableHeaderCell = defineComponent({
657
1113
  },
658
1114
  setup(props, { slots }) {
659
1115
  const table = injectDataTable("<DataTableHeaderCell>");
660
- const virtual = injectVirtual();
1116
+ const columnOffset = injectColumnOffset();
661
1117
  return () => {
662
1118
  const { column } = props;
663
1119
  const sort = table.getSort(column.id);
@@ -675,7 +1131,7 @@ const DataTableHeaderCell = defineComponent({
675
1131
  "data-sortable": flag(column.sortable),
676
1132
  "data-sort": sort?.direction,
677
1133
  style: columnStyle(column),
678
- ...virtual?.columns ? { "aria-colindex": table.columns.indexOf(column) + 1 } : {}
1134
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
679
1135
  }, content);
680
1136
  };
681
1137
  }
@@ -729,20 +1185,28 @@ const DataTableBody = defineComponent({
729
1185
  setup(props, { slots }) {
730
1186
  const table = injectDataTable("<DataTableBody>");
731
1187
  const virtual = injectVirtual();
1188
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
732
1189
  return () => {
733
1190
  if (props.virtual && !virtual) return h(VirtualDataTableBody, props, slots);
734
- if (virtual) return renderTableBody(table, virtual, props.as, props.colspan, slots);
1191
+ if (virtual) return renderTableBody(table, virtual, props.as, props.colspan, slots, details);
735
1192
  let content = slots.default?.({ rows: table.rows });
736
- 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, {
737
- key: row.key,
738
- row
739
- }));
1193
+ 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, {
1194
+ key: item.key,
1195
+ row: item.row,
1196
+ rowIndex: item.rowIndex
1197
+ }) : details.render(item.row, item.rowIndex));
1198
+ const visibleDetails = new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key));
1199
+ const retained = slots.default ? [] : details.retained(visibleDetails);
740
1200
  const sentinel = loadSentinel(table, props.colspan ?? table.columns.length);
741
- return h(props.as, sentinel ? [content, sentinel] : content);
1201
+ return h(props.as, [
1202
+ ...Array.isArray(content) ? content : content ? [content] : [],
1203
+ ...retained,
1204
+ ...sentinel ? [sentinel] : []
1205
+ ]);
742
1206
  };
743
1207
  }
744
1208
  });
745
- function renderTableBody(table, virtual, as, colspan, slots, element) {
1209
+ function renderTableBody(table, virtual, as, colspan, slots, details, element) {
746
1210
  const width = colspan ?? table.columns.length;
747
1211
  const attrs = element ? { ref: element } : {};
748
1212
  const content = slots.default?.({ rows: virtual.rowItems.filter((item) => item.renderItem.kind === "row").map((item) => item.renderItem.row) });
@@ -753,15 +1217,15 @@ function renderTableBody(table, virtual, as, colspan, slots, element) {
753
1217
  rowSpacer(virtual.rows.paddingEnd, width, "gap-end"),
754
1218
  ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []
755
1219
  ]);
756
- return h(as, attrs, [...renderRows(virtual, width, (item) => item.renderItem.kind === "row" ? h(DataTableRow, {
757
- key: item.key,
758
- row: item.renderItem.row,
759
- rowIndex: item.renderItem.rowIndex
760
- }) : h("tr", {
761
- key: item.key,
762
- "data-detail": "",
763
- ref: (target) => virtual.rows.measureElement(target, item.key)
764
- }, [h("td", { colspan: Math.max(1, width) }, slots.detail?.(item.renderItem))])), ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []]);
1220
+ return h(as, attrs, [
1221
+ ...renderRows(virtual, width, (item) => item.renderItem.kind === "row" ? slots.row ? h(Fragment, { key: item.key }, [slots.row(item.renderItem)]) : h(DataTableRow, {
1222
+ key: item.key,
1223
+ row: item.renderItem.row,
1224
+ rowIndex: item.renderItem.rowIndex
1225
+ }) : details.render(item.renderItem.row, item.renderItem.rowIndex)),
1226
+ ...details.retained(new Set(virtual.rowItems.filter((item) => item.renderItem.kind === "detail").map((item) => item.renderItem.row.key))),
1227
+ ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []
1228
+ ]);
765
1229
  }
766
1230
  const VirtualDataTableBody = defineComponent({
767
1231
  name: "DataTableVirtualBody",
@@ -778,7 +1242,8 @@ const VirtualDataTableBody = defineComponent({
778
1242
  const table = injectDataTable("<DataTableBody>");
779
1243
  const element = shallowRef();
780
1244
  const virtual = createComponentVirtual(table, virtualOptions(props), () => element.value?.closest("table"));
781
- return () => renderTableBody(table, virtual, props.as, props.colspan, slots, element);
1245
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
1246
+ return () => renderTableBody(table, virtual, props.as, props.colspan, slots, details, element);
782
1247
  }
783
1248
  });
784
1249
  /** A body row; `aria-selected` and `data-selected` follow the selection. */
@@ -796,6 +1261,7 @@ const DataTableRow = defineComponent({
796
1261
  setup(props, { slots }) {
797
1262
  const table = injectDataTable("<DataTableRow>");
798
1263
  const virtual = injectVirtual();
1264
+ const hierarchy = injectHierarchy();
799
1265
  return () => {
800
1266
  const { row } = props;
801
1267
  const selected = table.isSelected(row.key);
@@ -806,6 +1272,12 @@ const DataTableRow = defineComponent({
806
1272
  "aria-selected": table.selectionMode === "none" ? void 0 : String(selected),
807
1273
  "data-selected": flag(selected),
808
1274
  "data-key": String(row.key),
1275
+ "data-dirty": flag(row.isDirty),
1276
+ ...hierarchyRowAttrs(table, row, hierarchy),
1277
+ tabindex: table.tree ? (hierarchy?.focusKey ?? table.rows[0]?.key) === row.key ? 0 : -1 : void 0,
1278
+ onFocus: () => {
1279
+ if (hierarchy) hierarchy.focus.value = row.key;
1280
+ },
809
1281
  ...virtual && item ? {
810
1282
  ref: (element) => virtual.rows.measureElement(element, item.key),
811
1283
  "data-virtual-row": ""
@@ -829,22 +1301,31 @@ const DataTableCell = defineComponent({
829
1301
  props: {
830
1302
  row: rowProp,
831
1303
  column: columnProp,
832
- as: asProp("td")
1304
+ as: asProp("td"),
1305
+ tree: {
1306
+ type: Boolean,
1307
+ default: true
1308
+ }
833
1309
  },
834
1310
  setup(props, { slots }) {
1311
+ const table = injectDataTable("<DataTableCell>");
1312
+ const columnOffset = injectColumnOffset();
1313
+ const hierarchy = injectHierarchy();
835
1314
  return () => {
836
1315
  const { row, column } = props;
837
1316
  const value = row.getValue(column.id);
838
1317
  const display = row.getDisplay(column.id);
839
- return h(props.as, {
840
- "data-column": column.id,
841
- "data-align": column.align
842
- }, slots.default?.({
1318
+ const content = () => slots.default?.({
843
1319
  row,
844
1320
  column,
845
1321
  value,
846
1322
  display
847
- }) ?? display);
1323
+ }) ?? display;
1324
+ return h(props.as, {
1325
+ "data-column": column.id,
1326
+ "data-align": column.align,
1327
+ "aria-colindex": table.columns.indexOf(column) + columnOffset() + 1
1328
+ }, props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: content }) : content());
848
1329
  };
849
1330
  }
850
1331
  });
@@ -859,6 +1340,298 @@ const DataTableEmpty = defineComponent({
859
1340
  }
860
1341
  });
861
1342
 
1343
+ //#endregion
1344
+ //#region src/editing.ts
1345
+ const editingKey = Symbol("vueye-table-editing");
1346
+ function injectEditingGrid() {
1347
+ return inject(editingKey, void 0)?.grid;
1348
+ }
1349
+ /** Renderless provider for editable data-table cells and row forms. */
1350
+ const DataTableEditRoot = defineComponent({
1351
+ name: "DataTableEditRoot",
1352
+ slots: Object,
1353
+ props: {
1354
+ table: {
1355
+ type: Object,
1356
+ required: true
1357
+ },
1358
+ mode: {
1359
+ type: String,
1360
+ default: "cell"
1361
+ }
1362
+ },
1363
+ emits: {
1364
+ save: (_result) => true,
1365
+ cancel: (_key) => true
1366
+ },
1367
+ setup(props, { slots, emit, expose }) {
1368
+ const grid = useDataGrid(props.table);
1369
+ const draft = shallowRef();
1370
+ const context = {
1371
+ grid,
1372
+ get mode() {
1373
+ return props.mode;
1374
+ },
1375
+ get draft() {
1376
+ return draft.value;
1377
+ },
1378
+ start(row) {
1379
+ if (draft.value?.pending) return;
1380
+ if (draft.value) context.cancel(draft.value.rowKey);
1381
+ grid.cancelEdit();
1382
+ const column = props.table.columns.findIndex((candidate) => candidate.isEditable(row.original));
1383
+ grid.focusCell({
1384
+ row: props.table.rows.findIndex((candidate) => candidate.key === row.key),
1385
+ column
1386
+ });
1387
+ grid.startEdit();
1388
+ draft.value = props.table.editRow(row.key);
1389
+ },
1390
+ async save() {
1391
+ const active = draft.value;
1392
+ if (!active || active.pending) return void 0;
1393
+ const result = await active.save();
1394
+ if (draft.value === active && active.status !== "editing" && !active.pending) {
1395
+ draft.value = void 0;
1396
+ grid.cancelEdit();
1397
+ }
1398
+ if (result.status !== "rejected") emit("save", result);
1399
+ return result;
1400
+ },
1401
+ cancel(key) {
1402
+ if (draft.value?.rowKey === key && !draft.value.pending) {
1403
+ draft.value.cancel();
1404
+ draft.value = void 0;
1405
+ grid.cancelEdit();
1406
+ emit("cancel", key);
1407
+ }
1408
+ }
1409
+ };
1410
+ provide(editingKey, context);
1411
+ expose({ editing: context });
1412
+ watch(() => props.table.rows, (rows) => {
1413
+ if (draft.value && !rows.some((row) => row.key === draft.value?.rowKey)) context.cancel(draft.value.rowKey);
1414
+ });
1415
+ onScopeDispose(() => draft.value?.cancel());
1416
+ return () => slots.default?.({ editing: context });
1417
+ }
1418
+ });
1419
+ /** A cell with on-demand editing; row mode shares one draft and one save operation. */
1420
+ const DataTableEditCell = defineComponent({
1421
+ name: "DataTableEditCell",
1422
+ props: {
1423
+ row: rowProp,
1424
+ column: columnProp,
1425
+ rowIndex: {
1426
+ type: Number,
1427
+ default: void 0
1428
+ },
1429
+ tree: {
1430
+ type: Boolean,
1431
+ default: true
1432
+ }
1433
+ },
1434
+ slots: Object,
1435
+ emits: {
1436
+ save: (_result) => true,
1437
+ cancel: (_key) => true
1438
+ },
1439
+ setup(props, { slots, emit }) {
1440
+ const table = injectDataTable("<DataTableEditCell>");
1441
+ const context = inject(editingKey);
1442
+ if (!context) throw new Error("<DataTableEditCell> requires <DataTableEditRoot>.");
1443
+ const { grid } = context;
1444
+ const id = useEditorId();
1445
+ const element = shallowRef();
1446
+ const position = () => ({
1447
+ row: props.rowIndex ?? table.rows.indexOf(props.row),
1448
+ column: table.columns.indexOf(props.column)
1449
+ });
1450
+ const active = computed(() => context.mode === "row" ? context.draft?.rowKey === props.row.key : grid.isEditing(position()));
1451
+ const pending = computed(() => context.mode === "row" ? !!context.draft?.pending : active.value && grid.lastResult?.status === "pending");
1452
+ const localText = shallowRef("");
1453
+ watch(active, (value) => {
1454
+ if (value) localText.value = editorText(props.row.getValue(props.column.id), grid.editorFor(position())?.kind ?? "text");
1455
+ });
1456
+ const focus = (direction) => {
1457
+ nextTick(() => {
1458
+ if (!direction) {
1459
+ element.value?.focus();
1460
+ return;
1461
+ }
1462
+ const all = [...element.value?.closest("table")?.querySelectorAll("[data-edit-cell][data-editable]") ?? []];
1463
+ all[all.indexOf(element.value) + (direction === "left" ? -1 : 1)]?.focus();
1464
+ });
1465
+ };
1466
+ const submit = (value, direction) => {
1467
+ if (context.mode === "row") {
1468
+ if (value !== void 0) {
1469
+ if (typeof value === "string") {
1470
+ localText.value = value;
1471
+ context.draft?.setValue(props.column.id, value);
1472
+ context.draft?.setInput(props.column.id, value);
1473
+ } else context.draft?.setValue(props.column.id, value);
1474
+ }
1475
+ return;
1476
+ }
1477
+ const result = grid.submitEdit(value === void 0 ? void 0 : typeof value === "string" ? { input: value } : { value });
1478
+ const finish = (final) => {
1479
+ if (final.status !== "rejected") {
1480
+ emit("save", final);
1481
+ focus(direction);
1482
+ }
1483
+ };
1484
+ if (result?.completion) result.completion.then(finish);
1485
+ else if (result) finish(result);
1486
+ return result;
1487
+ };
1488
+ return () => {
1489
+ const { row, column } = props;
1490
+ const editable = column.isEditable(row.original);
1491
+ const editing = editable && active.value;
1492
+ const draft = context.draft?.rowKey === row.key ? context.draft : void 0;
1493
+ 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)) ?? [] : []);
1494
+ const spec = editing ? grid.editorFor(position()) ?? { kind: "text" } : void 0;
1495
+ const input = (text) => {
1496
+ localText.value = text;
1497
+ if (context.mode === "row") {
1498
+ draft?.setValue(column.id, text);
1499
+ draft?.setInput(column.id, text);
1500
+ } else grid.updateDraft(text);
1501
+ };
1502
+ const cancel = () => {
1503
+ if (pending.value) return;
1504
+ if (context.mode === "row") context.cancel(row.key);
1505
+ else {
1506
+ grid.cancelEdit();
1507
+ emit("cancel", row.key);
1508
+ }
1509
+ focus();
1510
+ };
1511
+ const editor = {
1512
+ row,
1513
+ column,
1514
+ value: context.mode === "row" ? Reflect.get(draft?.values ?? {}, column.id) : row.getValue(column.id),
1515
+ draft: context.mode === "row" ? localText.value : grid.editor?.draft ?? "",
1516
+ spec: spec ?? { kind: "text" },
1517
+ finish: (direction) => {
1518
+ if (context.mode === "row") context.save();
1519
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1520
+ },
1521
+ input,
1522
+ update: input,
1523
+ commit: submit,
1524
+ cancel,
1525
+ issues,
1526
+ pending: pending.value,
1527
+ attrs: issueAttrs(id, issues, pending.value)
1528
+ };
1529
+ return h(DataTableCell, {
1530
+ ref: (target) => {
1531
+ element.value = target && typeof target === "object" && "$el" in target ? target.$el : void 0;
1532
+ },
1533
+ row,
1534
+ column,
1535
+ tree: props.tree,
1536
+ "data-edit-cell": "",
1537
+ "data-editable": editable ? "" : void 0,
1538
+ "data-editing": editing ? "" : void 0,
1539
+ "data-dirty": row.isDirty ? "" : void 0,
1540
+ ...issueAttrs(id, issues, pending.value),
1541
+ tabindex: editable ? 0 : void 0,
1542
+ onFocus: (event) => {
1543
+ if (event.target === element.value && !grid.editor) grid.focusCell(position());
1544
+ },
1545
+ onDblclick: () => {
1546
+ if (editable && context.mode === "cell") {
1547
+ grid.focusCell(position());
1548
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1549
+ }
1550
+ },
1551
+ onKeydown: (event) => {
1552
+ if (event.target === element.value && event.key === "Enter" && editable) {
1553
+ event.preventDefault();
1554
+ event.stopPropagation();
1555
+ if (context.mode === "row") context.start(row);
1556
+ else {
1557
+ grid.focusCell(position());
1558
+ grid.startEdit(editorText(row.getValue(column.id), grid.editorFor(position())?.kind ?? "text"));
1559
+ }
1560
+ }
1561
+ }
1562
+ }, { default: ({ value, display }) => [editing ? slots.editor?.(editor) ?? h(DataCellEditor, {
1563
+ editor,
1564
+ spec: spec ?? { kind: "text" },
1565
+ autofocus: context.mode === "cell" || table.columns.find((candidate) => candidate.isEditable(row.original))?.id === column.id,
1566
+ blurCommit: context.mode === "cell",
1567
+ tabCommit: context.mode === "cell",
1568
+ finish: (direction) => {
1569
+ if (context.mode === "row") context.save();
1570
+ else submit(void 0, direction === "left" || direction === "right" ? direction : void 0);
1571
+ }
1572
+ }) : slots.default?.({
1573
+ row,
1574
+ value,
1575
+ display
1576
+ }) ?? display, editorIssues(id, issues, pending.value)] });
1577
+ };
1578
+ }
1579
+ });
1580
+ /** One active row form. Controls share the provider's draft and core save/undo path. */
1581
+ const DataTableRowActions = defineComponent({
1582
+ name: "DataTableRowActions",
1583
+ props: {
1584
+ row: rowProp,
1585
+ editable: {
1586
+ type: Boolean,
1587
+ default: true
1588
+ },
1589
+ removable: {
1590
+ type: Boolean,
1591
+ default: false
1592
+ }
1593
+ },
1594
+ setup(props) {
1595
+ const table = injectDataTable("<DataTableRowActions>");
1596
+ const context = inject(editingKey, void 0);
1597
+ const id = useEditorId();
1598
+ const element = shallowRef();
1599
+ watch(() => context?.draft?.rowKey === props.row.key, (active, previous) => {
1600
+ const target = element.value;
1601
+ if (previous && !active && !context?.draft && target?.closest("tr")?.contains(target.ownerDocument.activeElement)) nextTick(() => target.querySelector("button[data-edit-row]")?.focus());
1602
+ });
1603
+ return () => {
1604
+ const draft = context?.draft?.rowKey === props.row.key ? context.draft : void 0;
1605
+ const issues = [...new Map(draft?.issues.map((problem) => [problem.message, problem])).values()];
1606
+ const button = (label, action, disabled = false) => h("button", {
1607
+ type: "button",
1608
+ disabled,
1609
+ "aria-label": `${label}, row ${String(props.row.key)}`,
1610
+ "data-edit-row": label === "Edit row" ? "" : void 0,
1611
+ ...label === "Save row" ? issueAttrs(id, issues, !!draft?.pending) : {},
1612
+ onClick: action
1613
+ }, label);
1614
+ return h("div", {
1615
+ ref: element,
1616
+ "data-row-actions": "",
1617
+ role: "group",
1618
+ "aria-label": `Row ${String(props.row.key)} actions`,
1619
+ "aria-busy": draft?.pending ? "true" : void 0
1620
+ }, [
1621
+ ...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))] : [],
1622
+ h("details", { onToggle: (event) => {
1623
+ const menu = event.currentTarget;
1624
+ if (menu.open) nextTick(() => menu.scrollIntoView?.({
1625
+ block: "nearest",
1626
+ inline: "nearest"
1627
+ }));
1628
+ } }, [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])]),
1629
+ editorIssues(id, issues, !!draft?.pending)
1630
+ ]);
1631
+ };
1632
+ }
1633
+ });
1634
+
862
1635
  //#endregion
863
1636
  //#region src/grid.ts
864
1637
  const gridContextKey = Symbol("vueye-table-grid-context");
@@ -893,24 +1666,42 @@ const DataGridRoot = defineComponent({
893
1666
  default: void 0
894
1667
  },
895
1668
  ...virtualProps,
1669
+ ...hierarchyProps,
1670
+ ...columnLayoutProps,
896
1671
  /** Width reserved before data columns, for a row-number/header column. */
897
1672
  gutter: {
898
1673
  type: Number,
899
1674
  default: 0
900
1675
  }
901
1676
  },
902
- setup(props, { slots, expose }) {
1677
+ emits: {
1678
+ save: (_result) => true,
1679
+ cancel: (_key) => true
1680
+ },
1681
+ setup(props, { slots, expose, emit }) {
903
1682
  provideDataTable(props.table);
904
- const grid = useDataGrid(props.table);
1683
+ provideColumnOffset(() => props.leadingColumns);
1684
+ const grid = useDataGrid(props.table, { treeColumn: () => props.treeColumn });
905
1685
  provideDataGrid(grid);
1686
+ provideHierarchy(props.table, props);
906
1687
  const element = ref();
907
1688
  const id = `vt-grid-${useId()}`;
908
1689
  const focus = () => {
909
- nextTick(() => element.value?.focus());
1690
+ nextTick(() => {
1691
+ if (!grid.editor) element.value?.focus({ preventScroll: true });
1692
+ });
910
1693
  };
911
1694
  provide(gridContextKey, {
912
1695
  id,
913
- focus
1696
+ focus,
1697
+ cancel: (key) => emit("cancel", key)
1698
+ });
1699
+ watch(() => grid.lastResult, (result) => {
1700
+ if (result && [
1701
+ "applied",
1702
+ "partial",
1703
+ "unchanged"
1704
+ ].includes(result.status)) emit("save", result);
914
1705
  });
915
1706
  expose({
916
1707
  grid,
@@ -919,6 +1710,10 @@ const DataGridRoot = defineComponent({
919
1710
  let virtual;
920
1711
  const onKeydown = (event) => {
921
1712
  if (event.target !== element.value) return;
1713
+ if (event.key === " " && !grid.editor && !event.ctrlKey && !event.metaKey && !event.altKey && grid.toggleCheckbox()) {
1714
+ event.preventDefault();
1715
+ return;
1716
+ }
922
1717
  if (virtual && !grid.editor && (event.ctrlKey || event.metaKey) && (event.key === "Home" || event.key === "End")) {
923
1718
  grid.focusCell(event.key === "Home" ? {
924
1719
  row: 0,
@@ -953,8 +1748,9 @@ const DataGridRoot = defineComponent({
953
1748
  if (cut) grid.clear();
954
1749
  };
955
1750
  const onPaste = (event) => {
1751
+ if (grid.editor || event.defaultPrevented) return;
956
1752
  const text = event.clipboardData?.getData("text/plain");
957
- if (grid.editor || text === void 0 || text === "") return;
1753
+ if (text === void 0 || text === "") return;
958
1754
  event.preventDefault();
959
1755
  grid.paste(text);
960
1756
  };
@@ -964,12 +1760,12 @@ const DataGridRoot = defineComponent({
964
1760
  return h(props.as, {
965
1761
  ref: element,
966
1762
  id,
967
- role: "grid",
1763
+ role: props.table.tree ? "treegrid" : "grid",
968
1764
  tabindex: 0,
969
1765
  "aria-label": props.label,
970
1766
  "aria-multiselectable": "true",
971
- "aria-rowcount": (binding ? props.table.rowCount : props.table.rows.length) + 1,
972
- "aria-colcount": props.table.columns.length,
1767
+ "aria-rowcount": (props.table.tree ? props.table.processedRows.length : binding ? props.table.rowCount : props.table.rows.length) + 1,
1768
+ "aria-colcount": props.table.columns.length + props.leadingColumns,
973
1769
  "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
974
1770
  "aria-activedescendant": focusPosition && !grid.editor ? cellId(id, focusPosition.row, focusPosition.column) : void 0,
975
1771
  onKeydown,
@@ -1015,12 +1811,18 @@ const DataGridBody = defineComponent({
1015
1811
  colspan: {
1016
1812
  type: Number,
1017
1813
  default: void 0
1814
+ },
1815
+ treeCell: {
1816
+ type: Boolean,
1817
+ default: true
1018
1818
  }
1019
1819
  },
1020
1820
  setup(props, { slots }) {
1021
1821
  const table = injectDataTable("<DataGridBody>");
1022
1822
  const { grid } = useGrid("<DataGridBody>");
1023
1823
  const virtual = injectVirtual();
1824
+ const hierarchy = injectHierarchy();
1825
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1024
1826
  const element = ref();
1025
1827
  const render = (binding) => {
1026
1828
  if (binding && slots.default) {
@@ -1028,53 +1830,70 @@ const DataGridBody = defineComponent({
1028
1830
  const content = slots.default({ rows: binding.rowItems.filter((item) => item.renderItem.kind === "row").map((item) => item.renderItem.row) });
1029
1831
  return h(props.as, { ref: element }, sentinel ? [...content, sentinel] : content);
1030
1832
  }
1031
- if (binding) return h(props.as, { ref: element }, [...renderRows(binding, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0), (item) => {
1032
- if (item.renderItem.kind !== "row") return h("tr", {
1033
- key: item.key,
1034
- "aria-hidden": "true",
1035
- "data-detail": "",
1036
- style: { height: `${item.size}px` }
1037
- }, [h("td", {
1038
- colspan: Math.max(1, table.columns.length),
1039
- style: { padding: "0" }
1040
- })]);
1041
- const { row, rowIndex } = item.renderItem;
1042
- const columns = renderColumns(table.columns, binding, (column, columnIndex) => h(DataGridCell, {
1043
- key: column.id,
1044
- row,
1045
- column,
1046
- rowIndex,
1047
- columnIndex
1048
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0), "td");
1833
+ if (binding) return h(props.as, { ref: element }, [
1834
+ ...renderRows(binding, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0), (item) => {
1835
+ if (item.renderItem.kind !== "row") return details.render(item.renderItem.row, item.renderItem.rowIndex);
1836
+ const { row, rowIndex } = item.renderItem;
1837
+ const columns = renderColumns(table.columns, binding, (column, columnIndex) => h(DataGridCell, {
1838
+ key: column.id,
1839
+ row,
1840
+ column,
1841
+ rowIndex,
1842
+ columnIndex,
1843
+ tree: props.treeCell
1844
+ }, {
1845
+ default: slots.cell,
1846
+ editor: slots.editor,
1847
+ $stable: true
1848
+ }), "td");
1849
+ return h("tr", {
1850
+ key: item.key,
1851
+ role: "row",
1852
+ "aria-rowindex": table.pageStart + rowIndex + 1,
1853
+ "data-key": String(row.key),
1854
+ "data-dirty": flag(row.isDirty),
1855
+ ...hierarchyRowAttrs(table, row, hierarchy),
1856
+ "data-virtual-row": "",
1857
+ ref: (target) => binding.rows.measureElement(target, item.key)
1858
+ }, [slots.rowHeader?.({
1859
+ row,
1860
+ index: rowIndex
1861
+ }), ...columns]);
1862
+ }),
1863
+ ...details.retained(new Set(binding.rowItems.filter((item) => item.renderItem.kind === "detail").map((item) => item.renderItem.row.key))),
1864
+ ...loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0)) ? [loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0))] : []
1865
+ ]);
1866
+ const content = slots.default?.({ rows: table.rows }) ?? table.renderItems.map((item) => item.kind === "detail" ? details.render(item.row, item.rowIndex) : (() => {
1867
+ const { row, rowIndex } = item;
1049
1868
  return h("tr", {
1050
1869
  key: item.key,
1051
1870
  role: "row",
1052
- "aria-rowindex": table.pageStart + rowIndex + 1,
1871
+ "aria-rowindex": rowIndex + 2,
1053
1872
  "data-key": String(row.key),
1054
- "data-virtual-row": "",
1055
- ref: (target) => binding.rows.measureElement(target, item.key)
1873
+ "data-dirty": flag(row.isDirty),
1874
+ ...hierarchyRowAttrs(table, row, hierarchy)
1056
1875
  }, [slots.rowHeader?.({
1057
1876
  row,
1058
1877
  index: rowIndex
1059
- }), ...columns]);
1060
- }), ...loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0)) ? [loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0))] : []]);
1061
- const content = slots.default?.({ rows: table.rows }) ?? table.rows.map((row, rowIndex) => h("tr", {
1062
- key: row.key,
1063
- role: "row",
1064
- "aria-rowindex": rowIndex + 2,
1065
- "data-key": String(row.key)
1066
- }, [slots.rowHeader?.({
1067
- row,
1068
- index: rowIndex
1069
- }), ...table.columns.map((column, columnIndex) => h(DataGridCell, {
1070
- key: column.id,
1071
- row,
1072
- column,
1073
- rowIndex,
1074
- columnIndex
1075
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0))]));
1878
+ }), ...table.columns.map((column, columnIndex) => h(DataGridCell, {
1879
+ key: column.id,
1880
+ row,
1881
+ column,
1882
+ rowIndex,
1883
+ columnIndex,
1884
+ tree: props.treeCell
1885
+ }, {
1886
+ default: slots.cell,
1887
+ editor: slots.editor,
1888
+ $stable: true
1889
+ }))]);
1890
+ })());
1076
1891
  const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1077
- return h(props.as, sentinel ? [...content, sentinel] : content);
1892
+ return h(props.as, [
1893
+ ...content,
1894
+ ...slots.default ? [] : details.retained(new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key))),
1895
+ sentinel
1896
+ ]);
1078
1897
  };
1079
1898
  return () => props.virtual && !virtual ? h(VirtualContent, {
1080
1899
  table,
@@ -1085,52 +1904,6 @@ const DataGridBody = defineComponent({
1085
1904
  }) : render(virtual);
1086
1905
  }
1087
1906
  });
1088
- /** The text box shown in a cell while it is edited. */
1089
- const DataGridEditor = defineComponent({
1090
- name: "DataGridEditor",
1091
- props: { label: {
1092
- type: String,
1093
- required: true
1094
- } },
1095
- setup(props) {
1096
- const { grid, context } = useGrid("<DataGridEditor>");
1097
- const input = ref();
1098
- onMounted(() => {
1099
- const element = input.value;
1100
- if (element) {
1101
- element.focus();
1102
- element.setSelectionRange(element.value.length, element.value.length);
1103
- }
1104
- });
1105
- const finish = (then) => {
1106
- grid.commitEdit(then);
1107
- context.focus();
1108
- };
1109
- return () => h("input", {
1110
- ref: input,
1111
- value: grid.editor?.draft ?? "",
1112
- "aria-label": props.label,
1113
- "data-editor": "",
1114
- onInput: (event) => grid.updateDraft(event.target.value),
1115
- onBlur: () => {
1116
- if (grid.editor) grid.commitEdit();
1117
- },
1118
- onKeydown: (event) => {
1119
- if (event.key === "Enter") {
1120
- event.preventDefault();
1121
- finish(event.shiftKey ? "up" : "down");
1122
- } else if (event.key === "Tab") {
1123
- event.preventDefault();
1124
- finish(event.shiftKey ? "left" : "right");
1125
- } else if (event.key === "Escape") {
1126
- event.preventDefault();
1127
- grid.cancelEdit();
1128
- context.focus();
1129
- }
1130
- }
1131
- });
1132
- }
1133
- });
1134
1907
  /**
1135
1908
  * A grid cell. A click selects it, Shift-click or dragging extends the range, and a double click
1136
1909
  * edits it. The `editor` slot replaces the default text box.
@@ -1142,6 +1915,10 @@ const DataGridCell = defineComponent({
1142
1915
  row: rowProp,
1143
1916
  column: columnProp,
1144
1917
  as: asProp("td"),
1918
+ tree: {
1919
+ type: Boolean,
1920
+ default: true
1921
+ },
1145
1922
  /** The row's position on the page. Found by searching the rows when left out. */
1146
1923
  rowIndex: {
1147
1924
  type: Number,
@@ -1155,9 +1932,12 @@ const DataGridCell = defineComponent({
1155
1932
  },
1156
1933
  setup(props, { slots }) {
1157
1934
  const table = injectDataTable("<DataGridCell>");
1935
+ const columnOffset = injectColumnOffset();
1158
1936
  const { grid, context } = useGrid("<DataGridCell>");
1159
1937
  const virtual = injectVirtual();
1938
+ const hierarchy = injectHierarchy();
1160
1939
  const element = ref();
1940
+ const issueId = useEditorId();
1161
1941
  const position = () => ({
1162
1942
  row: props.rowIndex ?? table.rows.indexOf(props.row),
1163
1943
  column: props.columnIndex ?? table.columns.indexOf(props.column)
@@ -1175,33 +1955,72 @@ const DataGridCell = defineComponent({
1175
1955
  const editable = column.isEditable(row.original);
1176
1956
  const value = row.getValue(column.id);
1177
1957
  const display = row.getDisplay(column.id);
1958
+ 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)) ?? [];
1959
+ const pending = table.pendingCells.some((cell) => cell.rowKey === row.key && cell.column === column.id);
1960
+ const finish = (edit, direction) => {
1961
+ const result = grid.submitEdit(edit, direction);
1962
+ const focus = () => {
1963
+ if (!grid.editor) context.focus();
1964
+ };
1965
+ if (result?.completion) result.completion.then(focus);
1966
+ else focus();
1967
+ return result;
1968
+ };
1178
1969
  let content;
1179
- if (editing) content = slots.editor?.({
1180
- draft: grid.editor?.draft ?? "",
1181
- update: (draft) => grid.updateDraft(draft),
1182
- commit: (then) => grid.commitEdit(then),
1183
- cancel: () => grid.cancelEdit()
1184
- }) ?? h(DataGridEditor, { label: `Edit ${column.header}` });
1185
- else content = slots.default?.({
1186
- row,
1187
- column,
1188
- value,
1189
- display,
1190
- editable
1191
- }) ?? display;
1970
+ if (editing) {
1971
+ const editor = {
1972
+ row,
1973
+ column,
1974
+ value,
1975
+ draft: grid.editor?.draft ?? "",
1976
+ spec: grid.editorFor(at) ?? { kind: "text" },
1977
+ finish: (direction) => {
1978
+ finish(void 0, direction);
1979
+ },
1980
+ input: (text) => grid.updateDraft(text),
1981
+ update: (text) => grid.updateDraft(text),
1982
+ commit: (next) => finish(next === void 0 ? void 0 : typeof next === "string" ? { input: next } : { value: next }),
1983
+ cancel: () => {
1984
+ if (!pending) {
1985
+ grid.cancelEdit();
1986
+ context.cancel(row.key);
1987
+ context.focus();
1988
+ }
1989
+ },
1990
+ issues,
1991
+ pending,
1992
+ attrs: issueAttrs(issueId, issues, pending)
1993
+ };
1994
+ content = slots.editor?.(editor) ?? h(DataCellEditor, {
1995
+ editor,
1996
+ spec: grid.editorFor(at) ?? { kind: "text" },
1997
+ finish: (direction) => finish(void 0, direction)
1998
+ });
1999
+ } else {
2000
+ const children = () => slots.default?.({
2001
+ row,
2002
+ column,
2003
+ value,
2004
+ display,
2005
+ editable
2006
+ }) ?? display;
2007
+ content = props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: children }) : children();
2008
+ }
1192
2009
  return h(props.as, {
1193
2010
  ref: element,
1194
2011
  id: cellId(context.id, at.row, at.column),
1195
2012
  role: "gridcell",
1196
2013
  "aria-selected": String(grid.isSelected(at)),
1197
2014
  "aria-readonly": editable ? void 0 : "true",
1198
- "aria-colindex": at.column + 1,
2015
+ "aria-colindex": at.column + columnOffset() + 1,
1199
2016
  "data-column": column.id,
1200
2017
  "data-align": column.align,
1201
2018
  "data-focused": flag(grid.isFocused(at)),
1202
2019
  "data-selected": flag(grid.isSelected(at)),
1203
2020
  "data-editing": flag(editing),
1204
2021
  "data-readonly": flag(!editable),
2022
+ "data-dirty": flag(row.isDirty),
2023
+ ...issueAttrs(issueId, issues, pending),
1205
2024
  style: columnStyle(column),
1206
2025
  onMousedown: (event) => {
1207
2026
  if (editing || event.button !== 0) return;
@@ -1213,13 +2032,13 @@ const DataGridCell = defineComponent({
1213
2032
  },
1214
2033
  onDblclick: () => {
1215
2034
  grid.focusCell(at);
1216
- grid.startEdit();
2035
+ grid.startEdit(editorText(value, grid.editorFor(at)?.kind ?? "text"));
1217
2036
  }
1218
- }, content);
2037
+ }, [content, editorIssues(issueId, issues, pending)]);
1219
2038
  };
1220
2039
  }
1221
2040
  });
1222
2041
 
1223
2042
  //#endregion
1224
- export { DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableEmpty, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTableLoadMore, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, DataTableViewport, DataTableVirtualColumns, cellId, columnStyle, injectVirtual as injectVirtualRenderer, virtualProps };
2043
+ 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 };
1225
2044
  //# sourceMappingURL=index.js.map