niuma-ui 1.0.0 → 1.0.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 (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -0,0 +1,606 @@
1
+ /**
2
+ * RsTable 编辑动作层:单元格提交、键盘导航、粘贴、行提交/回滚。
3
+ *
4
+ * 职责边界:
5
+ * - 做:编排 useTableEdit 状态机与业务 emit
6
+ * - 不做:草稿 Map 本身(useTableEdit);虚拟滚动卸载行后草稿仍由 staged/editing 保留
7
+ *
8
+ * 性能约定:可编辑单元格列表仅在导航时构建,不在每次渲染扫描全表。
9
+ */
10
+
11
+ import type { useTableEdit } from './useTableEdit'
12
+ import {
13
+ isBooleanToggleColumn,
14
+ isCellValueChanged,
15
+ isColumnEditable,
16
+ isColumnNullable,
17
+ isRowEditPending,
18
+ listBatchColumnTargets,
19
+ navigateEditableCell,
20
+ nullToEditText,
21
+ parseCellEditInput,
22
+ parseClipboardGrid,
23
+ resolveCellEditText,
24
+ resolveColumnEditTrigger,
25
+ resolveColumnEditorOptions,
26
+ resolveColumnRawValue,
27
+ validateCellValueAsync,
28
+ type RsTableCellNavigateDirection,
29
+ } from '../components/table/table-edit-utils'
30
+ import type { RsTableColumn, RsTableRowData, RsTableRowEntry } from '../components/table-utils'
31
+
32
+ type TableEditApi = ReturnType<typeof useTableEdit>
33
+
34
+ export interface UseRsTableEditActionsOptions<T extends RsTableRowData> {
35
+ tableEdit: TableEditApi
36
+ editable: () => boolean
37
+ editTrigger: () => 'click' | 'dblclick'
38
+ editKeyboard: () => boolean
39
+ editUndo: () => boolean
40
+ editPaste: () => boolean
41
+ editBatch: () => boolean
42
+ allowNull: () => boolean
43
+ rowCommit: () => boolean
44
+ selectable: () => boolean
45
+ selectionType: () => string
46
+ selectedRowKeys: () => string[]
47
+ displayColumns: () => RsTableColumn<T>[]
48
+ dataRows: () => Array<Extract<RsTableRowEntry<T>, { type: 'row' }>>
49
+ rowKeyFor: (entry: Extract<RsTableRowEntry<T>, { type: 'row' }>) => string
50
+ rowPending?: (row: T, index: number) => boolean
51
+ applyRowClickHighlight: (rowKey: string) => void
52
+ onRowClick: (entry: RsTableRowEntry<T>, event?: MouseEvent) => void
53
+ onRowDblclick: (entry: RsTableRowEntry<T>, event?: MouseEvent) => void
54
+ emit: {
55
+ cellView: (row: T, column: RsTableColumn<T>, index: number) => void
56
+ cellEditStart: (row: T, column: RsTableColumn<T>, index: number) => void
57
+ cellEditDialog: (row: T, column: RsTableColumn<T>, index: number, draft: string) => void
58
+ cellEditCommit: (
59
+ row: T,
60
+ column: RsTableColumn<T>,
61
+ index: number,
62
+ value: unknown,
63
+ previous: unknown,
64
+ ) => void
65
+ cellEditCancel: (row: T, column: RsTableColumn<T>, index: number) => void
66
+ cellEditInvalid: (
67
+ row: T,
68
+ column: RsTableColumn<T>,
69
+ index: number,
70
+ message: string,
71
+ value: unknown,
72
+ ) => void
73
+ cellEditBatchCommit: (
74
+ column: RsTableColumn<T>,
75
+ changes: Array<{ row: T; index: number; value: unknown; previous: unknown }>,
76
+ ) => void
77
+ cellEditUndo: (entry: {
78
+ items: Array<{
79
+ rowKey: string
80
+ colKey: string
81
+ rowIndex: number
82
+ previous: unknown
83
+ next: unknown
84
+ }>
85
+ }) => void
86
+ cellEditRedo: (entry: {
87
+ items: Array<{
88
+ rowKey: string
89
+ colKey: string
90
+ rowIndex: number
91
+ previous: unknown
92
+ next: unknown
93
+ }>
94
+ }) => void
95
+ cellEditReject: (row: T, index: number, reason?: string) => void
96
+ rowEditCommit: (
97
+ row: T,
98
+ index: number,
99
+ changes: Array<{ colKey: string; value: unknown; previous: unknown }>,
100
+ ) => void
101
+ rowEditRollback: (row: T, index: number) => void
102
+ }
103
+ }
104
+
105
+ /**
106
+ * 编辑交互 API。
107
+ */
108
+ export function useRsTableEditActions<T extends RsTableRowData>(
109
+ options: UseRsTableEditActionsOptions<T>,
110
+ ) {
111
+ const { tableEdit, emit } = options
112
+
113
+ function resolveColumnByKey(colKey: string): RsTableColumn<T> | undefined {
114
+ return options.displayColumns().find((column) => column.key === colKey)
115
+ }
116
+
117
+ function findRowEntryByKey(
118
+ rowKey: string,
119
+ ): Extract<RsTableRowEntry<T>, { type: 'row' }> | undefined {
120
+ return options.dataRows().find((item) => options.rowKeyFor(item) === rowKey)
121
+ }
122
+
123
+ /** 仅导航时调用;勿在模板渲染路径扫描 */
124
+ function buildEditableCellList() {
125
+ const result: Array<{ rowKey: string; colKey: string; rowIndex: number }> = []
126
+ for (const entry of options.dataRows()) {
127
+ const rowKey = options.rowKeyFor(entry)
128
+ for (const column of options.displayColumns()) {
129
+ if (!isColumnEditable(column, entry.row, entry.rowIndex, options.editable())) continue
130
+ result.push({ rowKey, colKey: column.key, rowIndex: entry.rowIndex })
131
+ }
132
+ }
133
+ return result
134
+ }
135
+
136
+ function onCellStartEdit(
137
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
138
+ colKey: string,
139
+ ): void {
140
+ const column = resolveColumnByKey(colKey)
141
+ if (!column) return
142
+ if (isBooleanToggleColumn(column)) return
143
+ if (!isColumnEditable(column, entry.row, entry.rowIndex, options.editable())) return
144
+ const key = options.rowKeyFor(entry)
145
+ const initialText = resolveCellEditText(
146
+ entry.row,
147
+ column,
148
+ entry.rowIndex,
149
+ tableEdit.getDraft(key, colKey),
150
+ )
151
+ const editorOpts = resolveColumnEditorOptions(column, entry.row, entry.rowIndex)
152
+ if (editorOpts.presentation === 'dialog') {
153
+ emit.cellEditDialog(entry.row, column, entry.rowIndex, initialText)
154
+ return
155
+ }
156
+ tableEdit.startEdit({ rowKey: key, colKey, rowIndex: entry.rowIndex }, initialText)
157
+ emit.cellEditStart(entry.row, column, entry.rowIndex)
158
+ }
159
+
160
+ function onCellUpdateDraft(
161
+ _entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
162
+ _colKey: string,
163
+ value: string,
164
+ ): void {
165
+ tableEdit.updateDraft(value)
166
+ }
167
+
168
+ function onCellCancelEdit(
169
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
170
+ colKey: string,
171
+ ): void {
172
+ const column = resolveColumnByKey(colKey)
173
+ tableEdit.cancelEdit()
174
+ if (column) emit.cellEditCancel(entry.row, column, entry.rowIndex)
175
+ }
176
+
177
+ function moveEditFocus(
178
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
179
+ colKey: string,
180
+ direction: RsTableCellNavigateDirection,
181
+ startEditNext: boolean,
182
+ ): void {
183
+ if (!options.editKeyboard()) return
184
+ const cells = buildEditableCellList()
185
+ const next = navigateEditableCell(
186
+ cells,
187
+ options.rowKeyFor(entry),
188
+ colKey,
189
+ direction,
190
+ options.displayColumns().length || 1,
191
+ )
192
+ if (!next) return
193
+ const nextEntry = findRowEntryByKey(next.rowKey)
194
+ if (!nextEntry) return
195
+ tableEdit.setFocusCell(next)
196
+ options.applyRowClickHighlight(next.rowKey)
197
+ if (startEditNext) {
198
+ const column = resolveColumnByKey(next.colKey)
199
+ if (column && isBooleanToggleColumn(column)) return
200
+ onCellStartEdit(nextEntry, next.colKey)
201
+ }
202
+ }
203
+
204
+ async function onCellCommitEdit(
205
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
206
+ colKey: string,
207
+ rawText: string,
208
+ commitOptions?: {
209
+ navigate?: RsTableCellNavigateDirection
210
+ skipUndo?: boolean
211
+ skipBatch?: boolean
212
+ },
213
+ ): Promise<boolean> {
214
+ const column = resolveColumnByKey(colKey)
215
+ if (!column) return false
216
+ const allowNull = isColumnNullable(column, options.allowNull())
217
+ const parsed = parseCellEditInput(rawText, entry.row, column, entry.rowIndex, { allowNull })
218
+ const rowKey = options.rowKeyFor(entry)
219
+
220
+ tableEdit.setValidating(rowKey, colKey, true)
221
+ let error: string | null = null
222
+ try {
223
+ error = await validateCellValueAsync(parsed, entry.row, column, entry.rowIndex)
224
+ } finally {
225
+ tableEdit.setValidating(rowKey, colKey, false)
226
+ }
227
+
228
+ if (error) {
229
+ tableEdit.updateDraft(rawText)
230
+ tableEdit.setCellError(rowKey, colKey, error)
231
+ emit.cellEditInvalid(entry.row, column, entry.rowIndex, error, parsed)
232
+ return false
233
+ }
234
+ tableEdit.clearCellError(rowKey, colKey)
235
+
236
+ const batchEnabled =
237
+ options.editBatch() &&
238
+ options.selectable() &&
239
+ options.selectionType() !== 'radio' &&
240
+ !options.rowCommit() &&
241
+ !commitOptions?.skipBatch
242
+
243
+ const targets = batchEnabled
244
+ ? listBatchColumnTargets({
245
+ rows: options.dataRows().map((item) => ({
246
+ row: item.row,
247
+ rowIndex: item.rowIndex,
248
+ rowKey: options.rowKeyFor(item),
249
+ })),
250
+ column,
251
+ tableEditable: options.editable(),
252
+ selectedKeys: options.selectedRowKeys(),
253
+ anchorRowKey: rowKey,
254
+ })
255
+ : [{ row: entry.row, rowIndex: entry.rowIndex, rowKey }]
256
+
257
+ if (options.rowCommit()) {
258
+ const previous = resolveColumnRawValue(entry.row, column, entry.rowIndex)
259
+ if (!isCellValueChanged(previous, parsed)) {
260
+ tableEdit.unstageCell(rowKey, colKey)
261
+ tableEdit.cancelEdit()
262
+ if (commitOptions?.navigate) moveEditFocus(entry, colKey, commitOptions.navigate, false)
263
+ return true
264
+ }
265
+ tableEdit.stageCell({
266
+ rowKey,
267
+ colKey,
268
+ rowIndex: entry.rowIndex,
269
+ draft: rawText,
270
+ original: previous,
271
+ })
272
+ if (commitOptions?.navigate) moveEditFocus(entry, colKey, commitOptions.navigate, true)
273
+ return true
274
+ }
275
+
276
+ tableEdit.cancelEdit()
277
+
278
+ const undoItems: Array<{
279
+ rowKey: string
280
+ colKey: string
281
+ rowIndex: number
282
+ previous: unknown
283
+ next: unknown
284
+ }> = []
285
+ const batchChanges: Array<{ row: T; index: number; value: unknown; previous: unknown }> = []
286
+
287
+ for (const target of targets) {
288
+ const previous = resolveColumnRawValue(target.row, column, target.rowIndex)
289
+ if (!isCellValueChanged(previous, parsed)) continue
290
+ undoItems.push({
291
+ rowKey: target.rowKey,
292
+ colKey,
293
+ rowIndex: target.rowIndex,
294
+ previous,
295
+ next: parsed,
296
+ })
297
+ batchChanges.push({
298
+ row: target.row,
299
+ index: target.rowIndex,
300
+ value: parsed,
301
+ previous,
302
+ })
303
+ emit.cellEditCommit(target.row, column, target.rowIndex, parsed, previous)
304
+ }
305
+
306
+ if (batchChanges.length > 1) {
307
+ emit.cellEditBatchCommit(column, batchChanges)
308
+ }
309
+
310
+ if (!commitOptions?.skipUndo && options.editUndo() && undoItems.length) {
311
+ tableEdit.pushUndo({ items: undoItems })
312
+ }
313
+
314
+ if (commitOptions?.navigate) moveEditFocus(entry, colKey, commitOptions.navigate, true)
315
+ return true
316
+ }
317
+
318
+ async function onCellNavigate(
319
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
320
+ colKey: string,
321
+ direction: RsTableCellNavigateDirection,
322
+ ): Promise<void> {
323
+ const draft = tableEdit.editingDraft.value
324
+ await onCellCommitEdit(entry, colKey, draft, { navigate: direction })
325
+ }
326
+
327
+ function onCellClick(entry: RsTableRowEntry<T>, colKey: string, event?: MouseEvent): void {
328
+ if (entry.type !== 'row') return
329
+ options.onRowClick(entry, event)
330
+ const column = resolveColumnByKey(colKey)
331
+ if (!column) return
332
+ // 只读表也维护 focusCell,供 ARIA Grid 键盘漫游使用
333
+ tableEdit.setFocusCell({
334
+ rowKey: options.rowKeyFor(entry),
335
+ colKey,
336
+ rowIndex: entry.rowIndex,
337
+ })
338
+ }
339
+
340
+ function onCellDblclick(entry: RsTableRowEntry<T>, colKey: string, event?: MouseEvent): void {
341
+ if (entry.type !== 'row') return
342
+ const column = resolveColumnByKey(colKey)
343
+ if (
344
+ column &&
345
+ options.editable() &&
346
+ resolveColumnEditTrigger(column, options.editTrigger()) === 'dblclick' &&
347
+ isColumnEditable(column, entry.row, entry.rowIndex, options.editable()) &&
348
+ !isBooleanToggleColumn(column)
349
+ ) {
350
+ onCellStartEdit(entry, colKey)
351
+ return
352
+ }
353
+ if (column) {
354
+ emit.cellView(entry.row, column, entry.rowIndex)
355
+ }
356
+ options.onRowDblclick(entry, event)
357
+ }
358
+
359
+ function applyUndoRedoValue(
360
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
361
+ colKey: string,
362
+ value: unknown,
363
+ ): void {
364
+ const column = resolveColumnByKey(colKey)
365
+ if (!column) return
366
+ const previous = resolveColumnRawValue(entry.row, column, entry.rowIndex)
367
+ emit.cellEditCommit(entry.row, column, entry.rowIndex, value, previous)
368
+ }
369
+
370
+ function onEditUndo(): void {
371
+ if (!options.editUndo()) return
372
+ const entry = tableEdit.undo()
373
+ if (!entry) return
374
+ for (const item of entry.items) {
375
+ const rowEntry = findRowEntryByKey(item.rowKey)
376
+ if (!rowEntry) continue
377
+ applyUndoRedoValue(rowEntry, item.colKey, item.previous)
378
+ }
379
+ emit.cellEditUndo(entry)
380
+ }
381
+
382
+ function onEditRedo(): void {
383
+ if (!options.editUndo()) return
384
+ const entry = tableEdit.redo()
385
+ if (!entry) return
386
+ for (const item of entry.items) {
387
+ const rowEntry = findRowEntryByKey(item.rowKey)
388
+ if (!rowEntry) continue
389
+ applyUndoRedoValue(rowEntry, item.colKey, item.next)
390
+ }
391
+ emit.cellEditRedo(entry)
392
+ }
393
+
394
+ function coerceBooleanFromRow(
395
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
396
+ column: RsTableColumn<T>,
397
+ ): boolean {
398
+ const raw = resolveColumnRawValue(entry.row, column, entry.rowIndex)
399
+ if (raw == null) return false
400
+ if (typeof raw === 'boolean') return raw
401
+ return String(raw).toLowerCase() === 'true' || raw === 1 || raw === '1'
402
+ }
403
+
404
+ function onTableKeydown(event: KeyboardEvent): void {
405
+ if (!options.editable() || !options.editKeyboard()) return
406
+ const target = event.target as HTMLElement | null
407
+ if (target?.closest('input, textarea, select, [contenteditable="true"]')) {
408
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z' && !event.shiftKey) {
409
+ if (!tableEdit.editingCell.value) {
410
+ event.preventDefault()
411
+ onEditUndo()
412
+ }
413
+ }
414
+ if (
415
+ (event.ctrlKey || event.metaKey) &&
416
+ (event.key.toLowerCase() === 'y' || (event.key.toLowerCase() === 'z' && event.shiftKey))
417
+ ) {
418
+ if (!tableEdit.editingCell.value) {
419
+ event.preventDefault()
420
+ onEditRedo()
421
+ }
422
+ }
423
+ return
424
+ }
425
+
426
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z' && !event.shiftKey) {
427
+ event.preventDefault()
428
+ onEditUndo()
429
+ return
430
+ }
431
+ if (
432
+ (event.ctrlKey || event.metaKey) &&
433
+ (event.key.toLowerCase() === 'y' || (event.key.toLowerCase() === 'z' && event.shiftKey))
434
+ ) {
435
+ event.preventDefault()
436
+ onEditRedo()
437
+ return
438
+ }
439
+
440
+ const focus = tableEdit.focusCell.value
441
+ if (!focus) return
442
+ const entry = findRowEntryByKey(focus.rowKey)
443
+ if (!entry) return
444
+ const column = resolveColumnByKey(focus.colKey)
445
+ if (!column) return
446
+
447
+ if (event.key === 'F2' || event.key === 'Enter') {
448
+ event.preventDefault()
449
+ if (isBooleanToggleColumn(column)) {
450
+ const next = !coerceBooleanFromRow(entry, column)
451
+ void onCellCommitEdit(entry, focus.colKey, next ? 'true' : 'false')
452
+ return
453
+ }
454
+ onCellStartEdit(entry, focus.colKey)
455
+ return
456
+ }
457
+
458
+ if (event.key === 'Escape') {
459
+ event.preventDefault()
460
+ tableEdit.cancelEdit()
461
+ return
462
+ }
463
+
464
+ if (event.key === 'Delete' || event.key === 'Backspace') {
465
+ if (!isColumnEditable(column, entry.row, entry.rowIndex, options.editable())) return
466
+ event.preventDefault()
467
+ const allowNull = isColumnNullable(column, options.allowNull())
468
+ void onCellCommitEdit(entry, focus.colKey, allowNull ? nullToEditText() : '')
469
+ return
470
+ }
471
+
472
+ // 箭头 / Home / End / Page* 由 useRsTableGridKeyboard 统一处理(全列漫游)
473
+ if (event.key === 'Tab') {
474
+ event.preventDefault()
475
+ moveEditFocus(entry, focus.colKey, event.shiftKey ? 'prev' : 'next', false)
476
+ }
477
+ }
478
+
479
+ function onTablePaste(event: ClipboardEvent): void {
480
+ if (!options.editable() || !options.editPaste()) return
481
+ if (tableEdit.editingCell.value) return
482
+ const focus = tableEdit.focusCell.value
483
+ if (!focus) return
484
+ const text = event.clipboardData?.getData('text/plain')
485
+ if (!text) return
486
+ const grid = parseClipboardGrid(text)
487
+ if (!grid.length) return
488
+ event.preventDefault()
489
+
490
+ const startEntry = findRowEntryByKey(focus.rowKey)
491
+ if (!startEntry) return
492
+ const rows = options.dataRows()
493
+ const startRowPos = rows.findIndex((item) => options.rowKeyFor(item) === focus.rowKey)
494
+ const cols = options.displayColumns()
495
+ const startColIndex = cols.findIndex((column) => column.key === focus.colKey)
496
+ if (startRowPos < 0 || startColIndex < 0) return
497
+
498
+ for (let r = 0; r < grid.length; r += 1) {
499
+ const rowEntry = rows[startRowPos + r]
500
+ if (!rowEntry) break
501
+ const line = grid[r] ?? []
502
+ for (let c = 0; c < line.length; c += 1) {
503
+ const column = cols[startColIndex + c]
504
+ if (!column) break
505
+ if (!isColumnEditable(column, rowEntry.row, rowEntry.rowIndex, options.editable())) continue
506
+ void onCellCommitEdit(rowEntry, column.key, line[c] ?? '')
507
+ }
508
+ }
509
+ }
510
+
511
+ function rejectRowEdit(rowKey: string, reason?: string): void {
512
+ const entry = findRowEntryByKey(rowKey)
513
+ tableEdit.rollbackRow(rowKey)
514
+ if (entry) emit.cellEditReject(entry.row, entry.rowIndex, reason)
515
+ }
516
+
517
+ async function onRowCommitEdit(
518
+ entry: Extract<RsTableRowEntry<T>, { type: 'row' }>,
519
+ ): Promise<void> {
520
+ const rowKey = options.rowKeyFor(entry)
521
+ const editing = tableEdit.editingCell.value
522
+ if (editing?.rowKey === rowKey) {
523
+ const ok = await onCellCommitEdit(entry, editing.colKey, tableEdit.editingDraft.value)
524
+ if (!ok) return
525
+ }
526
+ const staged = tableEdit.commitRow(rowKey)
527
+ if (!staged.length) return
528
+ const changes = staged.map((item) => {
529
+ const column = resolveColumnByKey(item.colKey)
530
+ const allowNull = column ? isColumnNullable(column, options.allowNull()) : options.allowNull()
531
+ const value = column
532
+ ? parseCellEditInput(item.draft, entry.row, column, entry.rowIndex, { allowNull })
533
+ : item.draft
534
+ return {
535
+ colKey: item.colKey,
536
+ value,
537
+ previous: item.original,
538
+ }
539
+ })
540
+ emit.rowEditCommit(entry.row, entry.rowIndex, changes)
541
+ }
542
+
543
+ function isExternalRowPending(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
544
+ return Boolean(options.rowPending?.(entry.row, entry.rowIndex))
545
+ }
546
+
547
+ function onRowRollbackEdit(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
548
+ if (!options.rowCommit() && isExternalRowPending(entry)) {
549
+ emit.rowEditRollback(entry.row, entry.rowIndex)
550
+ return
551
+ }
552
+ tableEdit.rollbackRow(options.rowKeyFor(entry))
553
+ emit.rowEditRollback(entry.row, entry.rowIndex)
554
+ }
555
+
556
+ function rowEditPending(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): boolean {
557
+ if (isExternalRowPending(entry)) return true
558
+ const rowKey = options.rowKeyFor(entry)
559
+ return isRowEditPending(rowKey, entry.row, entry.rowIndex, {
560
+ isRowDirty: tableEdit.isRowDirty,
561
+ editingCell: tableEdit.editingCell.value,
562
+ editingDraft: tableEdit.editingDraft.value,
563
+ resolveColumn: resolveColumnByKey,
564
+ })
565
+ }
566
+
567
+ function onGutterCommit(entry: Extract<RsTableRowEntry<T>, { type: 'row' }>): void {
568
+ if (options.rowCommit()) {
569
+ void onRowCommitEdit(entry)
570
+ return
571
+ }
572
+ if (isExternalRowPending(entry)) {
573
+ emit.rowEditCommit(entry.row, entry.rowIndex, [])
574
+ return
575
+ }
576
+ const editing = tableEdit.editingCell.value
577
+ if (editing?.rowKey !== options.rowKeyFor(entry)) return
578
+ void onCellCommitEdit(entry, editing.colKey, tableEdit.editingDraft.value)
579
+ }
580
+
581
+ return {
582
+ resolveColumnByKey,
583
+ findRowEntryByKey,
584
+ onCellStartEdit,
585
+ onCellUpdateDraft,
586
+ onCellCancelEdit,
587
+ onCellCommitEdit,
588
+ onCellNavigate,
589
+ onCellClick,
590
+ onCellDblclick,
591
+ onEditUndo,
592
+ onEditRedo,
593
+ onTableKeydown,
594
+ onTablePaste,
595
+ rejectRowEdit,
596
+ onRowCommitEdit,
597
+ onRowRollbackEdit,
598
+ isExternalRowPending,
599
+ rowEditPending,
600
+ onGutterCommit,
601
+ }
602
+ }
603
+
604
+ export type RsTableEditActionsApi<T extends RsTableRowData> = ReturnType<
605
+ typeof useRsTableEditActions<T>
606
+ >