@quadrats/react 1.0.0 → 1.1.0
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.
- package/card/components/Card.js +83 -75
- package/card/index.cjs.js +82 -74
- package/carousel/components/Carousel.js +32 -28
- package/carousel/index.cjs.js +32 -28
- package/components/Tooltip/index.js +5 -2
- package/components/index.cjs.js +5 -2
- package/embed/renderers/base/components/BaseEmbedElement.js +51 -43
- package/embed/renderers/base/index.cjs.js +50 -42
- package/image/components/Image.js +34 -26
- package/image/createReactImage.js +1 -1
- package/image/index.cjs.js +34 -26
- package/package.json +4 -4
- package/table/components/Table.d.ts +9 -0
- package/table/components/Table.js +231 -0
- package/table/components/TableBody.d.ts +5 -0
- package/table/components/TableBody.js +8 -0
- package/table/components/TableCell.d.ts +5 -0
- package/table/components/TableCell.js +191 -0
- package/table/components/TableHeader.d.ts +5 -0
- package/table/components/TableHeader.js +13 -0
- package/table/components/TableMain.d.ts +5 -0
- package/table/components/TableMain.js +225 -0
- package/table/components/TableRow.d.ts +5 -0
- package/table/components/TableRow.js +8 -0
- package/table/components/TableTitle.d.ts +5 -0
- package/table/components/TableTitle.js +18 -0
- package/table/contexts/TableActionsContext.d.ts +3 -0
- package/table/contexts/TableActionsContext.js +5 -0
- package/table/contexts/TableHeaderContext.d.ts +2 -0
- package/table/contexts/TableHeaderContext.js +7 -0
- package/table/contexts/TableMetadataContext.d.ts +3 -0
- package/table/contexts/TableMetadataContext.js +5 -0
- package/table/contexts/TableScrollContext.d.ts +2 -0
- package/table/contexts/TableScrollContext.js +9 -0
- package/table/contexts/TableStateContext.d.ts +3 -0
- package/table/contexts/TableStateContext.js +5 -0
- package/table/createReactTable.d.ts +4 -0
- package/table/createReactTable.js +297 -0
- package/table/defaultRenderTableElements.d.ts +2 -0
- package/table/defaultRenderTableElements.js +20 -0
- package/table/hooks/useColumnResize.d.ts +12 -0
- package/table/hooks/useColumnResize.js +139 -0
- package/table/hooks/useTableActions.d.ts +25 -0
- package/table/hooks/useTableActions.js +886 -0
- package/table/hooks/useTableActionsContext.d.ts +1 -0
- package/table/hooks/useTableActionsContext.js +12 -0
- package/table/hooks/useTableCell.d.ts +16 -0
- package/table/hooks/useTableCell.js +166 -0
- package/table/hooks/useTableCellToolbarActions.d.ts +34 -0
- package/table/hooks/useTableCellToolbarActions.js +404 -0
- package/table/hooks/useTableMetadata.d.ts +1 -0
- package/table/hooks/useTableMetadata.js +12 -0
- package/table/hooks/useTableStateContext.d.ts +1 -0
- package/table/hooks/useTableStateContext.js +12 -0
- package/table/hooks/useTableStates.d.ts +18 -0
- package/table/hooks/useTableStates.js +14 -0
- package/table/index.cjs.js +3254 -0
- package/table/index.d.ts +16 -0
- package/table/index.js +24 -0
- package/table/jsx-serializer/components/Table.d.ts +3 -0
- package/table/jsx-serializer/components/Table.js +7 -0
- package/table/jsx-serializer/components/TableBody.d.ts +3 -0
- package/table/jsx-serializer/components/TableBody.js +7 -0
- package/table/jsx-serializer/components/TableCell.d.ts +5 -0
- package/table/jsx-serializer/components/TableCell.js +33 -0
- package/table/jsx-serializer/components/TableHeader.d.ts +3 -0
- package/table/jsx-serializer/components/TableHeader.js +10 -0
- package/table/jsx-serializer/components/TableMain.d.ts +6 -0
- package/table/jsx-serializer/components/TableMain.js +18 -0
- package/table/jsx-serializer/components/TableRow.d.ts +3 -0
- package/table/jsx-serializer/components/TableRow.js +7 -0
- package/table/jsx-serializer/components/TableTitle.d.ts +3 -0
- package/table/jsx-serializer/components/TableTitle.js +7 -0
- package/table/jsx-serializer/contexts/TableHeaderContext.d.ts +1 -0
- package/table/jsx-serializer/contexts/TableHeaderContext.js +5 -0
- package/table/jsx-serializer/contexts/TableScrollContext.d.ts +2 -0
- package/table/jsx-serializer/contexts/TableScrollContext.js +7 -0
- package/table/jsx-serializer/createJsxSerializeTable.d.ts +5 -0
- package/table/jsx-serializer/createJsxSerializeTable.js +113 -0
- package/table/jsx-serializer/defaultRenderTableElements.d.ts +2 -0
- package/table/jsx-serializer/defaultRenderTableElements.js +20 -0
- package/table/jsx-serializer/index.cjs.js +195 -0
- package/table/jsx-serializer/index.d.ts +3 -0
- package/table/jsx-serializer/index.js +2 -0
- package/table/jsx-serializer/package.json +7 -0
- package/table/jsx-serializer/typings.d.ts +12 -0
- package/table/package.json +10 -0
- package/table/table.css +1 -0
- package/table/table.scss +393 -0
- package/table/toolbar/TableToolbarIcon.d.ts +8 -0
- package/table/toolbar/TableToolbarIcon.js +12 -0
- package/table/toolbar/index.cjs.js +24 -0
- package/table/toolbar/index.d.ts +2 -0
- package/table/toolbar/index.js +2 -0
- package/table/toolbar/package.json +7 -0
- package/table/toolbar/useTableTool.d.ts +4 -0
- package/table/toolbar/useTableTool.js +13 -0
- package/table/typings.d.ts +66 -0
- package/table/utils/helper.d.ts +160 -0
- package/table/utils/helper.js +693 -0
- package/toolbar/components/InlineToolbar.d.ts +12 -11
- package/toolbar/components/InlineToolbar.js +23 -19
- package/toolbar/components/Toolbar.js +2 -2
- package/toolbar/index.cjs.js +24 -21
- package/toolbar/toolbar.css +1 -1
- package/toolbar/toolbar.scss +4 -1
- package/utils/index.cjs.js +7 -1
- package/utils/removePreviousElement.js +7 -1
|
@@ -0,0 +1,886 @@
|
|
|
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, calculateColumnWidthsAfterAdd, setColumnWidths, getReferenceRowFromHeaderOrBody, calculateColumnWidthsAfterDelete, hasAnyPinnedRows, moveColumnWidth, getPinnedColumnsInfo, convertToMixedWidthMode, hasAnyPinnedColumns } 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
|
+
const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex);
|
|
124
|
+
setColumnWidths(editor, element, newWidths);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn('Failed to add column:', error);
|
|
130
|
+
}
|
|
131
|
+
}, [editor, element]);
|
|
132
|
+
const addRow = useCallback((options = {}) => {
|
|
133
|
+
const { position = 'bottom', rowIndex } = options;
|
|
134
|
+
try {
|
|
135
|
+
const tableStructure = getTableStructure(editor, element);
|
|
136
|
+
if (!tableStructure)
|
|
137
|
+
return;
|
|
138
|
+
const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, headerRowCount, columnCount } = tableStructure;
|
|
139
|
+
// 計算插入位置和參考行
|
|
140
|
+
let insertIndex;
|
|
141
|
+
let referenceRowElement;
|
|
142
|
+
let targetPath;
|
|
143
|
+
if (typeof rowIndex === 'number') {
|
|
144
|
+
// 檢查是在 Header / Body 之中
|
|
145
|
+
if (tableHeaderElement && rowIndex < headerRowCount) {
|
|
146
|
+
targetPath = tableHeaderPath;
|
|
147
|
+
if (position === 'top') {
|
|
148
|
+
insertIndex = Math.max(0, rowIndex);
|
|
149
|
+
referenceRowElement = getReferenceRowFromHeaderOrBody(tableHeaderElement, insertIndex);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
insertIndex = Math.min(headerRowCount, rowIndex + 1);
|
|
153
|
+
referenceRowElement = getReferenceRowFromHeaderOrBody(tableHeaderElement, rowIndex);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
targetPath = tableBodyPath;
|
|
158
|
+
const bodyRowIndex = rowIndex - headerRowCount;
|
|
159
|
+
if (position === 'top') {
|
|
160
|
+
insertIndex = Math.max(0, bodyRowIndex);
|
|
161
|
+
referenceRowElement = getReferenceRowFromHeaderOrBody(tableBodyElement, insertIndex);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
insertIndex = Math.min(tableBodyElement.children.length, bodyRowIndex + 1);
|
|
165
|
+
referenceRowElement = getReferenceRowFromHeaderOrBody(tableBodyElement, bodyRowIndex);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// 預設:在 Body 尾端加入列
|
|
171
|
+
targetPath = tableBodyPath;
|
|
172
|
+
insertIndex = tableBodyElement.children.length;
|
|
173
|
+
referenceRowElement = getReferenceRowFromHeaderOrBody(tableBodyElement, insertIndex - 1);
|
|
174
|
+
}
|
|
175
|
+
// 創建新行
|
|
176
|
+
const newRow = {
|
|
177
|
+
type: TABLE_ROW_TYPE,
|
|
178
|
+
children: Array.from({ length: columnCount }, (_, cellIndex) => {
|
|
179
|
+
let referenceCell;
|
|
180
|
+
if (referenceRowElement && referenceRowElement.children[cellIndex]) {
|
|
181
|
+
const cell = referenceRowElement.children[cellIndex];
|
|
182
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
183
|
+
referenceCell = cell;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return createTableCell(referenceCell);
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
// 插入新行
|
|
190
|
+
const newRowPath = [...targetPath, insertIndex];
|
|
191
|
+
Transforms.insertNodes(editor, newRow, { at: newRowPath });
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.warn('Failed to add row:', error);
|
|
195
|
+
}
|
|
196
|
+
}, [editor, element]);
|
|
197
|
+
const addColumnAndRow = useCallback(() => {
|
|
198
|
+
try {
|
|
199
|
+
const tableStructure = getTableStructure(editor, element);
|
|
200
|
+
if (!tableStructure)
|
|
201
|
+
return;
|
|
202
|
+
const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, columnCount } = tableStructure;
|
|
203
|
+
if (columnCount >= TABLE_DEFAULT_MAX_COLUMNS) {
|
|
204
|
+
console.warn(`Maximum columns limit (${TABLE_DEFAULT_MAX_COLUMNS}) reached`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
Editor.withoutNormalizing(editor, () => {
|
|
208
|
+
// 在 Header 中加入新列
|
|
209
|
+
if (tableHeaderElement && tableHeaderPath) {
|
|
210
|
+
tableHeaderElement.children.forEach((row, rowIndex) => {
|
|
211
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
212
|
+
const lastCell = row.children[row.children.length - 1];
|
|
213
|
+
const newHeaderCell = createTableCell(lastCell);
|
|
214
|
+
const cellPath = [...tableHeaderPath, rowIndex, row.children.length];
|
|
215
|
+
Transforms.insertNodes(editor, newHeaderCell, { at: cellPath });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// 在 Body 中加入新列
|
|
220
|
+
tableBodyElement.children.forEach((row, rowIndex) => {
|
|
221
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
222
|
+
const lastCell = row.children[row.children.length - 1];
|
|
223
|
+
const newCell = createTableCell(lastCell);
|
|
224
|
+
const cellPath = [...tableBodyPath, rowIndex, row.children.length];
|
|
225
|
+
Transforms.insertNodes(editor, newCell, { at: cellPath });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// 加入新行
|
|
229
|
+
const newColumnCount = columnCount + 1;
|
|
230
|
+
const lastRow = getReferenceRowFromHeaderOrBody(tableBodyElement, tableBodyElement.children.length - 1);
|
|
231
|
+
const newRow = {
|
|
232
|
+
type: TABLE_ROW_TYPE,
|
|
233
|
+
children: Array.from({ length: newColumnCount }, (_, cellIndex) => {
|
|
234
|
+
let referenceCell;
|
|
235
|
+
if (cellIndex < newColumnCount - 1 && Element.isElement(lastRow) && lastRow.type.includes(TABLE_ROW_TYPE)) {
|
|
236
|
+
const cell = lastRow.children[cellIndex];
|
|
237
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
238
|
+
referenceCell = cell;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
if (Element.isElement(lastRow) && lastRow.type.includes(TABLE_ROW_TYPE)) {
|
|
243
|
+
const cell = lastRow.children[lastRow.children.length - 1];
|
|
244
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
245
|
+
referenceCell = cell;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return createTableCell(referenceCell);
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
const newRowPath = [...tableBodyPath, tableBodyElement.children.length];
|
|
253
|
+
Transforms.insertNodes(editor, newRow, { at: newRowPath });
|
|
254
|
+
// 調整欄位寬度(新增欄位在最後)
|
|
255
|
+
const currentWidths = getColumnWidths(element);
|
|
256
|
+
if (currentWidths.length > 0) {
|
|
257
|
+
// 新欄位插入在最後(columnCount 位置)
|
|
258
|
+
const insertIndex = columnCount;
|
|
259
|
+
const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex);
|
|
260
|
+
setColumnWidths(editor, element, newWidths);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
console.warn('Failed to add column and row:', error);
|
|
266
|
+
}
|
|
267
|
+
}, [editor, element]);
|
|
268
|
+
const deleteRow = useCallback((rowIndex) => {
|
|
269
|
+
try {
|
|
270
|
+
const tableStructure = getTableStructure(editor, element);
|
|
271
|
+
if (!tableStructure)
|
|
272
|
+
return;
|
|
273
|
+
const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, headerRowCount } = tableStructure;
|
|
274
|
+
// 檢查是否刪除 Header 行
|
|
275
|
+
if (rowIndex < headerRowCount) {
|
|
276
|
+
if (!tableHeaderElement || !tableHeaderPath)
|
|
277
|
+
return;
|
|
278
|
+
const headerRowPath = [...tableHeaderPath, rowIndex];
|
|
279
|
+
Transforms.removeNodes(editor, { at: headerRowPath });
|
|
280
|
+
// 如果是最後一個 header 行,移除整個 header 元素
|
|
281
|
+
if (headerRowCount <= 1) {
|
|
282
|
+
Transforms.removeNodes(editor, { at: tableHeaderPath });
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// 刪除 Body 行
|
|
287
|
+
const bodyRowIndex = rowIndex - headerRowCount;
|
|
288
|
+
if (bodyRowIndex < 0 || bodyRowIndex >= tableBodyElement.children.length) {
|
|
289
|
+
console.warn('Invalid row index for deletion');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (tableBodyElement.children.length <= 1) {
|
|
293
|
+
console.warn('Cannot delete the last row');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const rowPath = [...tableBodyPath, bodyRowIndex];
|
|
297
|
+
Transforms.removeNodes(editor, { at: rowPath });
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.warn('Failed to delete row:', error);
|
|
301
|
+
}
|
|
302
|
+
}, [editor, element]);
|
|
303
|
+
const deleteColumn = useCallback((columnIndex) => {
|
|
304
|
+
try {
|
|
305
|
+
const tableStructure = getTableStructure(editor, element);
|
|
306
|
+
if (!tableStructure)
|
|
307
|
+
return;
|
|
308
|
+
const { tableHeaderElement, tableBodyElement, tableHeaderPath, tableBodyPath, columnCount } = tableStructure;
|
|
309
|
+
// 檢查是否有足夠的列(不允許刪除最後一列)
|
|
310
|
+
if (columnCount <= 1) {
|
|
311
|
+
console.warn('Cannot delete the last column');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// 檢查 columnIndex 是否有效
|
|
315
|
+
if (columnIndex < 0 || columnIndex >= columnCount) {
|
|
316
|
+
console.warn('Invalid column index for deletion');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
editor.withoutNormalizing(() => {
|
|
320
|
+
// 從 Header 中刪除列
|
|
321
|
+
if (tableHeaderElement && tableHeaderPath) {
|
|
322
|
+
// 以反向順序刪除
|
|
323
|
+
for (let rowIndex = tableHeaderElement.children.length - 1; rowIndex >= 0; rowIndex--) {
|
|
324
|
+
const headerRow = tableHeaderElement.children[rowIndex];
|
|
325
|
+
if (Element.isElement(headerRow) && headerRow.type.includes(TABLE_ROW_TYPE)) {
|
|
326
|
+
const headerCellPath = [...tableHeaderPath, rowIndex, columnIndex];
|
|
327
|
+
Transforms.removeNodes(editor, { at: headerCellPath });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// 從 Body 中刪除列
|
|
332
|
+
for (let rowIndex = tableBodyElement.children.length - 1; rowIndex >= 0; rowIndex--) {
|
|
333
|
+
const row = tableBodyElement.children[rowIndex];
|
|
334
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
335
|
+
const cellPath = [...tableBodyPath, rowIndex, columnIndex];
|
|
336
|
+
Transforms.removeNodes(editor, { at: cellPath });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// 調整欄位寬度
|
|
340
|
+
const currentWidths = getColumnWidths(element);
|
|
341
|
+
if (currentWidths.length > 0) {
|
|
342
|
+
const newWidths = calculateColumnWidthsAfterDelete(currentWidths, columnIndex);
|
|
343
|
+
setColumnWidths(editor, element, newWidths);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
console.warn('Failed to delete column:', error);
|
|
349
|
+
}
|
|
350
|
+
}, [editor, element]);
|
|
351
|
+
const moveRowToBody = useCallback((rowIndex) => {
|
|
352
|
+
try {
|
|
353
|
+
const tableStructure = getTableStructure(editor, element);
|
|
354
|
+
if (!tableStructure)
|
|
355
|
+
return;
|
|
356
|
+
const { tableHeaderElement, tableHeaderPath, tableBodyPath } = tableStructure;
|
|
357
|
+
if (!tableHeaderElement || !tableHeaderPath)
|
|
358
|
+
return;
|
|
359
|
+
// 檢查行是否存在於 header 中
|
|
360
|
+
if (rowIndex >= tableHeaderElement.children.length) {
|
|
361
|
+
console.warn('Invalid header row index:', rowIndex);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const rowToMove = tableHeaderElement.children[rowIndex];
|
|
365
|
+
if (!Element.isElement(rowToMove) || !rowToMove.type.includes(TABLE_ROW_TYPE))
|
|
366
|
+
return;
|
|
367
|
+
const rowPath = [...tableHeaderPath, rowIndex];
|
|
368
|
+
// 移動前移除所有 cell 的 pinned 屬性
|
|
369
|
+
rowToMove.children.forEach((cell, columnIndex) => {
|
|
370
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE) && cell.pinned) {
|
|
371
|
+
if (cell.pinned && isColumnPinned(columnIndex)) {
|
|
372
|
+
const cellPath = [...rowPath, columnIndex];
|
|
373
|
+
Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// 移動行到 body 的開始位置
|
|
378
|
+
const bodyTargetPath = [...tableBodyPath, 0];
|
|
379
|
+
Transforms.moveNodes(editor, {
|
|
380
|
+
at: rowPath,
|
|
381
|
+
to: bodyTargetPath,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
console.warn('Failed to move row to body:', error);
|
|
386
|
+
}
|
|
387
|
+
}, [editor, element, isColumnPinned]);
|
|
388
|
+
const moveRowToHeader = useCallback((rowIndex, customProps) => {
|
|
389
|
+
try {
|
|
390
|
+
const tableStructure = getTableStructure(editor, element);
|
|
391
|
+
if (!tableStructure)
|
|
392
|
+
return;
|
|
393
|
+
const { tableHeaderElement, tableBodyElement, tableMainPath, tableHeaderPath, tableBodyPath, headerRowCount } = tableStructure;
|
|
394
|
+
// 計算正確的 body 行索引
|
|
395
|
+
const bodyRowIndex = rowIndex - headerRowCount;
|
|
396
|
+
// 檢查 body 行索引是否有效
|
|
397
|
+
if (bodyRowIndex < 0 || bodyRowIndex >= tableBodyElement.children.length) {
|
|
398
|
+
console.warn('Invalid body row index:', bodyRowIndex);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// 檢查行是否存在
|
|
402
|
+
const rowToMove = tableBodyElement.children[bodyRowIndex];
|
|
403
|
+
if (!Element.isElement(rowToMove) || !rowToMove.type.includes(TABLE_ROW_TYPE))
|
|
404
|
+
return;
|
|
405
|
+
// 檢查 header 中是否已有 pinned rows(一致性規則檢查)
|
|
406
|
+
const hasExistingPinnedRows = tableStructure ? hasAnyPinnedRows(tableStructure) : false;
|
|
407
|
+
// 如果有現有的 pinned rows 且沒有提供自定義屬性,自動設置 pinned 以保持一致性
|
|
408
|
+
const finalProps = customProps || (hasExistingPinnedRows ? { pinned: true } : undefined);
|
|
409
|
+
// 如果提供了 finalProps,則應用到 cells
|
|
410
|
+
const processedRow = finalProps
|
|
411
|
+
? Object.assign(Object.assign({}, rowToMove), { children: rowToMove.children.map((cell) => {
|
|
412
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
413
|
+
return Object.assign(Object.assign({}, cell), finalProps);
|
|
414
|
+
}
|
|
415
|
+
return cell;
|
|
416
|
+
}) }) : rowToMove;
|
|
417
|
+
const rowPath = [...tableBodyPath, bodyRowIndex];
|
|
418
|
+
// 如果 header 不存在,先創建它
|
|
419
|
+
if (!tableHeaderElement) {
|
|
420
|
+
const newHeader = {
|
|
421
|
+
type: TABLE_HEADER_TYPE,
|
|
422
|
+
children: [processedRow],
|
|
423
|
+
};
|
|
424
|
+
const headerInsertPath = [...tableMainPath, 0];
|
|
425
|
+
Editor.withoutNormalizing(editor, () => {
|
|
426
|
+
Transforms.removeNodes(editor, { at: rowPath });
|
|
427
|
+
Transforms.insertNodes(editor, newHeader, { at: headerInsertPath });
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// 如果這是 pinned row,找到正確的插入位置(pinned rows 在頂部)
|
|
432
|
+
let headerTargetPath;
|
|
433
|
+
if (finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) {
|
|
434
|
+
let insertIndex = 0;
|
|
435
|
+
for (const [index, headerRow] of tableHeaderElement.children.entries()) {
|
|
436
|
+
if (Element.isElement(headerRow)) {
|
|
437
|
+
const hasNonPinnedCell = headerRow.children.some((cell) => Element.isElement(cell) && !cell.pinned);
|
|
438
|
+
if (hasNonPinnedCell) {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
insertIndex = index + 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
headerTargetPath = [...tableHeaderPath, insertIndex];
|
|
445
|
+
Editor.withoutNormalizing(editor, () => {
|
|
446
|
+
Transforms.removeNodes(editor, { at: rowPath });
|
|
447
|
+
Transforms.insertNodes(editor, processedRow, { at: headerTargetPath });
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
// 移動行到現有 header 的末尾
|
|
452
|
+
headerTargetPath = [...tableHeaderPath, tableHeaderElement.children.length];
|
|
453
|
+
Transforms.moveNodes(editor, {
|
|
454
|
+
at: rowPath,
|
|
455
|
+
to: headerTargetPath,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.warn('Failed to move row to header:', error);
|
|
462
|
+
}
|
|
463
|
+
}, [editor, element]);
|
|
464
|
+
const unsetColumnAsTitle = useCallback((columnIndex) => {
|
|
465
|
+
try {
|
|
466
|
+
const tableStructure = getTableStructure(editor, element);
|
|
467
|
+
if (!tableStructure)
|
|
468
|
+
return;
|
|
469
|
+
const { tableHeaderElement, tableBodyElement, tableMainElement } = tableStructure;
|
|
470
|
+
// 獲取 table 的實際寬度(用於轉換為混合模式)
|
|
471
|
+
let tableWidth = 0;
|
|
472
|
+
if (tableMainElement) {
|
|
473
|
+
const tableDOMElement = ReactEditor.toDOMNode(editor, tableMainElement);
|
|
474
|
+
if (tableDOMElement instanceof HTMLElement) {
|
|
475
|
+
tableWidth = tableDOMElement.getBoundingClientRect().width;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const processContainer = (containerElement) => {
|
|
479
|
+
if (!Element.isElement(containerElement))
|
|
480
|
+
return;
|
|
481
|
+
const containerPath = ReactEditor.findPath(editor, containerElement);
|
|
482
|
+
const firstRow = containerElement.children[0];
|
|
483
|
+
// 找到 column 標題列的尾端
|
|
484
|
+
let targetColumnIndex = 0;
|
|
485
|
+
if (Element.isElement(firstRow) && firstRow.type.includes(TABLE_ROW_TYPE)) {
|
|
486
|
+
for (let i = 0; i < firstRow.children.length; i++) {
|
|
487
|
+
const cell = firstRow.children[i];
|
|
488
|
+
if (Element.isElement(cell) &&
|
|
489
|
+
cell.type.includes(TABLE_CELL_TYPE) &&
|
|
490
|
+
cell.treatAsTitle &&
|
|
491
|
+
i !== columnIndex) {
|
|
492
|
+
targetColumnIndex = i + 1;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
containerElement.children.forEach((row, rowIndex) => {
|
|
497
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
498
|
+
const cell = row.children[columnIndex];
|
|
499
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
500
|
+
const cellPath = [...containerPath, rowIndex, columnIndex];
|
|
501
|
+
Transforms.unsetNodes(editor, 'treatAsTitle', { at: cellPath });
|
|
502
|
+
if (cell.pinned && !isRowPinned(rowIndex)) {
|
|
503
|
+
Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
if (columnIndex < targetColumnIndex) {
|
|
509
|
+
const actualTargetIndex = targetColumnIndex - 1;
|
|
510
|
+
for (let rowIndex = containerElement.children.length - 1; rowIndex >= 0; rowIndex--) {
|
|
511
|
+
const row = containerElement.children[rowIndex];
|
|
512
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
513
|
+
const fromPath = [...containerPath, rowIndex, columnIndex];
|
|
514
|
+
const toPath = [...containerPath, rowIndex, actualTargetIndex];
|
|
515
|
+
Transforms.moveNodes(editor, {
|
|
516
|
+
at: fromPath,
|
|
517
|
+
to: toPath,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// 調整 columnWidths:將 columnIndex 的寬度移動到 actualTargetIndex
|
|
522
|
+
const currentWidths = getColumnWidths(element);
|
|
523
|
+
if (currentWidths.length > 0) {
|
|
524
|
+
const movedWidths = moveColumnWidth(currentWidths, columnIndex, actualTargetIndex);
|
|
525
|
+
// 檢查移動後是否還有 pinned columns
|
|
526
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
527
|
+
// 更新釘選欄位索引(移除當前欄位,並調整其他欄位的索引)
|
|
528
|
+
const updatedPinnedIndices = pinnedColumnIndices
|
|
529
|
+
.filter((idx) => idx !== columnIndex)
|
|
530
|
+
.map((idx) => {
|
|
531
|
+
if (idx > columnIndex && idx <= actualTargetIndex)
|
|
532
|
+
return idx - 1;
|
|
533
|
+
return idx;
|
|
534
|
+
})
|
|
535
|
+
.sort((a, b) => a - b);
|
|
536
|
+
// 如果還有 pinned columns,轉換為混合模式;否則可能轉回全 percentage 模式
|
|
537
|
+
if (updatedPinnedIndices.length > 0 && tableWidth > 0) {
|
|
538
|
+
const mixedWidths = convertToMixedWidthMode(movedWidths, updatedPinnedIndices, tableWidth);
|
|
539
|
+
setColumnWidths(editor, element, mixedWidths);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
// 沒有 pinned columns 了,使用原本的寬度
|
|
543
|
+
setColumnWidths(editor, element, movedWidths);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// 即使沒有移動位置,也需要檢查是否需要更新寬度模式
|
|
549
|
+
const currentWidths = getColumnWidths(element);
|
|
550
|
+
if (currentWidths.length > 0) {
|
|
551
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
552
|
+
// 移除當前欄位
|
|
553
|
+
const updatedPinnedIndices = pinnedColumnIndices
|
|
554
|
+
.filter((idx) => idx !== columnIndex)
|
|
555
|
+
.sort((a, b) => a - b);
|
|
556
|
+
// 如果還有 pinned columns,轉換為混合模式;否則可能轉回全 percentage 模式
|
|
557
|
+
if (updatedPinnedIndices.length > 0 && tableWidth > 0) {
|
|
558
|
+
const mixedWidths = convertToMixedWidthMode(currentWidths, updatedPinnedIndices, tableWidth);
|
|
559
|
+
setColumnWidths(editor, element, mixedWidths);
|
|
560
|
+
}
|
|
561
|
+
// 如果沒有 pinned columns,保持原樣(可能已經是全 percentage 了)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
if (tableHeaderElement) {
|
|
566
|
+
processContainer(tableHeaderElement);
|
|
567
|
+
}
|
|
568
|
+
if (tableBodyElement) {
|
|
569
|
+
processContainer(tableBodyElement);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
console.warn('Failed to unset column as title:', error);
|
|
574
|
+
}
|
|
575
|
+
}, [editor, element, isRowPinned]);
|
|
576
|
+
const setColumnAsTitle = useCallback((columnIndex, customProps) => {
|
|
577
|
+
try {
|
|
578
|
+
const tableStructure = getTableStructure(editor, element);
|
|
579
|
+
if (!tableStructure)
|
|
580
|
+
return;
|
|
581
|
+
const { tableHeaderElement, tableBodyElement, tableMainElement } = tableStructure;
|
|
582
|
+
// 檢查是否已有 pinned columns(一致性規則檢查)
|
|
583
|
+
const hasExistingPinnedColumns = hasAnyPinnedColumns(tableStructure);
|
|
584
|
+
// 如果有現有的 pinned columns 且沒有提供自定義屬性,自動設置 pinned 以保持一致性
|
|
585
|
+
const finalProps = customProps || (hasExistingPinnedColumns ? { pinned: true } : undefined);
|
|
586
|
+
// 獲取 table 的實際寬度(用於轉換為混合模式)
|
|
587
|
+
let tableWidth = 0;
|
|
588
|
+
if (tableMainElement) {
|
|
589
|
+
const tableDOMElement = ReactEditor.toDOMNode(editor, tableMainElement);
|
|
590
|
+
if (tableDOMElement instanceof HTMLElement) {
|
|
591
|
+
tableWidth = tableDOMElement.getBoundingClientRect().width;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const processContainer = (containerElement) => {
|
|
595
|
+
if (!Element.isElement(containerElement))
|
|
596
|
+
return;
|
|
597
|
+
const containerPath = ReactEditor.findPath(editor, containerElement);
|
|
598
|
+
const firstRow = containerElement.children[0];
|
|
599
|
+
// 先找到 column 標題列的尾端
|
|
600
|
+
let targetColumnIndex = 0;
|
|
601
|
+
if (Element.isElement(firstRow) && firstRow.type.includes(TABLE_ROW_TYPE)) {
|
|
602
|
+
for (let i = 0; i < firstRow.children.length; i++) {
|
|
603
|
+
const cell = firstRow.children[i];
|
|
604
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE) && cell.treatAsTitle) {
|
|
605
|
+
targetColumnIndex = i + 1;
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
containerElement.children.forEach((row, rowIndex) => {
|
|
613
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
614
|
+
row.children.forEach((cell, childColIndex) => {
|
|
615
|
+
const cellPath = [...containerPath, rowIndex, childColIndex];
|
|
616
|
+
if (childColIndex === columnIndex) {
|
|
617
|
+
const nodeProps = finalProps ? Object.assign({ treatAsTitle: true }, finalProps) : { treatAsTitle: true };
|
|
618
|
+
Transforms.setNodes(editor, nodeProps, { at: cellPath });
|
|
619
|
+
}
|
|
620
|
+
else if (finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) {
|
|
621
|
+
// 確保其他 title column 也有 pinned 屬性以保持一致性
|
|
622
|
+
if (Element.isElement(cell) && cell.treatAsTitle) {
|
|
623
|
+
Transforms.setNodes(editor, { pinned: true }, { at: cellPath });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
// 如果目標位置並不需要移動,則直接返回
|
|
630
|
+
if (columnIndex < targetColumnIndex)
|
|
631
|
+
return;
|
|
632
|
+
if (columnIndex !== targetColumnIndex) {
|
|
633
|
+
for (let rowIndex = containerElement.children.length - 1; rowIndex >= 0; rowIndex--) {
|
|
634
|
+
const row = containerElement.children[rowIndex];
|
|
635
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
636
|
+
const fromPath = [...containerPath, rowIndex, columnIndex];
|
|
637
|
+
const toPath = [...containerPath, rowIndex, targetColumnIndex];
|
|
638
|
+
Transforms.moveNodes(editor, {
|
|
639
|
+
at: fromPath,
|
|
640
|
+
to: toPath,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// 調整 columnWidths:將 columnIndex 的寬度移動到 targetColumnIndex
|
|
645
|
+
const currentWidths = getColumnWidths(element);
|
|
646
|
+
if (currentWidths.length > 0) {
|
|
647
|
+
const movedWidths = moveColumnWidth(currentWidths, columnIndex, targetColumnIndex);
|
|
648
|
+
// 如果設定了 pinned,需要轉換為混合模式
|
|
649
|
+
if ((finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) && tableWidth > 0) {
|
|
650
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
651
|
+
// 更新釘選欄位索引
|
|
652
|
+
const updatedPinnedIndices = pinnedColumnIndices
|
|
653
|
+
.map((idx) => {
|
|
654
|
+
if (idx === columnIndex)
|
|
655
|
+
return targetColumnIndex;
|
|
656
|
+
if (idx >= targetColumnIndex && idx < columnIndex)
|
|
657
|
+
return idx + 1;
|
|
658
|
+
return idx;
|
|
659
|
+
})
|
|
660
|
+
.concat(targetColumnIndex)
|
|
661
|
+
.filter((idx, i, arr) => arr.indexOf(idx) === i)
|
|
662
|
+
.sort((a, b) => a - b);
|
|
663
|
+
const mixedWidths = convertToMixedWidthMode(movedWidths, updatedPinnedIndices, tableWidth);
|
|
664
|
+
setColumnWidths(editor, element, mixedWidths);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
setColumnWidths(editor, element, movedWidths);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
else if ((finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) && tableWidth > 0) {
|
|
672
|
+
// 即使沒有移動位置,如果設定了 pinned,也需要轉換為混合模式
|
|
673
|
+
const currentWidths = getColumnWidths(element);
|
|
674
|
+
if (currentWidths.length > 0) {
|
|
675
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
676
|
+
const updatedPinnedIndices = [...new Set([...pinnedColumnIndices, columnIndex])].sort((a, b) => a - b);
|
|
677
|
+
const mixedWidths = convertToMixedWidthMode(currentWidths, updatedPinnedIndices, tableWidth);
|
|
678
|
+
setColumnWidths(editor, element, mixedWidths);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
if (tableHeaderElement) {
|
|
683
|
+
processContainer(tableHeaderElement);
|
|
684
|
+
}
|
|
685
|
+
if (tableBodyElement) {
|
|
686
|
+
processContainer(tableBodyElement);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
console.warn('Failed to set column as title:', error);
|
|
691
|
+
}
|
|
692
|
+
}, [editor, element]);
|
|
693
|
+
const pinColumn = useCallback((columnIndex) => {
|
|
694
|
+
try {
|
|
695
|
+
setColumnAsTitle(columnIndex, { pinned: true });
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
console.warn('Failed to pin column:', error);
|
|
699
|
+
}
|
|
700
|
+
}, [setColumnAsTitle]);
|
|
701
|
+
const unpinColumn = useCallback(() => {
|
|
702
|
+
try {
|
|
703
|
+
const tableStructure = getTableStructure(editor, element);
|
|
704
|
+
if (!tableStructure)
|
|
705
|
+
return;
|
|
706
|
+
const { tableHeaderElement, tableBodyElement } = tableStructure;
|
|
707
|
+
// 檢查 column 與 row 之間是否有交叉 pinned 狀態的關係
|
|
708
|
+
const shouldRowRemainPinned = (rowElement, excludeColumns) => {
|
|
709
|
+
let hasNonExcludedCells = false;
|
|
710
|
+
for (let colIndex = 0; colIndex < rowElement.children.length; colIndex++) {
|
|
711
|
+
const cell = rowElement.children[colIndex];
|
|
712
|
+
if (!Element.isElement(cell) || !cell.type.includes(TABLE_CELL_TYPE))
|
|
713
|
+
continue;
|
|
714
|
+
if (excludeColumns.has(colIndex))
|
|
715
|
+
continue;
|
|
716
|
+
hasNonExcludedCells = true;
|
|
717
|
+
if (!cell.pinned) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return hasNonExcludedCells;
|
|
722
|
+
};
|
|
723
|
+
const processContainer = (containerElement) => {
|
|
724
|
+
const containerPath = ReactEditor.findPath(editor, containerElement);
|
|
725
|
+
const treatAsTitleColumns = new Set();
|
|
726
|
+
containerElement.children.forEach((row) => {
|
|
727
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
728
|
+
row.children.forEach((cell, colIndex) => {
|
|
729
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE) && cell.treatAsTitle) {
|
|
730
|
+
treatAsTitleColumns.add(colIndex);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
containerElement.children.forEach((row, rowIndex) => {
|
|
736
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
737
|
+
const rowShouldRemainPinned = shouldRowRemainPinned(row, treatAsTitleColumns);
|
|
738
|
+
row.children.forEach((cell, colIndex) => {
|
|
739
|
+
if (treatAsTitleColumns.has(colIndex) && Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
740
|
+
const cellPath = [...containerPath, rowIndex, colIndex];
|
|
741
|
+
Transforms.unsetNodes(editor, 'treatAsTitle', { at: cellPath });
|
|
742
|
+
if (!rowShouldRemainPinned) {
|
|
743
|
+
Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
};
|
|
750
|
+
if (tableHeaderElement) {
|
|
751
|
+
processContainer(tableHeaderElement);
|
|
752
|
+
}
|
|
753
|
+
if (tableBodyElement) {
|
|
754
|
+
processContainer(tableBodyElement);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
console.warn('Failed to unpin column:', error);
|
|
759
|
+
}
|
|
760
|
+
}, [editor, element]);
|
|
761
|
+
const setPinnedOnRowCells = useCallback((row, pinned) => {
|
|
762
|
+
try {
|
|
763
|
+
for (const [, cell] of row.children.entries()) {
|
|
764
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
765
|
+
const cellPath = ReactEditor.findPath(editor, cell);
|
|
766
|
+
if (pinned) {
|
|
767
|
+
Transforms.setNodes(editor, { pinned: true }, { at: cellPath });
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
console.warn('Failed to set pinned on row cells:', error);
|
|
777
|
+
}
|
|
778
|
+
}, [editor]);
|
|
779
|
+
const setPinnedOnAllHeaderRows = useCallback((headerElement, pinned) => {
|
|
780
|
+
try {
|
|
781
|
+
for (const headerRow of headerElement.children) {
|
|
782
|
+
if (Element.isElement(headerRow) && headerRow.type.includes(TABLE_ROW_TYPE)) {
|
|
783
|
+
setPinnedOnRowCells(headerRow, pinned);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
console.warn('Failed to set pinned on all header rows:', error);
|
|
789
|
+
}
|
|
790
|
+
}, [setPinnedOnRowCells]);
|
|
791
|
+
const pinRow = useCallback((rowIndex) => {
|
|
792
|
+
try {
|
|
793
|
+
const tableStructure = getTableStructure(editor, element);
|
|
794
|
+
if (!tableStructure)
|
|
795
|
+
return;
|
|
796
|
+
const { tableHeaderElement, headerRowCount } = tableStructure;
|
|
797
|
+
// 先將目前所有的 header rows 都設為 pinned
|
|
798
|
+
if (tableHeaderElement) {
|
|
799
|
+
setPinnedOnAllHeaderRows(tableHeaderElement, true);
|
|
800
|
+
}
|
|
801
|
+
// 然後將目標 row 移動到 header 中並設為 pinned
|
|
802
|
+
if (rowIndex >= headerRowCount) {
|
|
803
|
+
moveRowToHeader(rowIndex, { pinned: true });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
console.warn('Failed to pin row:', error);
|
|
808
|
+
}
|
|
809
|
+
}, [editor, element, moveRowToHeader, setPinnedOnAllHeaderRows]);
|
|
810
|
+
const unpinRow = useCallback(() => {
|
|
811
|
+
try {
|
|
812
|
+
const tableStructure = getTableStructure(editor, element);
|
|
813
|
+
if (!tableStructure)
|
|
814
|
+
return;
|
|
815
|
+
const { tableHeaderElement, tableBodyElement, tableHeaderPath } = tableStructure;
|
|
816
|
+
if (!tableHeaderElement || !tableBodyElement)
|
|
817
|
+
return;
|
|
818
|
+
// 檢查 column 與 row 之間是否有交叉 pinned 狀態的關係
|
|
819
|
+
const shouldColumnRemainPinned = (columnIndex) => {
|
|
820
|
+
const containers = [tableHeaderElement, tableBodyElement];
|
|
821
|
+
for (const container of containers) {
|
|
822
|
+
if (!Element.isElement(container))
|
|
823
|
+
continue;
|
|
824
|
+
for (const row of container.children) {
|
|
825
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
826
|
+
const cell = row.children[columnIndex];
|
|
827
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
828
|
+
// 如果這個 cell 在 body 中且有 pinned 屬性,則 column 應該保持 pinned
|
|
829
|
+
if (container.type === tableBodyElement.type && cell.pinned) {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return false;
|
|
837
|
+
};
|
|
838
|
+
tableHeaderElement.children.forEach((row, headerRowIndex) => {
|
|
839
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
840
|
+
row.children.forEach((cell, colIndex) => {
|
|
841
|
+
if (Element.isElement(cell) && cell.type.includes(TABLE_CELL_TYPE)) {
|
|
842
|
+
const cellPath = [...tableHeaderPath, headerRowIndex, colIndex];
|
|
843
|
+
if (!shouldColumnRemainPinned(colIndex)) {
|
|
844
|
+
Transforms.unsetNodes(editor, 'pinned', { at: cellPath });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
const tableBodyPath = ReactEditor.findPath(editor, tableBodyElement);
|
|
851
|
+
for (let i = tableHeaderElement.children.length - 1; i >= 0; i--) {
|
|
852
|
+
const row = tableHeaderElement.children[i];
|
|
853
|
+
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
854
|
+
const fromPath = [...tableHeaderPath, i];
|
|
855
|
+
const toPath = [...tableBodyPath, 0];
|
|
856
|
+
Transforms.moveNodes(editor, {
|
|
857
|
+
at: fromPath,
|
|
858
|
+
to: toPath,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
console.warn('Failed to unpin row:', error);
|
|
865
|
+
}
|
|
866
|
+
}, [editor, element]);
|
|
867
|
+
return {
|
|
868
|
+
addColumn,
|
|
869
|
+
addRow,
|
|
870
|
+
addColumnAndRow,
|
|
871
|
+
deleteRow,
|
|
872
|
+
deleteColumn,
|
|
873
|
+
moveRowToBody,
|
|
874
|
+
moveRowToHeader,
|
|
875
|
+
unsetColumnAsTitle,
|
|
876
|
+
setColumnAsTitle,
|
|
877
|
+
pinColumn,
|
|
878
|
+
unpinColumn,
|
|
879
|
+
pinRow,
|
|
880
|
+
unpinRow,
|
|
881
|
+
isColumnPinned,
|
|
882
|
+
isRowPinned,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export { useTableActions };
|