@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.
Files changed (52) hide show
  1. package/es/editor-kernel/react/useDecorators.js +14 -8
  2. package/es/headless.d.ts +2 -0
  3. package/es/headless.js +710 -46
  4. package/es/index.d.ts +4 -3
  5. package/es/index.js +4 -3
  6. package/es/locale/index.d.ts +2 -0
  7. package/es/locale/index.js +5 -1
  8. package/es/plugins/auto-complete/plugin/index.js +3 -3
  9. package/es/plugins/block/index.d.ts +1 -1
  10. package/es/plugins/block/plugin/index.js +78 -2
  11. package/es/plugins/block/react/ReactBlockPlugin.js +172 -16
  12. package/es/plugins/block/react/drag/drag-utils.js +37 -10
  13. package/es/plugins/block/service/i-block-menu-service.d.ts +18 -1
  14. package/es/plugins/block/service/i-block-menu-service.js +24 -0
  15. package/es/plugins/block/service/index.d.ts +1 -1
  16. package/es/plugins/codeblock/plugin/index.js +25 -1
  17. package/es/plugins/common/plugin/register.js +2 -2
  18. package/es/plugins/litexml/plugin/index.js +8 -2
  19. package/es/plugins/slash/plugin/index.js +1 -1
  20. package/es/plugins/slash/react/ReactSlashPlugin.js +4 -4
  21. package/es/plugins/table/command/index.d.ts +13 -1
  22. package/es/plugins/table/command/index.js +220 -39
  23. package/es/plugins/table/index.d.ts +3 -2
  24. package/es/plugins/table/node/index.d.ts +2 -0
  25. package/es/plugins/table/node/index.js +130 -2
  26. package/es/plugins/table/plugin/index.d.ts +6 -0
  27. package/es/plugins/table/plugin/index.js +193 -4
  28. package/es/plugins/table/react/TableActionMenu/ActionMenu.js +82 -6
  29. package/es/plugins/table/react/TableActionMenu/index.js +9 -4
  30. package/es/plugins/table/react/TableColController.js +354 -0
  31. package/es/plugins/table/react/TableController/hooks.js +201 -0
  32. package/es/plugins/table/react/TableController/style.js +264 -0
  33. package/es/plugins/table/react/TableController/utils.js +25 -0
  34. package/es/plugins/table/react/TableControllerButton.js +81 -0
  35. package/es/plugins/table/react/TableControllerMenu.js +123 -0
  36. package/es/plugins/table/react/TableInsertButton.js +25 -0
  37. package/es/plugins/table/react/TableResize/index.js +153 -78
  38. package/es/plugins/table/react/TableRowController.js +349 -0
  39. package/es/plugins/table/react/hooks.js +77 -0
  40. package/es/plugins/table/react/index.js +139 -16
  41. package/es/plugins/table/react/style.js +89 -8
  42. package/es/plugins/table/react/type.d.ts +2 -0
  43. package/es/plugins/table/react/useAutoFitPastedTable.js +189 -0
  44. package/es/plugins/table/service/i-table-controller-menu-service.d.ts +44 -0
  45. package/es/plugins/table/service/i-table-controller-menu-service.js +31 -0
  46. package/es/plugins/table/service/index.d.ts +1 -0
  47. package/es/plugins/table/utils/autoFitColumnWidth.js +87 -0
  48. package/es/plugins/table/utils/distributeColumnWidth.js +37 -0
  49. package/es/plugins/table/utils/index.js +102 -2
  50. package/es/react/EditorProvider/index.d.ts +2 -2
  51. package/es/renderer/LexicalDiff.d.ts +2 -2
  52. 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 TableCellResize = memo(({ editor, eventEmitter }) => {
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 pointerStartPosRef = useRef(null);
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
- pointerStartPosRef.current = null;
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
- updatePointerCurrentPos({
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
- hasTable
208
+ resizeMode
92
209
  ]);
93
- const isHeightChanging = (direction) => {
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 (pointerStartPosRef.current) {
149
- const { x, y } = pointerStartPosRef.current;
215
+ if (resizeStartStateRef.current) {
150
216
  if (activeCell === null) return;
151
- const zoom = calculateZoomLevel(event.target);
152
- if (isHeightChanging(direction)) updateRowHeight((event.clientY - y) / zoom);
153
- else updateColumnWidth((event.clientX - x) / zoom);
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
- updateColumnWidth,
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
- pointerStartPosRef.current = {
236
+ const startPos = {
170
237
  x: event.clientX,
171
238
  y: event.clientY
172
239
  };
173
- updatePointerCurrentPos(pointerStartPosRef.current);
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(direction));
176
- }, [activeCell, pointerUpHandler]);
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 };