@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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getTableSelectionIndexes, isTableFullySelected } from "../utils/index.js";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { $findTableNode, $isTableSelection } from "@lexical/table";
|
|
4
|
+
import "es-toolkit/compat";
|
|
5
|
+
import { $getNodeByKey, $getSelection, $isRangeSelection } from "lexical";
|
|
6
|
+
//#region src/plugins/table/react/hooks.ts
|
|
7
|
+
/**
|
|
8
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
9
|
+
*
|
|
10
|
+
* This source code is licensed under the MIT license found in the
|
|
11
|
+
* LICENSE file in the root directory of this source tree.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
const isSameSelection = (current, next) => {
|
|
15
|
+
return current.isTableFocused === next.isTableFocused && current.isTableSelected === next.isTableSelected && current.selectedColumns.length === next.selectedColumns.length && current.selectedRows.length === next.selectedRows.length && current.selectedColumns.every((column, index) => column === next.selectedColumns[index]) && current.selectedRows.every((row, index) => row === next.selectedRows[index]);
|
|
16
|
+
};
|
|
17
|
+
const readTableControllerSelection = (editor, tableKey) => {
|
|
18
|
+
const rootElement = editor.getRootElement();
|
|
19
|
+
const activeElement = rootElement?.ownerDocument.activeElement;
|
|
20
|
+
const isEditorFocused = Boolean(rootElement && activeElement && (activeElement === rootElement || rootElement.contains(activeElement)));
|
|
21
|
+
return editor.getEditorState().read(() => {
|
|
22
|
+
const selection = $getSelection();
|
|
23
|
+
const tableNode = $getNodeByKey(tableKey);
|
|
24
|
+
if (!tableNode) return {
|
|
25
|
+
isTableFocused: false,
|
|
26
|
+
isTableSelected: false,
|
|
27
|
+
selectedColumns: [],
|
|
28
|
+
selectedRows: []
|
|
29
|
+
};
|
|
30
|
+
const columnCount = tableNode.getColumnCount();
|
|
31
|
+
const rowCount = tableNode.getChildrenSize();
|
|
32
|
+
const selectionIndexes = getTableSelectionIndexes(selection, tableKey, columnCount, rowCount);
|
|
33
|
+
const anchorNode = $isRangeSelection(selection) ? $getNodeByKey(selection.anchor.key) : null;
|
|
34
|
+
return {
|
|
35
|
+
isTableFocused: isEditorFocused && ($isTableSelection(selection) && selection.tableKey === tableKey || Boolean(anchorNode && $findTableNode(anchorNode)?.getKey() === tableKey)),
|
|
36
|
+
isTableSelected: isEditorFocused && isTableFullySelected(selection, tableKey, columnCount, rowCount),
|
|
37
|
+
selectedColumns: isEditorFocused ? selectionIndexes.selectedColumns : [],
|
|
38
|
+
selectedRows: isEditorFocused ? selectionIndexes.selectedRows : []
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const useTableControllerSelection = (editor, node) => {
|
|
43
|
+
const tableKey = node.getKey();
|
|
44
|
+
const [selection, setSelection] = useState(() => readTableControllerSelection(editor, tableKey));
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let frame = null;
|
|
47
|
+
const updateSelection = () => {
|
|
48
|
+
const nextSelection = readTableControllerSelection(editor, tableKey);
|
|
49
|
+
setSelection((currentSelection) => {
|
|
50
|
+
return isSameSelection(currentSelection, nextSelection) ? currentSelection : nextSelection;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const scheduleUpdateSelection = () => {
|
|
54
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
55
|
+
frame = requestAnimationFrame(() => {
|
|
56
|
+
frame = null;
|
|
57
|
+
updateSelection();
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const unregisterRootListener = editor.registerRootListener((rootElement, prevRootElement) => {
|
|
61
|
+
prevRootElement?.removeEventListener("focusin", scheduleUpdateSelection);
|
|
62
|
+
prevRootElement?.removeEventListener("focusout", scheduleUpdateSelection);
|
|
63
|
+
rootElement?.addEventListener("focusin", scheduleUpdateSelection);
|
|
64
|
+
rootElement?.addEventListener("focusout", scheduleUpdateSelection);
|
|
65
|
+
updateSelection();
|
|
66
|
+
});
|
|
67
|
+
const unregisterUpdateListener = editor.registerUpdateListener(updateSelection);
|
|
68
|
+
return () => {
|
|
69
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
70
|
+
unregisterRootListener();
|
|
71
|
+
unregisterUpdateListener();
|
|
72
|
+
};
|
|
73
|
+
}, [editor, tableKey]);
|
|
74
|
+
return selection;
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { useTableControllerSelection };
|
|
@@ -1,49 +1,172 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useLexicalComposerContext } from "../../../editor-kernel/react/react-context.js";
|
|
3
3
|
import { useLexicalEditor } from "../../../editor-kernel/react/useLexicalEditor.js";
|
|
4
|
+
import { IBlockMenuService } from "../../block/service/i-block-menu-service.js";
|
|
5
|
+
import { $updateDOMForSelection } from "../utils/index.js";
|
|
6
|
+
import { ITableControllerMenuService } from "../service/i-table-controller-menu-service.js";
|
|
4
7
|
import { TablePlugin } from "../plugin/index.js";
|
|
5
8
|
import PortalAnchor from "../../../editor-kernel/react/PortalAnchor.js";
|
|
6
|
-
import { $updateDOMForSelection } from "../utils/index.js";
|
|
7
9
|
import TableActionMenu from "./TableActionMenu/index.js";
|
|
10
|
+
import TableColController from "./TableColController.js";
|
|
8
11
|
import TableHoverActions from "./TableHoverActions/index.js";
|
|
9
12
|
import TableResize_default from "./TableResize/index.js";
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
13
|
+
import TableRowController from "./TableRowController.js";
|
|
14
|
+
import { selectionOutlineStyles, styles } from "./style.js";
|
|
15
|
+
import { useAutoFitPastedTable } from "./useAutoFitPastedTable.js";
|
|
16
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
|
|
12
17
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
13
18
|
import { cx } from "antd-style";
|
|
14
|
-
import { $findTableNode, $getElementForTableNode, $isTableSelection } from "@lexical/table";
|
|
19
|
+
import { $findTableNode, $getElementForTableNode, $isTableNode, $isTableSelection } from "@lexical/table";
|
|
15
20
|
import EventEmitter from "eventemitter3";
|
|
16
|
-
import { $getSelection, $isRangeSelection } from "lexical";
|
|
21
|
+
import { $getNodeByKey, $getSelection, $isRangeSelection } from "lexical";
|
|
17
22
|
//#region src/plugins/table/react/index.tsx
|
|
18
|
-
const
|
|
23
|
+
const isEditorRootFocused = (activeEditor) => {
|
|
24
|
+
const rootElement = activeEditor.getRootElement();
|
|
25
|
+
const activeElement = rootElement?.ownerDocument.activeElement;
|
|
26
|
+
return Boolean(rootElement && activeElement && (activeElement === rootElement || rootElement.contains(activeElement)));
|
|
27
|
+
};
|
|
28
|
+
const ReactTablePlugin = ({ className, locale, resizeMode = "realtime" }) => {
|
|
19
29
|
const [editor] = useLexicalComposerContext();
|
|
20
30
|
const [lexicalEditor, setLexicalEditor] = useState(null);
|
|
31
|
+
const [selectionOutlineRect, setSelectionOutlineRect] = useState(null);
|
|
32
|
+
const [selectionOutlinePreviewSide, setSelectionOutlinePreviewSide] = useState(null);
|
|
21
33
|
const eventEmitter = useMemo(() => {
|
|
22
34
|
return new EventEmitter();
|
|
23
35
|
}, []);
|
|
36
|
+
useAutoFitPastedTable(lexicalEditor);
|
|
37
|
+
const selectionOutlineStyle = useMemo(() => {
|
|
38
|
+
if (!selectionOutlineRect) return;
|
|
39
|
+
const style = {
|
|
40
|
+
height: selectionOutlineRect.height,
|
|
41
|
+
left: selectionOutlineRect.left,
|
|
42
|
+
top: selectionOutlineRect.top,
|
|
43
|
+
width: selectionOutlineRect.width
|
|
44
|
+
};
|
|
45
|
+
if (!selectionOutlinePreviewSide) return style;
|
|
46
|
+
return {
|
|
47
|
+
...style,
|
|
48
|
+
borderBottomWidth: selectionOutlinePreviewSide === "bottom" ? void 0 : 0,
|
|
49
|
+
borderLeftWidth: selectionOutlinePreviewSide === "left" ? void 0 : 0,
|
|
50
|
+
borderRightWidth: selectionOutlinePreviewSide === "right" ? void 0 : 0,
|
|
51
|
+
borderTopWidth: selectionOutlinePreviewSide === "top" ? void 0 : 0
|
|
52
|
+
};
|
|
53
|
+
}, [selectionOutlinePreviewSide, selectionOutlineRect]);
|
|
54
|
+
const updateSelectionOutlineRect = useCallback((rect) => {
|
|
55
|
+
setSelectionOutlineRect((current) => {
|
|
56
|
+
if (current?.height === rect?.height && current?.left === rect?.left && current?.top === rect?.top && current?.width === rect?.width) return current;
|
|
57
|
+
return rect;
|
|
58
|
+
});
|
|
59
|
+
}, []);
|
|
60
|
+
const refreshSelectionOutlineRect = useCallback((activeEditor) => {
|
|
61
|
+
if (!isEditorRootFocused(activeEditor)) {
|
|
62
|
+
updateSelectionOutlineRect(null);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
activeEditor.read(() => {
|
|
66
|
+
const selection = $getSelection();
|
|
67
|
+
if (!$isTableSelection(selection) && !$isRangeSelection(selection)) {
|
|
68
|
+
updateSelectionOutlineRect(null);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const anchorNode = $isRangeSelection(selection) ? $getNodeByKey(selection.anchor.key) : null;
|
|
72
|
+
const tableNode = $isTableSelection(selection) ? $getNodeByKey(selection.tableKey) : anchorNode ? $findTableNode(anchorNode) : null;
|
|
73
|
+
if (!tableNode || !$isTableNode(tableNode)) {
|
|
74
|
+
updateSelectionOutlineRect(null);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
updateSelectionOutlineRect($updateDOMForSelection(activeEditor, $getElementForTableNode(activeEditor, tableNode), selection));
|
|
78
|
+
});
|
|
79
|
+
}, [updateSelectionOutlineRect]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!lexicalEditor || !selectionOutlineRect) return;
|
|
82
|
+
let frame = null;
|
|
83
|
+
const scheduleRefresh = () => {
|
|
84
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
85
|
+
frame = requestAnimationFrame(() => {
|
|
86
|
+
frame = null;
|
|
87
|
+
refreshSelectionOutlineRect(lexicalEditor);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
document.addEventListener("scroll", scheduleRefresh, true);
|
|
91
|
+
window.addEventListener("resize", scheduleRefresh);
|
|
92
|
+
return () => {
|
|
93
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
94
|
+
document.removeEventListener("scroll", scheduleRefresh, true);
|
|
95
|
+
window.removeEventListener("resize", scheduleRefresh);
|
|
96
|
+
};
|
|
97
|
+
}, [
|
|
98
|
+
lexicalEditor,
|
|
99
|
+
refreshSelectionOutlineRect,
|
|
100
|
+
selectionOutlineRect
|
|
101
|
+
]);
|
|
24
102
|
useLayoutEffect(() => {
|
|
25
103
|
if (locale) editor.registerLocale(locale);
|
|
26
|
-
editor.registerPlugin(TablePlugin, {
|
|
104
|
+
editor.registerPlugin(TablePlugin, {
|
|
105
|
+
decoratorCol: (node, lexicalEditor) => {
|
|
106
|
+
return /* @__PURE__ */ jsx(TableColController, {
|
|
107
|
+
blockMenuService: editor.requireService(IBlockMenuService),
|
|
108
|
+
editor: lexicalEditor,
|
|
109
|
+
menuService: editor.requireService(ITableControllerMenuService),
|
|
110
|
+
node,
|
|
111
|
+
onColumnMetricsChange: () => {
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
refreshSelectionOutlineRect(lexicalEditor);
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
onInsertPreviewChange: setSelectionOutlinePreviewSide
|
|
117
|
+
}, node.getColumnCount());
|
|
118
|
+
},
|
|
119
|
+
decoratorRow: (node, lexicalEditor) => {
|
|
120
|
+
return /* @__PURE__ */ jsx(TableRowController, {
|
|
121
|
+
blockMenuService: editor.requireService(IBlockMenuService),
|
|
122
|
+
editor: lexicalEditor,
|
|
123
|
+
menuService: editor.requireService(ITableControllerMenuService),
|
|
124
|
+
node,
|
|
125
|
+
onInsertPreviewChange: setSelectionOutlinePreviewSide
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
theme: cx(styles, className)
|
|
129
|
+
});
|
|
27
130
|
}, []);
|
|
28
131
|
useLexicalEditor((editor) => {
|
|
29
132
|
setLexicalEditor(editor);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
$updateDOMForSelection(editor, $getElementForTableNode(editor, tableNode), selection);
|
|
133
|
+
let frame = null;
|
|
134
|
+
const scheduleRefreshSelectionOutlineRect = () => {
|
|
135
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
136
|
+
frame = requestAnimationFrame(() => {
|
|
137
|
+
frame = null;
|
|
138
|
+
refreshSelectionOutlineRect(editor);
|
|
37
139
|
});
|
|
140
|
+
};
|
|
141
|
+
const unregisterUpdateListener = editor.registerUpdateListener(scheduleRefreshSelectionOutlineRect);
|
|
142
|
+
const unregisterRootListener = editor.registerRootListener((rootElement, prevRootElement) => {
|
|
143
|
+
prevRootElement?.removeEventListener("focusin", scheduleRefreshSelectionOutlineRect);
|
|
144
|
+
prevRootElement?.removeEventListener("focusout", scheduleRefreshSelectionOutlineRect);
|
|
145
|
+
rootElement?.addEventListener("focusin", scheduleRefreshSelectionOutlineRect);
|
|
146
|
+
rootElement?.addEventListener("focusout", scheduleRefreshSelectionOutlineRect);
|
|
38
147
|
});
|
|
39
148
|
return () => {
|
|
149
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
150
|
+
unregisterRootListener();
|
|
151
|
+
unregisterUpdateListener();
|
|
152
|
+
updateSelectionOutlineRect(null);
|
|
153
|
+
setSelectionOutlinePreviewSide(null);
|
|
40
154
|
setLexicalEditor(null);
|
|
41
155
|
};
|
|
42
156
|
}, []);
|
|
43
157
|
return lexicalEditor && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TableResize_default, {
|
|
44
158
|
editor: lexicalEditor,
|
|
45
|
-
eventEmitter
|
|
46
|
-
|
|
159
|
+
eventEmitter,
|
|
160
|
+
resizeMode
|
|
161
|
+
}), /* @__PURE__ */ jsxs(PortalAnchor, { children: [
|
|
162
|
+
selectionOutlineStyle && /* @__PURE__ */ jsx("div", {
|
|
163
|
+
className: selectionOutlineStyles.outline,
|
|
164
|
+
contentEditable: false,
|
|
165
|
+
style: selectionOutlineStyle
|
|
166
|
+
}),
|
|
167
|
+
/* @__PURE__ */ jsx(TableActionMenu, { editor: lexicalEditor }),
|
|
168
|
+
/* @__PURE__ */ jsx(TableHoverActions, { editor: lexicalEditor })
|
|
169
|
+
] })] });
|
|
47
170
|
};
|
|
48
171
|
//#endregion
|
|
49
172
|
export { ReactTablePlugin };
|
|
@@ -1,10 +1,82 @@
|
|
|
1
1
|
import { createStaticStyles } from "antd-style";
|
|
2
2
|
//#region src/plugins/table/react/style.ts
|
|
3
3
|
const styles = createStaticStyles(({ css, cssVar }) => css`
|
|
4
|
-
|
|
4
|
+
position: relative;
|
|
5
|
+
overflow: visible;
|
|
5
6
|
margin-block: calc(var(--lobe-markdown-margin-multiple) * 0.5em)
|
|
6
7
|
calc(var(--lobe-markdown-margin-multiple) * 0.5em + 16px);
|
|
7
8
|
|
|
9
|
+
.lobe-editor-table-scroll-wrapper {
|
|
10
|
+
position: relative;
|
|
11
|
+
overflow: auto visible;
|
|
12
|
+
padding-block-start: 14px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.lobe-editor-table-scroll-indicator {
|
|
16
|
+
pointer-events: none;
|
|
17
|
+
|
|
18
|
+
position: absolute;
|
|
19
|
+
z-index: 3;
|
|
20
|
+
inset-block: 14px 0;
|
|
21
|
+
|
|
22
|
+
inline-size: 24px;
|
|
23
|
+
|
|
24
|
+
opacity: 0;
|
|
25
|
+
|
|
26
|
+
transition: opacity 0.12s ease;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.lobe-editor-table-scroll-indicator-visible {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.lobe-editor-table-scroll-indicator-start {
|
|
34
|
+
inset-inline-start: 0;
|
|
35
|
+
background: linear-gradient(
|
|
36
|
+
to right,
|
|
37
|
+
color-mix(in srgb, ${cssVar.colorBgContainer} 82%, transparent),
|
|
38
|
+
transparent
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.lobe-editor-table-scroll-indicator-end {
|
|
43
|
+
inset-inline-start: 0;
|
|
44
|
+
background: linear-gradient(
|
|
45
|
+
to left,
|
|
46
|
+
color-mix(in srgb, ${cssVar.colorBgContainer} 82%, transparent),
|
|
47
|
+
transparent
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.toolbar,
|
|
52
|
+
.toolbar-col,
|
|
53
|
+
.toolbar-row {
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
|
|
56
|
+
position: absolute;
|
|
57
|
+
z-index: 2;
|
|
58
|
+
inset-block-start: 0;
|
|
59
|
+
inset-inline-start: 0;
|
|
60
|
+
|
|
61
|
+
width: max-content;
|
|
62
|
+
height: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.table-controller,
|
|
66
|
+
.table-controller-col,
|
|
67
|
+
.table-controller-row {
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
position: relative;
|
|
70
|
+
width: max-content;
|
|
71
|
+
height: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.table-controller-col .top,
|
|
75
|
+
.table-controller-row .left,
|
|
76
|
+
.table-controller-row .corner {
|
|
77
|
+
pointer-events: all;
|
|
78
|
+
}
|
|
79
|
+
|
|
8
80
|
.editor_table {
|
|
9
81
|
table-layout: fixed;
|
|
10
82
|
border-spacing: 0;
|
|
@@ -18,11 +90,7 @@ const styles = createStaticStyles(({ css, cssVar }) => css`
|
|
|
18
90
|
word-break: auto-phrase;
|
|
19
91
|
overflow-wrap: break-word;
|
|
20
92
|
|
|
21
|
-
background: ${cssVar.colorFillQuaternary};
|
|
22
|
-
|
|
23
93
|
> tr:first-of-type {
|
|
24
|
-
background: ${cssVar.colorFillQuaternary};
|
|
25
|
-
|
|
26
94
|
.editor_table_cell_header {
|
|
27
95
|
font-weight: bold;
|
|
28
96
|
}
|
|
@@ -54,10 +122,23 @@ const styles = createStaticStyles(({ css, cssVar }) => css`
|
|
|
54
122
|
}
|
|
55
123
|
|
|
56
124
|
.editor_table_cell_selected {
|
|
57
|
-
color:
|
|
58
|
-
background-color: ${cssVar.yellow};
|
|
125
|
+
background-color: color-mix(in srgb, ${cssVar.yellow} 3%, transparent);
|
|
59
126
|
caret-color: transparent;
|
|
60
127
|
}
|
|
128
|
+
|
|
129
|
+
.lobe-editor-table-delete-preview {
|
|
130
|
+
background-color: color-mix(in srgb, ${cssVar.colorError} 20%, transparent) !important;
|
|
131
|
+
}
|
|
61
132
|
`);
|
|
133
|
+
const selectionOutlineStyles = createStaticStyles(({ css, cssVar }) => ({ outline: css`
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
|
|
136
|
+
position: fixed;
|
|
137
|
+
z-index: 3;
|
|
138
|
+
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
border: 1.5px solid color-mix(in srgb, ${cssVar.colorText} 22%, transparent);
|
|
141
|
+
border-radius: 3px;
|
|
142
|
+
` }));
|
|
62
143
|
//#endregion
|
|
63
|
-
export { styles };
|
|
144
|
+
export { selectionOutlineStyles, styles };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ILocaleKeys } from "../../../types/locale.js";
|
|
2
2
|
//#region src/plugins/table/react/type.d.ts
|
|
3
|
+
type TableResizeMode = 'realtime' | 'deferred';
|
|
3
4
|
interface ReactTablePluginProps {
|
|
4
5
|
className?: string;
|
|
5
6
|
locale?: Partial<Record<keyof ILocaleKeys, string>>;
|
|
7
|
+
resizeMode?: TableResizeMode;
|
|
6
8
|
}
|
|
7
9
|
//#endregion
|
|
8
10
|
export { ReactTablePluginProps };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { syncTableWidthDOM } from "../utils/index.js";
|
|
2
|
+
import { getDistributedTableColumnWidths } from "../utils/distributeColumnWidth.js";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { $computeTableMapSkipCellCheck, $isTableNode, TableNode } from "@lexical/table";
|
|
5
|
+
import { $getNodeByKey, HISTORIC_TAG, PASTE_TAG, SKIP_SCROLL_INTO_VIEW_TAG } from "lexical";
|
|
6
|
+
//#region src/plugins/table/react/useAutoFitPastedTable.ts
|
|
7
|
+
const MAX_AUTO_FIT_ATTEMPTS = 3;
|
|
8
|
+
const MIN_COLUMN_WIDTH = 75;
|
|
9
|
+
const isPositiveNumber = (width) => {
|
|
10
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0;
|
|
11
|
+
};
|
|
12
|
+
const hasValidColumnWidths = (colWidths, columnCount) => {
|
|
13
|
+
return colWidths?.length === columnCount && colWidths.every((width) => Number.isFinite(width) && width > 0);
|
|
14
|
+
};
|
|
15
|
+
const parseWidth = (value) => {
|
|
16
|
+
if (!value) return null;
|
|
17
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)px?$|^(\d+(?:\.\d+)?)$/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const width = Number.parseFloat(match[1] || match[2]);
|
|
20
|
+
return Number.isFinite(width) && width > 0 ? width : null;
|
|
21
|
+
};
|
|
22
|
+
const getTableElement$1 = (editor, tableKey) => {
|
|
23
|
+
const tableElement = editor.getElementByKey(tableKey);
|
|
24
|
+
return tableElement instanceof HTMLTableElement ? tableElement : tableElement?.querySelector("table.editor_table, table") || null;
|
|
25
|
+
};
|
|
26
|
+
const getHorizontalBorderWidth = (tableElement) => {
|
|
27
|
+
const firstRow = tableElement.rows[0];
|
|
28
|
+
if (!firstRow) return 0;
|
|
29
|
+
return Array.from(firstRow.cells).reduce((total, cell, index) => {
|
|
30
|
+
const style = getComputedStyle(cell);
|
|
31
|
+
const borderInlineStartWidth = index === 0 ? Number.parseFloat(style.borderInlineStartWidth) || 0 : 0;
|
|
32
|
+
const borderInlineEndWidth = Number.parseFloat(style.borderInlineEndWidth) || 0;
|
|
33
|
+
return total + borderInlineStartWidth + borderInlineEndWidth;
|
|
34
|
+
}, 0);
|
|
35
|
+
};
|
|
36
|
+
const getTargetTableWidth = (tableElement, columnCount) => {
|
|
37
|
+
const container = tableElement.closest(".lobe-editor-table-scroll-wrapper") ?? tableElement.parentElement;
|
|
38
|
+
const containerWidth = container?.clientWidth || container?.getBoundingClientRect().width || 0;
|
|
39
|
+
if (containerWidth <= 0) return 0;
|
|
40
|
+
return Math.max(containerWidth - getHorizontalBorderWidth(tableElement), columnCount * MIN_COLUMN_WIDTH);
|
|
41
|
+
};
|
|
42
|
+
const fitColumnWidths = (sourceWidths, targetWidth, minWidth = MIN_COLUMN_WIDTH) => {
|
|
43
|
+
const columnCount = sourceWidths.length;
|
|
44
|
+
if (columnCount === 0 || targetWidth <= 0) return null;
|
|
45
|
+
const nextWidths = Array.from({ length: columnCount }, () => 0);
|
|
46
|
+
const remainingIndexes = new Set(sourceWidths.map((_, index) => index));
|
|
47
|
+
let remainingWidth = Math.max(targetWidth, minWidth * columnCount);
|
|
48
|
+
while (remainingIndexes.size > 0) {
|
|
49
|
+
const remainingSourceWidth = Array.from(remainingIndexes).reduce((total, index) => total + (sourceWidths[index] ?? 0), 0);
|
|
50
|
+
const clampedIndexes = [];
|
|
51
|
+
for (const index of remainingIndexes) if ((remainingSourceWidth > 0 ? (sourceWidths[index] ?? 0) / remainingSourceWidth * remainingWidth : remainingWidth / remainingIndexes.size) < minWidth) {
|
|
52
|
+
nextWidths[index] = minWidth;
|
|
53
|
+
remainingWidth -= minWidth;
|
|
54
|
+
clampedIndexes.push(index);
|
|
55
|
+
}
|
|
56
|
+
if (clampedIndexes.length === 0) {
|
|
57
|
+
const indexes = Array.from(remainingIndexes);
|
|
58
|
+
const widthForRemaining = remainingWidth;
|
|
59
|
+
const flooredWidths = indexes.map((index) => {
|
|
60
|
+
return remainingSourceWidth > 0 ? (sourceWidths[index] ?? 0) / remainingSourceWidth * widthForRemaining : widthForRemaining / indexes.length;
|
|
61
|
+
}).map((width) => Math.floor(width));
|
|
62
|
+
const flooredTotal = flooredWidths.reduce((total, width) => total + width, 0);
|
|
63
|
+
const lastIndex = indexes.at(-1);
|
|
64
|
+
indexes.forEach((index, offset) => {
|
|
65
|
+
nextWidths[index] = flooredWidths[offset] ?? minWidth;
|
|
66
|
+
});
|
|
67
|
+
if (lastIndex !== void 0) nextWidths[lastIndex] += Math.round(widthForRemaining - flooredTotal);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
clampedIndexes.forEach((index) => {
|
|
71
|
+
remainingIndexes.delete(index);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return nextWidths;
|
|
75
|
+
};
|
|
76
|
+
const getDOMColumnWidths = (tableElement, columnCount) => {
|
|
77
|
+
const colWidths = Array.from(tableElement.querySelectorAll(":scope > colgroup > col")).slice(0, columnCount).map((col) => parseWidth(col.style.width) ?? parseWidth(col.getAttribute("width")));
|
|
78
|
+
if (colWidths.length === columnCount && colWidths.every(isPositiveNumber)) return colWidths;
|
|
79
|
+
const firstRow = tableElement.rows[0];
|
|
80
|
+
if (!firstRow) return null;
|
|
81
|
+
const widths = Array.from({ length: columnCount }, () => null);
|
|
82
|
+
let columnIndex = 0;
|
|
83
|
+
for (const cell of Array.from(firstRow.cells)) {
|
|
84
|
+
while (columnIndex < columnCount && widths[columnIndex] !== null) columnIndex += 1;
|
|
85
|
+
if (columnIndex >= columnCount) break;
|
|
86
|
+
const colSpan = Math.max(1, cell.colSpan || 1);
|
|
87
|
+
const cellWidth = parseWidth(cell.style.width) ?? parseWidth(cell.getAttribute("width")) ?? cell.getBoundingClientRect().width;
|
|
88
|
+
const width = Number.isFinite(cellWidth) && cellWidth > 0 ? cellWidth / colSpan : null;
|
|
89
|
+
for (let index = 0; index < colSpan && columnIndex + index < columnCount; index += 1) widths[columnIndex + index] = width;
|
|
90
|
+
columnIndex += colSpan;
|
|
91
|
+
}
|
|
92
|
+
return widths.every(isPositiveNumber) ? widths : null;
|
|
93
|
+
};
|
|
94
|
+
const getCellColumnWidths = (tableNode, columnCount) => {
|
|
95
|
+
const [tableMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
|
|
96
|
+
const firstRow = tableMap[0];
|
|
97
|
+
if (!firstRow) return null;
|
|
98
|
+
const widths = Array.from({ length: columnCount }, () => null);
|
|
99
|
+
for (const mapCell of firstRow) {
|
|
100
|
+
const { cell, startColumn } = mapCell;
|
|
101
|
+
const colSpan = Math.max(1, cell.getColSpan());
|
|
102
|
+
const cellWidth = cell.getWidth();
|
|
103
|
+
if (!cellWidth || cellWidth <= 0) continue;
|
|
104
|
+
const width = cellWidth / colSpan;
|
|
105
|
+
for (let index = 0; index < colSpan && startColumn + index < columnCount; index += 1) widths[startColumn + index] = width;
|
|
106
|
+
}
|
|
107
|
+
return widths.every(isPositiveNumber) ? widths : null;
|
|
108
|
+
};
|
|
109
|
+
const getSourceColumnWidths = (editor, tableNode, tableElement, columnCount) => {
|
|
110
|
+
const colWidths = tableNode.getColWidths();
|
|
111
|
+
if (hasValidColumnWidths(colWidths, columnCount)) return [...colWidths];
|
|
112
|
+
return getCellColumnWidths(tableNode, columnCount) ?? getDOMColumnWidths(tableElement, columnCount);
|
|
113
|
+
};
|
|
114
|
+
const getFittedTableColumnWidths = (editor, tableNode, isPastedTable) => {
|
|
115
|
+
const columnCount = tableNode.getColumnCount();
|
|
116
|
+
if (columnCount === 0) return null;
|
|
117
|
+
const tableElement = getTableElement$1(editor, tableNode.getKey());
|
|
118
|
+
if (!tableElement) return null;
|
|
119
|
+
const targetWidth = getTargetTableWidth(tableElement, columnCount);
|
|
120
|
+
if (targetWidth <= 0) return null;
|
|
121
|
+
const sourceWidths = getSourceColumnWidths(editor, tableNode, tableElement, columnCount);
|
|
122
|
+
if (sourceWidths) return fitColumnWidths(sourceWidths, targetWidth);
|
|
123
|
+
return isPastedTable ? getDistributedTableColumnWidths(editor, tableNode) : null;
|
|
124
|
+
};
|
|
125
|
+
const useAutoFitPastedTable = (editor) => {
|
|
126
|
+
const pendingTableKeysRef = useRef(/* @__PURE__ */ new Set());
|
|
127
|
+
const pasteTableKeysRef = useRef(/* @__PURE__ */ new Set());
|
|
128
|
+
const frameIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!editor) return;
|
|
131
|
+
const clearFrame = () => {
|
|
132
|
+
for (const frameId of frameIdsRef.current) cancelAnimationFrame(frameId);
|
|
133
|
+
frameIdsRef.current.clear();
|
|
134
|
+
};
|
|
135
|
+
const fitTable = (tableKey, remainingAttempts = MAX_AUTO_FIT_ATTEMPTS) => {
|
|
136
|
+
const frameId = requestAnimationFrame(() => {
|
|
137
|
+
frameIdsRef.current.delete(frameId);
|
|
138
|
+
let nextColWidths = null;
|
|
139
|
+
editor.update(() => {
|
|
140
|
+
const tableNode = $getNodeByKey(tableKey);
|
|
141
|
+
if (!$isTableNode(tableNode)) {
|
|
142
|
+
pendingTableKeysRef.current.delete(tableKey);
|
|
143
|
+
pasteTableKeysRef.current.delete(tableKey);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const columnCount = tableNode.getColumnCount();
|
|
147
|
+
const isPastedTable = pasteTableKeysRef.current.has(tableKey);
|
|
148
|
+
if (columnCount === 0 || !isPastedTable && hasValidColumnWidths(tableNode.getColWidths(), columnCount)) {
|
|
149
|
+
pendingTableKeysRef.current.delete(tableKey);
|
|
150
|
+
pasteTableKeysRef.current.delete(tableKey);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
nextColWidths = getFittedTableColumnWidths(editor, tableNode, isPastedTable);
|
|
154
|
+
if (!nextColWidths) return;
|
|
155
|
+
tableNode.setColWidths(nextColWidths);
|
|
156
|
+
pendingTableKeysRef.current.delete(tableKey);
|
|
157
|
+
pasteTableKeysRef.current.delete(tableKey);
|
|
158
|
+
}, { tag: [HISTORIC_TAG, SKIP_SCROLL_INTO_VIEW_TAG] });
|
|
159
|
+
if (nextColWidths) {
|
|
160
|
+
const syncFrameId = requestAnimationFrame(() => {
|
|
161
|
+
frameIdsRef.current.delete(syncFrameId);
|
|
162
|
+
if (nextColWidths) syncTableWidthDOM(editor, tableKey, nextColWidths);
|
|
163
|
+
});
|
|
164
|
+
frameIdsRef.current.add(syncFrameId);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (remainingAttempts > 0 && pendingTableKeysRef.current.has(tableKey)) fitTable(tableKey, remainingAttempts - 1);
|
|
168
|
+
});
|
|
169
|
+
frameIdsRef.current.add(frameId);
|
|
170
|
+
};
|
|
171
|
+
const unregisterMutationListener = editor.registerMutationListener(TableNode, (nodeMutations, { updateTags }) => {
|
|
172
|
+
const isPasteUpdate = updateTags.has(PASTE_TAG);
|
|
173
|
+
for (const [tableKey, mutation] of nodeMutations) {
|
|
174
|
+
if (mutation !== "created") continue;
|
|
175
|
+
pendingTableKeysRef.current.add(tableKey);
|
|
176
|
+
if (isPasteUpdate) pasteTableKeysRef.current.add(tableKey);
|
|
177
|
+
fitTable(tableKey);
|
|
178
|
+
}
|
|
179
|
+
}, { skipInitialization: true });
|
|
180
|
+
return () => {
|
|
181
|
+
clearFrame();
|
|
182
|
+
pendingTableKeysRef.current.clear();
|
|
183
|
+
pasteTableKeysRef.current.clear();
|
|
184
|
+
unregisterMutationListener();
|
|
185
|
+
};
|
|
186
|
+
}, [editor]);
|
|
187
|
+
};
|
|
188
|
+
//#endregion
|
|
189
|
+
export { useAutoFitPastedTable };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { IServiceID } from "../../../types/kernel.js";
|
|
2
|
+
import { TableNode } from "@lexical/table";
|
|
3
|
+
import { LexicalEditor } from "lexical";
|
|
4
|
+
|
|
5
|
+
//#region src/plugins/table/service/i-table-controller-menu-service.d.ts
|
|
6
|
+
type TableControllerMenuAxis = 'column' | 'row';
|
|
7
|
+
interface ITableControllerMenuRenderContext {
|
|
8
|
+
axis: TableControllerMenuAxis;
|
|
9
|
+
editor: LexicalEditor;
|
|
10
|
+
node: TableNode;
|
|
11
|
+
selectedIndexes: number[];
|
|
12
|
+
}
|
|
13
|
+
interface ITableControllerBaseMenuItem {
|
|
14
|
+
key: string;
|
|
15
|
+
order?: number;
|
|
16
|
+
when?: (context: ITableControllerMenuRenderContext) => boolean;
|
|
17
|
+
}
|
|
18
|
+
interface ITableControllerMenuActionItem extends ITableControllerBaseMenuItem {
|
|
19
|
+
danger?: boolean;
|
|
20
|
+
label: string | ((context: ITableControllerMenuRenderContext) => string);
|
|
21
|
+
onClick: (context: ITableControllerMenuRenderContext) => void;
|
|
22
|
+
preview?: 'delete' | 'insert-after' | 'insert-before';
|
|
23
|
+
type?: 'item';
|
|
24
|
+
}
|
|
25
|
+
interface ITableControllerMenuSeparatorItem extends ITableControllerBaseMenuItem {
|
|
26
|
+
type: 'separator';
|
|
27
|
+
}
|
|
28
|
+
type ITableControllerMenuItem = ITableControllerMenuActionItem | ITableControllerMenuSeparatorItem;
|
|
29
|
+
interface ITableControllerMenuService {
|
|
30
|
+
getItems(context: ITableControllerMenuRenderContext): ITableControllerMenuItem[];
|
|
31
|
+
registerItem(item: ITableControllerMenuItem): () => void;
|
|
32
|
+
subscribe(listener: () => void): () => void;
|
|
33
|
+
}
|
|
34
|
+
declare const ITableControllerMenuService: IServiceID<ITableControllerMenuService>;
|
|
35
|
+
declare class TableControllerMenuService implements ITableControllerMenuService {
|
|
36
|
+
private items;
|
|
37
|
+
private listeners;
|
|
38
|
+
getItems(context: ITableControllerMenuRenderContext): ITableControllerMenuItem[];
|
|
39
|
+
registerItem(item: ITableControllerMenuItem): () => void;
|
|
40
|
+
subscribe(listener: () => void): () => void;
|
|
41
|
+
private notify;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { ITableControllerMenuActionItem, ITableControllerMenuItem, ITableControllerMenuRenderContext, ITableControllerMenuSeparatorItem, ITableControllerMenuService, TableControllerMenuAxis, TableControllerMenuService };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { genServiceId } from "../../../editor-kernel/utils.js";
|
|
2
|
+
//#region src/plugins/table/service/i-table-controller-menu-service.ts
|
|
3
|
+
const ITableControllerMenuService = genServiceId("TableControllerMenuService");
|
|
4
|
+
var TableControllerMenuService = class {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.items = /* @__PURE__ */ new Map();
|
|
7
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
8
|
+
}
|
|
9
|
+
getItems(context) {
|
|
10
|
+
return Array.from(this.items.values()).filter((item) => item.when ? item.when(context) : true).sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
11
|
+
}
|
|
12
|
+
registerItem(item) {
|
|
13
|
+
this.items.set(item.key, item);
|
|
14
|
+
this.notify();
|
|
15
|
+
return () => {
|
|
16
|
+
this.items.delete(item.key);
|
|
17
|
+
this.notify();
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
subscribe(listener) {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
this.listeners.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
notify() {
|
|
27
|
+
for (const listener of this.listeners) listener();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ITableControllerMenuService, TableControllerMenuService };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { ITableControllerMenuActionItem, ITableControllerMenuItem, ITableControllerMenuRenderContext, ITableControllerMenuSeparatorItem, ITableControllerMenuService, TableControllerMenuAxis, TableControllerMenuService } from "./i-table-controller-menu-service.js";
|