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.
- package/dist/module.json +1 -1
- package/dist/runtime/i18n/locales/en.js +8 -0
- package/dist/runtime/i18n/locales/fr.js +8 -0
- package/dist/runtime/i18n/types.d.ts +8 -0
- package/dist/runtime/table/components/table/table-layout.d.ts +2 -0
- package/dist/runtime/table/components/table/table-layout.js +1 -0
- package/dist/runtime/table/components/table/table-renderer.d.vue.ts +6 -6
- package/dist/runtime/table/components/table/table-renderer.vue +75 -11
- package/dist/runtime/table/components/table/table-renderer.vue.d.ts +6 -6
- package/dist/runtime/table/components/table/table-row.d.vue.ts +15 -2
- package/dist/runtime/table/components/table/table-row.vue +23 -2
- package/dist/runtime/table/components/table/table-row.vue.d.ts +15 -2
- package/dist/runtime/table/components/table/table-tree-control.d.vue.ts +19 -0
- package/dist/runtime/table/components/table/table-tree-control.vue +112 -0
- package/dist/runtime/table/components/table/table-tree-control.vue.d.ts +19 -0
- package/dist/runtime/table/components/table/table-tree-header.d.vue.ts +3 -0
- package/dist/runtime/table/components/table/table-tree-header.vue +66 -0
- package/dist/runtime/table/components/table/table-tree-header.vue.d.ts +3 -0
- package/dist/runtime/table/composables/use-table-api.d.ts +2 -0
- package/dist/runtime/table/composables/use-table-api.js +2 -0
- package/dist/runtime/table/composables/use-table-internals.d.ts +48 -0
- package/dist/runtime/table/composables/use-table-internals.js +13 -4
- package/dist/runtime/table/composables/use-table-selection.d.ts +2 -0
- package/dist/runtime/table/composables/use-table-selection.js +15 -6
- package/dist/runtime/table/composables/use-table-tree.d.ts +36 -0
- package/dist/runtime/table/composables/use-table-tree.js +199 -0
- package/dist/runtime/table/types/index.d.ts +1 -0
- package/dist/runtime/table/types/layout.d.ts +3 -2
- package/dist/runtime/table/types/table-api.d.ts +6 -0
- package/dist/runtime/table/types/tree.d.ts +100 -0
- package/dist/runtime/table/types/tree.js +0 -0
- package/dist/runtime/table/utils/columns/defs.js +19 -2
- package/dist/runtime/table/utils/columns/types.d.ts +6 -1
- package/dist/runtime/table/utils/columns/types.js +7 -0
- package/dist/runtime/table/utils/index.d.ts +1 -0
- package/dist/runtime/table/utils/index.js +1 -0
- package/dist/runtime/table/utils/tree.d.ts +64 -0
- package/dist/runtime/table/utils/tree.js +112 -0
- 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
|
|
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
|
-
|
|
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 =
|
|
116
|
-
const targetIndex =
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
57
|
-
|
|
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
|