@hina-ui/vue 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/index.js +3170 -461
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/anchor/Anchor.vue.d.ts +26 -10
  4. package/dist/types/components/anchor/Anchor.vue.d.ts.map +1 -1
  5. package/dist/types/components/anchor/composables/useAnchor.d.ts +15 -0
  6. package/dist/types/components/anchor/composables/useAnchor.d.ts.map +1 -0
  7. package/dist/types/components/anchor/composables/useScrollSpy.d.ts.map +1 -1
  8. package/dist/types/components/anchor/types.d.ts +9 -0
  9. package/dist/types/components/anchor/types.d.ts.map +1 -0
  10. package/dist/types/components/app-shell/AppShell.vue.d.ts.map +1 -1
  11. package/dist/types/components/app-shell/composables/useSidebarScrollUpdates.d.ts +4 -0
  12. package/dist/types/components/app-shell/composables/useSidebarScrollUpdates.d.ts.map +1 -0
  13. package/dist/types/components/combobox/Combobox.vue.d.ts +1 -0
  14. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  15. package/dist/types/components/data-table/DataTable.vue.d.ts +58 -0
  16. package/dist/types/components/data-table/DataTable.vue.d.ts.map +1 -0
  17. package/dist/types/components/data-table/DataTableAction.vue.d.ts +18 -0
  18. package/dist/types/components/data-table/DataTableAction.vue.d.ts.map +1 -0
  19. package/dist/types/components/data-table/DataTableDragPreview.vue.d.ts +24 -0
  20. package/dist/types/components/data-table/DataTableDragPreview.vue.d.ts.map +1 -0
  21. package/dist/types/components/data-table/DataTableEditor.vue.d.ts +24 -0
  22. package/dist/types/components/data-table/DataTableEditor.vue.d.ts.map +1 -0
  23. package/dist/types/components/data-table/DataTableHead.vue.d.ts +32 -0
  24. package/dist/types/components/data-table/DataTableHead.vue.d.ts.map +1 -0
  25. package/dist/types/components/data-table/DataTableRow.vue.d.ts +39 -0
  26. package/dist/types/components/data-table/DataTableRow.vue.d.ts.map +1 -0
  27. package/dist/types/components/data-table/DataTableSelection.vue.d.ts +15 -0
  28. package/dist/types/components/data-table/DataTableSelection.vue.d.ts.map +1 -0
  29. package/dist/types/components/data-table/DataTableText.vue.d.ts +8 -0
  30. package/dist/types/components/data-table/DataTableText.vue.d.ts.map +1 -0
  31. package/dist/types/components/data-table/Slot.d.ts +8 -0
  32. package/dist/types/components/data-table/Slot.d.ts.map +1 -0
  33. package/dist/types/components/data-table/column-sizing.d.ts +22 -0
  34. package/dist/types/components/data-table/column-sizing.d.ts.map +1 -0
  35. package/dist/types/components/data-table/composables/useDataTable.d.ts +80 -0
  36. package/dist/types/components/data-table/composables/useDataTable.d.ts.map +1 -0
  37. package/dist/types/components/data-table/composables/useTableColumns.d.ts +51 -0
  38. package/dist/types/components/data-table/composables/useTableColumns.d.ts.map +1 -0
  39. package/dist/types/components/data-table/composables/useTableDrag.d.ts +53 -0
  40. package/dist/types/components/data-table/composables/useTableDrag.d.ts.map +1 -0
  41. package/dist/types/components/data-table/composables/useTableEditing.d.ts +27 -0
  42. package/dist/types/components/data-table/composables/useTableEditing.d.ts.map +1 -0
  43. package/dist/types/components/data-table/composables/useTableExport.d.ts +3 -0
  44. package/dist/types/components/data-table/composables/useTableExport.d.ts.map +1 -0
  45. package/dist/types/components/data-table/composables/useTableResize.d.ts +42 -0
  46. package/dist/types/components/data-table/composables/useTableResize.d.ts.map +1 -0
  47. package/dist/types/components/data-table/composables/useTableResizeHandles.d.ts +8 -0
  48. package/dist/types/components/data-table/composables/useTableResizeHandles.d.ts.map +1 -0
  49. package/dist/types/components/data-table/composables/useTableText.d.ts +6 -0
  50. package/dist/types/components/data-table/composables/useTableText.d.ts.map +1 -0
  51. package/dist/types/components/data-table/composables/useTableVirtual.d.ts +55 -0
  52. package/dist/types/components/data-table/composables/useTableVirtual.d.ts.map +1 -0
  53. package/dist/types/components/data-table/features.d.ts +16 -0
  54. package/dist/types/components/data-table/features.d.ts.map +1 -0
  55. package/dist/types/components/data-table/types.d.ts +225 -0
  56. package/dist/types/components/data-table/types.d.ts.map +1 -0
  57. package/dist/types/components/data-table/utils.d.ts +10 -0
  58. package/dist/types/components/data-table/utils.d.ts.map +1 -0
  59. package/dist/types/components/form/composables/useForm.d.ts.map +1 -1
  60. package/dist/types/components/sidebar/DrawerScope.d.ts +13 -2
  61. package/dist/types/components/sidebar/DrawerScope.d.ts.map +1 -1
  62. package/dist/types/components/sidebar/Sidebar.vue.d.ts +4 -1
  63. package/dist/types/components/sidebar/Sidebar.vue.d.ts.map +1 -1
  64. package/dist/types/components/sidebar/context.d.ts +1 -0
  65. package/dist/types/components/sidebar/context.d.ts.map +1 -1
  66. package/dist/types/components/sidebar/sidebar.variants.d.ts +3 -3
  67. package/dist/types/components/table/TableHead.vue.d.ts +2 -2
  68. package/dist/types/components/table/TableHead.vue.d.ts.map +1 -1
  69. package/dist/types/index.d.ts +3 -1
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/types/locale/en-US.d.ts.map +1 -1
  72. package/dist/types/locale/types.d.ts +12 -0
  73. package/dist/types/locale/types.d.ts.map +1 -1
  74. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  75. package/package.json +14 -12
  76. package/src/components/anchor/Anchor.vue +15 -19
  77. package/src/components/anchor/composables/useAnchor.ts +19 -0
  78. package/src/components/anchor/composables/useScrollSpy.ts +6 -1
  79. package/src/components/anchor/types.ts +8 -0
  80. package/src/components/app-shell/AppShell.vue +5 -2
  81. package/src/components/app-shell/composables/useSidebarScrollUpdates.ts +52 -0
  82. package/src/components/combobox/Combobox.vue +11 -1
  83. package/src/components/data-table/DataTable.vue +340 -0
  84. package/src/components/data-table/DataTableAction.vue +15 -0
  85. package/src/components/data-table/DataTableDragPreview.vue +72 -0
  86. package/src/components/data-table/DataTableEditor.vue +59 -0
  87. package/src/components/data-table/DataTableHead.vue +181 -0
  88. package/src/components/data-table/DataTableRow.vue +255 -0
  89. package/src/components/data-table/DataTableSelection.vue +51 -0
  90. package/src/components/data-table/DataTableText.vue +20 -0
  91. package/src/components/data-table/Slot.ts +12 -0
  92. package/src/components/data-table/column-sizing.ts +101 -0
  93. package/src/components/data-table/composables/useDataTable.ts +425 -0
  94. package/src/components/data-table/composables/useTableColumns.ts +253 -0
  95. package/src/components/data-table/composables/useTableDrag.ts +345 -0
  96. package/src/components/data-table/composables/useTableEditing.ts +184 -0
  97. package/src/components/data-table/composables/useTableExport.ts +44 -0
  98. package/src/components/data-table/composables/useTableResize.ts +292 -0
  99. package/src/components/data-table/composables/useTableResizeHandles.ts +72 -0
  100. package/src/components/data-table/composables/useTableText.ts +13 -0
  101. package/src/components/data-table/composables/useTableVirtual.ts +109 -0
  102. package/src/components/data-table/features.ts +32 -0
  103. package/src/components/data-table/types.ts +218 -0
  104. package/src/components/data-table/utils.ts +75 -0
  105. package/src/components/form/composables/useForm.ts +44 -8
  106. package/src/components/sidebar/DrawerScope.ts +12 -4
  107. package/src/components/sidebar/Sidebar.vue +27 -7
  108. package/src/components/sidebar/context.ts +1 -0
  109. package/src/components/sidebar/sidebar.variants.ts +1 -1
  110. package/src/components/table/TableHead.vue +1 -1
  111. package/src/index.ts +26 -1
  112. package/src/locale/en-US.ts +13 -0
  113. package/src/locale/types.ts +13 -0
  114. package/src/locale/zh-CN.ts +13 -0
  115. package/src/styles/data-table.css +331 -0
  116. package/src/styles/tokens.css +1 -0
  117. package/src/styles/typography.css +3 -0
@@ -0,0 +1,292 @@
1
+ import { nextTick, onScopeDispose, shallowRef, watch, type Ref } from 'vue'
2
+ import { clampWidth, resizeBoundary, resizeBounds, resizeWidths } from '../column-sizing'
3
+ import type { DataTableColumn, DataTableProps } from '../types'
4
+ import type { DataTableController, DataTableModels } from './useDataTable'
5
+
6
+ export function useTableResize<T extends object>(
7
+ props: DataTableProps<T>,
8
+ models: DataTableModels,
9
+ ctl: DataTableController<T>,
10
+ element: Ref<HTMLTableElement | undefined>,
11
+ viewport: Ref<HTMLElement | undefined>,
12
+ widths: Ref<Record<string, number>>,
13
+ active: Ref<Record<string, number> | undefined>,
14
+ available: Ref<number>,
15
+ ) {
16
+ const resizing = shallowRef<string>()
17
+ const guide = shallowRef<{
18
+ x: number
19
+ y: number
20
+ height: number
21
+ columns: { key: string; label: string; width: number }[]
22
+ labelX: number
23
+ }>()
24
+ let release: (() => void) | undefined
25
+ const boundary = (column: DataTableColumn<T>) =>
26
+ resizeBoundary(ctl.visibleColumns.value, column, props.resizeMode ?? 'fit')
27
+ const affectedColumns = (column: DataTableColumn<T>) => {
28
+ const edge = boundary(column)
29
+ return edge?.neighbor
30
+ ? edge.side === 'start'
31
+ ? [edge.neighbor, column]
32
+ : [column, edge.neighbor]
33
+ : [column]
34
+ }
35
+ const resizeLabel = (column: DataTableColumn<T>) =>
36
+ affectedColumns(column)
37
+ .map(item => item.label)
38
+ .join(' / ')
39
+ const resizeValueText = (column: DataTableColumn<T>) =>
40
+ affectedColumns(column)
41
+ .map(item => `${item.label}: ${Math.round(widths.value[item.key]!)}px`)
42
+ .join(' / ')
43
+ const minimumTotalWidth = () => (props.resizeMode === 'expand' ? Math.max(0, available.value) : 0)
44
+ const maximumWidth = (column: DataTableColumn<T>, source: Record<string, number>) => {
45
+ if (!column.pin || boundary(column)?.neighbor?.pin) return Infinity
46
+ const columns = ctl.visibleColumns.value
47
+ const otherPinned = columns.reduce(
48
+ (sum, item) => sum + (item.pin && item.key !== column.key ? source[item.key]! : 0),
49
+ 0,
50
+ )
51
+ const center = columns.some(item => !item.pin) ? 48 : 0
52
+ return Math.max(source[column.key]!, available.value - otherPinned - center)
53
+ }
54
+ const bounds = (column: DataTableColumn<T>) =>
55
+ resizeBounds(
56
+ column,
57
+ boundary(column)?.neighbor,
58
+ widths.value,
59
+ minimumTotalWidth(),
60
+ maximumWidth(column, widths.value),
61
+ )
62
+ const canResize = (column: DataTableColumn<T>) => {
63
+ const range = bounds(column)
64
+ return props.resizable && !!boundary(column) && range.max - range.min > 0.01
65
+ }
66
+ function setWidth(key: string, value: number) {
67
+ if (ctl.blocked.value || !Number.isFinite(value)) return
68
+ const column = ctl.leaves.value.find(column => column.key === key)
69
+ if (column)
70
+ models.columnWidths.value = { ...models.columnWidths.value, [key]: clampWidth(column, value) }
71
+ }
72
+ function resize(column: DataTableColumn<T>, event: PointerEvent) {
73
+ const edge = boundary(column)
74
+ if (event.button !== 0 || ctl.blocked.value || !edge || !canResize(column)) return
75
+ event.preventDefault()
76
+ event.stopPropagation()
77
+ release?.()
78
+ const handle = event.currentTarget as HTMLElement
79
+ const document = handle.ownerDocument
80
+ const window = document.defaultView!
81
+ const table = element.value!
82
+ const cell = handle.closest('th')!
83
+ const ratio =
84
+ table.getBoundingClientRect().width / parseFloat(window.getComputedStyle(table).width) || 1
85
+ const initial = { ...widths.value }
86
+ table.querySelectorAll<HTMLElement>('thead [data-hn-column]').forEach(cell => {
87
+ initial[cell.dataset.hnColumn!] = cell.getBoundingClientRect().width / ratio
88
+ })
89
+ active.value = initial
90
+ const previous = models.columnWidths.value
91
+ const direction = window.getComputedStyle(table).direction === 'rtl' ? -1 : 1
92
+ const sign = direction * (edge.side === 'start' ? -1 : 1)
93
+ const coordinate = (rect: DOMRect) => (sign === 1 ? rect.right : rect.left)
94
+ const areaAnchor = () => {
95
+ const box = viewport.value?.getBoundingClientRect()
96
+ return box ? (direction === 1 ? box.left : box.right) : 0
97
+ }
98
+ const anchor = areaAnchor()
99
+ let lastScroll = viewport.value?.scrollLeft ?? 0
100
+ let scrollDelta = 0
101
+ const trackScroll = () => {
102
+ const area = viewport.value
103
+ if (!area) return
104
+ const range = Math.max(0, area.scrollWidth - area.clientWidth)
105
+ const previous = Math.max(
106
+ direction === 1 ? 0 : -range,
107
+ Math.min(direction === 1 ? range : 0, lastScroll),
108
+ )
109
+ scrollDelta += area.scrollLeft - previous
110
+ lastScroll = area.scrollLeft
111
+ }
112
+ const gesture = document.documentElement.dataset.hnTableGesture
113
+ document.documentElement.dataset.hnTableGesture = 'resize'
114
+ handle.setPointerCapture?.(event.pointerId)
115
+ resizing.value = column.key
116
+ let pointerX = event.clientX
117
+ let changed = false
118
+ let moved = false
119
+ let frame = 0
120
+ let stopped = false
121
+ const updateGuide = () => {
122
+ if (stopped) return
123
+ const rect = cell.getBoundingClientRect()
124
+ const area = viewport.value?.getBoundingClientRect()
125
+ const box = table.getBoundingClientRect()
126
+ if (!area) return
127
+ const x = coordinate(handle.getBoundingClientRect())
128
+ guide.value =
129
+ x >= area.left - 1 && x <= area.right + 1
130
+ ? {
131
+ x,
132
+ columns: affectedColumns(column).map(item => ({
133
+ key: item.key,
134
+ label: item.label,
135
+ width: Math.round(widths.value[item.key]!),
136
+ })),
137
+ labelX: Math.max(8, Math.min(x - 90, window.innerWidth - 188)),
138
+ y: Math.max(rect.top, area.top),
139
+ height: Math.max(0, Math.min(box.bottom, area.bottom) - Math.max(rect.top, area.top)),
140
+ }
141
+ : undefined
142
+ }
143
+ const apply = () => {
144
+ if (stopped) return
145
+ if (moved) {
146
+ trackScroll()
147
+ const delta =
148
+ pointerX - event.clientX + anchor - areaAnchor() + (column.pin ? 0 : scrollDelta)
149
+ const desired = initial[column.key]! + (delta * sign) / ratio
150
+ const changes = resizeWidths(
151
+ column,
152
+ edge.neighbor,
153
+ initial,
154
+ desired,
155
+ minimumTotalWidth(),
156
+ maximumWidth(column, initial),
157
+ )
158
+ if (
159
+ Object.entries(changes).some(
160
+ ([key, width]) => Math.abs(width - widths.value[key]!) > 0.01,
161
+ )
162
+ ) {
163
+ changed = true
164
+ active.value = { ...initial, ...changes }
165
+ models.columnWidths.value =
166
+ props.resizeMode === 'expand'
167
+ ? { ...previous, ...active.value }
168
+ : { ...previous, ...changes }
169
+ }
170
+ }
171
+ void nextTick(updateGuide)
172
+ }
173
+ const schedule = () => {
174
+ window.cancelAnimationFrame(frame)
175
+ frame = window.requestAnimationFrame(apply)
176
+ }
177
+ const move = (next: PointerEvent) => {
178
+ if (next.pointerId !== event.pointerId) return
179
+ moved ||= Math.abs(next.clientX - event.clientX) > 0.5
180
+ pointerX = next.clientX
181
+ schedule()
182
+ }
183
+ const stop = (cancel = false) => {
184
+ window.cancelAnimationFrame(frame)
185
+ if (!cancel) apply()
186
+ stopped = true
187
+ if (cancel && changed) models.columnWidths.value = previous
188
+ active.value = undefined
189
+ window.removeEventListener('pointermove', move)
190
+ window.removeEventListener('pointerup', up)
191
+ window.removeEventListener('pointercancel', cancelPointer)
192
+ window.removeEventListener('blur', cancelDrag)
193
+ document.removeEventListener('keydown', keydown, true)
194
+ document.removeEventListener('scroll', schedule, true)
195
+ window.removeEventListener('resize', schedule)
196
+ if (handle.hasPointerCapture?.(event.pointerId)) handle.releasePointerCapture(event.pointerId)
197
+ if (gesture === undefined) delete document.documentElement.dataset.hnTableGesture
198
+ else document.documentElement.dataset.hnTableGesture = gesture
199
+ resizing.value = undefined
200
+ guide.value = undefined
201
+ release = undefined
202
+ }
203
+ const up = (next: PointerEvent) => {
204
+ if (next.pointerId !== event.pointerId) return
205
+ pointerX = next.clientX
206
+ moved ||= Math.abs(next.clientX - event.clientX) > 0.5
207
+ stop()
208
+ }
209
+ const cancelDrag = () => stop(true)
210
+ const cancelPointer = (next: PointerEvent) => {
211
+ if (next.pointerId === event.pointerId) cancelDrag()
212
+ }
213
+ const keydown = (next: KeyboardEvent) => {
214
+ if (next.key === 'Escape') {
215
+ next.preventDefault()
216
+ next.stopPropagation()
217
+ cancelDrag()
218
+ }
219
+ }
220
+ release = cancelDrag
221
+ window.addEventListener('pointermove', move)
222
+ window.addEventListener('pointerup', up)
223
+ window.addEventListener('pointercancel', cancelPointer)
224
+ window.addEventListener('blur', cancelDrag)
225
+ document.addEventListener('keydown', keydown, true)
226
+ document.addEventListener('scroll', schedule, true)
227
+ window.addEventListener('resize', schedule)
228
+ updateGuide()
229
+ }
230
+ function resizeKey(column: DataTableColumn<T>, event: KeyboardEvent) {
231
+ if (
232
+ ctl.blocked.value ||
233
+ !canResize(column) ||
234
+ !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)
235
+ )
236
+ return
237
+ event.preventDefault()
238
+ event.stopPropagation()
239
+ const edge = boundary(column)!
240
+ const direction = element.value && getComputedStyle(element.value).direction === 'rtl' ? -1 : 1
241
+ const sign = direction * (edge.side === 'start' ? -1 : 1)
242
+ const range = bounds(column)
243
+ const value =
244
+ event.key === 'Home'
245
+ ? range.min
246
+ : event.key === 'End'
247
+ ? range.max
248
+ : widths.value[column.key]! +
249
+ (event.key === 'ArrowRight' ? 1 : -1) * sign * (event.shiftKey ? 10 : 1)
250
+ const initial = widths.value
251
+ const changes = resizeWidths(
252
+ column,
253
+ edge.neighbor,
254
+ initial,
255
+ value,
256
+ minimumTotalWidth(),
257
+ maximumWidth(column, initial),
258
+ )
259
+ if (Object.entries(changes).every(([key, width]) => Math.abs(width - initial[key]!) < 0.01))
260
+ return
261
+ models.columnWidths.value = {
262
+ ...models.columnWidths.value,
263
+ ...(props.resizeMode === 'expand' ? initial : {}),
264
+ ...changes,
265
+ }
266
+ }
267
+ watch(
268
+ () => [
269
+ ctl.blocked.value,
270
+ props.resizeMode,
271
+ ctl.visibleColumns.value
272
+ .map(column =>
273
+ [column.key, column.pin, column.minWidth, column.maxWidth, column.resizable].join(':'),
274
+ )
275
+ .join('|'),
276
+ ],
277
+ () => release?.(),
278
+ )
279
+ onScopeDispose(() => release?.())
280
+ return {
281
+ resizing,
282
+ guide,
283
+ boundary,
284
+ bounds,
285
+ canResize,
286
+ resizeLabel,
287
+ resizeValueText,
288
+ resize,
289
+ resizeKey,
290
+ setWidth,
291
+ }
292
+ }
@@ -0,0 +1,72 @@
1
+ import { computed, onScopeDispose, shallowRef, watch, type CSSProperties, type Ref } from 'vue'
2
+ import { useEventListener } from '@vueuse/core'
3
+ import type { DataTableColumn } from '../types'
4
+
5
+ export function useTableResizeHandles<T extends object>(
6
+ element: Ref<HTMLTableElement | undefined>,
7
+ viewport: Ref<HTMLElement | undefined>,
8
+ columns: Ref<DataTableColumn<T>[]>,
9
+ widths: Ref<Record<string, number>>,
10
+ ) {
11
+ const positions = shallowRef<Record<string, { inset: number; visible: boolean }>>({})
12
+ let frame = 0
13
+ function update() {
14
+ const table = element.value
15
+ const area = viewport.value
16
+ if (!table || !area) return
17
+ const box = area.getBoundingClientRect()
18
+ const scale = box.width / area.clientWidth || 1
19
+ const rtl = getComputedStyle(table).direction === 'rtl'
20
+ const cells = new Map(
21
+ [...table.querySelectorAll<HTMLElement>('thead [data-hn-column]')].map(cell => [
22
+ cell.dataset.hnColumn!,
23
+ cell.getBoundingClientRect(),
24
+ ]),
25
+ )
26
+ let left = box.left
27
+ let right = box.right
28
+ for (const column of columns.value) {
29
+ const rect = cells.get(column.key)
30
+ if (!rect || !column.pin) continue
31
+ if ((column.pin === 'start') !== rtl) left = Math.max(left, rect.right)
32
+ else right = Math.min(right, rect.left)
33
+ }
34
+ const next: typeof positions.value = {}
35
+ for (const column of columns.value) {
36
+ const rect = cells.get(column.key)
37
+ if (!rect) continue
38
+ const start = Math.max(rect.left, column.pin ? box.left : left)
39
+ const end = Math.min(rect.right, column.pin ? box.right : right)
40
+ const fromLeft = (column.pin === 'end') !== rtl
41
+ next[column.key] = {
42
+ inset: Math.max(0, (fromLeft ? start - rect.left : rect.right - end) / scale),
43
+ visible: end - start >= 9 * scale - 0.01,
44
+ }
45
+ }
46
+ if (JSON.stringify(next) !== JSON.stringify(positions.value)) positions.value = next
47
+ }
48
+ function schedule() {
49
+ cancelAnimationFrame(frame)
50
+ frame = requestAnimationFrame(update)
51
+ }
52
+ useEventListener(viewport, 'scroll', schedule, { passive: true })
53
+ watch([element, viewport, columns, widths], update, { flush: 'post' })
54
+ onScopeDispose(() => cancelAnimationFrame(frame))
55
+ const styles = computed(() =>
56
+ Object.fromEntries(
57
+ Object.entries(positions.value).map(([key, position]) => [
58
+ key,
59
+ {
60
+ '--hn-table-resize-inset': `${position.inset}px`,
61
+ visibility: position.visible ? 'visible' : 'hidden',
62
+ } satisfies CSSProperties,
63
+ ]),
64
+ ),
65
+ )
66
+ return {
67
+ update,
68
+ handleStyle: (column: DataTableColumn<T>): CSSProperties =>
69
+ styles.value[column.key] ?? { visibility: 'hidden' },
70
+ handleVisible: (column: DataTableColumn<T>) => positions.value[column.key]?.visible ?? false,
71
+ }
72
+ }
@@ -0,0 +1,13 @@
1
+ import { computed, shallowRef, watch, type Ref } from 'vue'
2
+ import { useEventListener, useResizeObserver } from '@vueuse/core'
3
+
4
+ export function useTableText(element: Ref<HTMLElement | undefined>, value: () => string | number) {
5
+ const overflow = shallowRef(false)
6
+ const measure = () => {
7
+ overflow.value = !!element.value && element.value.scrollWidth > element.value.clientWidth
8
+ }
9
+ useResizeObserver(element, measure)
10
+ useEventListener(() => element.value?.ownerDocument.fonts, 'loadingdone', measure)
11
+ watch(value, measure, { flush: 'post' })
12
+ return { overflow, tooltip: computed(() => (overflow.value ? String(value()) : false)) }
13
+ }
@@ -0,0 +1,109 @@
1
+ import { computed, nextTick, onScopeDispose, shallowRef, watch, type Ref } from 'vue'
2
+ import { useVirtualizer } from '@tanstack/vue-virtual'
3
+ import type { DataTableKey, DataTableProps } from '../types'
4
+ import type { DataTableController } from './useDataTable'
5
+
6
+ export function useTableVirtual<T extends object>(
7
+ props: DataTableProps<T>,
8
+ ctl: DataTableController<T>,
9
+ element: Ref<HTMLTableElement | undefined>,
10
+ viewport: Ref<HTMLElement | undefined>,
11
+ hasExpansion: () => boolean,
12
+ rowError: (key: DataTableKey) => string | undefined,
13
+ ) {
14
+ const margin = shallowRef(0)
15
+ let observer: ResizeObserver | undefined
16
+ let disposed = false
17
+ const entries = computed(() =>
18
+ ctl.rows.value.flatMap(entry => {
19
+ const rows = [{ id: entry.id, entry, detail: false, error: undefined as string | undefined }]
20
+ if (!entry.group) {
21
+ const error = rowError(entry.key)
22
+ if (error) rows.push({ id: `${entry.id}:error`, entry, detail: true, error })
23
+ if (entry.expanded && hasExpansion())
24
+ rows.push({ id: `${entry.id}:detail`, entry, detail: true, error: undefined })
25
+ }
26
+ return rows
27
+ }),
28
+ )
29
+ watch(element, async () => {
30
+ await nextTick()
31
+ observer?.disconnect()
32
+ const head = element.value?.tHead
33
+ if (disposed || !head || typeof ResizeObserver === 'undefined') return
34
+ const update = () => {
35
+ margin.value = head.offsetHeight + (element.value?.caption?.offsetHeight ?? 0)
36
+ }
37
+ observer = new ResizeObserver(update)
38
+ observer.observe(head)
39
+ if (element.value?.caption) observer.observe(element.value.caption)
40
+ update()
41
+ })
42
+ const virtualizer = useVirtualizer(
43
+ computed(() => ({
44
+ count: entries.value.length,
45
+ useAnimationFrameWithResizeObserver: true,
46
+ enabled: !!props.virtualize,
47
+ getScrollElement: () => viewport.value ?? null,
48
+ estimateSize: (index: number) =>
49
+ entries.value[index]?.detail && !entries.value[index]?.error
50
+ ? 140
51
+ : typeof props.virtualize === 'object'
52
+ ? (props.virtualize.estimateSize ?? 44)
53
+ : 44,
54
+ overscan: typeof props.virtualize === 'object' ? (props.virtualize.overscan ?? 6) : 6,
55
+ getItemKey: (index: number) => entries.value[index]!.id,
56
+ scrollMargin: margin.value,
57
+ scrollPaddingStart: props.stickyHeader ? margin.value : 0,
58
+ initialRect: { width: 0, height: 400 },
59
+ })),
60
+ )
61
+ const window = computed(() => virtualizer.value.getVirtualItems())
62
+ const visible = computed(() =>
63
+ props.virtualize
64
+ ? window.value.map(item => ({ ...entries.value[item.index]!, index: item.index }))
65
+ : entries.value.map((entry, index) => ({ ...entry, index })),
66
+ )
67
+ const before = computed(() =>
68
+ props.virtualize ? Math.max(0, (window.value[0]?.start ?? margin.value) - margin.value) : 0,
69
+ )
70
+ const after = computed(() =>
71
+ props.virtualize
72
+ ? Math.max(
73
+ 0,
74
+ virtualizer.value.getTotalSize() -
75
+ ((window.value.at(-1)?.end ?? margin.value) - margin.value),
76
+ )
77
+ : 0,
78
+ )
79
+ function measure(element: unknown) {
80
+ if (props.virtualize && element instanceof HTMLElement)
81
+ virtualizer.value.measureElement(element)
82
+ }
83
+ ctl.api.scrollToRow = key => {
84
+ const index = entries.value.findIndex(
85
+ item => !item.detail && item.entry.key === key && !item.entry.group,
86
+ )
87
+ if (index < 0) return
88
+ if (props.virtualize) virtualizer.value.scrollToIndex(index, { align: 'start' })
89
+ else {
90
+ const row = element.value?.querySelector<HTMLElement>(
91
+ `[data-hn-row="${CSS.escape(entries.value[index]!.id)}"]`,
92
+ )
93
+ if (row && viewport.value)
94
+ viewport.value.scrollTop +=
95
+ row.getBoundingClientRect().top -
96
+ viewport.value.getBoundingClientRect().top -
97
+ (props.stickyHeader ? margin.value : 0)
98
+ }
99
+ }
100
+ onScopeDispose(() => {
101
+ disposed = true
102
+ observer?.disconnect()
103
+ })
104
+ return { entries, visible, before, after, measure }
105
+ }
106
+
107
+ export type DataTableRenderEntry<T extends object> = ReturnType<
108
+ typeof useTableVirtual<T>
109
+ >['visible']['value'][number]
@@ -0,0 +1,32 @@
1
+ import {
2
+ columnFilteringFeature,
3
+ columnGroupingFeature,
4
+ rowExpandingFeature,
5
+ createGroupedRowModel,
6
+ createExpandedRowModel,
7
+ columnVisibilityFeature,
8
+ createFilteredRowModel,
9
+ createPaginatedRowModel,
10
+ createSortedRowModel,
11
+ globalFilteringFeature,
12
+ rowPaginationFeature,
13
+ rowSelectionFeature,
14
+ rowSortingFeature,
15
+ tableFeatures,
16
+ } from '@tanstack/vue-table'
17
+
18
+ export const features = tableFeatures({
19
+ columnFilteringFeature,
20
+ columnVisibilityFeature,
21
+ columnGroupingFeature,
22
+ rowExpandingFeature,
23
+ globalFilteringFeature,
24
+ rowPaginationFeature,
25
+ rowSelectionFeature,
26
+ rowSortingFeature,
27
+ filteredRowModel: createFilteredRowModel(),
28
+ groupedRowModel: createGroupedRowModel(),
29
+ sortedRowModel: createSortedRowModel(),
30
+ expandedRowModel: createExpandedRowModel(),
31
+ paginatedRowModel: createPaginatedRowModel(),
32
+ })