@hina-ui/vue 1.7.0 → 1.7.2

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 (98) hide show
  1. package/dist/index.js +3086 -444
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/anchor/composables/useScrollSpy.d.ts.map +1 -1
  4. package/dist/types/components/combobox/Combobox.vue.d.ts +1 -0
  5. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  6. package/dist/types/components/data-table/DataTable.vue.d.ts +58 -0
  7. package/dist/types/components/data-table/DataTable.vue.d.ts.map +1 -0
  8. package/dist/types/components/data-table/DataTableAction.vue.d.ts +18 -0
  9. package/dist/types/components/data-table/DataTableAction.vue.d.ts.map +1 -0
  10. package/dist/types/components/data-table/DataTableDragPreview.vue.d.ts +24 -0
  11. package/dist/types/components/data-table/DataTableDragPreview.vue.d.ts.map +1 -0
  12. package/dist/types/components/data-table/DataTableEditor.vue.d.ts +24 -0
  13. package/dist/types/components/data-table/DataTableEditor.vue.d.ts.map +1 -0
  14. package/dist/types/components/data-table/DataTableHead.vue.d.ts +32 -0
  15. package/dist/types/components/data-table/DataTableHead.vue.d.ts.map +1 -0
  16. package/dist/types/components/data-table/DataTableRow.vue.d.ts +39 -0
  17. package/dist/types/components/data-table/DataTableRow.vue.d.ts.map +1 -0
  18. package/dist/types/components/data-table/DataTableSelection.vue.d.ts +15 -0
  19. package/dist/types/components/data-table/DataTableSelection.vue.d.ts.map +1 -0
  20. package/dist/types/components/data-table/DataTableText.vue.d.ts +8 -0
  21. package/dist/types/components/data-table/DataTableText.vue.d.ts.map +1 -0
  22. package/dist/types/components/data-table/Slot.d.ts +8 -0
  23. package/dist/types/components/data-table/Slot.d.ts.map +1 -0
  24. package/dist/types/components/data-table/column-sizing-styles.d.ts +4 -0
  25. package/dist/types/components/data-table/column-sizing-styles.d.ts.map +1 -0
  26. package/dist/types/components/data-table/column-sizing.d.ts +22 -0
  27. package/dist/types/components/data-table/column-sizing.d.ts.map +1 -0
  28. package/dist/types/components/data-table/composables/useDataTable.d.ts +80 -0
  29. package/dist/types/components/data-table/composables/useDataTable.d.ts.map +1 -0
  30. package/dist/types/components/data-table/composables/useTableColumns.d.ts +52 -0
  31. package/dist/types/components/data-table/composables/useTableColumns.d.ts.map +1 -0
  32. package/dist/types/components/data-table/composables/useTableDrag.d.ts +53 -0
  33. package/dist/types/components/data-table/composables/useTableDrag.d.ts.map +1 -0
  34. package/dist/types/components/data-table/composables/useTableEditing.d.ts +27 -0
  35. package/dist/types/components/data-table/composables/useTableEditing.d.ts.map +1 -0
  36. package/dist/types/components/data-table/composables/useTableExport.d.ts +3 -0
  37. package/dist/types/components/data-table/composables/useTableExport.d.ts.map +1 -0
  38. package/dist/types/components/data-table/composables/useTableResize.d.ts +42 -0
  39. package/dist/types/components/data-table/composables/useTableResize.d.ts.map +1 -0
  40. package/dist/types/components/data-table/composables/useTableResizeHandles.d.ts +8 -0
  41. package/dist/types/components/data-table/composables/useTableResizeHandles.d.ts.map +1 -0
  42. package/dist/types/components/data-table/composables/useTableText.d.ts +6 -0
  43. package/dist/types/components/data-table/composables/useTableText.d.ts.map +1 -0
  44. package/dist/types/components/data-table/composables/useTableVirtual.d.ts +55 -0
  45. package/dist/types/components/data-table/composables/useTableVirtual.d.ts.map +1 -0
  46. package/dist/types/components/data-table/features.d.ts +16 -0
  47. package/dist/types/components/data-table/features.d.ts.map +1 -0
  48. package/dist/types/components/data-table/types.d.ts +225 -0
  49. package/dist/types/components/data-table/types.d.ts.map +1 -0
  50. package/dist/types/components/data-table/utils.d.ts +10 -0
  51. package/dist/types/components/data-table/utils.d.ts.map +1 -0
  52. package/dist/types/components/form/composables/useForm.d.ts.map +1 -1
  53. package/dist/types/components/table/TableHead.vue.d.ts +2 -2
  54. package/dist/types/components/table/TableHead.vue.d.ts.map +1 -1
  55. package/dist/types/components/table/table.variants.d.ts +1 -1
  56. package/dist/types/components/table/table.variants.d.ts.map +1 -1
  57. package/dist/types/index.d.ts +2 -0
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/types/locale/en-US.d.ts.map +1 -1
  60. package/dist/types/locale/types.d.ts +12 -0
  61. package/dist/types/locale/types.d.ts.map +1 -1
  62. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  63. package/package.json +14 -12
  64. package/src/components/anchor/composables/useScrollSpy.ts +8 -11
  65. package/src/components/combobox/Combobox.vue +11 -1
  66. package/src/components/data-table/DataTable.vue +341 -0
  67. package/src/components/data-table/DataTableAction.vue +15 -0
  68. package/src/components/data-table/DataTableDragPreview.vue +72 -0
  69. package/src/components/data-table/DataTableEditor.vue +59 -0
  70. package/src/components/data-table/DataTableHead.vue +181 -0
  71. package/src/components/data-table/DataTableRow.vue +255 -0
  72. package/src/components/data-table/DataTableSelection.vue +51 -0
  73. package/src/components/data-table/DataTableText.vue +20 -0
  74. package/src/components/data-table/Slot.ts +12 -0
  75. package/src/components/data-table/column-sizing-styles.ts +40 -0
  76. package/src/components/data-table/column-sizing.ts +101 -0
  77. package/src/components/data-table/composables/useDataTable.ts +425 -0
  78. package/src/components/data-table/composables/useTableColumns.ts +251 -0
  79. package/src/components/data-table/composables/useTableDrag.ts +345 -0
  80. package/src/components/data-table/composables/useTableEditing.ts +184 -0
  81. package/src/components/data-table/composables/useTableExport.ts +44 -0
  82. package/src/components/data-table/composables/useTableResize.ts +292 -0
  83. package/src/components/data-table/composables/useTableResizeHandles.ts +72 -0
  84. package/src/components/data-table/composables/useTableText.ts +13 -0
  85. package/src/components/data-table/composables/useTableVirtual.ts +109 -0
  86. package/src/components/data-table/features.ts +32 -0
  87. package/src/components/data-table/types.ts +218 -0
  88. package/src/components/data-table/utils.ts +75 -0
  89. package/src/components/form/composables/useForm.ts +44 -8
  90. package/src/components/table/TableHead.vue +1 -1
  91. package/src/components/table/table.variants.ts +1 -2
  92. package/src/index.ts +25 -0
  93. package/src/locale/en-US.ts +13 -0
  94. package/src/locale/types.ts +13 -0
  95. package/src/locale/zh-CN.ts +13 -0
  96. package/src/styles/data-table.css +330 -0
  97. package/src/styles/tokens.css +1 -0
  98. package/src/styles/typography.css +4 -2
@@ -0,0 +1,425 @@
1
+ import { computed, watch, type Ref } from 'vue'
2
+ import { useTable, type ColumnDef } from '@tanstack/vue-table'
3
+ import { useUiLocale } from '../../../locale'
4
+ import { features } from '../features'
5
+ import {
6
+ aggregate,
7
+ filterValue,
8
+ flattenColumns,
9
+ isRowAction,
10
+ positiveInteger,
11
+ rowId,
12
+ rowKey,
13
+ } from '../utils'
14
+ import type {
15
+ DataTableApi,
16
+ DataTableColumn,
17
+ DataTableFilter,
18
+ DataTableGroupContext,
19
+ DataTableKey,
20
+ DataTableProps,
21
+ DataTableQuery,
22
+ DataTableRowContext,
23
+ DataTableSort,
24
+ DataTableState,
25
+ } from '../types'
26
+
27
+ export interface DataTableModels {
28
+ page: Ref<number>
29
+ pageSize: Ref<number>
30
+ sorting: Ref<DataTableSort[]>
31
+ filter: Ref<string>
32
+ columnFilters: Ref<DataTableFilter[]>
33
+ grouping: Ref<string[]>
34
+ selected: Ref<DataTableKey[]>
35
+ expanded: Ref<DataTableKey[]>
36
+ expandedGroups: Ref<string[]>
37
+ hiddenColumns: Ref<string[]>
38
+ columnOrder: Ref<string[]>
39
+ columnWidths: Ref<Record<string, number>>
40
+ }
41
+ export function useDataTable<T extends object>(
42
+ props: DataTableProps<T>,
43
+ models: DataTableModels,
44
+ onChange: (query: DataTableQuery) => void,
45
+ onRowClick: (row: T, event: MouseEvent | KeyboardEvent) => void,
46
+ ) {
47
+ const t = useUiLocale()
48
+ const api = {} as DataTableApi<T>
49
+ const blocked = computed(() => !!props.loading || !!props.disabled)
50
+ const pageSize = computed(() => positiveInteger(models.pageSize.value, 10))
51
+ const page = computed(() => positiveInteger(models.page.value, 1))
52
+ const leaves = computed(() => flattenColumns(props.columns))
53
+ const collator = computed(
54
+ () => new Intl.Collator(t.value.tag, { numeric: true, sensitivity: 'base' }),
55
+ )
56
+ const keyOf = (row: T) =>
57
+ typeof props.rowKey === 'function' ? props.rowKey(row) : (row[props.rowKey] as DataTableKey)
58
+ const labelOf = (row: T) =>
59
+ String(
60
+ typeof props.rowLabel === 'function'
61
+ ? props.rowLabel(row)
62
+ : props.rowLabel === undefined
63
+ ? keyOf(row)
64
+ : row[props.rowLabel],
65
+ )
66
+ const valueOf = (row: T, column: DataTableColumn<T>) =>
67
+ column.accessor ? column.accessor(row) : row[column.field ?? (column.key as keyof T)]
68
+ const selectable = (row: T) =>
69
+ typeof props.selectable === 'function' ? props.selectable(row) : !!props.selectable
70
+ const expandable = (row: T) =>
71
+ typeof props.expandable === 'function' ? props.expandable(row) : !!props.expandable
72
+ const columns = computed<ColumnDef<typeof features, T>[]>(() => {
73
+ const compare = collator.value
74
+ return leaves.value.map(column => ({
75
+ id: column.key,
76
+ accessorFn: row => valueOf(row, column) ?? undefined,
77
+ enableSorting: !!column.sortable,
78
+ enableGlobalFilter: column.filterable !== false,
79
+ filterFn: (row, _id, value: unknown) =>
80
+ column.filterValue
81
+ ? column.filterValue(row.original, value)
82
+ : filterValue(valueOf(row.original, column), value, column.filterMode, t.value.tag),
83
+ sortDescFirst: false,
84
+ sortUndefined: 'last',
85
+ sortFn: (a, b, id) => {
86
+ if (column.sort) return column.sort(a.original, b.original)
87
+ const left = a.getValue(id),
88
+ right = b.getValue(id)
89
+ if (typeof left === 'number' && typeof right === 'number') return left - right
90
+ if (left instanceof Date && right instanceof Date) return left.getTime() - right.getTime()
91
+ return compare.compare(String(left), String(right))
92
+ },
93
+ }))
94
+ })
95
+ const table = useTable<typeof features, T>({
96
+ features,
97
+ data: computed(() => props.rows),
98
+ columns,
99
+ getRowId: row => rowId(keyOf(row)),
100
+ getSubRows: row => props.getChildren?.(row),
101
+ manualSorting: computed(() => !!props.manual),
102
+ manualFiltering: computed(() => !!props.manual),
103
+ manualGrouping: computed(() => !!props.manual),
104
+ manualPagination: computed(() => !!props.manual || !props.pagination),
105
+ groupedColumnMode: false,
106
+ filterFromLeafRows: true,
107
+ paginateExpandedRows: false,
108
+ autoResetExpanded: false,
109
+ getRowCanExpand: row =>
110
+ row.getIsGrouped() || row.subRows.length > 0 || expandable(row.original),
111
+ enableMultiSort: computed(() => !!props.multiSort),
112
+ enableMultiRowSelection: computed(() => props.selectionMode !== 'single'),
113
+ enableRowSelection: row => !row.getIsGrouped() && selectable(row.original),
114
+ enableSubRowSelection: computed(
115
+ () => props.selectChildren !== false && props.selectionMode !== 'single',
116
+ ),
117
+ autoResetPageIndex: false,
118
+ getColumnCanGlobalFilter: () => true,
119
+ globalFilterFn: (row, id, query: string) => {
120
+ const column = leaves.value.find(column => column.key === id)!
121
+ return column.filter
122
+ ? column.filter(row.original, query)
123
+ : filterValue(row.getValue(id), query, 'contains', t.value.tag)
124
+ },
125
+ state: computed(() => ({
126
+ sorting: models.sorting.value.map(sort => ({ id: sort.key, desc: sort.desc })),
127
+ globalFilter: models.filter.value.trim(),
128
+ columnFilters: models.columnFilters.value
129
+ .filter(filter => leaves.value.some(column => column.key === filter.key))
130
+ .map(filter => ({ id: filter.key, value: filter.value })),
131
+ grouping: models.grouping.value.filter(key =>
132
+ leaves.value.some(column => column.key === key),
133
+ ),
134
+ expanded: Object.fromEntries(
135
+ [...models.expanded.value.map(rowId), ...models.expandedGroups.value].map(key => [
136
+ key,
137
+ true,
138
+ ]),
139
+ ),
140
+ pagination: { pageIndex: page.value - 1, pageSize: pageSize.value },
141
+ rowSelection: Object.fromEntries(
142
+ models.selected.value.map(key => [rowId(key), true as const]),
143
+ ),
144
+ columnVisibility: Object.fromEntries(models.hiddenColumns.value.map(key => [key, false])),
145
+ })),
146
+ onSortingChange: updater => {
147
+ if (blocked.value) return
148
+ const current = models.sorting.value.map(sort => ({ id: sort.key, desc: sort.desc }))
149
+ const next = typeof updater === 'function' ? updater(current) : updater
150
+ models.sorting.value = next.map(sort => ({ key: sort.id, desc: sort.desc }))
151
+ },
152
+ onRowSelectionChange: updater => {
153
+ if (blocked.value) return
154
+ const current = Object.fromEntries(
155
+ models.selected.value.map(key => [rowId(key), true as const]),
156
+ )
157
+ const next = typeof updater === 'function' ? updater(current) : updater
158
+ models.selected.value = Object.keys(next)
159
+ .filter(key => next[key])
160
+ .map(rowKey)
161
+ },
162
+ })
163
+ const visibleColumns = computed(() => {
164
+ const visible = leaves.value.filter(column => !models.hiddenColumns.value.includes(column.key))
165
+ const order = [...new Set([...models.columnOrder.value, ...visible.map(column => column.key)])]
166
+ return visible.sort((a, b) => {
167
+ const rank = (column: DataTableColumn<T>) =>
168
+ column.pin === 'start' ? 0 : column.pin === 'end' ? 2 : 1
169
+ return rank(a) - rank(b) || order.indexOf(a.key) - order.indexOf(b.key)
170
+ })
171
+ })
172
+ const knownTotal = computed(() => !props.manual || props.total !== undefined)
173
+ const total = computed(() =>
174
+ props.manual
175
+ ? Math.max(0, Math.floor(Number.isFinite(props.total) ? props.total! : props.rows.length))
176
+ : table.getPreExpandedRowModel().rows.length,
177
+ )
178
+ const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
179
+ const canNext = computed(() =>
180
+ knownTotal.value
181
+ ? page.value < pageCount.value
182
+ : (props.hasNextPage ?? props.rows.length >= pageSize.value),
183
+ )
184
+ function toggleExpanded(key: DataTableKey, value?: boolean) {
185
+ if (blocked.value) return
186
+ const row = table.getCoreRowModel().rowsById[rowId(key)]
187
+ if (!row?.getCanExpand()) return
188
+ const next = value ?? !models.expanded.value.includes(key)
189
+ models.expanded.value = next
190
+ ? [...new Set([...models.expanded.value, key])]
191
+ : models.expanded.value.filter(item => item !== key)
192
+ }
193
+ function toggleSelected(key: DataTableKey, value?: boolean) {
194
+ if (blocked.value) return
195
+ const row = table.getCoreRowModel().rowsById[rowId(key)]
196
+ if (row?.getCanSelect()) row.toggleSelected(value ?? !row.getIsSelected())
197
+ }
198
+ const rows = computed(() =>
199
+ table.getRowModel().rows.map(row => {
200
+ const context: DataTableRowContext<T> = {
201
+ row: row.original,
202
+ key: keyOf(row.original),
203
+ index: row.index,
204
+ depth: row.depth,
205
+ selected: row.getIsSelected(),
206
+ selectable: row.getCanSelect(),
207
+ indeterminate: props.selectChildren !== false && row.getIsSomeSelected(),
208
+ expanded: row.getIsExpanded(),
209
+ expandable: row.getCanExpand(),
210
+ toggleSelected: value => toggleSelected(keyOf(row.original), value),
211
+ toggleExpanded: value => toggleExpanded(keyOf(row.original), value),
212
+ startEdit: column => api.startEdit(keyOf(row.original), column),
213
+ }
214
+ const group: DataTableGroupContext<T> | undefined = row.getIsGrouped()
215
+ ? {
216
+ key: row.id,
217
+ column: leaves.value.find(column => column.key === row.groupingColumnId)!,
218
+ value: row.groupingValue,
219
+ rows: row
220
+ .getLeafRows()
221
+ .filter(leaf => !leaf.getIsGrouped())
222
+ .map(leaf => leaf.original),
223
+ depth: row.depth,
224
+ expanded: row.getIsExpanded(),
225
+ toggleExpanded: value => {
226
+ if (blocked.value) return
227
+ const next = value ?? !models.expandedGroups.value.includes(row.id)
228
+ models.expandedGroups.value = next
229
+ ? [...new Set([...models.expandedGroups.value, row.id])]
230
+ : models.expandedGroups.value.filter(key => key !== row.id)
231
+ },
232
+ aggregate: key => {
233
+ const column = leaves.value.find(column => column.key === key)
234
+ return column
235
+ ? aggregate(
236
+ row.getLeafRows().map(leaf => leaf.original),
237
+ column,
238
+ valueOf,
239
+ )
240
+ : undefined
241
+ },
242
+ }
243
+ : undefined
244
+ return { ...context, id: row.id, label: labelOf(row.original), group }
245
+ }),
246
+ )
247
+ const selectionRows = computed(() => {
248
+ const source =
249
+ props.selectAll === 'filtered'
250
+ ? table.getFilteredRowModel().flatRows
251
+ : table.getRowModel().rows.flatMap(row => (row.getIsGrouped() ? row.getLeafRows() : [row]))
252
+ return [...new Map(source.filter(row => row.getCanSelect()).map(row => [row.id, row])).values()]
253
+ })
254
+ const pageSelection = computed(() => {
255
+ const eligible = selectionRows.value
256
+ if (!eligible.length) return false
257
+ const count = eligible.filter(row => row.getIsSelected()).length
258
+ return count === eligible.length
259
+ ? true
260
+ : count || eligible.some(row => row.getIsSomeSelected())
261
+ ? 'indeterminate'
262
+ : false
263
+ })
264
+ const selectionDisabled = computed(() => blocked.value || !selectionRows.value.length)
265
+ function togglePage(selected: boolean | 'indeterminate') {
266
+ if (blocked.value || props.selectionMode === 'single') return
267
+ const keys = new Set(models.selected.value)
268
+ for (const row of selectionRows.value) {
269
+ const descendants = props.selectChildren === false ? [row] : [row, ...row.getLeafRows()]
270
+ for (const item of descendants)
271
+ if (item.getCanSelect()) {
272
+ if (selected === true) keys.add(keyOf(item.original))
273
+ else keys.delete(keyOf(item.original))
274
+ }
275
+ }
276
+ models.selected.value = [...keys]
277
+ }
278
+ function setFilter(column: string, value: unknown) {
279
+ if (blocked.value) return
280
+ const next = models.columnFilters.value.filter(filter => filter.key !== column)
281
+ if (value != null && value !== '' && !(Array.isArray(value) && !value.length))
282
+ next.push({ key: column, value })
283
+ models.columnFilters.value = next
284
+ }
285
+ function headerContext(column: DataTableColumn<T>) {
286
+ const engine = leaves.value.some(item => item.key === column.key)
287
+ ? table.getColumn(column.key)
288
+ : undefined
289
+ const sorting = engine?.getIsSorted() ?? false
290
+ const next = engine?.getNextSortingOrder()
291
+ return {
292
+ column,
293
+ sorting,
294
+ sortIndex: engine?.getSortIndex() ?? -1,
295
+ filterValue: models.columnFilters.value.find(filter => filter.key === column.key)?.value,
296
+ setFilter: (value: unknown) => setFilter(column.key, value),
297
+ nextLabel:
298
+ next === 'asc'
299
+ ? t.value.table.sortAsc
300
+ : next === 'desc'
301
+ ? t.value.table.sortDesc
302
+ : t.value.table.sortNone,
303
+ ariaSort: sorting
304
+ ? sorting === 'asc'
305
+ ? ('ascending' as const)
306
+ : ('descending' as const)
307
+ : undefined,
308
+ toggleSort: (multi = false) => {
309
+ if (!blocked.value && column.sortable)
310
+ engine?.toggleSorting(undefined, !!props.multiSort && multi)
311
+ },
312
+ }
313
+ }
314
+ function getRows(scope = 'page' as Parameters<DataTableApi<T>['getRows']>[0]) {
315
+ const source =
316
+ scope === 'page'
317
+ ? table.getRowModel().rows
318
+ : scope === 'filtered'
319
+ ? table.getPreExpandedRowModel().flatRows
320
+ : table.getCoreRowModel().flatRows
321
+ return source
322
+ .filter(row => !row.getIsGrouped() && (scope !== 'selected' || row.getIsSelected()))
323
+ .map(row => row.original)
324
+ }
325
+ Object.assign(api, {
326
+ getRows,
327
+ toggleSelected,
328
+ toggleExpanded,
329
+ setFilter,
330
+ setColumnHidden: (key: string, hidden: boolean) => {
331
+ models.hiddenColumns.value = hidden
332
+ ? [...new Set([...models.hiddenColumns.value, key])]
333
+ : models.hiddenColumns.value.filter(value => value !== key)
334
+ },
335
+ })
336
+ const state = computed<DataTableState<T>>(() => ({
337
+ page: page.value,
338
+ pageSize: pageSize.value,
339
+ sorting: models.sorting.value,
340
+ filter: models.filter.value,
341
+ columnFilters: models.columnFilters.value,
342
+ grouping: models.grouping.value,
343
+ total: total.value,
344
+ rows: getRows(),
345
+ selected: models.selected.value,
346
+ expanded: models.expanded.value,
347
+ visibleColumns: visibleColumns.value,
348
+ api,
349
+ }))
350
+ watch(
351
+ () => [
352
+ models.filter.value,
353
+ models.columnFilters.value,
354
+ models.sorting.value,
355
+ models.grouping.value,
356
+ pageSize.value,
357
+ ],
358
+ () => {
359
+ if (props.autoResetPage !== false) models.page.value = 1
360
+ },
361
+ { deep: true },
362
+ )
363
+ watch(
364
+ [pageCount, page, () => props.loading, () => props.pagination, knownTotal],
365
+ () => {
366
+ if (props.pagination && !props.loading && knownTotal.value && page.value > pageCount.value)
367
+ models.page.value = pageCount.value
368
+ },
369
+ { immediate: true },
370
+ )
371
+ watch(
372
+ () => [
373
+ page.value,
374
+ pageSize.value,
375
+ models.sorting.value,
376
+ models.filter.value,
377
+ models.columnFilters.value,
378
+ models.grouping.value,
379
+ ],
380
+ () => {
381
+ onChange({
382
+ page: page.value,
383
+ pageSize: pageSize.value,
384
+ sorting: models.sorting.value.map(sort => ({ ...sort })),
385
+ filter: models.filter.value,
386
+ columnFilters: models.columnFilters.value.map(filter => ({ ...filter })),
387
+ grouping: [...models.grouping.value],
388
+ })
389
+ },
390
+ { deep: true, flush: 'post' },
391
+ )
392
+ function activate(row: T, event: MouseEvent | KeyboardEvent) {
393
+ if (!props.rowClickable || blocked.value || !isRowAction(event)) return
394
+ if (event.type === 'keydown') event.preventDefault()
395
+ onRowClick(row, event)
396
+ }
397
+ function displayValue(row: T, column: DataTableColumn<T>) {
398
+ const value = valueOf(row, column)
399
+ return column.format ? column.format(value, row) : value == null ? '—' : String(value)
400
+ }
401
+ return {
402
+ api,
403
+ table,
404
+ blocked,
405
+ page,
406
+ pageSize,
407
+ total,
408
+ knownTotal,
409
+ canNext,
410
+ rows,
411
+ leaves,
412
+ visibleColumns,
413
+ pageSelection,
414
+ selectionDisabled,
415
+ state,
416
+ valueOf,
417
+ displayValue,
418
+ togglePage,
419
+ activate,
420
+ headerContext,
421
+ keyOf,
422
+ labelOf,
423
+ }
424
+ }
425
+ export type DataTableController<T extends object> = ReturnType<typeof useDataTable<T>>
@@ -0,0 +1,251 @@
1
+ import {
2
+ computed,
3
+ nextTick,
4
+ onScopeDispose,
5
+ shallowRef,
6
+ watch,
7
+ type Ref,
8
+ type CSSProperties,
9
+ } from 'vue'
10
+ import { cssSize } from '../utils'
11
+ import { allocateWidths, pixelWidth } from '../column-sizing'
12
+ import { columnSizingStyles } from '../column-sizing-styles'
13
+ import { useTableResize } from './useTableResize'
14
+ import { useTableResizeHandles } from './useTableResizeHandles'
15
+ import type { DataTableColumn, DataTableHeader, DataTableProps } from '../types'
16
+ import type { DataTableController, DataTableModels } from './useDataTable'
17
+
18
+ export function useTableColumns<T extends object>(
19
+ props: DataTableProps<T>,
20
+ models: DataTableModels,
21
+ ctl: DataTableController<T>,
22
+ element: Ref<HTMLTableElement | undefined>,
23
+ viewport: Ref<HTMLElement | undefined>,
24
+ leading: Ref<number>,
25
+ trailing: Ref<number>,
26
+ ) {
27
+ const measured = shallowRef<Record<string, number>>({})
28
+ const heights = shallowRef<number[]>([])
29
+ const available = shallowRef(0)
30
+ const active = shallowRef<Record<string, number>>()
31
+ let observer: ResizeObserver | undefined
32
+ let disposed = false
33
+ let frame = 0
34
+ watch(
35
+ [element, viewport, ctl.visibleColumns],
36
+ async () => {
37
+ await nextTick()
38
+ observer?.disconnect()
39
+ cancelAnimationFrame(frame)
40
+ if (disposed || !element.value || typeof ResizeObserver === 'undefined') return
41
+ const update = () => {
42
+ available.value = viewport.value?.clientWidth ?? 0
43
+ const next: Record<string, number> = {}
44
+ element.value?.querySelectorAll<HTMLElement>('thead [data-hn-column]').forEach(cell => {
45
+ next[cell.dataset.hnColumn!] = cell.getBoundingClientRect().width
46
+ })
47
+ if (JSON.stringify(next) !== JSON.stringify(measured.value)) measured.value = next
48
+ const rows = [...(element.value?.querySelectorAll('thead tr') ?? [])].map(
49
+ row => row.getBoundingClientRect().height,
50
+ )
51
+ if (JSON.stringify(rows) !== JSON.stringify(heights.value)) heights.value = rows
52
+ handles.update()
53
+ }
54
+ observer = new ResizeObserver(() => {
55
+ cancelAnimationFrame(frame)
56
+ frame = requestAnimationFrame(update)
57
+ })
58
+ element.value.querySelectorAll('thead th, thead tr').forEach(cell => observer!.observe(cell))
59
+ if (viewport.value) observer.observe(viewport.value)
60
+ update()
61
+ },
62
+ { flush: 'post' },
63
+ )
64
+ const constrained = computed(
65
+ () =>
66
+ props.layout === 'fixed' ||
67
+ props.resizable ||
68
+ !!props.virtualize ||
69
+ ctl.visibleColumns.value.some(column => column.truncate || column.maxWidth !== undefined) ||
70
+ Object.keys(models.columnWidths.value).length > 0,
71
+ )
72
+ const availableColumnsWidth = computed(
73
+ () => available.value - leading.value * 48 - trailing.value * 72,
74
+ )
75
+ const widths = computed(
76
+ () =>
77
+ active.value ??
78
+ allocateWidths(
79
+ ctl.visibleColumns.value,
80
+ models.columnWidths.value,
81
+ availableColumnsWidth.value,
82
+ ),
83
+ )
84
+ const handles = useTableResizeHandles(element, viewport, ctl.visibleColumns, widths)
85
+ const resizing = useTableResize(
86
+ props,
87
+ models,
88
+ ctl,
89
+ element,
90
+ viewport,
91
+ widths,
92
+ active,
93
+ availableColumnsWidth,
94
+ )
95
+ const widthVariable = (column: DataTableColumn<T>) =>
96
+ `var(--hn-table-column-${ctl.visibleColumns.value.findIndex(item => item.key === column.key)})`
97
+ function width(column: DataTableColumn<T>) {
98
+ return constrained.value
99
+ ? widths.value[column.key]!
100
+ : (measured.value[column.key] ?? pixelWidth(column.width, 160))
101
+ }
102
+ function specifiedWidth(column: DataTableColumn<T>) {
103
+ return constrained.value ? widthVariable(column) : column.width
104
+ }
105
+ function pinStyle(column: DataTableColumn<T>, head = false): CSSProperties {
106
+ if (!column.pin) return {}
107
+ const siblings = ctl.visibleColumns.value.filter(item => item.pin === column.pin)
108
+ const index = siblings.findIndex(item => item.key === column.key)
109
+ const preceding = column.pin === 'start' ? siblings.slice(0, index) : siblings.slice(index + 1)
110
+ const controls = column.pin === 'start' ? leading.value * 48 : trailing.value * 72
111
+ const offset = constrained.value
112
+ ? `calc(${controls}px${preceding.map(column => ` + ${widthVariable(column)}`).join('')})`
113
+ : `${preceding.reduce((sum, column) => sum + width(column), controls)}px`
114
+ return {
115
+ position: 'sticky',
116
+ [column.pin === 'start' ? 'insetInlineStart' : 'insetInlineEnd']: offset,
117
+ zIndex: head ? 5 : 1,
118
+ backgroundColor: head ? 'var(--hn-table-head-bg)' : 'var(--hn-table-bg)',
119
+ boxShadow: `${column.pin === 'start' ? '1px' : '-1px'} 0 0 var(--hn-border)`,
120
+ }
121
+ }
122
+ function cellStyle(column: DataTableColumn<T>, head = false): CSSProperties {
123
+ return {
124
+ width: cssSize(specifiedWidth(column)),
125
+ minWidth: cssSize(column.minWidth ?? specifiedWidth(column)),
126
+ maxWidth: cssSize(column.maxWidth),
127
+ ...pinStyle(column, head),
128
+ }
129
+ }
130
+ function controlStyle(side: 'start' | 'end', index: number, head = false): CSSProperties {
131
+ return {
132
+ width: side === 'end' ? '72px' : '48px',
133
+ minWidth: side === 'end' ? '72px' : '48px',
134
+ paddingInline: '8px',
135
+ textAlign: 'center',
136
+ ...(ctl.visibleColumns.value.some(column => column.pin === side)
137
+ ? {
138
+ position: 'sticky',
139
+ [side === 'start' ? 'insetInlineStart' : 'insetInlineEnd']: `${index * 48}px`,
140
+ zIndex: head ? 5 : 1,
141
+ backgroundColor: head ? 'var(--hn-table-head-bg)' : 'var(--hn-table-bg)',
142
+ }
143
+ : {}),
144
+ }
145
+ }
146
+ const headerRows = computed<DataTableHeader<T>[][]>(() => {
147
+ const paths = new Map<string, DataTableColumn<T>[]>()
148
+ function visit(columns: DataTableColumn<T>[], parent: DataTableColumn<T>[] = []) {
149
+ for (const column of columns) {
150
+ const path = [...parent, column]
151
+ if (column.children?.length) visit(column.children, path)
152
+ else paths.set(column.key, path)
153
+ }
154
+ }
155
+ visit(props.columns)
156
+ const visible = ctl.visibleColumns.value
157
+ const depth = Math.max(1, ...visible.map(column => paths.get(column.key)!.length))
158
+ return Array.from({ length: depth }, (_, level) => {
159
+ const result: DataTableHeader<T>[] = []
160
+ for (let index = 0; index < visible.length;) {
161
+ const first = visible[index]!
162
+ const path = paths.get(first.key)!
163
+ const column = path[level]
164
+ if (!column) {
165
+ index++
166
+ continue
167
+ }
168
+ const leaf = level === path.length - 1
169
+ let end = index + 1
170
+ if (!leaf)
171
+ while (
172
+ end < visible.length &&
173
+ paths.get(visible[end]!.key)?.[level]?.key === column.key &&
174
+ visible[end]!.pin === first.pin
175
+ )
176
+ end++
177
+ const top = Array.from({ length: level }, (_, i) => heights.value[i] ?? 44).reduce(
178
+ (a, b) => a + b,
179
+ 0,
180
+ )
181
+ result.push({
182
+ ...ctl.headerContext(column),
183
+ id: leaf ? `leaf:${column.key}` : `${level}:${column.key}:${first.key}`,
184
+ leaf,
185
+ colspan: end - index,
186
+ rowspan: leaf ? depth - level : 1,
187
+ top,
188
+ style: {
189
+ ...(leaf
190
+ ? cellStyle(column, true)
191
+ : pinStyle(first.pin === 'end' ? visible[end - 1]! : first, true)),
192
+ ...(props.stickyHeader ? { top: `${top}px` } : {}),
193
+ },
194
+ })
195
+ index = end
196
+ }
197
+ return result
198
+ })
199
+ })
200
+ const tableStyle = computed<CSSProperties>(() => ({
201
+ '--hn-table-viewport-width': available.value ? `${available.value}px` : undefined,
202
+ tableLayout: constrained.value ? 'fixed' : 'auto',
203
+ ...(constrained.value
204
+ ? columnSizingStyles(
205
+ ctl.visibleColumns.value,
206
+ active.value ?? models.columnWidths.value,
207
+ leading.value * 48 + trailing.value * 72,
208
+ )
209
+ : {}),
210
+ }))
211
+ function moveColumn(key: string, target: string) {
212
+ if (ctl.blocked.value || key === target) return
213
+ const columns = ctl.visibleColumns.value
214
+ const source = columns.find(column => column.key === key),
215
+ destination = columns.find(column => column.key === target)
216
+ if (
217
+ !source ||
218
+ !destination ||
219
+ source.pin !== destination.pin ||
220
+ source.reorderable === false ||
221
+ destination.reorderable === false
222
+ )
223
+ return
224
+ const order = [
225
+ ...new Set([...models.columnOrder.value, ...ctl.leaves.value.map(column => column.key)]),
226
+ ]
227
+ const from = order.indexOf(key),
228
+ to = order.indexOf(target)
229
+ order.splice(from, 1)
230
+ order.splice(to, 0, key)
231
+ models.columnOrder.value = order
232
+ }
233
+ Object.assign(ctl.api, { setColumnWidth: resizing.setWidth, moveColumn })
234
+ onScopeDispose(() => {
235
+ disposed = true
236
+ cancelAnimationFrame(frame)
237
+ observer?.disconnect()
238
+ })
239
+ return {
240
+ constrained,
241
+ headerRows,
242
+ tableStyle,
243
+ cellStyle,
244
+ controlStyle,
245
+ ...resizing,
246
+ ...handles,
247
+ width,
248
+ }
249
+ }
250
+
251
+ export type DataTableLayout<T extends object> = ReturnType<typeof useTableColumns<T>>