@quadrats/react 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/components/Quadrats.js +5 -2
- package/core/contexts/modal/CarouselModal/CarouselModal.js +14 -17
- package/index.cjs.js +19 -19
- package/package.json +5 -5
- package/table/components/ColumnDragButton.d.ts +10 -0
- package/table/components/ColumnDragButton.js +41 -0
- package/table/components/RowDragButton.d.ts +10 -0
- package/table/components/RowDragButton.js +42 -0
- package/table/components/Table.js +22 -17
- package/table/components/TableCell.js +129 -23
- package/table/components/TableDragLayer.d.ts +6 -0
- package/table/components/TableDragLayer.js +89 -0
- package/table/components/TableMain.js +14 -6
- package/table/contexts/TableActionsContext.d.ts +1 -1
- package/table/contexts/TableDragContext.d.ts +26 -0
- package/table/contexts/TableDragContext.js +26 -0
- package/table/hooks/useColumnResize.js +29 -0
- package/table/hooks/useTableActions.d.ts +2 -0
- package/table/hooks/useTableActions.js +217 -11
- package/table/hooks/useTableCellToolbarActions.js +126 -4
- package/table/index.cjs.js +873 -125
- package/table/index.js +3 -0
- package/table/table.css +1 -1
- package/table/table.scss +35 -0
- package/table/typings.d.ts +2 -0
- package/table/utils/helper.d.ts +34 -8
- package/table/utils/helper.js +178 -72
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useDragLayer } from 'react-dnd';
|
|
3
|
+
import { useTableDragContext } from '../contexts/TableDragContext.js';
|
|
4
|
+
|
|
5
|
+
const TableDragLayer = ({ scrollRef }) => {
|
|
6
|
+
const { dragState } = useTableDragContext();
|
|
7
|
+
const [columnWidths, setColumnWidths] = useState([]);
|
|
8
|
+
const [rowHeights, setRowHeights] = useState([]);
|
|
9
|
+
const { isDragging, currentOffset } = useDragLayer((monitor) => ({
|
|
10
|
+
isDragging: monitor.isDragging(),
|
|
11
|
+
currentOffset: monitor.getClientOffset(),
|
|
12
|
+
}));
|
|
13
|
+
// 計算所有 column 的寬度和 row 的高度
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!scrollRef.current || !isDragging)
|
|
16
|
+
return;
|
|
17
|
+
const tableContainer = scrollRef.current;
|
|
18
|
+
const cells = tableContainer.querySelectorAll('.qdr-table__cell');
|
|
19
|
+
if ((dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'column') {
|
|
20
|
+
// 計算每個 column 的寬度
|
|
21
|
+
const widths = [];
|
|
22
|
+
const columnCells = Array.from(cells).filter((cell) => {
|
|
23
|
+
const cellElement = cell;
|
|
24
|
+
return cellElement.dataset.columnIndex !== undefined;
|
|
25
|
+
});
|
|
26
|
+
const columnCount = Math.max(...columnCells.map((cell) => {
|
|
27
|
+
const cellElement = cell;
|
|
28
|
+
return parseInt(cellElement.dataset.columnIndex || '0', 10);
|
|
29
|
+
})) + 1;
|
|
30
|
+
for (let i = 0; i < columnCount; i++) {
|
|
31
|
+
const cellsInColumn = columnCells.filter((cell) => {
|
|
32
|
+
const cellElement = cell;
|
|
33
|
+
return parseInt(cellElement.dataset.columnIndex || '0', 10) === i;
|
|
34
|
+
});
|
|
35
|
+
if (cellsInColumn.length > 0) {
|
|
36
|
+
const firstCell = cellsInColumn[0];
|
|
37
|
+
widths[i] = firstCell.getBoundingClientRect().width;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
setColumnWidths(widths);
|
|
41
|
+
}
|
|
42
|
+
else if ((dragState === null || dragState === void 0 ? void 0 : dragState.type) === 'row') {
|
|
43
|
+
// 計算每個 row 的高度
|
|
44
|
+
const heights = [];
|
|
45
|
+
const rowCells = Array.from(cells).filter((cell) => {
|
|
46
|
+
const cellElement = cell;
|
|
47
|
+
return cellElement.dataset.rowIndex !== undefined;
|
|
48
|
+
});
|
|
49
|
+
const rowCount = Math.max(...rowCells.map((cell) => {
|
|
50
|
+
const cellElement = cell;
|
|
51
|
+
return parseInt(cellElement.dataset.rowIndex || '0', 10);
|
|
52
|
+
})) + 1;
|
|
53
|
+
for (let i = 0; i < rowCount; i++) {
|
|
54
|
+
const cellsInRow = rowCells.filter((cell) => {
|
|
55
|
+
const cellElement = cell;
|
|
56
|
+
return parseInt(cellElement.dataset.rowIndex || '0', 10) === i;
|
|
57
|
+
});
|
|
58
|
+
if (cellsInRow.length > 0) {
|
|
59
|
+
const firstCell = cellsInRow[0];
|
|
60
|
+
heights[i] = firstCell.getBoundingClientRect().height;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
setRowHeights(heights);
|
|
64
|
+
}
|
|
65
|
+
}, [isDragging, dragState, scrollRef]);
|
|
66
|
+
if (!isDragging || !dragState || !currentOffset || !scrollRef.current) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const tableContainer = scrollRef.current;
|
|
70
|
+
const tableRect = tableContainer.getBoundingClientRect();
|
|
71
|
+
if (dragState.type) {
|
|
72
|
+
const sourceIndex = dragState.type === 'column' ? dragState.columnIndex : dragState.rowIndex;
|
|
73
|
+
const rowHeight = dragState.type === 'column' ? tableRect.height : rowHeights[sourceIndex];
|
|
74
|
+
const columnWidth = dragState.type === 'column' ? columnWidths[sourceIndex] : tableRect.width;
|
|
75
|
+
return (React.createElement("div", { className: "qdr-table__drag-overlay", style: {
|
|
76
|
+
left: tableRect.left,
|
|
77
|
+
top: tableRect.top,
|
|
78
|
+
width: columnWidth,
|
|
79
|
+
height: rowHeight,
|
|
80
|
+
transform: dragState.type === 'column'
|
|
81
|
+
? `translateX(${currentOffset.x - tableRect.left - columnWidth / 2}px)`
|
|
82
|
+
: `translateY(${currentOffset.y - tableRect.top - rowHeight / 2}px)`,
|
|
83
|
+
} },
|
|
84
|
+
React.createElement("div", { className: "qdr-table__drag-overlay-content" })));
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { TableDragLayer };
|
|
@@ -2,16 +2,18 @@ import React, { useRef, useState, useMemo, useEffect, useCallback } from 'react'
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useModal, useSlateStatic, ReactEditor } from '@quadrats/react';
|
|
4
4
|
import { Icon } from '@quadrats/react/components';
|
|
5
|
-
import {
|
|
5
|
+
import { AlignLeft, AlignRight, AlignCenter, Copy, Trash, Plus } from '@quadrats/icons';
|
|
6
6
|
import { useTableActionsContext } from '../hooks/useTableActionsContext.js';
|
|
7
7
|
import { useTableMetadata } from '../hooks/useTableMetadata.js';
|
|
8
8
|
import { useTableStateContext } from '../hooks/useTableStateContext.js';
|
|
9
|
+
import { useTableDragContext } from '../contexts/TableDragContext.js';
|
|
9
10
|
import { InlineToolbar, ToolbarGroupIcon, ToolbarIcon } from '@quadrats/react/toolbar';
|
|
10
11
|
import { Transforms } from 'slate';
|
|
11
12
|
import { calculateTableMinWidth, columnWidthToCSS } from '@quadrats/common/table';
|
|
12
13
|
import { TableScrollContext } from '../contexts/TableScrollContext.js';
|
|
13
14
|
import { useTableCellAlign, useTableCellAlignStatus } from '../hooks/useTableCell.js';
|
|
14
15
|
import { getTableElements, getColumnWidths } from '../utils/helper.js';
|
|
16
|
+
import { TableDragLayer } from './TableDragLayer.js';
|
|
15
17
|
|
|
16
18
|
function TableMain(props) {
|
|
17
19
|
const { attributes, children } = props;
|
|
@@ -20,6 +22,7 @@ function TableMain(props) {
|
|
|
20
22
|
const { addColumn, addRow, addColumnAndRow } = useTableActionsContext();
|
|
21
23
|
const { isReachMaximumColumns, isReachMaximumRows, tableElement } = useTableMetadata();
|
|
22
24
|
const { tableSelectedOn, setTableSelectedOn } = useTableStateContext();
|
|
25
|
+
const { dragState } = useTableDragContext();
|
|
23
26
|
// Table align functions
|
|
24
27
|
const setAlign = useTableCellAlign(tableElement, editor);
|
|
25
28
|
const getAlign = useTableCellAlignStatus(tableElement, editor);
|
|
@@ -63,10 +66,14 @@ function TableMain(props) {
|
|
|
63
66
|
const handleScroll = () => {
|
|
64
67
|
setScrollTop(scrollContainer.scrollTop);
|
|
65
68
|
setScrollLeft(scrollContainer.scrollLeft);
|
|
66
|
-
//
|
|
69
|
+
// 如果正在更新滾動位置,不要觸發 Slate 更新
|
|
67
70
|
if (isUpdatingScrollRef.current) {
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
73
|
+
// 如果正在拖曳,不要觸發 Slate 更新(避免 transform 導致事件遺失)
|
|
74
|
+
if (dragState) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
70
77
|
// 使用 debounce 來減少 Slate 更新頻率
|
|
71
78
|
if (scrollUpdateTimerRef.current) {
|
|
72
79
|
clearTimeout(scrollUpdateTimerRef.current);
|
|
@@ -89,7 +96,7 @@ function TableMain(props) {
|
|
|
89
96
|
clearTimeout(scrollUpdateTimerRef.current);
|
|
90
97
|
}
|
|
91
98
|
};
|
|
92
|
-
}, [editor, tableElement]);
|
|
99
|
+
}, [editor, tableElement, dragState]);
|
|
93
100
|
// 只在 columnWidths 改變時恢復滾動位置
|
|
94
101
|
useEffect(() => {
|
|
95
102
|
const { current: scrollContainer } = scrollRef;
|
|
@@ -133,7 +140,7 @@ function TableMain(props) {
|
|
|
133
140
|
// 獲取當前 table 的 align 狀態
|
|
134
141
|
const currentTableAlign = getAlign('table');
|
|
135
142
|
// 根據當前 table align 狀態選擇對應的 icon
|
|
136
|
-
const getCurrentTableAlignIcon = () => {
|
|
143
|
+
const getCurrentTableAlignIcon = useCallback(() => {
|
|
137
144
|
switch (currentTableAlign) {
|
|
138
145
|
case 'left':
|
|
139
146
|
return AlignLeft;
|
|
@@ -144,7 +151,7 @@ function TableMain(props) {
|
|
|
144
151
|
default:
|
|
145
152
|
return AlignLeft;
|
|
146
153
|
}
|
|
147
|
-
};
|
|
154
|
+
}, [currentTableAlign]);
|
|
148
155
|
return (React.createElement("div", { className: clsx('qdr-table__mainWrapper', {
|
|
149
156
|
'qdr-table__mainWrapper--selected': (tableSelectedOn === null || tableSelectedOn === void 0 ? void 0 : tableSelectedOn.region) === 'table',
|
|
150
157
|
}) },
|
|
@@ -206,7 +213,8 @@ function TableMain(props) {
|
|
|
206
213
|
width: columnWidthToCSS(width),
|
|
207
214
|
minWidth: columnWidthToCSS(width),
|
|
208
215
|
} })))),
|
|
209
|
-
children))
|
|
216
|
+
children)),
|
|
217
|
+
React.createElement(TableDragLayer, { scrollRef: scrollRef })),
|
|
210
218
|
React.createElement("div", { className: "qdr-table__size-indicators" }, firstRowCells === null || firstRowCells === void 0 ? void 0 : firstRowCells.map((cell, colIndex) => (React.createElement("div", { key: colIndex, className: "qdr-table__size-indicator", style: {
|
|
211
219
|
width: columnWidthToCSS(columnWidths[colIndex]),
|
|
212
220
|
minWidth: columnWidthToCSS(columnWidths[colIndex]),
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { TableContextType } from '../typings';
|
|
2
|
-
export type TableActionsContextType = Pick<TableContextType, 'addColumn' | 'addRow' | 'addColumnAndRow' | 'deleteRow' | 'deleteColumn' | 'moveRowToBody' | 'moveRowToHeader' | 'unsetColumnAsTitle' | 'setColumnAsTitle' | 'pinColumn' | 'unpinColumn' | 'pinRow' | 'unpinRow'>;
|
|
2
|
+
export type TableActionsContextType = Pick<TableContextType, 'addColumn' | 'addRow' | 'addColumnAndRow' | 'deleteRow' | 'deleteColumn' | 'moveRowToBody' | 'moveRowToHeader' | 'unsetColumnAsTitle' | 'setColumnAsTitle' | 'pinColumn' | 'unpinColumn' | 'pinRow' | 'unpinRow' | 'moveOrSwapRow' | 'moveOrSwapColumn'>;
|
|
3
3
|
export declare const TableActionsContext: import("react").Context<TableActionsContextType | undefined>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
interface RowDragState {
|
|
3
|
+
type: 'row';
|
|
4
|
+
rowIndex: number;
|
|
5
|
+
isInHeader: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface ColumnDragState {
|
|
8
|
+
type: 'column';
|
|
9
|
+
columnIndex: number;
|
|
10
|
+
isTitle: boolean;
|
|
11
|
+
}
|
|
12
|
+
type DragState = RowDragState | ColumnDragState | null;
|
|
13
|
+
type DragDirection = 'up' | 'down' | 'left' | 'right' | null;
|
|
14
|
+
interface TableDragContextValue {
|
|
15
|
+
dragState: DragState;
|
|
16
|
+
setDragState: Dispatch<SetStateAction<DragState>>;
|
|
17
|
+
dropTargetIndex: number | null;
|
|
18
|
+
setDropTargetIndex: Dispatch<SetStateAction<number | null>>;
|
|
19
|
+
dragDirection: DragDirection;
|
|
20
|
+
setDragDirection: Dispatch<SetStateAction<DragDirection>>;
|
|
21
|
+
}
|
|
22
|
+
export declare const TableDragProvider: React.FC<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>;
|
|
25
|
+
export declare const useTableDragContext: () => TableDragContextValue;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
const TableDragContext = createContext(null);
|
|
4
|
+
const TableDragProvider = ({ children }) => {
|
|
5
|
+
const [dragState, setDragState] = useState(null);
|
|
6
|
+
const [dropTargetIndex, setDropTargetIndex] = useState(null);
|
|
7
|
+
const [dragDirection, setDragDirection] = useState(null);
|
|
8
|
+
const value = useMemo(() => ({
|
|
9
|
+
dragState,
|
|
10
|
+
setDragState,
|
|
11
|
+
dropTargetIndex,
|
|
12
|
+
setDropTargetIndex,
|
|
13
|
+
dragDirection,
|
|
14
|
+
setDragDirection,
|
|
15
|
+
}), [dragState, dropTargetIndex, dragDirection]);
|
|
16
|
+
return React.createElement(TableDragContext.Provider, { value: value }, children);
|
|
17
|
+
};
|
|
18
|
+
const useTableDragContext = () => {
|
|
19
|
+
const context = useContext(TableDragContext);
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error('useTableDragContext must be used within TableDragProvider');
|
|
22
|
+
}
|
|
23
|
+
return context;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { TableDragProvider, useTableDragContext };
|
|
@@ -80,6 +80,35 @@ function useColumnResize({ tableElement, columnIndex, cellRef }) {
|
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
+
// 如果有 pinned columns,重新計算所有 pinned cells 的 left 位置
|
|
84
|
+
if (pinnedColumnIndices.length > 0) {
|
|
85
|
+
const allRows = tableDOMElement.querySelectorAll('tr');
|
|
86
|
+
const scrollContainer = tableDOMElement.closest('.qdr-table__scrollContainer');
|
|
87
|
+
const containerRect = scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.getBoundingClientRect();
|
|
88
|
+
const containerWidth = (containerRect === null || containerRect === void 0 ? void 0 : containerRect.width) || tableWidth;
|
|
89
|
+
// 為每個 pinned column 計算應該的絕對位置
|
|
90
|
+
pinnedColumnIndices.forEach((pinnedIndex) => {
|
|
91
|
+
// 計算此 column 之前所有 pinned columns 的累積寬度
|
|
92
|
+
let accumulatedLeft = 0;
|
|
93
|
+
for (let i = 0; i < pinnedIndex; i++) {
|
|
94
|
+
if (pinnedColumnIndices.includes(i)) {
|
|
95
|
+
const width = newWidths[i];
|
|
96
|
+
if (width.type === 'percentage') {
|
|
97
|
+
const pixelWidth = (containerWidth * width.value) / 100;
|
|
98
|
+
accumulatedLeft += pixelWidth;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 更新該 column 所有 cells 的 left 位置
|
|
103
|
+
allRows.forEach((row) => {
|
|
104
|
+
const cells = row.querySelectorAll('td, th');
|
|
105
|
+
const targetCell = cells[pinnedIndex];
|
|
106
|
+
if (targetCell && targetCell.classList.contains('qdr-table__cell--pinned')) {
|
|
107
|
+
targetCell.style.left = `${accumulatedLeft}px`;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
83
112
|
// 更新 size indicators
|
|
84
113
|
const sizeIndicatorsContainer = mainWrapper === null || mainWrapper === void 0 ? void 0 : mainWrapper.querySelector('.qdr-table__size-indicators');
|
|
85
114
|
if (sizeIndicatorsContainer) {
|
|
@@ -22,4 +22,6 @@ export declare function useTableActions(element: RenderTableElementProps['elemen
|
|
|
22
22
|
unpinRow: () => void;
|
|
23
23
|
isColumnPinned: (columnIndex: number) => boolean;
|
|
24
24
|
isRowPinned: (rowIndex: number) => boolean;
|
|
25
|
+
moveOrSwapRow: (sourceRowIndex: number, targetRowIndex: number, mode?: "swap" | "move") => void;
|
|
26
|
+
moveOrSwapColumn: (sourceColumnIndex: number, targetColumnIndex: number, mode?: "swap" | "move") => void;
|
|
25
27
|
};
|
|
@@ -3,7 +3,7 @@ import { Element, Editor, Transforms } from '@quadrats/core';
|
|
|
3
3
|
import { ReactEditor } from 'slate-react';
|
|
4
4
|
import { TABLE_ROW_TYPE, TABLE_CELL_TYPE, TABLE_DEFAULT_MAX_COLUMNS, TABLE_HEADER_TYPE } from '@quadrats/common/table';
|
|
5
5
|
import { useQuadrats } from '@quadrats/react';
|
|
6
|
-
import { getTableStructure, createTableCell, getColumnWidths, calculateColumnWidthsAfterAdd, setColumnWidths, getReferenceRowFromHeaderOrBody, calculateColumnWidthsAfterDelete, hasAnyPinnedRows,
|
|
6
|
+
import { getTableStructure, createTableCell, getColumnWidths, getPinnedColumnsInfo, calculateColumnWidthsAfterAdd, setColumnWidths, getReferenceRowFromHeaderOrBody, calculateColumnWidthsAfterDelete, hasAnyPinnedRows, moveOrSwapColumnWidth, convertToMixedWidthMode, hasAnyPinnedColumns, convertToPercentageMode } from '../utils/helper.js';
|
|
7
7
|
|
|
8
8
|
function useTableActions(element) {
|
|
9
9
|
const editor = useQuadrats();
|
|
@@ -120,7 +120,9 @@ function useTableActions(element) {
|
|
|
120
120
|
// 調整欄位寬度
|
|
121
121
|
const currentWidths = getColumnWidths(element);
|
|
122
122
|
if (currentWidths.length > 0) {
|
|
123
|
-
|
|
123
|
+
// 獲取當前的 pinned columns 資訊
|
|
124
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
125
|
+
const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices, columnIndex);
|
|
124
126
|
setColumnWidths(editor, element, newWidths);
|
|
125
127
|
}
|
|
126
128
|
});
|
|
@@ -256,7 +258,9 @@ function useTableActions(element) {
|
|
|
256
258
|
if (currentWidths.length > 0) {
|
|
257
259
|
// 新欄位插入在最後(columnCount 位置)
|
|
258
260
|
const insertIndex = columnCount;
|
|
259
|
-
|
|
261
|
+
// 獲取當前的 pinned columns 資訊
|
|
262
|
+
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
263
|
+
const newWidths = calculateColumnWidthsAfterAdd(currentWidths, insertIndex, pinnedColumnIndices);
|
|
260
264
|
setColumnWidths(editor, element, newWidths);
|
|
261
265
|
}
|
|
262
266
|
});
|
|
@@ -521,7 +525,7 @@ function useTableActions(element) {
|
|
|
521
525
|
// 調整 columnWidths:將 columnIndex 的寬度移動到 actualTargetIndex
|
|
522
526
|
const currentWidths = getColumnWidths(element);
|
|
523
527
|
if (currentWidths.length > 0) {
|
|
524
|
-
const movedWidths =
|
|
528
|
+
const movedWidths = moveOrSwapColumnWidth(currentWidths, columnIndex, actualTargetIndex, 'move');
|
|
525
529
|
// 檢查移動後是否還有 pinned columns
|
|
526
530
|
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
527
531
|
// 更新釘選欄位索引(移除當前欄位,並調整其他欄位的索引)
|
|
@@ -579,7 +583,7 @@ function useTableActions(element) {
|
|
|
579
583
|
if (!tableStructure)
|
|
580
584
|
return;
|
|
581
585
|
const { tableHeaderElement, tableBodyElement, tableMainElement } = tableStructure;
|
|
582
|
-
// 檢查是否已有 pinned columns
|
|
586
|
+
// 檢查是否已有 pinned columns
|
|
583
587
|
const hasExistingPinnedColumns = hasAnyPinnedColumns(tableStructure);
|
|
584
588
|
// 如果有現有的 pinned columns 且沒有提供自定義屬性,自動設置 pinned 以保持一致性
|
|
585
589
|
const finalProps = customProps || (hasExistingPinnedColumns ? { pinned: true } : undefined);
|
|
@@ -626,10 +630,9 @@ function useTableActions(element) {
|
|
|
626
630
|
});
|
|
627
631
|
}
|
|
628
632
|
});
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (columnIndex !== targetColumnIndex) {
|
|
633
|
+
// 檢查是否需要移動位置
|
|
634
|
+
const needsMove = columnIndex >= targetColumnIndex && columnIndex !== targetColumnIndex;
|
|
635
|
+
if (needsMove) {
|
|
633
636
|
for (let rowIndex = containerElement.children.length - 1; rowIndex >= 0; rowIndex--) {
|
|
634
637
|
const row = containerElement.children[rowIndex];
|
|
635
638
|
if (Element.isElement(row) && row.type.includes(TABLE_ROW_TYPE)) {
|
|
@@ -644,7 +647,7 @@ function useTableActions(element) {
|
|
|
644
647
|
// 調整 columnWidths:將 columnIndex 的寬度移動到 targetColumnIndex
|
|
645
648
|
const currentWidths = getColumnWidths(element);
|
|
646
649
|
if (currentWidths.length > 0) {
|
|
647
|
-
const movedWidths =
|
|
650
|
+
const movedWidths = moveOrSwapColumnWidth(currentWidths, columnIndex, targetColumnIndex, 'move');
|
|
648
651
|
// 如果設定了 pinned,需要轉換為混合模式
|
|
649
652
|
if ((finalProps === null || finalProps === void 0 ? void 0 : finalProps.pinned) && tableWidth > 0) {
|
|
650
653
|
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
@@ -673,7 +676,23 @@ function useTableActions(element) {
|
|
|
673
676
|
const currentWidths = getColumnWidths(element);
|
|
674
677
|
if (currentWidths.length > 0) {
|
|
675
678
|
const { pinnedColumnIndices } = getPinnedColumnsInfo(element);
|
|
676
|
-
|
|
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);
|
|
677
696
|
const mixedWidths = convertToMixedWidthMode(currentWidths, updatedPinnedIndices, tableWidth);
|
|
678
697
|
setColumnWidths(editor, element, mixedWidths);
|
|
679
698
|
}
|
|
@@ -753,6 +772,10 @@ function useTableActions(element) {
|
|
|
753
772
|
if (tableBodyElement) {
|
|
754
773
|
processContainer(tableBodyElement);
|
|
755
774
|
}
|
|
775
|
+
// 轉換回純百分比模式
|
|
776
|
+
const currentWidths = getColumnWidths(element);
|
|
777
|
+
const percentageWidths = convertToPercentageMode(currentWidths);
|
|
778
|
+
setColumnWidths(editor, element, percentageWidths);
|
|
756
779
|
}
|
|
757
780
|
catch (error) {
|
|
758
781
|
console.warn('Failed to unpin column:', error);
|
|
@@ -864,6 +887,187 @@ function useTableActions(element) {
|
|
|
864
887
|
console.warn('Failed to unpin row:', error);
|
|
865
888
|
}
|
|
866
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]);
|
|
867
1071
|
return {
|
|
868
1072
|
addColumn,
|
|
869
1073
|
addRow,
|
|
@@ -880,6 +1084,8 @@ function useTableActions(element) {
|
|
|
880
1084
|
unpinRow,
|
|
881
1085
|
isColumnPinned,
|
|
882
1086
|
isRowPinned,
|
|
1087
|
+
moveOrSwapRow,
|
|
1088
|
+
moveOrSwapColumn,
|
|
883
1089
|
};
|
|
884
1090
|
}
|
|
885
1091
|
|