@lobehub/editor 4.15.2 → 4.16.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/es/editor-kernel/react/useDecorators.js +14 -8
- package/es/headless.d.ts +2 -0
- package/es/headless.js +710 -46
- package/es/index.d.ts +4 -3
- package/es/index.js +4 -3
- package/es/locale/index.d.ts +2 -0
- package/es/locale/index.js +5 -1
- package/es/plugins/auto-complete/plugin/index.js +3 -3
- package/es/plugins/block/index.d.ts +1 -1
- package/es/plugins/block/plugin/index.js +78 -2
- package/es/plugins/block/react/ReactBlockPlugin.js +172 -16
- package/es/plugins/block/react/drag/drag-utils.js +37 -10
- package/es/plugins/block/service/i-block-menu-service.d.ts +18 -1
- package/es/plugins/block/service/i-block-menu-service.js +24 -0
- package/es/plugins/block/service/index.d.ts +1 -1
- package/es/plugins/codeblock/plugin/index.js +25 -1
- package/es/plugins/common/plugin/register.js +2 -2
- package/es/plugins/litexml/plugin/index.js +8 -2
- package/es/plugins/slash/plugin/index.js +1 -1
- package/es/plugins/slash/react/ReactSlashPlugin.js +4 -4
- package/es/plugins/table/command/index.d.ts +13 -1
- package/es/plugins/table/command/index.js +220 -39
- package/es/plugins/table/index.d.ts +3 -2
- package/es/plugins/table/node/index.d.ts +2 -0
- package/es/plugins/table/node/index.js +130 -2
- package/es/plugins/table/plugin/index.d.ts +6 -0
- package/es/plugins/table/plugin/index.js +193 -4
- package/es/plugins/table/react/TableActionMenu/ActionMenu.js +82 -6
- package/es/plugins/table/react/TableActionMenu/index.js +9 -4
- package/es/plugins/table/react/TableColController.js +354 -0
- package/es/plugins/table/react/TableController/hooks.js +201 -0
- package/es/plugins/table/react/TableController/style.js +264 -0
- package/es/plugins/table/react/TableController/utils.js +25 -0
- package/es/plugins/table/react/TableControllerButton.js +81 -0
- package/es/plugins/table/react/TableControllerMenu.js +123 -0
- package/es/plugins/table/react/TableInsertButton.js +25 -0
- package/es/plugins/table/react/TableResize/index.js +153 -78
- package/es/plugins/table/react/TableRowController.js +349 -0
- package/es/plugins/table/react/hooks.js +77 -0
- package/es/plugins/table/react/index.js +139 -16
- package/es/plugins/table/react/style.js +89 -8
- package/es/plugins/table/react/type.d.ts +2 -0
- package/es/plugins/table/react/useAutoFitPastedTable.js +189 -0
- package/es/plugins/table/service/i-table-controller-menu-service.d.ts +44 -0
- package/es/plugins/table/service/i-table-controller-menu-service.js +31 -0
- package/es/plugins/table/service/index.d.ts +1 -0
- package/es/plugins/table/utils/autoFitColumnWidth.js +87 -0
- package/es/plugins/table/utils/distributeColumnWidth.js +37 -0
- package/es/plugins/table/utils/index.js +102 -2
- package/es/react/EditorProvider/index.d.ts +2 -2
- package/es/renderer/LexicalDiff.d.ts +2 -2
- package/package.json +1 -1
|
@@ -1,19 +1,36 @@
|
|
|
1
|
+
import { createDefaultTableColWidths, syncTableWidthDOM } from "../../utils/index.js";
|
|
1
2
|
import { styles } from "./style.js";
|
|
2
3
|
import { getCellColumnIndex, getCellNodeHeight } from "./utils.js";
|
|
3
4
|
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
4
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { cssVar, cx } from "antd-style";
|
|
6
|
-
import { $computeTableMapSkipCellCheck, $getTableNodeFromLexicalNodeOrThrow, $getTableRowIndexFromTableCellNode, $isTableCellNode, $isTableRowNode, TableNode, getDOMCellFromTarget, getTableElement } from "@lexical/table";
|
|
7
|
-
import { $getNearestNodeFromDOMNode, SKIP_SCROLL_INTO_VIEW_TAG, isHTMLElement } from "lexical";
|
|
7
|
+
import { $computeTableMapSkipCellCheck, $getTableNodeFromLexicalNodeOrThrow, $getTableRowIndexFromTableCellNode, $isTableCellNode, $isTableNode, $isTableRowNode, TableNode, getDOMCellFromTarget, getTableElement } from "@lexical/table";
|
|
8
|
+
import { $getNearestNodeFromDOMNode, $getNodeByKey, HISTORIC_TAG, SKIP_SCROLL_INTO_VIEW_TAG, isHTMLElement } from "lexical";
|
|
8
9
|
import { calculateZoomLevel, mergeRegister } from "@lexical/utils";
|
|
9
10
|
import { createPortal } from "react-dom";
|
|
10
11
|
//#region src/plugins/table/react/TableResize/index.tsx
|
|
11
|
-
const
|
|
12
|
+
const isHeightChanging = (direction) => {
|
|
13
|
+
return direction === "bottom";
|
|
14
|
+
};
|
|
15
|
+
const getTableColWidths = (tableNode) => {
|
|
16
|
+
return tableNode.getColWidths() || createDefaultTableColWidths(tableNode.getColumnCount());
|
|
17
|
+
};
|
|
18
|
+
const $getTableNodeByKey = (tableKey) => {
|
|
19
|
+
const node = $getNodeByKey(tableKey);
|
|
20
|
+
return $isTableNode(node) ? node : null;
|
|
21
|
+
};
|
|
22
|
+
const getCurrentTableRect = (activeCell, fallbackRect) => {
|
|
23
|
+
return activeCell.elem.closest("table.editor_table, table")?.getBoundingClientRect() ?? fallbackRect;
|
|
24
|
+
};
|
|
25
|
+
const getResizeUpdateTags = (skipHistory) => {
|
|
26
|
+
return skipHistory ? [SKIP_SCROLL_INTO_VIEW_TAG, HISTORIC_TAG] : SKIP_SCROLL_INTO_VIEW_TAG;
|
|
27
|
+
};
|
|
28
|
+
const TableCellResize = memo(({ editor, eventEmitter, resizeMode }) => {
|
|
12
29
|
const targetRef = useRef(null);
|
|
13
30
|
const resizerRef = useRef(null);
|
|
14
31
|
const tableRectRef = useRef(null);
|
|
15
32
|
const [hasTable, setHasTable] = useState(false);
|
|
16
|
-
const
|
|
33
|
+
const resizeStartStateRef = useRef(null);
|
|
17
34
|
const [pointerCurrentPos, updatePointerCurrentPos] = useState(null);
|
|
18
35
|
const [activeCell, updateActiveCell] = useState(null);
|
|
19
36
|
const [draggingDirection, updateDraggingDirection] = useState(null);
|
|
@@ -21,7 +38,8 @@ const TableCellResize = memo(({ editor, eventEmitter }) => {
|
|
|
21
38
|
updateActiveCell(null);
|
|
22
39
|
targetRef.current = null;
|
|
23
40
|
updateDraggingDirection(null);
|
|
24
|
-
|
|
41
|
+
updatePointerCurrentPos(null);
|
|
42
|
+
resizeStartStateRef.current = null;
|
|
25
43
|
tableRectRef.current = null;
|
|
26
44
|
}, []);
|
|
27
45
|
useEffect(() => {
|
|
@@ -30,13 +48,108 @@ const TableCellResize = memo(({ editor, eventEmitter }) => {
|
|
|
30
48
|
for (const [nodeKey, mutation] of nodeMutations) if (mutation === "destroyed") tableKeys.delete(nodeKey);
|
|
31
49
|
else tableKeys.add(nodeKey);
|
|
32
50
|
setHasTable(tableKeys.size > 0);
|
|
33
|
-
}), editor.registerNodeTransform(TableNode, (tableNode) => {
|
|
34
|
-
if (tableNode.getColWidths()) return tableNode;
|
|
35
|
-
const numColumns = tableNode.getColumnCount();
|
|
36
|
-
tableNode.setColWidths(new Array(numColumns).fill(92));
|
|
37
|
-
return tableNode;
|
|
38
51
|
}));
|
|
39
52
|
}, [editor]);
|
|
53
|
+
const getResizeStartState = useCallback((direction, startPos) => {
|
|
54
|
+
if (!activeCell) throw new Error("TableCellResizer: Expected active cell.");
|
|
55
|
+
let resizeState = null;
|
|
56
|
+
editor.getEditorState().read(() => {
|
|
57
|
+
const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem);
|
|
58
|
+
if (!$isTableCellNode(tableCellNode)) throw new Error("TableCellResizer: Table cell node not found.");
|
|
59
|
+
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
60
|
+
if (!isHeightChanging(direction)) {
|
|
61
|
+
const [tableMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
|
|
62
|
+
const columnIndex = getCellColumnIndex(tableCellNode, tableMap);
|
|
63
|
+
if (columnIndex === void 0) throw new Error("TableCellResizer: Table column not found.");
|
|
64
|
+
const width = getTableColWidths(tableNode)[columnIndex] ?? 92;
|
|
65
|
+
resizeState = {
|
|
66
|
+
...startPos,
|
|
67
|
+
columnIndex,
|
|
68
|
+
direction: "right",
|
|
69
|
+
size: width,
|
|
70
|
+
tableKey: tableNode.getKey()
|
|
71
|
+
};
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const baseRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode);
|
|
75
|
+
const tableRows = tableNode.getChildren();
|
|
76
|
+
const rowIndex = tableCellNode.getColSpan() === tableNode.getColumnCount() ? baseRowIndex : baseRowIndex + tableCellNode.getRowSpan() - 1;
|
|
77
|
+
if (rowIndex >= tableRows.length || rowIndex < 0) throw new Error("Expected table cell to be inside of table row.");
|
|
78
|
+
const tableRow = tableRows[rowIndex];
|
|
79
|
+
if (!$isTableRowNode(tableRow)) throw new Error("Expected table row");
|
|
80
|
+
let height = tableRow.getHeight();
|
|
81
|
+
if (height === void 0) {
|
|
82
|
+
const rowCells = tableRow.getChildren();
|
|
83
|
+
height = Math.min(...rowCells.map((cell) => getCellNodeHeight(cell, editor) ?? Infinity));
|
|
84
|
+
}
|
|
85
|
+
resizeState = {
|
|
86
|
+
...startPos,
|
|
87
|
+
direction: "bottom",
|
|
88
|
+
rowIndex,
|
|
89
|
+
size: height,
|
|
90
|
+
tableKey: tableNode.getKey()
|
|
91
|
+
};
|
|
92
|
+
}, { editor });
|
|
93
|
+
return resizeState;
|
|
94
|
+
}, [activeCell, editor]);
|
|
95
|
+
const updateRowHeight = useCallback((tableKey, rowIndex, nextHeight, skipHistory = false) => {
|
|
96
|
+
let didUpdate = false;
|
|
97
|
+
editor.update(() => {
|
|
98
|
+
const tableNode = $getTableNodeByKey(tableKey);
|
|
99
|
+
if (!tableNode) return;
|
|
100
|
+
const tableRows = tableNode.getChildren();
|
|
101
|
+
if (rowIndex >= tableRows.length || rowIndex < 0) throw new Error("Expected table cell to be inside of table row.");
|
|
102
|
+
const tableRow = tableRows[rowIndex];
|
|
103
|
+
if (!$isTableRowNode(tableRow)) throw new Error("Expected table row");
|
|
104
|
+
let height = tableRow.getHeight();
|
|
105
|
+
if (height === void 0) {
|
|
106
|
+
const rowCells = tableRow.getChildren();
|
|
107
|
+
height = Math.min(...rowCells.map((cell) => getCellNodeHeight(cell, editor) ?? Infinity));
|
|
108
|
+
}
|
|
109
|
+
const newHeight = Math.max(nextHeight, 33);
|
|
110
|
+
tableRow.setHeight(newHeight);
|
|
111
|
+
didUpdate = true;
|
|
112
|
+
eventEmitter.emit("table:resize", {
|
|
113
|
+
heightChange: newHeight - height,
|
|
114
|
+
newHeight
|
|
115
|
+
});
|
|
116
|
+
}, { tag: getResizeUpdateTags(skipHistory) });
|
|
117
|
+
return didUpdate;
|
|
118
|
+
}, [editor, eventEmitter]);
|
|
119
|
+
const updateColumnWidth = useCallback((tableKey, columnIndex, nextWidth, skipHistory = false) => {
|
|
120
|
+
let didUpdate = false;
|
|
121
|
+
editor.update(() => {
|
|
122
|
+
const tableNode = $getTableNodeByKey(tableKey);
|
|
123
|
+
if (!tableNode) return;
|
|
124
|
+
const colWidths = getTableColWidths(tableNode);
|
|
125
|
+
if (colWidths[columnIndex] === void 0) return;
|
|
126
|
+
const newColWidths = [...colWidths];
|
|
127
|
+
newColWidths[columnIndex] = Math.max(nextWidth, 92);
|
|
128
|
+
tableNode.setColWidths(newColWidths);
|
|
129
|
+
didUpdate = true;
|
|
130
|
+
requestAnimationFrame(() => {
|
|
131
|
+
syncTableWidthDOM(editor, tableKey, newColWidths);
|
|
132
|
+
eventEmitter.emit("table:resize", { newColWidths });
|
|
133
|
+
});
|
|
134
|
+
}, { tag: getResizeUpdateTags(skipHistory) });
|
|
135
|
+
return didUpdate;
|
|
136
|
+
}, [editor, eventEmitter]);
|
|
137
|
+
const commitResizeChange = useCallback((currentPos, startState, target, options) => {
|
|
138
|
+
const zoom = calculateZoomLevel(target);
|
|
139
|
+
const skipHistory = options?.skipHistory ?? resizeMode === "realtime";
|
|
140
|
+
if (startState.direction === "bottom") {
|
|
141
|
+
const heightChange = (currentPos.y - startState.y) / zoom;
|
|
142
|
+
if (heightChange === 0) return false;
|
|
143
|
+
return updateRowHeight(startState.tableKey, startState.rowIndex, startState.size + heightChange, skipHistory);
|
|
144
|
+
}
|
|
145
|
+
const widthChange = (currentPos.x - startState.x) / zoom;
|
|
146
|
+
if (widthChange === 0) return false;
|
|
147
|
+
return updateColumnWidth(startState.tableKey, startState.columnIndex, startState.size + widthChange, skipHistory);
|
|
148
|
+
}, [
|
|
149
|
+
resizeMode,
|
|
150
|
+
updateColumnWidth,
|
|
151
|
+
updateRowHeight
|
|
152
|
+
]);
|
|
40
153
|
useEffect(() => {
|
|
41
154
|
if (!hasTable) return;
|
|
42
155
|
const onPointerMove = (event) => {
|
|
@@ -45,10 +158,12 @@ const TableCellResize = memo(({ editor, eventEmitter }) => {
|
|
|
45
158
|
if (draggingDirection) {
|
|
46
159
|
event.preventDefault();
|
|
47
160
|
event.stopPropagation();
|
|
48
|
-
|
|
161
|
+
const currentPos = {
|
|
49
162
|
x: event.clientX,
|
|
50
163
|
y: event.clientY
|
|
51
|
-
}
|
|
164
|
+
};
|
|
165
|
+
updatePointerCurrentPos(currentPos);
|
|
166
|
+
if (resizeMode === "realtime" && activeCell && resizeStartStateRef.current) commitResizeChange(currentPos, resizeStartStateRef.current, activeCell.elem);
|
|
52
167
|
return;
|
|
53
168
|
}
|
|
54
169
|
if (resizerRef.current && resizerRef.current.contains(target)) return;
|
|
@@ -85,72 +200,24 @@ const TableCellResize = memo(({ editor, eventEmitter }) => {
|
|
|
85
200
|
};
|
|
86
201
|
}, [
|
|
87
202
|
activeCell,
|
|
203
|
+
commitResizeChange,
|
|
88
204
|
draggingDirection,
|
|
89
205
|
editor,
|
|
206
|
+
hasTable,
|
|
90
207
|
resetState,
|
|
91
|
-
|
|
208
|
+
resizeMode
|
|
92
209
|
]);
|
|
93
|
-
const
|
|
94
|
-
if (direction === "bottom") return true;
|
|
95
|
-
return false;
|
|
96
|
-
};
|
|
97
|
-
const updateRowHeight = useCallback((heightChange) => {
|
|
98
|
-
if (!activeCell) throw new Error("TableCellResizer: Expected active cell.");
|
|
99
|
-
editor.update(() => {
|
|
100
|
-
const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem);
|
|
101
|
-
if (!$isTableCellNode(tableCellNode)) throw new Error("TableCellResizer: Table cell node not found.");
|
|
102
|
-
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
103
|
-
const baseRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode);
|
|
104
|
-
const tableRows = tableNode.getChildren();
|
|
105
|
-
const tableRowIndex = tableCellNode.getColSpan() === tableNode.getColumnCount() ? baseRowIndex : baseRowIndex + tableCellNode.getRowSpan() - 1;
|
|
106
|
-
if (tableRowIndex >= tableRows.length || tableRowIndex < 0) throw new Error("Expected table cell to be inside of table row.");
|
|
107
|
-
const tableRow = tableRows[tableRowIndex];
|
|
108
|
-
if (!$isTableRowNode(tableRow)) throw new Error("Expected table row");
|
|
109
|
-
let height = tableRow.getHeight();
|
|
110
|
-
if (height === void 0) {
|
|
111
|
-
const rowCells = tableRow.getChildren();
|
|
112
|
-
height = Math.min(...rowCells.map((cell) => getCellNodeHeight(cell, editor) ?? Infinity));
|
|
113
|
-
}
|
|
114
|
-
const newHeight = Math.max(height + heightChange, 33);
|
|
115
|
-
tableRow.setHeight(newHeight);
|
|
116
|
-
eventEmitter.emit("table:resize", {
|
|
117
|
-
heightChange,
|
|
118
|
-
newHeight
|
|
119
|
-
});
|
|
120
|
-
}, { tag: SKIP_SCROLL_INTO_VIEW_TAG });
|
|
121
|
-
}, [activeCell, editor]);
|
|
122
|
-
const updateColumnWidth = useCallback((widthChange) => {
|
|
123
|
-
if (!activeCell) throw new Error("TableCellResizer: Expected active cell.");
|
|
124
|
-
editor.update(() => {
|
|
125
|
-
const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem);
|
|
126
|
-
if (!$isTableCellNode(tableCellNode)) throw new Error("TableCellResizer: Table cell node not found.");
|
|
127
|
-
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
128
|
-
const [tableMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
|
|
129
|
-
const columnIndex = getCellColumnIndex(tableCellNode, tableMap);
|
|
130
|
-
if (columnIndex === void 0) throw new Error("TableCellResizer: Table column not found.");
|
|
131
|
-
const colWidths = tableNode.getColWidths();
|
|
132
|
-
if (!colWidths) return;
|
|
133
|
-
const width = colWidths[columnIndex];
|
|
134
|
-
if (width === void 0) return;
|
|
135
|
-
const newColWidths = [...colWidths];
|
|
136
|
-
newColWidths[columnIndex] = Math.max(width + widthChange, 92);
|
|
137
|
-
tableNode.setColWidths(newColWidths);
|
|
138
|
-
requestAnimationFrame(() => {
|
|
139
|
-
eventEmitter.emit("table:resize", { newColWidths });
|
|
140
|
-
});
|
|
141
|
-
}, { tag: SKIP_SCROLL_INTO_VIEW_TAG });
|
|
142
|
-
}, [activeCell, editor]);
|
|
143
|
-
const pointerUpHandler = useCallback((direction) => {
|
|
210
|
+
const pointerUpHandler = useCallback(() => {
|
|
144
211
|
const handler = (event) => {
|
|
145
212
|
event.preventDefault();
|
|
146
213
|
event.stopPropagation();
|
|
147
214
|
if (!activeCell) throw new Error("TableCellResizer: Expected active cell.");
|
|
148
|
-
if (
|
|
149
|
-
const { x, y } = pointerStartPosRef.current;
|
|
215
|
+
if (resizeStartStateRef.current) {
|
|
150
216
|
if (activeCell === null) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
217
|
+
commitResizeChange({
|
|
218
|
+
x: event.clientX,
|
|
219
|
+
y: event.clientY
|
|
220
|
+
}, resizeStartStateRef.current, activeCell.elem, { skipHistory: false });
|
|
154
221
|
resetState();
|
|
155
222
|
if (typeof document !== "undefined") document.removeEventListener("pointerup", handler);
|
|
156
223
|
}
|
|
@@ -158,22 +225,29 @@ const TableCellResize = memo(({ editor, eventEmitter }) => {
|
|
|
158
225
|
return handler;
|
|
159
226
|
}, [
|
|
160
227
|
activeCell,
|
|
228
|
+
commitResizeChange,
|
|
161
229
|
resetState,
|
|
162
|
-
|
|
163
|
-
updateRowHeight
|
|
230
|
+
resizeMode
|
|
164
231
|
]);
|
|
165
232
|
const toggleResize = useCallback((direction) => (event) => {
|
|
166
233
|
event.preventDefault();
|
|
167
234
|
event.stopPropagation();
|
|
168
235
|
if (!activeCell) throw new Error("TableCellResizer: Expected active cell.");
|
|
169
|
-
|
|
236
|
+
const startPos = {
|
|
170
237
|
x: event.clientX,
|
|
171
238
|
y: event.clientY
|
|
172
239
|
};
|
|
173
|
-
|
|
240
|
+
const resizeStartState = getResizeStartState(direction, startPos);
|
|
241
|
+
if (!resizeStartState) return;
|
|
242
|
+
resizeStartStateRef.current = resizeStartState;
|
|
243
|
+
updatePointerCurrentPos(startPos);
|
|
174
244
|
updateDraggingDirection(direction);
|
|
175
|
-
if (typeof document !== "undefined") document.addEventListener("pointerup", pointerUpHandler(
|
|
176
|
-
}, [
|
|
245
|
+
if (typeof document !== "undefined") document.addEventListener("pointerup", pointerUpHandler());
|
|
246
|
+
}, [
|
|
247
|
+
activeCell,
|
|
248
|
+
getResizeStartState,
|
|
249
|
+
pointerUpHandler
|
|
250
|
+
]);
|
|
177
251
|
const resizerStyles = useCallback(() => {
|
|
178
252
|
if (activeCell) {
|
|
179
253
|
const { height, width, top, left } = activeCell.elem.getBoundingClientRect();
|
|
@@ -199,7 +273,7 @@ const TableCellResize = memo(({ editor, eventEmitter }) => {
|
|
|
199
273
|
width: `${zoneWidth}px`
|
|
200
274
|
}
|
|
201
275
|
};
|
|
202
|
-
const tableRect = tableRectRef.current;
|
|
276
|
+
const tableRect = getCurrentTableRect(activeCell, tableRectRef.current);
|
|
203
277
|
if (draggingDirection && pointerCurrentPos && tableRect) {
|
|
204
278
|
if (isHeightChanging(draggingDirection)) {
|
|
205
279
|
styles[draggingDirection].left = `${scrollX + tableRect.left}px`;
|
|
@@ -241,12 +315,13 @@ const TableCellResize = memo(({ editor, eventEmitter }) => {
|
|
|
241
315
|
})] })
|
|
242
316
|
});
|
|
243
317
|
});
|
|
244
|
-
var TableResize_default = memo(({ editor, eventEmitter }) => {
|
|
318
|
+
var TableResize_default = memo(({ editor, eventEmitter, resizeMode }) => {
|
|
245
319
|
if (typeof document === "undefined") return null;
|
|
246
320
|
const container = document.querySelector(".ant-app") || document.body;
|
|
247
321
|
return createPortal(/* @__PURE__ */ jsx(TableCellResize, {
|
|
248
322
|
editor,
|
|
249
|
-
eventEmitter
|
|
323
|
+
eventEmitter,
|
|
324
|
+
resizeMode
|
|
250
325
|
}), container);
|
|
251
326
|
});
|
|
252
327
|
//#endregion
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { INSERT_TABLE_ROW_COMMAND, MOVE_TABLE_ROW_COMMAND, SELECT_TABLE_COMMAND } from "../command/index.js";
|
|
2
|
+
import { sum, useTableAxisDrag, useTableRowMetrics } from "./TableController/hooks.js";
|
|
3
|
+
import { styles } from "./TableController/style.js";
|
|
4
|
+
import { createTableDragImage } from "./TableController/utils.js";
|
|
5
|
+
import TableControllerMenu from "./TableControllerMenu.js";
|
|
6
|
+
import TableInsertButton from "./TableInsertButton.js";
|
|
7
|
+
import { useTableControllerSelection } from "./hooks.js";
|
|
8
|
+
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { cx } from "antd-style";
|
|
11
|
+
import { $computeTableMapSkipCellCheck } from "@lexical/table";
|
|
12
|
+
//#region src/plugins/table/react/TableRowController.tsx
|
|
13
|
+
const INSERT_BUTTON_HIDE_DELAY = 160;
|
|
14
|
+
const TABLE_DELETE_PREVIEW_CLASS = "lobe-editor-table-delete-preview";
|
|
15
|
+
const DOTS = Array.from({ length: 6 }, (_, index) => index);
|
|
16
|
+
const isBeforeFirstRow = (target) => {
|
|
17
|
+
return target?.index === 0 && !target.insertAfter;
|
|
18
|
+
};
|
|
19
|
+
const TableRowController = memo((props) => {
|
|
20
|
+
const { blockMenuService, editor, menuService, node, onInsertPreviewChange } = props;
|
|
21
|
+
const { rowHeights, tableWidth } = useTableRowMetrics(editor, node);
|
|
22
|
+
const { isTableFocused, isTableSelected, selectedRows } = useTableControllerSelection(editor, node);
|
|
23
|
+
const anchorRowIndexRef = useRef(null);
|
|
24
|
+
const deletePreviewElementsRef = useRef([]);
|
|
25
|
+
const insertButtonHoveredRef = useRef(false);
|
|
26
|
+
const insertButtonHideTimerRef = useRef(null);
|
|
27
|
+
const rowLeftRef = useRef(null);
|
|
28
|
+
const showMenu = selectedRows.length > 0 && !isTableSelected;
|
|
29
|
+
const [insertTarget, setInsertTarget] = useState(null);
|
|
30
|
+
const [menuAnchorElement, setMenuAnchorElement] = useState(null);
|
|
31
|
+
const [, setMenuVersion] = useState(0);
|
|
32
|
+
const pendingDragRowsRef = useRef(null);
|
|
33
|
+
const [isControllerHovered, setControllerHovered] = useState(false);
|
|
34
|
+
const clearInsertButtonHideTimer = () => {
|
|
35
|
+
if (insertButtonHideTimerRef.current) {
|
|
36
|
+
clearTimeout(insertButtonHideTimerRef.current);
|
|
37
|
+
insertButtonHideTimerRef.current = null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const clearDeletePreview = useCallback(() => {
|
|
41
|
+
deletePreviewElementsRef.current.forEach((element) => {
|
|
42
|
+
element.classList.remove(TABLE_DELETE_PREVIEW_CLASS);
|
|
43
|
+
});
|
|
44
|
+
deletePreviewElementsRef.current = [];
|
|
45
|
+
}, []);
|
|
46
|
+
const showDeletePreview = useCallback(() => {
|
|
47
|
+
clearDeletePreview();
|
|
48
|
+
if (!showMenu) return;
|
|
49
|
+
editor.getEditorState().read(() => {
|
|
50
|
+
const [tableMap] = $computeTableMapSkipCellCheck(node.getLatest(), null, null);
|
|
51
|
+
const previewCellKeys = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const rowIndex of selectedRows) {
|
|
53
|
+
const row = tableMap[rowIndex];
|
|
54
|
+
if (!row) continue;
|
|
55
|
+
for (const mapCell of row) if (mapCell?.cell) previewCellKeys.add(mapCell.cell.getKey());
|
|
56
|
+
}
|
|
57
|
+
for (const cellKey of previewCellKeys) {
|
|
58
|
+
const element = editor.getElementByKey(cellKey);
|
|
59
|
+
if (element instanceof HTMLElement) {
|
|
60
|
+
element.classList.add(TABLE_DELETE_PREVIEW_CLASS);
|
|
61
|
+
deletePreviewElementsRef.current.push(element);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}, [
|
|
66
|
+
clearDeletePreview,
|
|
67
|
+
editor,
|
|
68
|
+
node,
|
|
69
|
+
selectedRows,
|
|
70
|
+
showMenu
|
|
71
|
+
]);
|
|
72
|
+
const closeMenu = useCallback(() => {
|
|
73
|
+
setMenuAnchorElement(null);
|
|
74
|
+
onInsertPreviewChange?.(null);
|
|
75
|
+
clearDeletePreview();
|
|
76
|
+
}, [clearDeletePreview, onInsertPreviewChange]);
|
|
77
|
+
const menuContext = {
|
|
78
|
+
axis: "row",
|
|
79
|
+
editor,
|
|
80
|
+
node,
|
|
81
|
+
selectedIndexes: selectedRows
|
|
82
|
+
};
|
|
83
|
+
const menuItems = menuService?.getItems(menuContext).map((item) => {
|
|
84
|
+
if (item.type === "separator") return {
|
|
85
|
+
key: item.key,
|
|
86
|
+
type: "separator"
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
danger: item.danger,
|
|
90
|
+
key: item.key,
|
|
91
|
+
label: typeof item.label === "function" ? item.label(menuContext) : item.label,
|
|
92
|
+
onClick: () => {
|
|
93
|
+
item.onClick(menuContext);
|
|
94
|
+
},
|
|
95
|
+
onMouseEnter: item.preview === "delete" ? showDeletePreview : item.preview === "insert-before" ? () => {
|
|
96
|
+
onInsertPreviewChange?.("top");
|
|
97
|
+
} : item.preview === "insert-after" ? () => {
|
|
98
|
+
onInsertPreviewChange?.("bottom");
|
|
99
|
+
} : void 0,
|
|
100
|
+
onMouseLeave: item.preview === "delete" ? clearDeletePreview : item.preview === "insert-before" || item.preview === "insert-after" ? () => {
|
|
101
|
+
onInsertPreviewChange?.(null);
|
|
102
|
+
} : void 0
|
|
103
|
+
};
|
|
104
|
+
}) ?? [];
|
|
105
|
+
const scheduleHideInsertButton = () => {
|
|
106
|
+
clearInsertButtonHideTimer();
|
|
107
|
+
insertButtonHideTimerRef.current = setTimeout(() => {
|
|
108
|
+
if (!insertButtonHoveredRef.current) setInsertTarget(null);
|
|
109
|
+
}, INSERT_BUTTON_HIDE_DELAY);
|
|
110
|
+
};
|
|
111
|
+
const { clearDragState, dragTarget, finishDrag: finishRowDrag, isDragging, startDrag } = useTableAxisDrag({
|
|
112
|
+
getDropTarget: useCallback((event) => {
|
|
113
|
+
const controller = rowLeftRef.current;
|
|
114
|
+
if (!controller) return null;
|
|
115
|
+
const rows = Array.from(controller.querySelectorAll(".row"));
|
|
116
|
+
if (rows.length === 0) return null;
|
|
117
|
+
for (const [index, row] of rows.entries()) {
|
|
118
|
+
const rect = row.getBoundingClientRect();
|
|
119
|
+
if (event.clientY <= rect.top + rect.height / 2) return {
|
|
120
|
+
index,
|
|
121
|
+
insertAfter: false
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
index: rows.length - 1,
|
|
126
|
+
insertAfter: true
|
|
127
|
+
};
|
|
128
|
+
}, []),
|
|
129
|
+
onMove: useCallback((target, selectedIndexes) => {
|
|
130
|
+
editor.dispatchCommand(MOVE_TABLE_ROW_COMMAND, {
|
|
131
|
+
insertAfter: target.insertAfter,
|
|
132
|
+
rowIndex: target.index,
|
|
133
|
+
selectedRows: selectedIndexes,
|
|
134
|
+
table: node.getKey()
|
|
135
|
+
});
|
|
136
|
+
}, [editor, node])
|
|
137
|
+
});
|
|
138
|
+
const startRowDrag = useCallback((event, selectedIndexes) => {
|
|
139
|
+
event.dataTransfer.effectAllowed = "move";
|
|
140
|
+
event.dataTransfer.dropEffect = "move";
|
|
141
|
+
event.dataTransfer.setData("text/plain", "");
|
|
142
|
+
const dragImage = createTableDragImage(`拖拽 ${selectedIndexes.length} 行`);
|
|
143
|
+
event.dataTransfer.setDragImage(dragImage, dragImage.offsetWidth / 2, dragImage.offsetHeight / 2);
|
|
144
|
+
setInsertTarget(null);
|
|
145
|
+
closeMenu();
|
|
146
|
+
startDrag(event.nativeEvent, selectedIndexes);
|
|
147
|
+
}, [closeMenu, startDrag]);
|
|
148
|
+
const insertRowOffset = insertTarget ? sum(rowHeights.slice(0, insertTarget.index)) + (insertTarget.insertAfter ? rowHeights[insertTarget.index] || 0 : 0) : 0;
|
|
149
|
+
const dragRowOffset = dragTarget ? sum(rowHeights.slice(0, dragTarget.index)) + (dragTarget.insertAfter ? rowHeights[dragTarget.index] || 0 : 0) : 0;
|
|
150
|
+
const showRowInsertButton = !isDragging && Boolean(insertTarget) && !isBeforeFirstRow(insertTarget);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (selectedRows.length > 0) anchorRowIndexRef.current = selectedRows[0];
|
|
153
|
+
}, [selectedRows]);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
return menuService?.subscribe(() => {
|
|
156
|
+
setMenuVersion((version) => version + 1);
|
|
157
|
+
});
|
|
158
|
+
}, [menuService]);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
return () => {
|
|
161
|
+
clearInsertButtonHideTimer();
|
|
162
|
+
clearDeletePreview();
|
|
163
|
+
onInsertPreviewChange?.(null);
|
|
164
|
+
setMenuAnchorElement(null);
|
|
165
|
+
pendingDragRowsRef.current = null;
|
|
166
|
+
clearDragState();
|
|
167
|
+
};
|
|
168
|
+
}, [
|
|
169
|
+
clearDeletePreview,
|
|
170
|
+
clearDragState,
|
|
171
|
+
onInsertPreviewChange
|
|
172
|
+
]);
|
|
173
|
+
const shouldShowController = isTableFocused || isControllerHovered;
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!shouldShowController) {
|
|
176
|
+
clearInsertButtonHideTimer();
|
|
177
|
+
clearDeletePreview();
|
|
178
|
+
setInsertTarget(null);
|
|
179
|
+
onInsertPreviewChange?.(null);
|
|
180
|
+
setMenuAnchorElement(null);
|
|
181
|
+
pendingDragRowsRef.current = null;
|
|
182
|
+
clearDragState();
|
|
183
|
+
}
|
|
184
|
+
}, [
|
|
185
|
+
clearDeletePreview,
|
|
186
|
+
clearDragState,
|
|
187
|
+
onInsertPreviewChange,
|
|
188
|
+
shouldShowController
|
|
189
|
+
]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!showMenu) closeMenu();
|
|
192
|
+
}, [closeMenu, showMenu]);
|
|
193
|
+
if (!shouldShowController) return null;
|
|
194
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
195
|
+
className: "table-controller-row",
|
|
196
|
+
contentEditable: false,
|
|
197
|
+
onMouseEnter: () => {
|
|
198
|
+
setControllerHovered(true);
|
|
199
|
+
},
|
|
200
|
+
onMouseLeave: () => {
|
|
201
|
+
setControllerHovered(false);
|
|
202
|
+
scheduleHideInsertButton();
|
|
203
|
+
},
|
|
204
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
205
|
+
className: cx("left", styles.rowLeft),
|
|
206
|
+
ref: rowLeftRef,
|
|
207
|
+
children: [
|
|
208
|
+
/* @__PURE__ */ jsx(TableControllerMenu, {
|
|
209
|
+
anchorElement: menuAnchorElement,
|
|
210
|
+
blockMenuService,
|
|
211
|
+
items: menuItems,
|
|
212
|
+
onOpenChange: (open) => {
|
|
213
|
+
if (!open) closeMenu();
|
|
214
|
+
},
|
|
215
|
+
open: Boolean(menuAnchorElement) && showMenu && menuItems.length > 0,
|
|
216
|
+
position: "left"
|
|
217
|
+
}),
|
|
218
|
+
/* @__PURE__ */ jsx(TableInsertButton, {
|
|
219
|
+
ariaLabel: "Insert row",
|
|
220
|
+
offset: insertRowOffset,
|
|
221
|
+
onInsert: () => {
|
|
222
|
+
if (!insertTarget) return;
|
|
223
|
+
editor.dispatchCommand(INSERT_TABLE_ROW_COMMAND, {
|
|
224
|
+
insertAfter: insertTarget.insertAfter,
|
|
225
|
+
rowIndex: insertTarget.index,
|
|
226
|
+
table: node.getKey()
|
|
227
|
+
});
|
|
228
|
+
setInsertTarget(null);
|
|
229
|
+
},
|
|
230
|
+
onMouseEnter: () => {
|
|
231
|
+
insertButtonHoveredRef.current = true;
|
|
232
|
+
clearInsertButtonHideTimer();
|
|
233
|
+
},
|
|
234
|
+
onMouseLeave: () => {
|
|
235
|
+
insertButtonHoveredRef.current = false;
|
|
236
|
+
scheduleHideInsertButton();
|
|
237
|
+
},
|
|
238
|
+
position: "left",
|
|
239
|
+
reference: rowLeftRef.current,
|
|
240
|
+
visible: showRowInsertButton
|
|
241
|
+
}),
|
|
242
|
+
rowHeights.map((height, index) => {
|
|
243
|
+
const isLastRow = index + 1 === rowHeights.length;
|
|
244
|
+
const isSelected = selectedRows.includes(index);
|
|
245
|
+
const isSelectedController = isSelected && !isTableSelected;
|
|
246
|
+
const showSelectionDots = isSelected && !isTableSelected;
|
|
247
|
+
return /* @__PURE__ */ jsx("div", {
|
|
248
|
+
className: cx("row", styles.row, isLastRow && styles.rowLast, isSelected && styles.selected),
|
|
249
|
+
draggable: true,
|
|
250
|
+
onClickCapture: (event) => {
|
|
251
|
+
if (isSelectedController) {
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
event.stopPropagation();
|
|
254
|
+
clearInsertButtonHideTimer();
|
|
255
|
+
setInsertTarget(null);
|
|
256
|
+
setMenuAnchorElement(event.currentTarget);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
onDragEnd: () => {
|
|
260
|
+
pendingDragRowsRef.current = null;
|
|
261
|
+
finishRowDrag();
|
|
262
|
+
},
|
|
263
|
+
onDragStart: (event) => {
|
|
264
|
+
event.stopPropagation();
|
|
265
|
+
startRowDrag(event, pendingDragRowsRef.current || (isSelectedController ? selectedRows : [index]));
|
|
266
|
+
},
|
|
267
|
+
onMouseDown: (event) => {
|
|
268
|
+
event.stopPropagation();
|
|
269
|
+
if (isSelectedController) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
pendingDragRowsRef.current = selectedRows;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
closeMenu();
|
|
275
|
+
pendingDragRowsRef.current = [index];
|
|
276
|
+
const anchorIndex = event.shiftKey ? selectedRows[0] ?? anchorRowIndexRef.current : index;
|
|
277
|
+
editor.dispatchCommand(SELECT_TABLE_COMMAND, {
|
|
278
|
+
anchorIndex,
|
|
279
|
+
extend: event.shiftKey,
|
|
280
|
+
rowIndex: index,
|
|
281
|
+
table: node.getKey()
|
|
282
|
+
});
|
|
283
|
+
if (!event.shiftKey) anchorRowIndexRef.current = index;
|
|
284
|
+
},
|
|
285
|
+
onMouseDownCapture: (event) => {
|
|
286
|
+
if (isSelectedController) {
|
|
287
|
+
event.stopPropagation();
|
|
288
|
+
pendingDragRowsRef.current = selectedRows;
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
onMouseMove: (event) => {
|
|
292
|
+
if (isDragging || isSelectedController) {
|
|
293
|
+
setInsertTarget(null);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
297
|
+
const insertAfter = event.clientY - rect.top > rect.height / 2;
|
|
298
|
+
if (isBeforeFirstRow({
|
|
299
|
+
index,
|
|
300
|
+
insertAfter
|
|
301
|
+
})) {
|
|
302
|
+
setInsertTarget(null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
setInsertTarget({
|
|
306
|
+
index,
|
|
307
|
+
insertAfter
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
onMouseUpCapture: (event) => {
|
|
311
|
+
if (isSelectedController) {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
event.stopPropagation();
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
onPointerDownCapture: (event) => {
|
|
317
|
+
if (isSelectedController) {
|
|
318
|
+
event.stopPropagation();
|
|
319
|
+
pendingDragRowsRef.current = selectedRows;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
style: { height },
|
|
323
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
324
|
+
className: cx(styles.selectionDots, styles.rowSelectionDots, showSelectionDots && styles.selectionDotsVisible),
|
|
325
|
+
children: DOTS.map((dot) => /* @__PURE__ */ jsx("span", {}, dot))
|
|
326
|
+
})
|
|
327
|
+
}, index);
|
|
328
|
+
}),
|
|
329
|
+
/* @__PURE__ */ jsx("span", {
|
|
330
|
+
className: cx(styles.dragIndicator, styles.rowDragIndicator, Boolean(dragTarget) && isDragging && styles.dragIndicatorVisible),
|
|
331
|
+
style: {
|
|
332
|
+
inlineSize: tableWidth,
|
|
333
|
+
top: dragRowOffset
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
]
|
|
337
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
338
|
+
className: cx("corner", styles.corner, isTableSelected && styles.selected, isTableSelected && styles.selectedCorner),
|
|
339
|
+
onMouseDown: (event) => {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
event.stopPropagation();
|
|
342
|
+
editor.dispatchCommand(SELECT_TABLE_COMMAND, { table: node.getKey() });
|
|
343
|
+
}
|
|
344
|
+
})]
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
TableRowController.displayName = "TableRowController";
|
|
348
|
+
//#endregion
|
|
349
|
+
export { TableRowController as default };
|