@quadrats/react 1.0.0 → 1.1.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 (119) hide show
  1. package/card/components/Card.js +83 -75
  2. package/card/index.cjs.js +82 -74
  3. package/carousel/components/Carousel.js +32 -28
  4. package/carousel/index.cjs.js +32 -28
  5. package/components/Tooltip/index.js +5 -2
  6. package/components/index.cjs.js +5 -2
  7. package/core/components/Quadrats.js +5 -2
  8. package/core/contexts/modal/CarouselModal/CarouselModal.js +14 -17
  9. package/embed/renderers/base/components/BaseEmbedElement.js +51 -43
  10. package/embed/renderers/base/index.cjs.js +50 -42
  11. package/image/components/Image.js +34 -26
  12. package/image/createReactImage.js +1 -1
  13. package/image/index.cjs.js +34 -26
  14. package/index.cjs.js +19 -19
  15. package/package.json +4 -4
  16. package/table/components/ColumnDragButton.d.ts +10 -0
  17. package/table/components/ColumnDragButton.js +41 -0
  18. package/table/components/RowDragButton.d.ts +10 -0
  19. package/table/components/RowDragButton.js +42 -0
  20. package/table/components/Table.d.ts +9 -0
  21. package/table/components/Table.js +236 -0
  22. package/table/components/TableBody.d.ts +5 -0
  23. package/table/components/TableBody.js +8 -0
  24. package/table/components/TableCell.d.ts +5 -0
  25. package/table/components/TableCell.js +297 -0
  26. package/table/components/TableDragLayer.d.ts +6 -0
  27. package/table/components/TableDragLayer.js +89 -0
  28. package/table/components/TableHeader.d.ts +5 -0
  29. package/table/components/TableHeader.js +13 -0
  30. package/table/components/TableMain.d.ts +5 -0
  31. package/table/components/TableMain.js +233 -0
  32. package/table/components/TableRow.d.ts +5 -0
  33. package/table/components/TableRow.js +8 -0
  34. package/table/components/TableTitle.d.ts +5 -0
  35. package/table/components/TableTitle.js +18 -0
  36. package/table/contexts/TableActionsContext.d.ts +3 -0
  37. package/table/contexts/TableActionsContext.js +5 -0
  38. package/table/contexts/TableDragContext.d.ts +26 -0
  39. package/table/contexts/TableDragContext.js +26 -0
  40. package/table/contexts/TableHeaderContext.d.ts +2 -0
  41. package/table/contexts/TableHeaderContext.js +7 -0
  42. package/table/contexts/TableMetadataContext.d.ts +3 -0
  43. package/table/contexts/TableMetadataContext.js +5 -0
  44. package/table/contexts/TableScrollContext.d.ts +2 -0
  45. package/table/contexts/TableScrollContext.js +9 -0
  46. package/table/contexts/TableStateContext.d.ts +3 -0
  47. package/table/contexts/TableStateContext.js +5 -0
  48. package/table/createReactTable.d.ts +4 -0
  49. package/table/createReactTable.js +297 -0
  50. package/table/defaultRenderTableElements.d.ts +2 -0
  51. package/table/defaultRenderTableElements.js +20 -0
  52. package/table/hooks/useColumnResize.d.ts +12 -0
  53. package/table/hooks/useColumnResize.js +168 -0
  54. package/table/hooks/useTableActions.d.ts +27 -0
  55. package/table/hooks/useTableActions.js +1092 -0
  56. package/table/hooks/useTableActionsContext.d.ts +1 -0
  57. package/table/hooks/useTableActionsContext.js +12 -0
  58. package/table/hooks/useTableCell.d.ts +16 -0
  59. package/table/hooks/useTableCell.js +166 -0
  60. package/table/hooks/useTableCellToolbarActions.d.ts +34 -0
  61. package/table/hooks/useTableCellToolbarActions.js +526 -0
  62. package/table/hooks/useTableMetadata.d.ts +1 -0
  63. package/table/hooks/useTableMetadata.js +12 -0
  64. package/table/hooks/useTableStateContext.d.ts +1 -0
  65. package/table/hooks/useTableStateContext.js +12 -0
  66. package/table/hooks/useTableStates.d.ts +18 -0
  67. package/table/hooks/useTableStates.js +14 -0
  68. package/table/index.cjs.js +4002 -0
  69. package/table/index.d.ts +16 -0
  70. package/table/index.js +27 -0
  71. package/table/jsx-serializer/components/Table.d.ts +3 -0
  72. package/table/jsx-serializer/components/Table.js +7 -0
  73. package/table/jsx-serializer/components/TableBody.d.ts +3 -0
  74. package/table/jsx-serializer/components/TableBody.js +7 -0
  75. package/table/jsx-serializer/components/TableCell.d.ts +5 -0
  76. package/table/jsx-serializer/components/TableCell.js +33 -0
  77. package/table/jsx-serializer/components/TableHeader.d.ts +3 -0
  78. package/table/jsx-serializer/components/TableHeader.js +10 -0
  79. package/table/jsx-serializer/components/TableMain.d.ts +6 -0
  80. package/table/jsx-serializer/components/TableMain.js +18 -0
  81. package/table/jsx-serializer/components/TableRow.d.ts +3 -0
  82. package/table/jsx-serializer/components/TableRow.js +7 -0
  83. package/table/jsx-serializer/components/TableTitle.d.ts +3 -0
  84. package/table/jsx-serializer/components/TableTitle.js +7 -0
  85. package/table/jsx-serializer/contexts/TableHeaderContext.d.ts +1 -0
  86. package/table/jsx-serializer/contexts/TableHeaderContext.js +5 -0
  87. package/table/jsx-serializer/contexts/TableScrollContext.d.ts +2 -0
  88. package/table/jsx-serializer/contexts/TableScrollContext.js +7 -0
  89. package/table/jsx-serializer/createJsxSerializeTable.d.ts +5 -0
  90. package/table/jsx-serializer/createJsxSerializeTable.js +113 -0
  91. package/table/jsx-serializer/defaultRenderTableElements.d.ts +2 -0
  92. package/table/jsx-serializer/defaultRenderTableElements.js +20 -0
  93. package/table/jsx-serializer/index.cjs.js +195 -0
  94. package/table/jsx-serializer/index.d.ts +3 -0
  95. package/table/jsx-serializer/index.js +2 -0
  96. package/table/jsx-serializer/package.json +7 -0
  97. package/table/jsx-serializer/typings.d.ts +12 -0
  98. package/table/package.json +10 -0
  99. package/table/table.css +1 -0
  100. package/table/table.scss +428 -0
  101. package/table/toolbar/TableToolbarIcon.d.ts +8 -0
  102. package/table/toolbar/TableToolbarIcon.js +12 -0
  103. package/table/toolbar/index.cjs.js +24 -0
  104. package/table/toolbar/index.d.ts +2 -0
  105. package/table/toolbar/index.js +2 -0
  106. package/table/toolbar/package.json +7 -0
  107. package/table/toolbar/useTableTool.d.ts +4 -0
  108. package/table/toolbar/useTableTool.js +13 -0
  109. package/table/typings.d.ts +68 -0
  110. package/table/utils/helper.d.ts +186 -0
  111. package/table/utils/helper.js +799 -0
  112. package/toolbar/components/InlineToolbar.d.ts +12 -11
  113. package/toolbar/components/InlineToolbar.js +23 -19
  114. package/toolbar/components/Toolbar.js +2 -2
  115. package/toolbar/index.cjs.js +24 -21
  116. package/toolbar/toolbar.css +1 -1
  117. package/toolbar/toolbar.scss +4 -1
  118. package/utils/index.cjs.js +7 -1
  119. package/utils/removePreviousElement.js +7 -1
@@ -0,0 +1,1092 @@
1
+ import { useCallback } from 'react';
2
+ import { Element, Editor, Transforms } from '@quadrats/core';
3
+ import { ReactEditor } from 'slate-react';
4
+ import { TABLE_ROW_TYPE, TABLE_CELL_TYPE, TABLE_DEFAULT_MAX_COLUMNS, TABLE_HEADER_TYPE } from '@quadrats/common/table';
5
+ import { useQuadrats } from '@quadrats/react';
6
+ import { getTableStructure, createTableCell, getColumnWidths, getPinnedColumnsInfo, calculateColumnWidthsAfterAdd, setColumnWidths, getReferenceRowFromHeaderOrBody, calculateColumnWidthsAfterDelete, hasAnyPinnedRows, moveOrSwapColumnWidth, convertToMixedWidthMode, hasAnyPinnedColumns, convertToPercentageMode } from '../utils/helper.js';
7
+
8
+ function useTableActions(element) {
9
+ const editor = useQuadrats();
10
+ const isColumnPinned = useCallback((columnIndex) => {
11
+ try {
12
+ const tableStructure = getTableStructure(editor, element);
13
+ if (!tableStructure)
14
+ return false;
15
+ const { tableMainElement } = tableStructure;
16
+ if (!tableMainElement)
17
+ return false;
18
+ for (const container of tableMainElement.children) {
19
+ if (!Element.isElement(container))
20
+ continue;
21
+ for (const row of container.children) {
22
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
23
+ const cell = row.children[columnIndex];
24
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
25
+ // 如果有任何一個 cell 沒有 pinned 屬性,則整個 column 不算 pinned
26
+ if (!cell.pinned) {
27
+ return false;
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return true;
34
+ }
35
+ catch (error) {
36
+ return false;
37
+ }
38
+ }, [element, editor]);
39
+ const isRowPinned = useCallback((rowIndex) => {
40
+ try {
41
+ const tableStructure = getTableStructure(editor, element);
42
+ if (!tableStructure)
43
+ return false;
44
+ const { tableHeaderElement, tableBodyElement } = tableStructure;
45
+ const headerRowCount = tableHeaderElement && Element.isElement(tableHeaderElement) ? tableHeaderElement.children.length : 0;
46
+ let targetRow;
47
+ if (rowIndex < headerRowCount && tableHeaderElement && Element.isElement(tableHeaderElement)) {
48
+ // 在 Header 中
49
+ const rowElement = tableHeaderElement.children[rowIndex];
50
+ if (Element.isElement(rowElement)) {
51
+ targetRow = rowElement;
52
+ }
53
+ }
54
+ else if (tableBodyElement && Element.isElement(tableBodyElement)) {
55
+ // 在 Body 中
56
+ const bodyRowIndex = rowIndex - headerRowCount;
57
+ const rowElement = tableBodyElement.children[bodyRowIndex];
58
+ if (Element.isElement(rowElement)) {
59
+ targetRow = rowElement;
60
+ }
61
+ }
62
+ if (!Element.isElement(targetRow) || !targetRow.type.includes(TABLE_ROW_TYPE)) {
63
+ return false;
64
+ }
65
+ // 檢查所有 cell 是否都有 pinned 屬性
66
+ for (const cell of targetRow.children) {
67
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
68
+ if (!cell.pinned) {
69
+ return false;
70
+ }
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+ catch (error) {
76
+ return false;
77
+ }
78
+ }, [element, editor]);
79
+ const addColumn = useCallback((options = {}) => {
80
+ const { position = 'right', columnIndex } = options;
81
+ try {
82
+ const tableStructure = getTableStructure(editor, element);
83
+ if (!tableStructure)
84
+ return;
85
+ const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, columnCount } = tableStructure;
86
+ if (columnCount >= TABLE_DEFAULT_MAX_COLUMNS) {
87
+ console.warn(`Maximum columns limit (${TABLE_DEFAULT_MAX_COLUMNS}) reached`);
88
+ return;
89
+ }
90
+ // 計算插入位置
91
+ let insertIndex;
92
+ if (typeof columnIndex === 'number') {
93
+ insertIndex = position === 'left' ? Math.max(0, columnIndex) : Math.min(columnCount, columnIndex + 1);
94
+ }
95
+ else {
96
+ insertIndex = columnCount;
97
+ }
98
+ // 使用 Editor.withoutNormalizing 來批次執行所有操作
99
+ Editor.withoutNormalizing(editor, () => {
100
+ // 在 Header 中加入 cell
101
+ if (tableHeaderElement && tableHeaderPath) {
102
+ tableHeaderElement.children.forEach((row, rowIndex) => {
103
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
104
+ const referenceCell = row.children[insertIndex - (position === 'left' ? 0 : 1)];
105
+ const newCell = createTableCell(referenceCell);
106
+ const cellPath = [...tableHeaderPath, rowIndex, insertIndex];
107
+ Transforms.insertNodes(editor, newCell, { at: cellPath });
108
+ }
109
+ });
110
+ }
111
+ // 在 Body 中加入 cell
112
+ tableBodyElement.children.forEach((row, rowIndex) => {
113
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
114
+ const referenceCell = row.children[insertIndex - (position === 'left' ? 0 : 1)];
115
+ const newCell = createTableCell(referenceCell);
116
+ const cellPath = [...tableBodyPath, rowIndex, insertIndex];
117
+ Transforms.insertNodes(editor, newCell, { at: cellPath });
118
+ }
119
+ });
120
+ // 調整欄位寬度
121
+ const currentWidths = getColumnWidths(element);
122
+ if (currentWidths.length > 0) {
123
+ // 獲取當前的 pinned columns 資訊
124
+ const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
125
+ const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices, columnIndex);
126
+ setColumnWidths(editor, element, newWidths);
127
+ }
128
+ });
129
+ }
130
+ catch (error) {
131
+ console.warn('Failed to add column:', error);
132
+ }
133
+ }, [editor, element]);
134
+ const addRow = useCallback((options = {}) => {
135
+ const { position = 'bottom', rowIndex } = options;
136
+ try {
137
+ const tableStructure = getTableStructure(editor, element);
138
+ if (!tableStructure)
139
+ return;
140
+ const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, headerRowCount, columnCount } = tableStructure;
141
+ // 計算插入位置和參考行
142
+ let insertIndex;
143
+ let referenceRowElement;
144
+ let targetPath;
145
+ if (typeof rowIndex === 'number') {
146
+ // 檢查是在 Header / Body 之中
147
+ if (tableHeaderElement && rowIndex < headerRowCount) {
148
+ targetPath = tableHeaderPath;
149
+ if (position === 'top') {
150
+ insertIndex = Math.max(0, rowIndex);
151
+ referenceRowElement = getReferenceRowFromHeaderOrBody(tableHeaderElement, insertIndex);
152
+ }
153
+ else {
154
+ insertIndex = Math.min(headerRowCount, rowIndex + 1);
155
+ referenceRowElement = getReferenceRowFromHeaderOrBody(tableHeaderElement, rowIndex);
156
+ }
157
+ }
158
+ else {
159
+ targetPath = tableBodyPath;
160
+ const bodyRowIndex = rowIndex - headerRowCount;
161
+ if (position === 'top') {
162
+ insertIndex = Math.max(0, bodyRowIndex);
163
+ referenceRowElement = getReferenceRowFromHeaderOrBody(tableBodyElement, insertIndex);
164
+ }
165
+ else {
166
+ insertIndex = Math.min(tableBodyElement.children.length, bodyRowIndex + 1);
167
+ referenceRowElement = getReferenceRowFromHeaderOrBody(tableBodyElement, bodyRowIndex);
168
+ }
169
+ }
170
+ }
171
+ else {
172
+ // 預設:在 Body 尾端加入列
173
+ targetPath = tableBodyPath;
174
+ insertIndex = tableBodyElement.children.length;
175
+ referenceRowElement = getReferenceRowFromHeaderOrBody(tableBodyElement, insertIndex - 1);
176
+ }
177
+ // 創建新行
178
+ const newRow = {
179
+ type: TABLE_ROW_TYPE,
180
+ children: Array.from({ length: columnCount }, (_, cellIndex) => {
181
+ let referenceCell;
182
+ if (referenceRowElement && referenceRowElement.children[cellIndex]) {
183
+ const cell = referenceRowElement.children[cellIndex];
184
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
185
+ referenceCell = cell;
186
+ }
187
+ }
188
+ return createTableCell(referenceCell);
189
+ }),
190
+ };
191
+ // 插入新行
192
+ const newRowPath = [...targetPath, insertIndex];
193
+ Transforms.insertNodes(editor, newRow, { at: newRowPath });
194
+ }
195
+ catch (error) {
196
+ console.warn('Failed to add row:', error);
197
+ }
198
+ }, [editor, element]);
199
+ const addColumnAndRow = useCallback(() => {
200
+ try {
201
+ const tableStructure = getTableStructure(editor, element);
202
+ if (!tableStructure)
203
+ return;
204
+ const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, columnCount } = tableStructure;
205
+ if (columnCount >= TABLE_DEFAULT_MAX_COLUMNS) {
206
+ console.warn(`Maximum columns limit (${TABLE_DEFAULT_MAX_COLUMNS}) reached`);
207
+ return;
208
+ }
209
+ Editor.withoutNormalizing(editor, () => {
210
+ // 在 Header 中加入新列
211
+ if (tableHeaderElement && tableHeaderPath) {
212
+ tableHeaderElement.children.forEach((row, rowIndex) => {
213
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
214
+ const lastCell = row.children[row.children.length - 1];
215
+ const newHeaderCell = createTableCell(lastCell);
216
+ const cellPath = [...tableHeaderPath, rowIndex, row.children.length];
217
+ Transforms.insertNodes(editor, newHeaderCell, { at: cellPath });
218
+ }
219
+ });
220
+ }
221
+ // 在 Body 中加入新列
222
+ tableBodyElement.children.forEach((row, rowIndex) => {
223
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
224
+ const lastCell = row.children[row.children.length - 1];
225
+ const newCell = createTableCell(lastCell);
226
+ const cellPath = [...tableBodyPath, rowIndex, row.children.length];
227
+ Transforms.insertNodes(editor, newCell, { at: cellPath });
228
+ }
229
+ });
230
+ // 加入新行
231
+ const newColumnCount = columnCount + 1;
232
+ const lastRow = getReferenceRowFromHeaderOrBody(tableBodyElement, tableBodyElement.children.length - 1);
233
+ const newRow = {
234
+ type: TABLE_ROW_TYPE,
235
+ children: Array.from({ length: newColumnCount }, (_, cellIndex) => {
236
+ let referenceCell;
237
+ if (cellIndex < newColumnCount - 1 && Element.isElement(lastRow) && lastRow.type.includes(TABLE_ROW_TYPE)) {
238
+ const cell = lastRow.children[cellIndex];
239
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
240
+ referenceCell = cell;
241
+ }
242
+ }
243
+ else {
244
+ if (Element.isElement(lastRow) && lastRow.type.includes(TABLE_ROW_TYPE)) {
245
+ const cell = lastRow.children[lastRow.children.length - 1];
246
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
247
+ referenceCell = cell;
248
+ }
249
+ }
250
+ }
251
+ return createTableCell(referenceCell);
252
+ }),
253
+ };
254
+ const newRowPath = [...tableBodyPath, tableBodyElement.children.length];
255
+ Transforms.insertNodes(editor, newRow, { at: newRowPath });
256
+ // 調整欄位寬度(新增欄位在最後)
257
+ const currentWidths = getColumnWidths(element);
258
+ if (currentWidths.length > 0) {
259
+ // 新欄位插入在最後(columnCount 位置)
260
+ const insertIndex = columnCount;
261
+ // 獲取當前的 pinned columns 資訊
262
+ const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
263
+ const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices);
264
+ setColumnWidths(editor, element, newWidths);
265
+ }
266
+ });
267
+ }
268
+ catch (error) {
269
+ console.warn('Failed to add column and row:', error);
270
+ }
271
+ }, [editor, element]);
272
+ const deleteRow = useCallback((rowIndex) => {
273
+ try {
274
+ const tableStructure = getTableStructure(editor, element);
275
+ if (!tableStructure)
276
+ return;
277
+ const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, headerRowCount } = tableStructure;
278
+ // 檢查是否刪除 Header 行
279
+ if (rowIndex < headerRowCount) {
280
+ if (!tableHeaderElement || !tableHeaderPath)
281
+ return;
282
+ const headerRowPath = [...tableHeaderPath, rowIndex];
283
+ Transforms.removeNodes(editor, { at: headerRowPath });
284
+ // 如果是最後一個 header 行,移除整個 header 元素
285
+ if (headerRowCount <= 1) {
286
+ Transforms.removeNodes(editor, { at: tableHeaderPath });
287
+ }
288
+ return;
289
+ }
290
+ // 刪除 Body 行
291
+ const bodyRowIndex = rowIndex - headerRowCount;
292
+ if (bodyRowIndex < 0 || bodyRowIndex >= tableBodyElement.children.length) {
293
+ console.warn('Invalid row index for deletion');
294
+ return;
295
+ }
296
+ if (tableBodyElement.children.length <= 1) {
297
+ console.warn('Cannot delete the last row');
298
+ return;
299
+ }
300
+ const rowPath = [...tableBodyPath, bodyRowIndex];
301
+ Transforms.removeNodes(editor, { at: rowPath });
302
+ }
303
+ catch (error) {
304
+ console.warn('Failed to delete row:', error);
305
+ }
306
+ }, [editor, element]);
307
+ const deleteColumn = useCallback((columnIndex) => {
308
+ try {
309
+ const tableStructure = getTableStructure(editor, element);
310
+ if (!tableStructure)
311
+ return;
312
+ const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, columnCount } = tableStructure;
313
+ // 檢查是否有足夠的列(不允許刪除最後一列)
314
+ if (columnCount <= 1) {
315
+ console.warn('Cannot delete the last column');
316
+ return;
317
+ }
318
+ // 檢查 columnIndex 是否有效
319
+ if (columnIndex < 0 || columnIndex >= columnCount) {
320
+ console.warn('Invalid column index for deletion');
321
+ return;
322
+ }
323
+ editor.withoutNormalizing(() => {
324
+ // 從 Header 中刪除列
325
+ if (tableHeaderElement && tableHeaderPath) {
326
+ // 以反向順序刪除
327
+ for (let rowIndex = tableHeaderElement.children.length - 1; rowIndex >= 0; rowIndex--) {
328
+ const headerRow = tableHeaderElement.children[rowIndex];
329
+ if (Element.isElement(headerRow) && headerRow.type.includes(TABLE_ROW_TYPE)) {
330
+ const headerCellPath = [...tableHeaderPath, rowIndex, columnIndex];
331
+ Transforms.removeNodes(editor, { at: headerCellPath });
332
+ }
333
+ }
334
+ }
335
+ // 從 Body 中刪除列
336
+ for (let rowIndex = tableBodyElement.children.length - 1; rowIndex >= 0; rowIndex--) {
337
+ const row = tableBodyElement.children[rowIndex];
338
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
339
+ const cellPath = [...tableBodyPath, rowIndex, columnIndex];
340
+ Transforms.removeNodes(editor, { at: cellPath });
341
+ }
342
+ }
343
+ // 調整欄位寬度
344
+ const currentWidths = getColumnWidths(element);
345
+ if (currentWidths.length > 0) {
346
+ const newWidths = calculateColumnWidthsAfterDelete(currentWidths, columnIndex);
347
+ setColumnWidths(editor, element, newWidths);
348
+ }
349
+ });
350
+ }
351
+ catch (error) {
352
+ console.warn('Failed to delete column:', error);
353
+ }
354
+ }, [editor, element]);
355
+ const moveRowToBody = useCallback((rowIndex) => {
356
+ try {
357
+ const tableStructure = getTableStructure(editor, element);
358
+ if (!tableStructure)
359
+ return;
360
+ const { tableHeaderElement, tableHeaderPath, tableBodyPath } = tableStructure;
361
+ if (!tableHeaderElement || !tableHeaderPath)
362
+ return;
363
+ // 檢查行是否存在於 header 中
364
+ if (rowIndex >= tableHeaderElement.children.length) {
365
+ console.warn('Invalid header row index:', rowIndex);
366
+ return;
367
+ }
368
+ const rowToMove = tableHeaderElement.children[rowIndex];
369
+ if (!Element.isElement(rowToMove) || !rowToMove.type.includes(TABLE_ROW_TYPE))
370
+ return;
371
+ const rowPath = [...tableHeaderPath, rowIndex];
372
+ // 移動前移除所有 cell 的 pinned 屬性
373
+ rowToMove.children.forEach((cell, columnIndex) => {
374
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE) && cell.pinned) {
375
+ if (cell.pinned && isColumnPinned(columnIndex)) {
376
+ const cellPath = [...rowPath, columnIndex];
377
+ Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
378
+ }
379
+ }
380
+ });
381
+ // 移動行到 body 的開始位置
382
+ const bodyTargetPath = [...tableBodyPath, 0];
383
+ Transforms.moveNodes(editor, {
384
+ at: rowPath,
385
+ to: bodyTargetPath,
386
+ });
387
+ }
388
+ catch (error) {
389
+ console.warn('Failed to move row to body:', error);
390
+ }
391
+ }, [editor, element, isColumnPinned]);
392
+ const moveRowToHeader = useCallback((rowIndex, customProps) => {
393
+ try {
394
+ const tableStructure = getTableStructure(editor, element);
395
+ if (!tableStructure)
396
+ return;
397
+ const { tableHeaderElement, tableBodyElement, tableMainPath, tableHeaderPath, tableBodyPath, headerRowCount } = tableStructure;
398
+ // 計算正確的 body 行索引
399
+ const bodyRowIndex = rowIndex - headerRowCount;
400
+ // 檢查 body 行索引是否有效
401
+ if (bodyRowIndex < 0 || bodyRowIndex >= tableBodyElement.children.length) {
402
+ console.warn('Invalid body row index:', bodyRowIndex);
403
+ return;
404
+ }
405
+ // 檢查行是否存在
406
+ const rowToMove = tableBodyElement.children[bodyRowIndex];
407
+ if (!Element.isElement(rowToMove) || !rowToMove.type.includes(TABLE_ROW_TYPE))
408
+ return;
409
+ // 檢查 header 中是否已有 pinned rows(一致性規則檢查)
410
+ const hasExistingPinnedRows = tableStructure ? hasAnyPinnedRows(tableStructure) : false;
411
+ // 如果有現有的 pinned rows 且沒有提供自定義屬性,自動設置 pinned 以保持一致性
412
+ const finalProps = customProps || (hasExistingPinnedRows ? { pinned: true } : undefined);
413
+ // 如果提供了 finalProps,則應用到 cells
414
+ const processedRow = finalProps
415
+ ? Object.assign(Object.assign({}, rowToMove), { children: rowToMove.children.map((cell) => {
416
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
417
+ return Object.assign(Object.assign({}, cell), finalProps);
418
+ }
419
+ return cell;
420
+ }) }) : rowToMove;
421
+ const rowPath = [...tableBodyPath, bodyRowIndex];
422
+ // 如果 header 不存在,先創建它
423
+ if (!tableHeaderElement) {
424
+ const newHeader = {
425
+ type: TABLE_HEADER_TYPE,
426
+ children: [processedRow],
427
+ };
428
+ const headerInsertPath = [...tableMainPath, 0];
429
+ Editor.withoutNormalizing(editor, () => {
430
+ Transforms.removeNodes(editor, { at: rowPath });
431
+ Transforms.insertNodes(editor, newHeader, { at: headerInsertPath });
432
+ });
433
+ }
434
+ else {
435
+ // 如果這是 pinned row,找到正確的插入位置(pinned rows 在頂部)
436
+ let headerTargetPath;
437
+ if (finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) {
438
+ let insertIndex = 0;
439
+ for (const [index, headerRow] of tableHeaderElement.children.entries()) {
440
+ if (Element.isElement(headerRow)) {
441
+ const hasNonPinnedCell = headerRow.children.some((cell) => Element.isElement(cell) && !cell.pinned);
442
+ if (hasNonPinnedCell) {
443
+ break;
444
+ }
445
+ insertIndex = index + 1;
446
+ }
447
+ }
448
+ headerTargetPath = [...tableHeaderPath, insertIndex];
449
+ Editor.withoutNormalizing(editor, () => {
450
+ Transforms.removeNodes(editor, { at: rowPath });
451
+ Transforms.insertNodes(editor, processedRow, { at: headerTargetPath });
452
+ });
453
+ }
454
+ else {
455
+ // 移動行到現有 header 的末尾
456
+ headerTargetPath = [...tableHeaderPath, tableHeaderElement.children.length];
457
+ Transforms.moveNodes(editor, {
458
+ at: rowPath,
459
+ to: headerTargetPath,
460
+ });
461
+ }
462
+ }
463
+ }
464
+ catch (error) {
465
+ console.warn('Failed to move row to header:', error);
466
+ }
467
+ }, [editor, element]);
468
+ const unsetColumnAsTitle = useCallback((columnIndex) => {
469
+ try {
470
+ const tableStructure = getTableStructure(editor, element);
471
+ if (!tableStructure)
472
+ return;
473
+ const { tableHeaderElement, tableBodyElement, tableMainElement } = tableStructure;
474
+ // 獲取 table 的實際寬度(用於轉換為混合模式)
475
+ let tableWidth = 0;
476
+ if (tableMainElement) {
477
+ const tableDOMElement = ReactEditor.toDOMNode(editor, tableMainElement);
478
+ if (tableDOMElement instanceof HTMLElement) {
479
+ tableWidth = tableDOMElement.getBoundingClientRect().width;
480
+ }
481
+ }
482
+ const processContainer = (containerElement) => {
483
+ if (!Element.isElement(containerElement))
484
+ return;
485
+ const containerPath = ReactEditor.findPath(editor, containerElement);
486
+ const firstRow = containerElement.children[0];
487
+ // 找到 column 標題列的尾端
488
+ let targetColumnIndex = 0;
489
+ if (Element.isElement(firstRow) && firstRow.type.includes(TABLE_ROW_TYPE)) {
490
+ for (let i = 0; i < firstRow.children.length; i++) {
491
+ const cell = firstRow.children[i];
492
+ if (Element.isElement(cell) &&
493
+ cell.type.includes(TABLE_CELL_TYPE) &&
494
+ cell.treatAsTitle &&
495
+ i !== columnIndex) {
496
+ targetColumnIndex = i + 1;
497
+ }
498
+ }
499
+ }
500
+ containerElement.children.forEach((row, rowIndex) => {
501
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
502
+ const cell = row.children[columnIndex];
503
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
504
+ const cellPath = [...containerPath, rowIndex, columnIndex];
505
+ Transforms.unsetNodes(editor, 'treatAsTitle', { at: cellPath });
506
+ if (cell.pinned && !isRowPinned(rowIndex)) {
507
+ Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
508
+ }
509
+ }
510
+ }
511
+ });
512
+ if (columnIndex < targetColumnIndex) {
513
+ const actualTargetIndex = targetColumnIndex - 1;
514
+ for (let rowIndex = containerElement.children.length - 1; rowIndex >= 0; rowIndex--) {
515
+ const row = containerElement.children[rowIndex];
516
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
517
+ const fromPath = [...containerPath, rowIndex, columnIndex];
518
+ const toPath = [...containerPath, rowIndex, actualTargetIndex];
519
+ Transforms.moveNodes(editor, {
520
+ at: fromPath,
521
+ to: toPath,
522
+ });
523
+ }
524
+ }
525
+ // 調整 columnWidths:將 columnIndex 的寬度移動到 actualTargetIndex
526
+ const currentWidths = getColumnWidths(element);
527
+ if (currentWidths.length > 0) {
528
+ const movedWidths = moveOrSwapColumnWidth(currentWidths, columnIndex, actualTargetIndex, 'move');
529
+ // 檢查移動後是否還有 pinned columns
530
+ const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
531
+ // 更新釘選欄位索引(移除當前欄位,並調整其他欄位的索引)
532
+ const updatedPinnedIndices = pinnedColumnIndices
533
+ .filter((idx) => idx !== columnIndex)
534
+ .map((idx) => {
535
+ if (idx > columnIndex && idx <= actualTargetIndex)
536
+ return idx - 1;
537
+ return idx;
538
+ })
539
+ .sort((a, b) => a - b);
540
+ // 如果還有 pinned columns,轉換為混合模式;否則可能轉回全 percentage 模式
541
+ if (updatedPinnedIndices.length > 0 && tableWidth > 0) {
542
+ const mixedWidths = convertToMixedWidthMode(movedWidths, updatedPinnedIndices, tableWidth);
543
+ setColumnWidths(editor, element, mixedWidths);
544
+ }
545
+ else {
546
+ // 沒有 pinned columns 了,使用原本的寬度
547
+ setColumnWidths(editor, element, movedWidths);
548
+ }
549
+ }
550
+ }
551
+ else {
552
+ // 即使沒有移動位置,也需要檢查是否需要更新寬度模式
553
+ const currentWidths = getColumnWidths(element);
554
+ if (currentWidths.length > 0) {
555
+ const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
556
+ // 移除當前欄位
557
+ const updatedPinnedIndices = pinnedColumnIndices
558
+ .filter((idx) => idx !== columnIndex)
559
+ .sort((a, b) => a - b);
560
+ // 如果還有 pinned columns,轉換為混合模式;否則可能轉回全 percentage 模式
561
+ if (updatedPinnedIndices.length > 0 && tableWidth > 0) {
562
+ const mixedWidths = convertToMixedWidthMode(currentWidths, updatedPinnedIndices, tableWidth);
563
+ setColumnWidths(editor, element, mixedWidths);
564
+ }
565
+ // 如果沒有 pinned columns,保持原樣(可能已經是全 percentage 了)
566
+ }
567
+ }
568
+ };
569
+ if (tableHeaderElement) {
570
+ processContainer(tableHeaderElement);
571
+ }
572
+ if (tableBodyElement) {
573
+ processContainer(tableBodyElement);
574
+ }
575
+ }
576
+ catch (error) {
577
+ console.warn('Failed to unset column as title:', error);
578
+ }
579
+ }, [editor, element, isRowPinned]);
580
+ const setColumnAsTitle = useCallback((columnIndex, customProps) => {
581
+ try {
582
+ const tableStructure = getTableStructure(editor, element);
583
+ if (!tableStructure)
584
+ return;
585
+ const { tableHeaderElement, tableBodyElement, tableMainElement } = tableStructure;
586
+ // 檢查是否已有 pinned columns
587
+ const hasExistingPinnedColumns = hasAnyPinnedColumns(tableStructure);
588
+ // 如果有現有的 pinned columns 且沒有提供自定義屬性,自動設置 pinned 以保持一致性
589
+ const finalProps = customProps || (hasExistingPinnedColumns ? { pinned: true } : undefined);
590
+ // 獲取 table 的實際寬度(用於轉換為混合模式)
591
+ let tableWidth = 0;
592
+ if (tableMainElement) {
593
+ const tableDOMElement = ReactEditor.toDOMNode(editor, tableMainElement);
594
+ if (tableDOMElement instanceof HTMLElement) {
595
+ tableWidth = tableDOMElement.getBoundingClientRect().width;
596
+ }
597
+ }
598
+ const processContainer = (containerElement) => {
599
+ if (!Element.isElement(containerElement))
600
+ return;
601
+ const containerPath = ReactEditor.findPath(editor, containerElement);
602
+ const firstRow = containerElement.children[0];
603
+ // 先找到 column 標題列的尾端
604
+ let targetColumnIndex = 0;
605
+ if (Element.isElement(firstRow) && firstRow.type.includes(TABLE_ROW_TYPE)) {
606
+ for (let i = 0; i < firstRow.children.length; i++) {
607
+ const cell = firstRow.children[i];
608
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE) && cell.treatAsTitle) {
609
+ targetColumnIndex = i + 1;
610
+ }
611
+ else {
612
+ break;
613
+ }
614
+ }
615
+ }
616
+ containerElement.children.forEach((row, rowIndex) => {
617
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
618
+ row.children.forEach((cell, childColIndex) => {
619
+ const cellPath = [...containerPath, rowIndex, childColIndex];
620
+ if (childColIndex === columnIndex) {
621
+ const nodeProps = finalProps ? Object.assign({ treatAsTitle: true }, finalProps) : { treatAsTitle: true };
622
+ Transforms.setNodes(editor, nodeProps, { at: cellPath });
623
+ }
624
+ else if (finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) {
625
+ // 確保其他 title column 也有 pinned 屬性以保持一致性
626
+ if (Element.isElement(cell) && cell.treatAsTitle) {
627
+ Transforms.setNodes(editor, { pinned: true }, { at: cellPath });
628
+ }
629
+ }
630
+ });
631
+ }
632
+ });
633
+ // 檢查是否需要移動位置
634
+ const needsMove = columnIndex >= targetColumnIndex && columnIndex !== targetColumnIndex;
635
+ if (needsMove) {
636
+ for (let rowIndex = containerElement.children.length - 1; rowIndex >= 0; rowIndex--) {
637
+ const row = containerElement.children[rowIndex];
638
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
639
+ const fromPath = [...containerPath, rowIndex, columnIndex];
640
+ const toPath = [...containerPath, rowIndex, targetColumnIndex];
641
+ Transforms.moveNodes(editor, {
642
+ at: fromPath,
643
+ to: toPath,
644
+ });
645
+ }
646
+ }
647
+ // 調整 columnWidths:將 columnIndex 的寬度移動到 targetColumnIndex
648
+ const currentWidths = getColumnWidths(element);
649
+ if (currentWidths.length > 0) {
650
+ const movedWidths = moveOrSwapColumnWidth(currentWidths, columnIndex, targetColumnIndex, 'move');
651
+ // 如果設定了 pinned,需要轉換為混合模式
652
+ if ((finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) && tableWidth > 0) {
653
+ const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
654
+ // 更新釘選欄位索引
655
+ const updatedPinnedIndices = pinnedColumnIndices
656
+ .map((idx) => {
657
+ if (idx === columnIndex)
658
+ return targetColumnIndex;
659
+ if (idx >= targetColumnIndex && idx < columnIndex)
660
+ return idx + 1;
661
+ return idx;
662
+ })
663
+ .concat(targetColumnIndex)
664
+ .filter((idx, i, arr) => arr.indexOf(idx) === i)
665
+ .sort((a, b) => a - b);
666
+ const mixedWidths = convertToMixedWidthMode(movedWidths, updatedPinnedIndices, tableWidth);
667
+ setColumnWidths(editor, element, mixedWidths);
668
+ }
669
+ else {
670
+ setColumnWidths(editor, element, movedWidths);
671
+ }
672
+ }
673
+ }
674
+ else if ((finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) && tableWidth > 0) {
675
+ // 即使沒有移動位置,如果設定了 pinned,也需要轉換為混合模式
676
+ const currentWidths = getColumnWidths(element);
677
+ if (currentWidths.length > 0) {
678
+ const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
679
+ // 找出所有已經是 title 的 columns
680
+ const titleColumnIndices = new Set();
681
+ const firstRow = containerElement.children[0];
682
+ if (Element.isElement(firstRow) && firstRow.type.includes(TABLE_ROW_TYPE)) {
683
+ firstRow.children.forEach((cell, colIndex) => {
684
+ if (Element.isElement(cell) &&
685
+ cell.type.includes(TABLE_CELL_TYPE) &&
686
+ cell.treatAsTitle) {
687
+ titleColumnIndices.add(colIndex);
688
+ }
689
+ });
690
+ }
691
+ // 將當前 column 加入 title columns
692
+ titleColumnIndices.add(columnIndex);
693
+ // 合併所有 pinned columns 和 title columns
694
+ const allPinnedIndices = new Set([...pinnedColumnIndices, ...Array.from(titleColumnIndices)]);
695
+ const updatedPinnedIndices = Array.from(allPinnedIndices).sort((a, b) => a - b);
696
+ const mixedWidths = convertToMixedWidthMode(currentWidths, updatedPinnedIndices, tableWidth);
697
+ setColumnWidths(editor, element, mixedWidths);
698
+ }
699
+ }
700
+ };
701
+ if (tableHeaderElement) {
702
+ processContainer(tableHeaderElement);
703
+ }
704
+ if (tableBodyElement) {
705
+ processContainer(tableBodyElement);
706
+ }
707
+ }
708
+ catch (error) {
709
+ console.warn('Failed to set column as title:', error);
710
+ }
711
+ }, [editor, element]);
712
+ const pinColumn = useCallback((columnIndex) => {
713
+ try {
714
+ setColumnAsTitle(columnIndex, { pinned: true });
715
+ }
716
+ catch (error) {
717
+ console.warn('Failed to pin column:', error);
718
+ }
719
+ }, [setColumnAsTitle]);
720
+ const unpinColumn = useCallback(() => {
721
+ try {
722
+ const tableStructure = getTableStructure(editor, element);
723
+ if (!tableStructure)
724
+ return;
725
+ const { tableHeaderElement, tableBodyElement } = tableStructure;
726
+ // 檢查 column 與 row 之間是否有交叉 pinned 狀態的關係
727
+ const shouldRowRemainPinned = (rowElement, excludeColumns) => {
728
+ let hasNonExcludedCells = false;
729
+ for (let colIndex = 0; colIndex < rowElement.children.length; colIndex++) {
730
+ const cell = rowElement.children[colIndex];
731
+ if (!Element.isElement(cell) || !cell.type.includes(TABLE_CELL_TYPE))
732
+ continue;
733
+ if (excludeColumns.has(colIndex))
734
+ continue;
735
+ hasNonExcludedCells = true;
736
+ if (!cell.pinned) {
737
+ return false;
738
+ }
739
+ }
740
+ return hasNonExcludedCells;
741
+ };
742
+ const processContainer = (containerElement) => {
743
+ const containerPath = ReactEditor.findPath(editor, containerElement);
744
+ const treatAsTitleColumns = new Set();
745
+ containerElement.children.forEach((row) => {
746
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
747
+ row.children.forEach((cell, colIndex) => {
748
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE) && cell.treatAsTitle) {
749
+ treatAsTitleColumns.add(colIndex);
750
+ }
751
+ });
752
+ }
753
+ });
754
+ containerElement.children.forEach((row, rowIndex) => {
755
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
756
+ const rowShouldRemainPinned = shouldRowRemainPinned(row, treatAsTitleColumns);
757
+ row.children.forEach((cell, colIndex) => {
758
+ if (treatAsTitleColumns.has(colIndex) && Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
759
+ const cellPath = [...containerPath, rowIndex, colIndex];
760
+ Transforms.unsetNodes(editor, 'treatAsTitle', { at: cellPath });
761
+ if (!rowShouldRemainPinned) {
762
+ Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
763
+ }
764
+ }
765
+ });
766
+ }
767
+ });
768
+ };
769
+ if (tableHeaderElement) {
770
+ processContainer(tableHeaderElement);
771
+ }
772
+ if (tableBodyElement) {
773
+ processContainer(tableBodyElement);
774
+ }
775
+ // 轉換回純百分比模式
776
+ const currentWidths = getColumnWidths(element);
777
+ const percentageWidths = convertToPercentageMode(currentWidths);
778
+ setColumnWidths(editor, element, percentageWidths);
779
+ }
780
+ catch (error) {
781
+ console.warn('Failed to unpin column:', error);
782
+ }
783
+ }, [editor, element]);
784
+ const setPinnedOnRowCells = useCallback((row, pinned) => {
785
+ try {
786
+ for (const [, cell] of row.children.entries()) {
787
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
788
+ const cellPath = ReactEditor.findPath(editor, cell);
789
+ if (pinned) {
790
+ Transforms.setNodes(editor, { pinned: true }, { at: cellPath });
791
+ }
792
+ else {
793
+ Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
794
+ }
795
+ }
796
+ }
797
+ }
798
+ catch (error) {
799
+ console.warn('Failed to set pinned on row cells:', error);
800
+ }
801
+ }, [editor]);
802
+ const setPinnedOnAllHeaderRows = useCallback((headerElement, pinned) => {
803
+ try {
804
+ for (const headerRow of headerElement.children) {
805
+ if (Element.isElement(headerRow) && headerRow.type.includes(TABLE_ROW_TYPE)) {
806
+ setPinnedOnRowCells(headerRow, pinned);
807
+ }
808
+ }
809
+ }
810
+ catch (error) {
811
+ console.warn('Failed to set pinned on all header rows:', error);
812
+ }
813
+ }, [setPinnedOnRowCells]);
814
+ const pinRow = useCallback((rowIndex) => {
815
+ try {
816
+ const tableStructure = getTableStructure(editor, element);
817
+ if (!tableStructure)
818
+ return;
819
+ const { tableHeaderElement, headerRowCount } = tableStructure;
820
+ // 先將目前所有的 header rows 都設為 pinned
821
+ if (tableHeaderElement) {
822
+ setPinnedOnAllHeaderRows(tableHeaderElement, true);
823
+ }
824
+ // 然後將目標 row 移動到 header 中並設為 pinned
825
+ if (rowIndex >= headerRowCount) {
826
+ moveRowToHeader(rowIndex, { pinned: true });
827
+ }
828
+ }
829
+ catch (error) {
830
+ console.warn('Failed to pin row:', error);
831
+ }
832
+ }, [editor, element, moveRowToHeader, setPinnedOnAllHeaderRows]);
833
+ const unpinRow = useCallback(() => {
834
+ try {
835
+ const tableStructure = getTableStructure(editor, element);
836
+ if (!tableStructure)
837
+ return;
838
+ const { tableHeaderElement, tableBodyElement, tableHeaderPath } = tableStructure;
839
+ if (!tableHeaderElement || !tableBodyElement)
840
+ return;
841
+ // 檢查 column 與 row 之間是否有交叉 pinned 狀態的關係
842
+ const shouldColumnRemainPinned = (columnIndex) => {
843
+ const containers = [tableHeaderElement, tableBodyElement];
844
+ for (const container of containers) {
845
+ if (!Element.isElement(container))
846
+ continue;
847
+ for (const row of container.children) {
848
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
849
+ const cell = row.children[columnIndex];
850
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
851
+ // 如果這個 cell 在 body 中且有 pinned 屬性,則 column 應該保持 pinned
852
+ if (container.type === tableBodyElement.type && cell.pinned) {
853
+ return true;
854
+ }
855
+ }
856
+ }
857
+ }
858
+ }
859
+ return false;
860
+ };
861
+ tableHeaderElement.children.forEach((row, headerRowIndex) => {
862
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
863
+ row.children.forEach((cell, colIndex) => {
864
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
865
+ const cellPath = [...tableHeaderPath, headerRowIndex, colIndex];
866
+ if (!shouldColumnRemainPinned(colIndex)) {
867
+ Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
868
+ }
869
+ }
870
+ });
871
+ }
872
+ });
873
+ const tableBodyPath = ReactEditor.findPath(editor, tableBodyElement);
874
+ for (let i = tableHeaderElement.children.length - 1; i >= 0; i--) {
875
+ const row = tableHeaderElement.children[i];
876
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
877
+ const fromPath = [...tableHeaderPath, i];
878
+ const toPath = [...tableBodyPath, 0];
879
+ Transforms.moveNodes(editor, {
880
+ at: fromPath,
881
+ to: toPath,
882
+ });
883
+ }
884
+ }
885
+ }
886
+ catch (error) {
887
+ console.warn('Failed to unpin row:', error);
888
+ }
889
+ }, [editor, element]);
890
+ /**
891
+ * 內部函數:移動或交換列的位置
892
+ * @param mode 'swap' 為交換相鄰位置(toolbar 按鈕),'move' 為移動到任意位置(拖曳)
893
+ */
894
+ const moveOrSwapRow = useCallback((sourceRowIndex, targetRowIndex, mode = 'move') => {
895
+ try {
896
+ const tableStructure = getTableStructure(editor, element);
897
+ if (!tableStructure)
898
+ return;
899
+ const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, headerRowCount } = tableStructure;
900
+ // 確定當前列和目標列所屬的容器
901
+ const sourceInHeader = sourceRowIndex < headerRowCount;
902
+ const targetInHeader = targetRowIndex < headerRowCount;
903
+ // 標題列只能與標題列互換/移動,一般列只能與一般列互換/移動
904
+ if (sourceInHeader !== targetInHeader) {
905
+ console.warn(`Cannot ${mode} row between header and body`);
906
+ return;
907
+ }
908
+ // 檢查邊界
909
+ if (sourceRowIndex === targetRowIndex) {
910
+ return;
911
+ }
912
+ let containerPath;
913
+ let sourceLocalIndex;
914
+ let targetLocalIndex;
915
+ if (sourceInHeader) {
916
+ // 在 header 中
917
+ if (!tableHeaderElement || !tableHeaderPath)
918
+ return;
919
+ containerPath = tableHeaderPath;
920
+ sourceLocalIndex = sourceRowIndex;
921
+ targetLocalIndex = targetRowIndex;
922
+ }
923
+ else {
924
+ // 在 body 中
925
+ if (!tableBodyElement)
926
+ return;
927
+ containerPath = tableBodyPath;
928
+ sourceLocalIndex = sourceRowIndex - headerRowCount;
929
+ targetLocalIndex = targetRowIndex - headerRowCount;
930
+ }
931
+ Editor.withoutNormalizing(editor, () => {
932
+ if (mode === 'swap') {
933
+ // swap 邏輯:交換兩個相鄰位置
934
+ if (sourceRowIndex < targetRowIndex) {
935
+ // 向下移動:先將源列移到目標位置之後
936
+ const sourcePath = [...containerPath, sourceLocalIndex];
937
+ const afterTargetPath = [...containerPath, targetLocalIndex];
938
+ Transforms.moveNodes(editor, {
939
+ at: sourcePath,
940
+ to: afterTargetPath,
941
+ });
942
+ }
943
+ else {
944
+ // 向上移動:先將目標列移到源位置之後
945
+ const targetPath = [...containerPath, targetLocalIndex];
946
+ const afterSourcePath = [...containerPath, sourceLocalIndex];
947
+ Transforms.moveNodes(editor, {
948
+ at: targetPath,
949
+ to: afterSourcePath,
950
+ });
951
+ }
952
+ }
953
+ else {
954
+ // move 邏輯:直接移動到目標位置
955
+ const sourcePath = [...containerPath, sourceLocalIndex];
956
+ const targetPath = [...containerPath, targetLocalIndex];
957
+ Transforms.moveNodes(editor, {
958
+ at: sourcePath,
959
+ to: targetPath,
960
+ });
961
+ }
962
+ });
963
+ }
964
+ catch (error) {
965
+ console.warn(`Failed to ${mode} row:`, error);
966
+ }
967
+ }, [editor, element]);
968
+ /**
969
+ * 內部函數:移動或交換行的位置
970
+ * @param mode 'swap' 為交換相鄰位置(toolbar 按鈕),'move' 為移動到任意位置(拖曳)
971
+ */
972
+ const moveOrSwapColumn = useCallback((sourceColumnIndex, targetColumnIndex, mode = 'move') => {
973
+ try {
974
+ const tableStructure = getTableStructure(editor, element);
975
+ if (!tableStructure)
976
+ return;
977
+ const { tableHeaderElement, tableBodyElement, columnCount } = tableStructure;
978
+ // 檢查邊界
979
+ if (targetColumnIndex < 0 || targetColumnIndex >= columnCount) {
980
+ console.warn('Target column index out of bounds');
981
+ return;
982
+ }
983
+ // 檢查是否為同一行
984
+ if (sourceColumnIndex === targetColumnIndex) {
985
+ return;
986
+ }
987
+ // 檢查當前行和目標行是否都是標題行或都是一般行
988
+ // 透過檢查第一個 cell 的 treatAsTitle 屬性來判斷
989
+ const checkIsTitleColumn = (container, colIndex) => {
990
+ if (!Element.isElement(container))
991
+ return false;
992
+ for (const row of container.children) {
993
+ if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
994
+ const cell = row.children[colIndex];
995
+ if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
996
+ return !!cell.treatAsTitle;
997
+ }
998
+ }
999
+ }
1000
+ return false;
1001
+ };
1002
+ // 檢查兩個 container 中的第一列來確定是否為標題行
1003
+ let sourceIsTitle = false;
1004
+ let targetIsTitle = false;
1005
+ if (tableHeaderElement) {
1006
+ sourceIsTitle = sourceIsTitle || checkIsTitleColumn(tableHeaderElement, sourceColumnIndex);
1007
+ targetIsTitle = targetIsTitle || checkIsTitleColumn(tableHeaderElement, targetColumnIndex);
1008
+ }
1009
+ if (tableBodyElement) {
1010
+ sourceIsTitle = sourceIsTitle || checkIsTitleColumn(tableBodyElement, sourceColumnIndex);
1011
+ targetIsTitle = targetIsTitle || checkIsTitleColumn(tableBodyElement, targetColumnIndex);
1012
+ }
1013
+ // 標題行只能與標題行互換/移動,一般行只能與一般行互換/移動
1014
+ if (sourceIsTitle !== targetIsTitle) {
1015
+ console.warn(`Cannot ${mode} column between title and normal columns`);
1016
+ return;
1017
+ }
1018
+ // 根據模式選擇不同的 columnWidths 處理方式
1019
+ const currentWidths = getColumnWidths(element);
1020
+ const newWidths = moveOrSwapColumnWidth(currentWidths, sourceColumnIndex, targetColumnIndex, mode);
1021
+ setColumnWidths(editor, element, newWidths);
1022
+ // 對 header 和 body 中的所有列進行操作
1023
+ Editor.withoutNormalizing(editor, () => {
1024
+ const containers = [tableHeaderElement, tableBodyElement].filter((c) => c && Element.isElement(c));
1025
+ for (const container of containers) {
1026
+ // 對每一列進行操作
1027
+ for (let rowIndex = 0; rowIndex < container.children.length; rowIndex++) {
1028
+ const row = container.children[rowIndex];
1029
+ if (!Element.isElement(row) || !row.type.includes(TABLE_ROW_TYPE))
1030
+ continue;
1031
+ const containerPath = ReactEditor.findPath(editor, container);
1032
+ const rowPath = [...containerPath, rowIndex];
1033
+ if (mode === 'swap') {
1034
+ // swap 邏輯:交換兩個相鄰位置
1035
+ if (sourceColumnIndex < targetColumnIndex) {
1036
+ // 向右移動:將源 cell 移到目標位置之後
1037
+ const sourceCellPath = [...rowPath, sourceColumnIndex];
1038
+ const afterTargetCellPath = [...rowPath, targetColumnIndex];
1039
+ Transforms.moveNodes(editor, {
1040
+ at: sourceCellPath,
1041
+ to: afterTargetCellPath,
1042
+ });
1043
+ }
1044
+ else {
1045
+ // 向左移動:將目標 cell 移到源位置之後
1046
+ const targetCellPath = [...rowPath, targetColumnIndex];
1047
+ const afterSourceCellPath = [...rowPath, sourceColumnIndex];
1048
+ Transforms.moveNodes(editor, {
1049
+ at: targetCellPath,
1050
+ to: afterSourceCellPath,
1051
+ });
1052
+ }
1053
+ }
1054
+ else {
1055
+ // move 邏輯:直接移動到目標位置
1056
+ const sourceCellPath = [...rowPath, sourceColumnIndex];
1057
+ const targetCellPath = [...rowPath, targetColumnIndex];
1058
+ Transforms.moveNodes(editor, {
1059
+ at: sourceCellPath,
1060
+ to: targetCellPath,
1061
+ });
1062
+ }
1063
+ }
1064
+ }
1065
+ });
1066
+ }
1067
+ catch (error) {
1068
+ console.warn(`Failed to ${mode} column:`, error);
1069
+ }
1070
+ }, [editor, element]);
1071
+ return {
1072
+ addColumn,
1073
+ addRow,
1074
+ addColumnAndRow,
1075
+ deleteRow,
1076
+ deleteColumn,
1077
+ moveRowToBody,
1078
+ moveRowToHeader,
1079
+ unsetColumnAsTitle,
1080
+ setColumnAsTitle,
1081
+ pinColumn,
1082
+ unpinColumn,
1083
+ pinRow,
1084
+ unpinRow,
1085
+ isColumnPinned,
1086
+ isRowPinned,
1087
+ moveOrSwapRow,
1088
+ moveOrSwapColumn,
1089
+ };
1090
+ }
1091
+
1092
+ export { useTableActions };