nuxt-ui-tools 1.16.10 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/i18n/locales/en.js +8 -0
  3. package/dist/runtime/i18n/locales/fr.js +8 -0
  4. package/dist/runtime/i18n/types.d.ts +8 -0
  5. package/dist/runtime/table/components/table/table-layout.d.ts +2 -0
  6. package/dist/runtime/table/components/table/table-layout.js +1 -0
  7. package/dist/runtime/table/components/table/table-renderer.d.vue.ts +6 -6
  8. package/dist/runtime/table/components/table/table-renderer.vue +75 -11
  9. package/dist/runtime/table/components/table/table-renderer.vue.d.ts +6 -6
  10. package/dist/runtime/table/components/table/table-row.d.vue.ts +15 -2
  11. package/dist/runtime/table/components/table/table-row.vue +23 -2
  12. package/dist/runtime/table/components/table/table-row.vue.d.ts +15 -2
  13. package/dist/runtime/table/components/table/table-tree-control.d.vue.ts +19 -0
  14. package/dist/runtime/table/components/table/table-tree-control.vue +112 -0
  15. package/dist/runtime/table/components/table/table-tree-control.vue.d.ts +19 -0
  16. package/dist/runtime/table/components/table/table-tree-header.d.vue.ts +3 -0
  17. package/dist/runtime/table/components/table/table-tree-header.vue +66 -0
  18. package/dist/runtime/table/components/table/table-tree-header.vue.d.ts +3 -0
  19. package/dist/runtime/table/composables/use-table-api.d.ts +2 -0
  20. package/dist/runtime/table/composables/use-table-api.js +2 -0
  21. package/dist/runtime/table/composables/use-table-internals.d.ts +48 -0
  22. package/dist/runtime/table/composables/use-table-internals.js +13 -4
  23. package/dist/runtime/table/composables/use-table-selection.d.ts +2 -0
  24. package/dist/runtime/table/composables/use-table-selection.js +15 -6
  25. package/dist/runtime/table/composables/use-table-tree.d.ts +36 -0
  26. package/dist/runtime/table/composables/use-table-tree.js +199 -0
  27. package/dist/runtime/table/types/index.d.ts +1 -0
  28. package/dist/runtime/table/types/layout.d.ts +3 -2
  29. package/dist/runtime/table/types/table-api.d.ts +6 -0
  30. package/dist/runtime/table/types/tree.d.ts +100 -0
  31. package/dist/runtime/table/types/tree.js +0 -0
  32. package/dist/runtime/table/utils/columns/defs.js +19 -2
  33. package/dist/runtime/table/utils/columns/types.d.ts +6 -1
  34. package/dist/runtime/table/utils/columns/types.js +7 -0
  35. package/dist/runtime/table/utils/index.d.ts +1 -0
  36. package/dist/runtime/table/utils/index.js +1 -0
  37. package/dist/runtime/table/utils/tree.d.ts +64 -0
  38. package/dist/runtime/table/utils/tree.js +112 -0
  39. package/package.json +1 -1
@@ -0,0 +1,66 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useUiToolsLocale } from "#ui-tools/i18n";
4
+ import { useTableInternals } from "../../composables/use-table-internals";
5
+ import TableSelectionControl from "./table-selection-control.vue";
6
+ const { t } = useUiToolsLocale();
7
+ const { selection, tree } = useTableInternals();
8
+ const state = computed(() => tree.expansion.state.value);
9
+ const label = computed(
10
+ () => state.value.allExpanded ? t("table.tree.collapseAll") : t("table.tree.expandAll")
11
+ );
12
+ const checkState = computed(
13
+ () => selection.allSelected.value ? true : selection.partiallySelected.value ? "indeterminate" : false
14
+ );
15
+ function toggle() {
16
+ if (state.value.allExpanded) tree.expansion.collapseAll();
17
+ else tree.expansion.expandAll();
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <div class="nut-dl-tree-head">
23
+ <span class="nut-dl-tree__node">
24
+ <button
25
+ v-if="state.branchCount > 0"
26
+ type="button"
27
+ class="nut-dl-tree__toggle"
28
+ :aria-label="label"
29
+ :title="label"
30
+ @click="toggle"
31
+ >
32
+ <svg viewBox="0 0 24 24" width="12" height="12" fill="none" aria-hidden="true">
33
+ <path
34
+ v-if="state.allExpanded"
35
+ d="m7 20 5-5 5 5M7 4l5 5 5-5"
36
+ stroke="currentColor"
37
+ stroke-width="2.5"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ />
41
+ <path
42
+ v-else
43
+ d="m7 15 5 5 5-5M7 9l5-5 5 5"
44
+ stroke="currentColor"
45
+ stroke-width="2.5"
46
+ stroke-linecap="round"
47
+ stroke-linejoin="round"
48
+ />
49
+ </svg>
50
+ </button>
51
+ <span v-else class="nut-dl-tree__slot" aria-hidden="true" />
52
+ </span>
53
+ <span class="nut-dl-tree__check">
54
+ <TableSelectionControl
55
+ v-if="selection.selectionEnabled.value"
56
+ :model-value="checkState"
57
+ :ariaLabel="t('table.tree.selectAll')"
58
+ @toggle="selection.toggleAllRows({ selected: !selection.allSelected.value })"
59
+ />
60
+ </span>
61
+ </div>
62
+ </template>
63
+
64
+ <style>
65
+ .nut-dl-tree-head{align-items:center;display:flex;height:100%}
66
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -8,12 +8,14 @@ import type { useTableLayout } from './use-table-layout.js';
8
8
  import type { useTablePagination } from './use-table-pagination.js';
9
9
  import type { useTableSelection } from './use-table-selection.js';
10
10
  import type { useTableState } from './use-table-state.js';
11
+ import type { useTableTree } from './use-table-tree.js';
11
12
  export interface UseTableApiParams<TSchema = TableSchemaView> {
12
13
  runtimeSchema: ComputedRef<TableSchemaView>;
13
14
  publicSchema: ComputedRef<TSchema>;
14
15
  layout: ReturnType<typeof useTableLayout>;
15
16
  state: ReturnType<typeof useTableState>;
16
17
  selection: ReturnType<typeof useTableSelection>;
18
+ tree: ReturnType<typeof useTableTree>;
17
19
  controls: ReturnType<typeof useTableControls>;
18
20
  columns: ReturnType<typeof useTableColumns>;
19
21
  filters: ReturnType<typeof useTableFilters>;
@@ -85,6 +85,7 @@ export function useTableApi(params) {
85
85
  all() {
86
86
  reset.query();
87
87
  selection.clear();
88
+ params.tree.expansion.reset();
88
89
  },
89
90
  query() {
90
91
  const { defaultLayout } = params.runtimeSchema.value;
@@ -101,6 +102,7 @@ export function useTableApi(params) {
101
102
  };
102
103
  return {
103
104
  data,
105
+ expansion: params.tree.expansion,
104
106
  filters,
105
107
  layout: layoutApi,
106
108
  pagination,
@@ -593,6 +593,22 @@ declare function createTableInternals<TSchema>(options: {
593
593
  toggleSorting: (key: string) => void;
594
594
  visibleOrderedColumns: import("vue").ComputedRef<import("../utils/index.js").TableRuntimeColumn[]>;
595
595
  };
596
+ tree: {
597
+ enabled: import("vue").ComputedRef<boolean>;
598
+ expansion: import("../types/index.js").TableExpansionApi;
599
+ index: import("vue").ComputedRef<import("../types/index.js").TableTreeIndex>;
600
+ isExpanded: (rowId: string) => boolean;
601
+ isOnPath: (rowId: string) => boolean;
602
+ isSelectable: (node: import("../types/index.js").TableTreeNode) => boolean;
603
+ lightLineage: (rowId?: string) => void;
604
+ litUntil: (groupId: string) => number;
605
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
606
+ nodeOf: (rowId: string) => import("../types/index.js").TableTreeNode | undefined;
607
+ selectable: import("vue").ComputedRef<"all" | "roots">;
608
+ toggleRow: (rowId: string) => void;
609
+ visibleNodes: import("vue").ComputedRef<import("../types/index.js").TableTreeNode[]>;
610
+ visibleRows: import("vue").ComputedRef<object[]>;
611
+ };
596
612
  };
597
613
  declare function provideTableInternals(internals: TableInternals): void;
598
614
  declare function useProvideTableInternals<TSchema>(options: {
@@ -1189,6 +1205,22 @@ declare function useProvideTableInternals<TSchema>(options: {
1189
1205
  toggleSorting: (key: string) => void;
1190
1206
  visibleOrderedColumns: import("vue").ComputedRef<import("../utils/index.js").TableRuntimeColumn[]>;
1191
1207
  };
1208
+ tree: {
1209
+ enabled: import("vue").ComputedRef<boolean>;
1210
+ expansion: import("../types/index.js").TableExpansionApi;
1211
+ index: import("vue").ComputedRef<import("../types/index.js").TableTreeIndex>;
1212
+ isExpanded: (rowId: string) => boolean;
1213
+ isOnPath: (rowId: string) => boolean;
1214
+ isSelectable: (node: import("../types/index.js").TableTreeNode) => boolean;
1215
+ lightLineage: (rowId?: string) => void;
1216
+ litUntil: (groupId: string) => number;
1217
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
1218
+ nodeOf: (rowId: string) => import("../types/index.js").TableTreeNode | undefined;
1219
+ selectable: import("vue").ComputedRef<"all" | "roots">;
1220
+ toggleRow: (rowId: string) => void;
1221
+ visibleNodes: import("vue").ComputedRef<import("../types/index.js").TableTreeNode[]>;
1222
+ visibleRows: import("vue").ComputedRef<object[]>;
1223
+ };
1192
1224
  };
1193
1225
  declare function useTableInternals(): {
1194
1226
  actions: {
@@ -1782,6 +1814,22 @@ declare function useTableInternals(): {
1782
1814
  toggleSorting: (key: string) => void;
1783
1815
  visibleOrderedColumns: import("vue").ComputedRef<import("../utils/index.js").TableRuntimeColumn[]>;
1784
1816
  };
1817
+ tree: {
1818
+ enabled: import("vue").ComputedRef<boolean>;
1819
+ expansion: import("../types/index.js").TableExpansionApi;
1820
+ index: import("vue").ComputedRef<import("../types/index.js").TableTreeIndex>;
1821
+ isExpanded: (rowId: string) => boolean;
1822
+ isOnPath: (rowId: string) => boolean;
1823
+ isSelectable: (node: import("../types/index.js").TableTreeNode) => boolean;
1824
+ lightLineage: (rowId?: string) => void;
1825
+ litUntil: (groupId: string) => number;
1826
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
1827
+ nodeOf: (rowId: string) => import("../types/index.js").TableTreeNode | undefined;
1828
+ selectable: import("vue").ComputedRef<"all" | "roots">;
1829
+ toggleRow: (rowId: string) => void;
1830
+ visibleNodes: import("vue").ComputedRef<import("../types/index.js").TableTreeNode[]>;
1831
+ visibleRows: import("vue").ComputedRef<object[]>;
1832
+ };
1785
1833
  };
1786
1834
  export type TableInternals = ReturnType<typeof createTableInternals>;
1787
1835
  export { createTableInternals, provideTableInternals, useProvideTableInternals, useTableInternals };
@@ -14,6 +14,7 @@ import { useTableSelection } from "./use-table-selection.js";
14
14
  import { useTableStartup } from "./use-table-startup.js";
15
15
  import { useTableState } from "./use-table-state.js";
16
16
  import { useTableSummaries } from "./use-table-summaries.js";
17
+ import { useTableTree } from "./use-table-tree.js";
17
18
  function createTableInternals(options) {
18
19
  const publicSchema = computed(() => resolveSchemaSource({ schema: options.rawSchema }));
19
20
  const schema = computed(() => publicSchema.value);
@@ -29,10 +30,15 @@ function createTableInternals(options) {
29
30
  startup,
30
31
  state
31
32
  });
32
- const selection = useTableSelection({
33
+ const tree = useTableTree({
33
34
  queryContent,
34
35
  schema
35
36
  });
37
+ const selection = useTableSelection({
38
+ queryContent,
39
+ schema,
40
+ tree
41
+ });
36
42
  const controls = useTableControls({
37
43
  layout,
38
44
  schema
@@ -55,7 +61,8 @@ function createTableInternals(options) {
55
61
  selection,
56
62
  state,
57
63
  tableApi,
58
- tableLayout: controls.tableLayout
64
+ tableLayout: controls.tableLayout,
65
+ tree
59
66
  });
60
67
  const summaries = useTableSummaries({
61
68
  queryContent,
@@ -79,7 +86,8 @@ function createTableInternals(options) {
79
86
  queryContent,
80
87
  runtimeSchema: schema,
81
88
  selection,
82
- state
89
+ state,
90
+ tree
83
91
  });
84
92
  if (!tableApi.value) {
85
93
  throw new Error("Failed to initialize table API");
@@ -106,7 +114,8 @@ function createTableInternals(options) {
106
114
  startup,
107
115
  summaries,
108
116
  tableApi: tableApi.value,
109
- tableColumns
117
+ tableColumns,
118
+ tree
110
119
  };
111
120
  }
112
121
  const TABLE_INTERNALS_KEY = Symbol("nuxt-ui-tools.table.internals");
@@ -1,9 +1,11 @@
1
1
  import type { ComputedRef } from 'vue';
2
2
  import type { GenericObject, TableSchemaView } from '../types/index.js';
3
3
  import type { useTableData } from './use-table-data.js';
4
+ import type { useTableTree } from './use-table-tree.js';
4
5
  export interface UseTableSelectionParams {
5
6
  schema: ComputedRef<TableSchemaView>;
6
7
  queryContent: ReturnType<typeof useTableData>;
8
+ tree: ReturnType<typeof useTableTree>;
7
9
  }
8
10
  export declare function useTableSelection(options: UseTableSelectionParams): {
9
11
  allSelected: ComputedRef<boolean>;
@@ -1,10 +1,16 @@
1
1
  import { computed, ref, watch } from "vue";
2
- import { hasConfiguredTableActions, resolveTableRowId } from "../utils/index.js";
2
+ import { flattenTreeRows, hasConfiguredTableActions, resolveTableRowId } from "../utils/index.js";
3
3
  export function useTableSelection(options) {
4
4
  const selectedKeys = ref([]);
5
5
  const bulkScope = ref("selection");
6
6
  const lastTouchedRowId = ref(null);
7
- const pageRows = computed(() => options.queryContent.data.value.rows);
7
+ function withTreeRows(rows) {
8
+ const childrenPath = options.schema.value.table?.tree?.children;
9
+ return childrenPath && options.tree.selectable.value === "all" ? flattenTreeRows(rows, childrenPath) : rows;
10
+ }
11
+ const pageRows = computed(
12
+ () => withTreeRows(options.queryContent.data.value.rows)
13
+ );
8
14
  const selectionEnabled = computed(() => {
9
15
  const mode = options.schema.value.table?.selection ?? options.schema.value.selection?.mode ?? "auto";
10
16
  if (mode !== "auto") {
@@ -19,7 +25,7 @@ export function useTableSelection(options) {
19
25
  return options.schema.value.selection?.scope ?? "all";
20
26
  });
21
27
  const selectionRows = computed(
22
- () => selectionScope.value === "all" ? options.queryContent.selectableRows.value : pageRows.value
28
+ () => selectionScope.value === "all" ? withTreeRows(options.queryContent.selectableRows.value) : pageRows.value
23
29
  );
24
30
  const pageRowIds = computed(() => pageRows.value.map((row, index) => getRowId({ index, row })));
25
31
  const scopeRowIds = computed(
@@ -111,15 +117,18 @@ export function useTableSelection(options) {
111
117
  }
112
118
  lastTouchedRowId.value = params.rowId;
113
119
  }
120
+ const rangeRowIds = computed(
121
+ () => options.tree.enabled.value ? options.tree.visibleNodes.value.filter((node) => options.tree.isSelectable(node)).map((node) => node.id) : scopeRowIds.value
122
+ );
114
123
  function getRangeRowIds(params) {
115
- const anchorIndex = scopeRowIds.value.indexOf(params.anchorRowId);
116
- const targetIndex = scopeRowIds.value.indexOf(params.targetRowId);
124
+ const anchorIndex = rangeRowIds.value.indexOf(params.anchorRowId);
125
+ const targetIndex = rangeRowIds.value.indexOf(params.targetRowId);
117
126
  if (anchorIndex === -1 || targetIndex === -1) {
118
127
  return [params.targetRowId];
119
128
  }
120
129
  const start = Math.min(anchorIndex, targetIndex);
121
130
  const end = Math.max(anchorIndex, targetIndex);
122
- return scopeRowIds.value.slice(start, end + 1);
131
+ return rangeRowIds.value.slice(start, end + 1);
123
132
  }
124
133
  watch(
125
134
  scopeRowIds,
@@ -0,0 +1,36 @@
1
+ import type { ComputedRef } from 'vue';
2
+ import type { TableExpansionApi, TableSchemaView, TableTreeIndex, TableTreeMotion, TableTreeNode } from '../types/index.js';
3
+ import type { UseTableDataReturn } from './use-table-data.js';
4
+ export interface UseTableTreeParams {
5
+ schema: ComputedRef<TableSchemaView>;
6
+ queryContent: Pick<UseTableDataReturn, 'data'>;
7
+ }
8
+ /**
9
+ * Owns tree rows: which rows are open, the flat list of rows that are on screen, and the motion
10
+ * of a branch opening or closing.
11
+ *
12
+ * Expansion is a baseline plus per-row overrides rather than a set of open ids. The baseline is
13
+ * the schema's `defaultExpanded` (or what `expandAll` / `collapseAll` last chose), so rows that
14
+ * load later, such as the next cursor page, follow it without being enumerated; an override only
15
+ * exists for a row someone actually toggled, and is dropped again if it matches the baseline.
16
+ *
17
+ * TanStack's expanded row model is deliberately not used: it materialises every descendant before
18
+ * the virtualizer sees a row. The nested source rows stay as they are, `visibleNodes` flattens
19
+ * only what is open, and that list is what the renderer virtualizes.
20
+ */
21
+ export declare function useTableTree(params: UseTableTreeParams): {
22
+ enabled: ComputedRef<boolean>;
23
+ expansion: TableExpansionApi;
24
+ index: ComputedRef<TableTreeIndex>;
25
+ isExpanded: (rowId: string) => boolean;
26
+ isOnPath: (rowId: string) => boolean;
27
+ isSelectable: (node: TableTreeNode) => boolean;
28
+ lightLineage: (rowId?: string) => void;
29
+ litUntil: (groupId: string) => number;
30
+ motionOf: (rowId: string) => TableTreeMotion | undefined;
31
+ nodeOf: (rowId: string) => TableTreeNode | undefined;
32
+ selectable: ComputedRef<"all" | "roots">;
33
+ toggleRow: (rowId: string) => void;
34
+ visibleNodes: ComputedRef<TableTreeNode[]>;
35
+ visibleRows: ComputedRef<object[]>;
36
+ };
@@ -0,0 +1,199 @@
1
+ import { usePreferredReducedMotion } from "@vueuse/core";
2
+ import { computed, onScopeDispose, reactive, ref, shallowRef } from "vue";
3
+ import { resolveTableRowId } from "../utils/rows.js";
4
+ import {
5
+ buildTableTree,
6
+ flattenVisibleTree,
7
+ getTreeLineage,
8
+ getVisibleDescendants,
9
+ isOpenByDefault
10
+ } from "../utils/tree.js";
11
+ const CONCEAL_MS = 120;
12
+ const REVEAL_MS = 190;
13
+ const STAGGER_MS = 24;
14
+ const MOTION_ROW_LIMIT = 24;
15
+ const STAGGER_ROW_LIMIT = 8;
16
+ const EMPTY_INDEX = {
17
+ branches: [],
18
+ byId: /* @__PURE__ */ new Map(),
19
+ duplicateIds: [],
20
+ maxDepth: 0,
21
+ roots: []
22
+ };
23
+ export function useTableTree(params) {
24
+ const reducedMotion = usePreferredReducedMotion();
25
+ const config = computed(() => params.schema.value.table?.tree);
26
+ const enabled = computed(() => Boolean(config.value));
27
+ const defaultExpanded = computed(() => config.value?.defaultExpanded ?? false);
28
+ const selectable = computed(() => config.value?.selectable ?? "all");
29
+ const index = computed(() => {
30
+ const tree = config.value;
31
+ if (!tree) return EMPTY_INDEX;
32
+ const built = buildTableTree({
33
+ childrenPath: tree.children,
34
+ getRowId: (row, position) => String(resolveTableRowId({ index: position, row, rowKey: params.schema.value.rowKey })),
35
+ rows: params.queryContent.data.value.rows
36
+ });
37
+ if (built.duplicateIds.length && process.env.NODE_ENV !== "production")
38
+ console.warn(
39
+ `[nuxt-ui-tools] Tree rows must have unique ids across the whole tree; left out duplicates of: ${built.duplicateIds.slice(0, 5).join(", ")}`
40
+ );
41
+ return built;
42
+ });
43
+ const baseline = ref(null);
44
+ const overrides = reactive(/* @__PURE__ */ new Map());
45
+ const closing = reactive(/* @__PURE__ */ new Set());
46
+ function openByDefault(node) {
47
+ return isOpenByDefault(baseline.value ?? defaultExpanded.value, node.depth);
48
+ }
49
+ function isNodeOpen(node) {
50
+ return node.hasChildren && (overrides.get(node.id) ?? openByDefault(node));
51
+ }
52
+ function setNodeOpen(node, open) {
53
+ if (!node.hasChildren) return;
54
+ if (open === openByDefault(node)) overrides.delete(node.id);
55
+ else overrides.set(node.id, open);
56
+ }
57
+ function isExpanded(rowId) {
58
+ const node = index.value.byId.get(rowId);
59
+ return node !== void 0 && isNodeOpen(node) && !closing.has(rowId);
60
+ }
61
+ function nodeOf(rowId) {
62
+ return index.value.byId.get(rowId);
63
+ }
64
+ function isSelectable(node) {
65
+ return selectable.value === "all" || node.depth === 0;
66
+ }
67
+ function setExpanded(rowIds, open) {
68
+ for (const rowId of rowIds) {
69
+ const node = index.value.byId.get(rowId);
70
+ if (node) setNodeOpen(node, open);
71
+ }
72
+ }
73
+ function setAll(open) {
74
+ settle();
75
+ baseline.value = open;
76
+ overrides.clear();
77
+ }
78
+ const visibleNodes = computed(() => flattenVisibleTree(index.value.roots, isNodeOpen));
79
+ const visibleRows = computed(() => visibleNodes.value.map((node) => node.row));
80
+ const motion = shallowRef(null);
81
+ let timer;
82
+ let closingNode;
83
+ function motionOf(rowId) {
84
+ return motion.value?.get(rowId);
85
+ }
86
+ function playMotion(kind, branch) {
87
+ const rows = getVisibleDescendants(visibleNodes.value, branch, MOTION_ROW_LIMIT);
88
+ motion.value = new Map(
89
+ rows.map((node, order) => [
90
+ node.id,
91
+ { kind, order: Math.min(order, STAGGER_ROW_LIMIT) }
92
+ ])
93
+ );
94
+ return rows.length;
95
+ }
96
+ function settle() {
97
+ clearTimeout(timer);
98
+ if (closingNode) {
99
+ setNodeOpen(closingNode, false);
100
+ closing.delete(closingNode.id);
101
+ closingNode = void 0;
102
+ }
103
+ motion.value = null;
104
+ }
105
+ function toggleRow(rowId) {
106
+ const node = index.value.byId.get(rowId);
107
+ if (!node?.hasChildren) return;
108
+ if (closingNode === node) {
109
+ clearTimeout(timer);
110
+ closing.delete(rowId);
111
+ closingNode = void 0;
112
+ motion.value = null;
113
+ return;
114
+ }
115
+ settle();
116
+ const open = isNodeOpen(node);
117
+ if (reducedMotion.value === "reduce") {
118
+ setNodeOpen(node, !open);
119
+ return;
120
+ }
121
+ if (!open) {
122
+ setNodeOpen(node, true);
123
+ const revealed = playMotion("enter", node);
124
+ timer = setTimeout(
125
+ () => motion.value = null,
126
+ REVEAL_MS + Math.min(revealed, STAGGER_ROW_LIMIT) * STAGGER_MS
127
+ );
128
+ return;
129
+ }
130
+ if (!playMotion("leave", node)) {
131
+ setNodeOpen(node, false);
132
+ return;
133
+ }
134
+ closingNode = node;
135
+ closing.add(rowId);
136
+ timer = setTimeout(settle, CONCEAL_MS);
137
+ }
138
+ onScopeDispose(() => clearTimeout(timer));
139
+ const flatIndexById = computed(
140
+ () => new Map(visibleNodes.value.map((node, position) => [node.id, position]))
141
+ );
142
+ const litUntilByGroup = reactive(/* @__PURE__ */ new Map());
143
+ const onPath = reactive(/* @__PURE__ */ new Set());
144
+ function lightLineage(rowId) {
145
+ const node = rowId === void 0 ? void 0 : index.value.byId.get(rowId);
146
+ const lineage = node ? getTreeLineage(node).map((entry) => entry.id) : [];
147
+ const groups = lineage.slice(0, -1);
148
+ for (const group of litUntilByGroup.keys())
149
+ if (!groups.includes(group)) litUntilByGroup.delete(group);
150
+ for (const group of onPath) if (!groups.includes(group)) onPath.delete(group);
151
+ for (const [position, group] of groups.entries()) {
152
+ const child = lineage[position + 1];
153
+ litUntilByGroup.set(group, child === void 0 ? -1 : flatIndexById.value.get(child) ?? -1);
154
+ onPath.add(group);
155
+ }
156
+ }
157
+ function litUntil(groupId) {
158
+ return litUntilByGroup.get(groupId) ?? -1;
159
+ }
160
+ function isOnPath(rowId) {
161
+ return onPath.has(rowId);
162
+ }
163
+ const expansion = {
164
+ collapse: (rowIds) => setExpanded(rowIds, false),
165
+ collapseAll: () => setAll(false),
166
+ expand: (rowIds) => setExpanded(rowIds, true),
167
+ expandAll: () => setAll(true),
168
+ isExpanded,
169
+ reset: () => setAll(null),
170
+ state: computed(() => {
171
+ const { branches } = index.value;
172
+ const expandedKeys = branches.filter(isNodeOpen).map((node) => node.id);
173
+ return {
174
+ allExpanded: branches.length > 0 && expandedKeys.length === branches.length,
175
+ branchCount: branches.length,
176
+ enabled: enabled.value,
177
+ expandedCount: expandedKeys.length,
178
+ expandedKeys
179
+ };
180
+ }),
181
+ toggle: ({ expanded, rowId }) => setExpanded([rowId], expanded ?? !isExpanded(rowId))
182
+ };
183
+ return {
184
+ enabled,
185
+ expansion,
186
+ index,
187
+ isExpanded,
188
+ isOnPath,
189
+ isSelectable,
190
+ lightLineage,
191
+ litUntil,
192
+ motionOf,
193
+ nodeOf,
194
+ selectable,
195
+ toggleRow,
196
+ visibleNodes,
197
+ visibleRows
198
+ };
199
+ }
@@ -10,4 +10,5 @@ export type * from './schema';
10
10
  export type * from './source';
11
11
  export type * from './state';
12
12
  export type * from './table-api';
13
+ export type * from './tree';
13
14
  export type * from './utils';
@@ -1,4 +1,5 @@
1
1
  import type { TableColumnCollection, TableSummaryRequest, TableSummaryScope, TableSummaryValue } from './columns.js';
2
+ import type { TableTreeSchema } from './tree.js';
2
3
  import type { GenericObject, TableDefaultSort, TableGridSortOption, TableLayout, TableRowRenderParams, RenderableType, TableSortKey, TableTextValue } from './utils.js';
3
4
  export interface TablePersistenceOptions {
4
5
  state?: boolean;
@@ -53,8 +54,8 @@ export interface TableGridSchema<TRow extends GenericObject = GenericObject, TCo
53
54
  export interface TableTableSchema<TRow extends GenericObject = GenericObject, TContext extends GenericObject = GenericObject, TPageContext extends GenericObject = GenericObject, TSortKey extends string = TableSortKey<TRow>, TData = unknown> {
54
55
  enabled?: boolean | string | (() => boolean | string);
55
56
  columns?: TableColumnCollection<TRow, TContext, TPageContext, string, TSortKey, TData>;
56
- treeMode?: boolean;
57
- childrenKey?: TableSortKey<TRow>;
57
+ /** Lets rows hold child rows that open beneath them. See `TableTreeSchema`. */
58
+ tree?: TableTreeSchema<TRow>;
58
59
  defaultSorting?: TableDefaultSort<TSortKey>;
59
60
  selection?: boolean | 'auto';
60
61
  summaries?: TableSummariesSchema<TRow>;
@@ -3,6 +3,7 @@ import type { useTableData } from '../composables/use-table-data.js';
3
3
  import type { useTablePagination } from '../composables/use-table-pagination.js';
4
4
  import type { TableFilterState } from './query-state.js';
5
5
  import type { TableSchemaView } from './schema.js';
6
+ import type { TableExpansionApi } from './tree.js';
6
7
  import type { ExtractTableContextData, ExtractTablePageContextData, ExtractTableRow, TableLayout, TablePaginationState, TableSortingDirection, TableSortingRule } from './utils.js';
7
8
  export interface PublicTableQueryState {
8
9
  layout: TableLayout;
@@ -143,6 +144,11 @@ export interface TableApi<TSchema = TableSchemaView> {
143
144
  }) => void;
144
145
  isSelected: (rowId: string) => boolean;
145
146
  };
147
+ /**
148
+ * Open and close tree rows. With no `table.tree` in the schema, `state.enabled` is `false` and
149
+ * nothing here has an effect.
150
+ */
151
+ expansion: TableExpansionApi;
146
152
  reset: {
147
153
  query: () => void;
148
154
  all: () => void;
@@ -0,0 +1,100 @@
1
+ import type { ComputedRef } from 'vue';
2
+ import type { GenericObject } from './utils.js';
3
+ /**
4
+ * Keys of a row that hold an array of child rows.
5
+ *
6
+ * A row type with no such key (or an untyped one) falls back to `string`, so the option never
7
+ * becomes unusable.
8
+ */
9
+ export type TableChildrenKey<TRow extends GenericObject> = [ChildrenKeys<TRow>] extends [never] ? string : ChildrenKeys<TRow>;
10
+ type ChildrenKeys<TRow extends GenericObject> = {
11
+ [TKey in keyof TRow & string]-?: NonNullable<TRow[TKey]> extends readonly GenericObject[] ? TKey : never;
12
+ }[keyof TRow & string];
13
+ /**
14
+ * Turns the table into a tree: a row can hold child rows that open beneath it.
15
+ *
16
+ * The children travel with their parent in the source payload. Row ids (`rowKey`) must be unique
17
+ * across the whole tree, not only among siblings. Pagination, sorting, filters, search and
18
+ * summaries act on the top-level rows; a search or filter field may reach into the children with a
19
+ * dotted path such as `creditNotes.number`, which keeps the parent row.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * table: {
24
+ * tree: { children: 'creditNotes', defaultExpanded: false },
25
+ * }
26
+ * ```
27
+ */
28
+ export interface TableTreeSchema<TRow extends GenericObject = GenericObject> {
29
+ /** Key of each row that holds its child rows. A row with no children shows no chevron. */
30
+ children: TableChildrenKey<TRow>;
31
+ /**
32
+ * Which branches start open. `true` opens every branch, a number opens that many levels
33
+ * (`1` opens the top-level rows), `false` (the default) opens none. A branch the user or
34
+ * `table.expansion` has toggled keeps that state.
35
+ */
36
+ defaultExpanded?: boolean | number;
37
+ /**
38
+ * Which rows can be selected when the table has selection. `'all'` (the default) lets every row
39
+ * carry a checkbox, children included, each one independent of its parent; `'roots'` limits the
40
+ * checkboxes to the top-level rows. A row without a checkbox keeps its width, so nothing moves.
41
+ */
42
+ selectable?: 'all' | 'roots';
43
+ }
44
+ /** One loaded row with its place in the tree, resolved once per data change. */
45
+ export interface TableTreeNode {
46
+ readonly id: string;
47
+ readonly row: GenericObject;
48
+ /** Top-level rows are depth `0`. */
49
+ readonly depth: number;
50
+ readonly parent: TableTreeNode | null;
51
+ /** Position of the top-level row this node sits under. */
52
+ readonly rootIndex: number;
53
+ readonly childNodes: readonly TableTreeNode[];
54
+ readonly hasChildren: boolean;
55
+ /** No later sibling follows: the rail stops at this row's elbow. */
56
+ readonly isLast: boolean;
57
+ }
58
+ /** The motion a row plays while a branch opens or closes. */
59
+ export interface TableTreeMotion {
60
+ kind: 'enter' | 'leave';
61
+ /** Position in the stagger, so rows arrive one after another instead of all at once. */
62
+ order: number;
63
+ }
64
+ export interface TableTreeIndex {
65
+ readonly roots: readonly TableTreeNode[];
66
+ readonly byId: ReadonlyMap<string, TableTreeNode>;
67
+ /** Every node that has children. */
68
+ readonly branches: readonly TableTreeNode[];
69
+ readonly maxDepth: number;
70
+ /** Ids met more than once; the later rows are left out of the tree. */
71
+ readonly duplicateIds: readonly string[];
72
+ }
73
+ export interface TableExpansionState {
74
+ /** Whether the schema configured `table.tree`. */
75
+ enabled: boolean;
76
+ /** Ids of the loaded rows that are open right now, defaults included. */
77
+ expandedKeys: string[];
78
+ expandedCount: number;
79
+ /** Loaded rows that have children. */
80
+ branchCount: number;
81
+ allExpanded: boolean;
82
+ }
83
+ export interface TableExpansionApi {
84
+ state: ComputedRef<TableExpansionState>;
85
+ isExpanded: (rowId: string) => boolean;
86
+ /** Opens the given rows; ids without children are ignored. */
87
+ expand: (rowIds: string[]) => void;
88
+ collapse: (rowIds: string[]) => void;
89
+ /** Flips a row, or sets it when `expanded` is given. */
90
+ toggle: (options: {
91
+ rowId: string;
92
+ expanded?: boolean;
93
+ }) => void;
94
+ /** Opens every loaded branch, and every branch that loads later, until collapsed again. */
95
+ expandAll: () => void;
96
+ collapseAll: () => void;
97
+ /** Returns every branch to the schema's `defaultExpanded`. */
98
+ reset: () => void;
99
+ }
100
+ export {};
File without changes