@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,345 @@
1
+ import { computed, nextTick, onScopeDispose, shallowRef, type Ref, type CSSProperties } from 'vue'
2
+ import { rowId } from '../utils'
3
+ import type { DataTableKey, DataTableProps, DataTableReorder } from '../types'
4
+ import type { DataTableController, DataTableModels } from './useDataTable'
5
+
6
+ export function useTableDrag<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
+ onReorder: (change: DataTableReorder<T>) => void,
13
+ ) {
14
+ const dragging = shallowRef<{
15
+ kind: 'row' | 'column'
16
+ key: string
17
+ target?: string
18
+ before?: boolean
19
+ width: number
20
+ offsetX: number
21
+ offsetY: number
22
+ samples: string[]
23
+ appearance: CSSProperties
24
+ marker?: { x: number; y: number; width: number; height: number }
25
+ label: string
26
+ x: number
27
+ y: number
28
+ }>()
29
+ const ordered = computed(
30
+ () =>
31
+ !models.sorting.value.length &&
32
+ !models.grouping.value.length &&
33
+ !models.filter.value &&
34
+ !models.columnFilters.value.length,
35
+ )
36
+ const canMove = (row: T) =>
37
+ !ctl.blocked.value &&
38
+ ordered.value &&
39
+ (typeof props.reorderable === 'function' ? props.reorderable(row) : !!props.reorderable)
40
+ let cleanup: (() => void) | undefined
41
+ let frame = 0
42
+ let releaseClick: (() => void) | undefined
43
+ let disposed = false
44
+ function moveRow(key: DataTableKey, targetKey: DataTableKey, before?: boolean) {
45
+ const all = ctl.table.getCoreRowModel().rowsById
46
+ const row = all[rowId(key)],
47
+ target = all[rowId(targetKey)]
48
+ if (
49
+ !row ||
50
+ !target ||
51
+ key === targetKey ||
52
+ row.parentId !== target.parentId ||
53
+ !canMove(row.original) ||
54
+ !canMove(target.original)
55
+ )
56
+ return
57
+ const parent = row.parentId ? all[row.parentId]?.original : undefined
58
+ const source = parent ? props.getChildren?.(parent) : props.rows
59
+ if (!source) return
60
+ const from = source.findIndex(item => ctl.keyOf(item) === key),
61
+ to = source.findIndex(item => ctl.keyOf(item) === targetKey)
62
+ const position = before === undefined ? to : to + (before ? 0 : 1) - (from < to ? 1 : 0)
63
+ if (position === from) return
64
+ const rows = [...source]
65
+ rows.splice(from, 1)
66
+ rows.splice(position, 0, row.original)
67
+ onReorder({ row: row.original, target: target.original, parent, from, to: position, rows })
68
+ }
69
+ function start(kind: 'row' | 'column', key: string, label: string, event: PointerEvent) {
70
+ if (event.button !== 0 || ctl.blocked.value || (kind === 'row' && !ordered.value)) return
71
+ const handle = event.currentTarget as HTMLElement
72
+ const interactive = (event.target as HTMLElement).closest(
73
+ 'button, a, input, select, textarea, [contenteditable], [role="separator"]',
74
+ )
75
+ if (kind === 'column' && interactive && !interactive.hasAttribute('data-hn-table-heading'))
76
+ return
77
+ const column = ctl.visibleColumns.value.find(column => column.key === key)
78
+ if (kind === 'column' && (!props.reorderColumns || column?.reorderable === false)) return
79
+ cleanup?.()
80
+ const document = handle.ownerDocument
81
+ const originX = event.clientX,
82
+ originY = event.clientY
83
+ const rect = handle.getBoundingClientRect()
84
+ const style = getComputedStyle(handle)
85
+ const appearance: CSSProperties = {
86
+ fontSize: style.fontSize,
87
+ lineHeight: style.lineHeight,
88
+ direction: style.direction as 'ltr' | 'rtl',
89
+ '--hn-row-h': `${rect.height}px`,
90
+ ...Object.fromEntries(
91
+ ['--hn-surface', '--hn-table-head-bg', '--hn-border', '--hn-fg-default'].map(token => [
92
+ token,
93
+ style.getPropertyValue(token),
94
+ ]),
95
+ ),
96
+ }
97
+ const rtl = element.value && getComputedStyle(element.value).direction === 'rtl'
98
+ let x = originX,
99
+ y = originY
100
+ let moved = false
101
+ let pointerReleased = false
102
+ const samples =
103
+ kind === 'column'
104
+ ? [...(element.value?.querySelectorAll<HTMLElement>('tbody [data-hn-cell]') ?? [])]
105
+ .filter(cell => cell.dataset.hnCell === key)
106
+ .slice(0, 3)
107
+ .map(cell => cell.textContent?.trim() ?? '')
108
+ : []
109
+ const targetAtPoint = () => {
110
+ const target = document
111
+ .elementFromPoint(x, y)
112
+ ?.closest<HTMLElement>(kind === 'row' ? '[data-hn-row]' : '[data-hn-column]')
113
+ if (!target || !element.value?.contains(target)) return
114
+ const targetId = (kind === 'row' ? target.dataset.hnRow : target.dataset.hnColumn)!
115
+ if (targetId === key) return
116
+ if (kind === 'column') {
117
+ const destination = ctl.visibleColumns.value.find(column => column.key === targetId)
118
+ if (!destination || destination.pin !== column?.pin || destination.reorderable === false)
119
+ return
120
+ } else {
121
+ const all = ctl.table.getCoreRowModel().rowsById
122
+ const row = all[key],
123
+ destination = all[targetId]
124
+ if (
125
+ !row ||
126
+ !destination ||
127
+ row.parentId !== destination.parentId ||
128
+ !canMove(destination.original)
129
+ )
130
+ return
131
+ }
132
+ const box = target.getBoundingClientRect()
133
+ const area = viewport.value?.getBoundingClientRect() ?? element.value.getBoundingClientRect()
134
+ const table = element.value.getBoundingClientRect()
135
+ const before =
136
+ kind === 'row'
137
+ ? y < box.top + box.height / 2
138
+ : rtl
139
+ ? x > box.left + box.width / 2
140
+ : x < box.left + box.width / 2
141
+ const ids =
142
+ kind === 'column'
143
+ ? ctl.visibleColumns.value.map(column => column.key)
144
+ : [...element.value.querySelectorAll<HTMLElement>('[data-hn-row]')].map(
145
+ row => row.dataset.hnRow!,
146
+ )
147
+ const from = ids.indexOf(key),
148
+ to = ids.indexOf(targetId)
149
+ if (from >= 0 && to + (before ? 0 : 1) - (from < to ? 1 : 0) === from) return
150
+ const left = Math.max(area.left, table.left),
151
+ top = Math.max(area.top, table.top)
152
+ return {
153
+ target: targetId,
154
+ before,
155
+ marker:
156
+ kind === 'column'
157
+ ? {
158
+ x: before !== !!rtl ? box.left : box.right,
159
+ y: top,
160
+ width: 2,
161
+ height: Math.max(0, Math.min(area.bottom, table.bottom) - top),
162
+ }
163
+ : {
164
+ x: left,
165
+ y: before ? box.top : box.bottom,
166
+ width: Math.max(0, Math.min(area.right, table.right) - left),
167
+ height: 2,
168
+ },
169
+ }
170
+ }
171
+ const update = () => {
172
+ if (!moved) return
173
+ const area = viewport.value?.getBoundingClientRect()
174
+ if (area && viewport.value) {
175
+ if (kind === 'row')
176
+ viewport.value.scrollTop += y < area.top + 28 ? -12 : y > area.bottom - 28 ? 12 : 0
177
+ else viewport.value.scrollLeft += x < area.left + 28 ? -12 : x > area.right - 28 ? 12 : 0
178
+ }
179
+ const destination = targetAtPoint()
180
+ dragging.value = {
181
+ kind,
182
+ key,
183
+ label,
184
+ x,
185
+ y,
186
+ width: rect.width,
187
+ offsetX: originX - rect.left,
188
+ offsetY: originY - rect.top,
189
+ samples,
190
+ appearance,
191
+ ...destination,
192
+ }
193
+ frame = requestAnimationFrame(update)
194
+ }
195
+ const move = (next: PointerEvent) => {
196
+ if (next.pointerId !== event.pointerId) return
197
+ x = next.clientX
198
+ y = next.clientY
199
+ if (!moved && Math.hypot(x - originX, y - originY) >= 6) {
200
+ moved = true
201
+ handle.setPointerCapture?.(event.pointerId)
202
+ document.documentElement.dataset.hnTableGesture = 'drag'
203
+ update()
204
+ }
205
+ if (moved) next.preventDefault()
206
+ }
207
+ const stop = (apply: boolean) => {
208
+ const destination = moved && targetAtPoint()
209
+ cleanup?.()
210
+ if (!apply || !destination) return
211
+ if (kind === 'column') {
212
+ const order = [
213
+ ...new Set([...models.columnOrder.value, ...ctl.leaves.value.map(column => column.key)]),
214
+ ]
215
+ order.splice(order.indexOf(key), 1)
216
+ order.splice(order.indexOf(destination.target) + (destination.before ? 0 : 1), 0, key)
217
+ models.columnOrder.value = order
218
+ } else {
219
+ const all = ctl.table.getCoreRowModel().rowsById
220
+ const row = all[key],
221
+ target = all[destination.target]
222
+ if (row && target)
223
+ moveRow(ctl.keyOf(row.original), ctl.keyOf(target.original), destination.before)
224
+ }
225
+ }
226
+ const up = (next: PointerEvent) => {
227
+ if (next.pointerId !== event.pointerId) return
228
+ x = next.clientX
229
+ y = next.clientY
230
+ pointerReleased = true
231
+ stop(true)
232
+ }
233
+ const cancel = () => stop(false)
234
+ const keydown = (next: KeyboardEvent) => {
235
+ if (next.key === 'Escape') {
236
+ next.preventDefault()
237
+ next.stopPropagation()
238
+ cancel()
239
+ }
240
+ }
241
+ cleanup = () => {
242
+ cancelAnimationFrame(frame)
243
+ window.removeEventListener('pointermove', move)
244
+ window.removeEventListener('pointerup', up)
245
+ window.removeEventListener('pointercancel', cancel)
246
+ window.removeEventListener('keydown', keydown)
247
+ window.removeEventListener('blur', cancel)
248
+ if (handle.hasPointerCapture?.(event.pointerId)) handle.releasePointerCapture(event.pointerId)
249
+ if (moved && !disposed) {
250
+ releaseClick?.()
251
+ let timer: ReturnType<typeof setTimeout> | undefined
252
+ const clear = () => {
253
+ clearTimeout(timer)
254
+ document.removeEventListener('click', preventClick, true)
255
+ document.removeEventListener('pointerdown', clear, true)
256
+ document.removeEventListener('keydown', clear, true)
257
+ document.removeEventListener('pointerup', afterUp, true)
258
+ releaseClick = undefined
259
+ }
260
+ const preventClick = (click: MouseEvent) => {
261
+ click.preventDefault()
262
+ click.stopImmediatePropagation()
263
+ clear()
264
+ }
265
+ const afterUp = () => {
266
+ timer = setTimeout(clear, 0)
267
+ }
268
+ document.addEventListener('click', preventClick, true)
269
+ document.addEventListener('pointerdown', clear, true)
270
+ document.addEventListener('keydown', clear, true)
271
+ document.addEventListener('pointerup', afterUp, true)
272
+ if (pointerReleased) afterUp()
273
+ releaseClick = clear
274
+ }
275
+ delete document.documentElement.dataset.hnTableGesture
276
+ dragging.value = undefined
277
+ cleanup = undefined
278
+ }
279
+ window.addEventListener('pointermove', move)
280
+ window.addEventListener('pointerup', up)
281
+ window.addEventListener('pointercancel', cancel)
282
+ window.addEventListener('keydown', keydown)
283
+ window.addEventListener('blur', cancel)
284
+ }
285
+ function rowKeydown(key: DataTableKey, event: KeyboardEvent) {
286
+ if (!['ArrowUp', 'ArrowDown'].includes(event.key)) return
287
+ event.preventDefault()
288
+ event.stopPropagation()
289
+ const row = ctl.table.getCoreRowModel().rowsById[rowId(key)]
290
+ if (!row) return
291
+ const parent = row.parentId
292
+ ? ctl.table.getCoreRowModel().rowsById[row.parentId]?.original
293
+ : undefined
294
+ const source = parent ? (props.getChildren?.(parent) ?? []) : props.rows
295
+ const eligible = source.filter(canMove),
296
+ index = eligible.findIndex(item => ctl.keyOf(item) === key)
297
+ const target = eligible[index + (event.key === 'ArrowDown' ? 1 : -1)]
298
+ if (target) {
299
+ moveRow(key, ctl.keyOf(target))
300
+ const handle = event.currentTarget as HTMLElement
301
+ void nextTick(() => {
302
+ if (handle.isConnected) handle.focus({ preventScroll: true })
303
+ })
304
+ }
305
+ }
306
+ function columnKeydown(key: string, event: KeyboardEvent) {
307
+ if (!event.altKey || !['ArrowLeft', 'ArrowRight'].includes(event.key)) return
308
+ event.preventDefault()
309
+ event.stopPropagation()
310
+ const column = ctl.visibleColumns.value.find(column => column.key === key)
311
+ const columns = ctl.visibleColumns.value.filter(
312
+ item => item.pin === column?.pin && item.reorderable !== false,
313
+ )
314
+ const direction = element.value && getComputedStyle(element.value).direction === 'rtl' ? -1 : 1
315
+ const target =
316
+ columns[
317
+ columns.findIndex(item => item.key === key) +
318
+ (event.key === 'ArrowRight' ? 1 : -1) * direction
319
+ ]
320
+ if (target) {
321
+ ctl.api.moveColumn(key, target.key)
322
+ const handle = event.currentTarget as HTMLElement
323
+ void nextTick(() => {
324
+ if (handle.isConnected) handle.focus({ preventScroll: true })
325
+ })
326
+ }
327
+ }
328
+ Object.assign(ctl.api, { moveRow })
329
+ onScopeDispose(() => {
330
+ disposed = true
331
+ cleanup?.()
332
+ releaseClick?.()
333
+ })
334
+ return {
335
+ dragging,
336
+ kind: computed(() => dragging.value?.kind),
337
+ target: computed(() => dragging.value?.target),
338
+ key: computed(() => dragging.value?.key),
339
+ canMove,
340
+ start,
341
+ rowKeydown,
342
+ columnKeydown,
343
+ }
344
+ }
345
+ export type DataTableDrag<T extends object> = ReturnType<typeof useTableDrag<T>>
@@ -0,0 +1,184 @@
1
+ import { computed, nextTick, onScopeDispose, shallowRef, toRaw, useId, watch, type Ref } from 'vue'
2
+ import { useUiLocale } from '../../../locale'
3
+ import { rowId } from '../utils'
4
+ import type {
5
+ DataTableColumn,
6
+ DataTableEdit,
7
+ DataTableEditorContext,
8
+ DataTableKey,
9
+ DataTableProps,
10
+ DataTableRowContext,
11
+ } from '../types'
12
+ import type { DataTableController } from './useDataTable'
13
+
14
+ export function useTableEditing<T extends object>(
15
+ props: DataTableProps<T>,
16
+ ctl: DataTableController<T>,
17
+ onEdit: (edit: DataTableEdit<T>) => void,
18
+ onError: (error: unknown, edit: DataTableEdit<T>) => void,
19
+ element: Ref<HTMLTableElement | undefined>,
20
+ ) {
21
+ const t = useUiLocale()
22
+ const errorId = useId()
23
+ const session = shallowRef<{
24
+ key: DataTableKey
25
+ column?: string
26
+ values: Record<string, unknown>
27
+ }>()
28
+ const errors = shallowRef<Record<string, string>>({})
29
+ const pending = shallowRef(false)
30
+ const failure = shallowRef<string>()
31
+ const editingKey = computed(() => session.value?.key)
32
+ const rowFailure = computed(() =>
33
+ props.editMode === 'row' && failure.value
34
+ ? { key: editingKey.value, message: failure.value }
35
+ : undefined,
36
+ )
37
+ let active = true
38
+ watch(
39
+ [() => session.value?.key, () => session.value?.column],
40
+ async ([key, column], [previousKey, previousColumn]) => {
41
+ const focused = element.value?.ownerDocument.activeElement
42
+ await nextTick()
43
+ if (!active) return
44
+ const findRow = (value: DataTableKey) =>
45
+ element.value?.querySelector<HTMLElement>(`[data-hn-row="${CSS.escape(rowId(value))}"]`)
46
+ if (key !== undefined) {
47
+ const row = findRow(key)
48
+ const cell = column
49
+ ? row?.querySelector<HTMLElement>(`[data-hn-cell="${CSS.escape(column)}"]`)
50
+ : row
51
+ cell
52
+ ?.querySelector<HTMLElement>(
53
+ 'input:not(:disabled), textarea:not(:disabled), [contenteditable="true"], [tabindex="0"]',
54
+ )
55
+ ?.focus({ preventScroll: true })
56
+ } else if (previousKey !== undefined && focused && !focused.isConnected) {
57
+ findRow(previousKey)
58
+ ?.querySelector<HTMLElement>(
59
+ previousColumn
60
+ ? `[data-hn-cell="${CSS.escape(previousColumn)}"]`
61
+ : '[data-hn-edit-trigger]',
62
+ )
63
+ ?.focus({ preventScroll: true })
64
+ }
65
+ },
66
+ { flush: 'pre' },
67
+ )
68
+ const canEdit = (row: T, column: DataTableColumn<T>) =>
69
+ !ctl.blocked.value &&
70
+ (typeof column.editable === 'function' ? column.editable(row) : !!column.editable)
71
+ function startEdit(key: DataTableKey, column?: string) {
72
+ if (!props.editMode || ctl.blocked.value || pending.value) return
73
+ const row = ctl.table.getCoreRowModel().rowsById[rowId(key)]?.original
74
+ if (!row) return
75
+ const editable = ctl.leaves.value.filter(
76
+ item => canEdit(row, item) && (props.editMode === 'row' || item.key === column),
77
+ )
78
+ if (!editable.length) return
79
+ const values: Record<string, unknown> = {}
80
+ for (const item of editable) {
81
+ const value = ctl.valueOf(row, item)
82
+ try {
83
+ values[item.key] = structuredClone(toRaw(value))
84
+ } catch {
85
+ values[item.key] = value
86
+ }
87
+ }
88
+ session.value = { key, column: props.editMode === 'cell' ? column : undefined, values }
89
+ errors.value = {}
90
+ failure.value = undefined
91
+ }
92
+ function cancel() {
93
+ if (pending.value) return
94
+ session.value = undefined
95
+ errors.value = {}
96
+ failure.value = undefined
97
+ }
98
+ async function commit() {
99
+ const current = session.value
100
+ if (!current || pending.value || ctl.blocked.value) return
101
+ const row = ctl.table.getCoreRowModel().rowsById[rowId(current.key)]?.original
102
+ if (!row) {
103
+ cancel()
104
+ return
105
+ }
106
+ const edit: DataTableEdit<T> = {
107
+ key: current.key,
108
+ row,
109
+ column: current.column,
110
+ values: { ...current.values },
111
+ }
112
+ pending.value = true
113
+ errors.value = {}
114
+ failure.value = undefined
115
+ try {
116
+ for (const column of ctl.leaves.value) {
117
+ if (!(column.key in edit.values)) continue
118
+ if (!canEdit(row, column)) {
119
+ delete edit.values[column.key]
120
+ continue
121
+ }
122
+ if (column.parse) edit.values[column.key] = column.parse(edit.values[column.key], row)
123
+ const error = await column.validate?.(edit.values[column.key], row)
124
+ if (error) errors.value = { ...errors.value, [column.key]: error }
125
+ }
126
+ if (!active || session.value !== current || Object.keys(errors.value).length) return
127
+ await props.onSave?.(edit)
128
+ if (!active || session.value !== current) return
129
+ onEdit(edit)
130
+ session.value = undefined
131
+ } catch (error) {
132
+ if (active && session.value === current) {
133
+ failure.value = error instanceof Error ? error.message : t.value.table.editFailed
134
+ onError(error, edit)
135
+ }
136
+ } finally {
137
+ if (active) pending.value = false
138
+ }
139
+ }
140
+ const isEditing = (key: DataTableKey, column?: string) =>
141
+ session.value?.key === key &&
142
+ (!column || session.value.column === undefined || session.value.column === column)
143
+ function context(
144
+ row: DataTableRowContext<T>,
145
+ column: DataTableColumn<T>,
146
+ ): DataTableEditorContext<T> {
147
+ return {
148
+ ...row,
149
+ column,
150
+ value: session.value?.values[column.key],
151
+ pending: pending.value,
152
+ error: errors.value[column.key] ?? (props.editMode === 'cell' ? failure.value : undefined),
153
+ updateValue: value => {
154
+ if (session.value && !pending.value)
155
+ session.value = {
156
+ ...session.value,
157
+ values: { ...session.value.values, [column.key]: value },
158
+ }
159
+ },
160
+ commit,
161
+ cancel,
162
+ }
163
+ }
164
+ Object.assign(ctl.api, { startEdit, cancelEdit: cancel, commitEdit: commit })
165
+ onScopeDispose(() => {
166
+ active = false
167
+ })
168
+ return {
169
+ session,
170
+ pending,
171
+ failure,
172
+ errorId,
173
+ rowError: (key: DataTableKey) =>
174
+ rowFailure.value?.key === key ? rowFailure.value.message : undefined,
175
+ canEdit,
176
+ startEdit,
177
+ isEditing,
178
+ context,
179
+ commit,
180
+ cancel,
181
+ hasErrors: computed(() => Object.keys(errors.value).length > 0),
182
+ }
183
+ }
184
+ export type DataTableEditing<T extends object> = ReturnType<typeof useTableEditing<T>>
@@ -0,0 +1,44 @@
1
+ import type { DataTableExportOptions } from '../types'
2
+ import type { DataTableController } from './useDataTable'
3
+
4
+ export function useTableExport<T extends object>(ctl: DataTableController<T>) {
5
+ function toCsv(options: DataTableExportOptions = {}) {
6
+ const delimiter = options.delimiter ?? ','
7
+ const columns = ctl.visibleColumns.value.filter(
8
+ column =>
9
+ column.exportable !== false && (!options.columns || options.columns.includes(column.key)),
10
+ )
11
+ const escape = (value: unknown) => {
12
+ let text = value == null ? '' : String(value)
13
+ if (typeof value === 'string' && /^[\s]*[=+@-]/.test(text)) text = "'" + text
14
+ return '"' + text.replaceAll('"', '""') + '"'
15
+ }
16
+ const lines = [columns.map(column => escape(column.label)).join(delimiter)]
17
+ for (const row of ctl.api.getRows(options.scope ?? 'filtered')) {
18
+ lines.push(
19
+ columns
20
+ .map(column =>
21
+ escape(
22
+ column.exportValue
23
+ ? column.exportValue(row)
24
+ : options.formatted
25
+ ? ctl.displayValue(row, column)
26
+ : ctl.valueOf(row, column),
27
+ ),
28
+ )
29
+ .join(delimiter),
30
+ )
31
+ }
32
+ return (options.bom === false ? '' : '\uFEFF') + lines.join('\r\n')
33
+ }
34
+ function exportCsv(options: DataTableExportOptions = {}) {
35
+ if (typeof document === 'undefined') return
36
+ const url = URL.createObjectURL(new Blob([toCsv(options)], { type: 'text/csv;charset=utf-8' }))
37
+ const anchor = document.createElement('a')
38
+ anchor.href = url
39
+ anchor.download = options.filename ?? 'data.csv'
40
+ anchor.click()
41
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
42
+ }
43
+ Object.assign(ctl.api, { toCsv, exportCsv })
44
+ }