@lobehub/editor 4.20.2 → 4.21.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.
@@ -6,7 +6,7 @@ import { ActionIcon, Flexbox } from "@lobehub/ui";
6
6
  import { ChevronDown, ChevronRight } from "lucide-react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/codemirror/components/Toolbar.tsx
9
- const Toolbar = ({ selectedLang, onLanguageChange, onCopy, tabSize, onTabSizeChange, useTabs, onUseTabsChange, showLineNumbers, onShowLineNumbersChange, onClick, expand, toggleExpand, labels, languageOptions }) => {
9
+ const Toolbar = ({ selectedLang, onLanguageChange, onCopy, tabSize, onTabSizeChange, useTabs, onUseTabsChange, showLineNumbers, onShowLineNumbersChange, onClick, expand, toggleExpand, labels, languageOptions, extra }) => {
10
10
  return /* @__PURE__ */ jsxs(Flexbox, {
11
11
  align: "center",
12
12
  className: "cm-header-toolbar",
@@ -24,6 +24,7 @@ const Toolbar = ({ selectedLang, onLanguageChange, onCopy, tabSize, onTabSizeCha
24
24
  horizontal: true,
25
25
  onClick: (e) => e.stopPropagation(),
26
26
  children: [
27
+ extra,
27
28
  /* @__PURE__ */ jsx(MoreOptions, {
28
29
  labels,
29
30
  onShowLineNumbersChange,
@@ -184,6 +184,12 @@ const LANGUAGES = [
184
184
  syntax: "markdown",
185
185
  value: "markdown"
186
186
  },
187
+ {
188
+ ext: ["mermaid", "mmd"],
189
+ name: "Mermaid",
190
+ syntax: "simplemode",
191
+ value: "mermaid"
192
+ },
187
193
  {
188
194
  name: "MATLAB",
189
195
  syntax: "octave",
@@ -1,3 +1,5 @@
1
+ import { ReactNode } from "react";
2
+
1
3
  //#region src/codemirror/types.d.ts
2
4
  interface CodeMirrorMode {
3
5
  ext?: string[];
@@ -7,6 +9,7 @@ interface CodeMirrorMode {
7
9
  }
8
10
  interface CodeMirrorLabels {
9
11
  copy?: string;
12
+ preview?: string;
10
13
  selectLanguage?: string;
11
14
  showLineNumbers?: string;
12
15
  tabSize?: string;
@@ -35,6 +38,7 @@ interface MoreOptionsProps {
35
38
  }
36
39
  interface ToolbarProps {
37
40
  expand?: boolean;
41
+ extra?: ReactNode;
38
42
  labels?: CodeMirrorLabels;
39
43
  languageOptions?: CodeMirrorMode[];
40
44
  onClick?: () => void;
package/es/headless.d.ts CHANGED
@@ -62,6 +62,7 @@ declare const _default: {
62
62
  cancel: string;
63
63
  codemirror: {
64
64
  copySuccess: string;
65
+ preview: string;
65
66
  selectLanguage: string;
66
67
  selectTheme: string;
67
68
  showLineNumbers: string;
package/es/headless.js CHANGED
@@ -28,6 +28,7 @@ var locale_default = {
28
28
  cancel: "Cancel",
29
29
  codemirror: {
30
30
  copySuccess: "Code copied to clipboard",
31
+ preview: "Preview",
31
32
  selectLanguage: "Select language",
32
33
  selectTheme: "Select theme",
33
34
  showLineNumbers: "Show Line Numbers",
@@ -5449,6 +5450,12 @@ const LANGUAGES = [
5449
5450
  syntax: "markdown",
5450
5451
  value: "markdown"
5451
5452
  },
5453
+ {
5454
+ ext: ["mermaid", "mmd"],
5455
+ name: "Mermaid",
5456
+ syntax: "simplemode",
5457
+ value: "mermaid"
5458
+ },
5452
5459
  {
5453
5460
  name: "MATLAB",
5454
5461
  syntax: "octave",
@@ -8,6 +8,7 @@ declare const _default: {
8
8
  cancel: string;
9
9
  codemirror: {
10
10
  copySuccess: string;
11
+ preview: string;
11
12
  selectLanguage: string;
12
13
  selectTheme: string;
13
14
  showLineNumbers: string;
@@ -11,6 +11,7 @@ var init_locale = __esmMin((() => {
11
11
  cancel: "Cancel",
12
12
  codemirror: {
13
13
  copySuccess: "Code copied to clipboard",
14
+ preview: "Preview",
14
15
  selectLanguage: "Select language",
15
16
  selectTheme: "Select theme",
16
17
  showLineNumbers: "Show Line Numbers",
@@ -6,7 +6,9 @@ import { lobeTheme } from "../../../codemirror/theme.js";
6
6
  import { useTranslation } from "../../../editor-kernel/react/useTranslation.js";
7
7
  import { SELECT_AFTER_CODEMIRROR_COMMAND, SELECT_BEFORE_CODEMIRROR_COMMAND } from "../command/index.js";
8
8
  import { useLexicalNodeSelection } from "../../../editor-kernel/react/useLexicalNodeSelection.js";
9
- import { Block } from "@lobehub/ui";
9
+ import MermaidPreview from "./MermaidPreview.js";
10
+ import { ActionIcon, Block } from "@lobehub/ui";
11
+ import { CodeXml, Eye } from "lucide-react";
10
12
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
13
  import { jsx, jsxs } from "react/jsx-runtime";
12
14
  import { cx } from "antd-style";
@@ -26,6 +28,8 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
26
28
  const [useTabs, setUseTabs] = useState(node.options.indentWithTabs ?? false);
27
29
  const [showLineNumbers, setShowLineNumbers] = useState(node.options.lineNumbers ?? false);
28
30
  const [expand, setExpand] = useState(true);
31
+ const [preview, setPreview] = useState(false);
32
+ const [code, setCode] = useState(node.code);
29
33
  const handleCopy = useCallback(async () => {
30
34
  if (instanceRef.current) {
31
35
  const code = instanceRef.current.getValue();
@@ -35,6 +39,7 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
35
39
  }
36
40
  }, []);
37
41
  const labels = useMemo(() => ({
42
+ preview: t("codemirror.preview"),
38
43
  selectLanguage: t("codemirror.selectLanguage"),
39
44
  showLineNumbers: t("codemirror.showLineNumbers"),
40
45
  tabSize: t("codemirror.tabSize"),
@@ -42,6 +47,7 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
42
47
  }), [t]);
43
48
  const handleLanguageChange = useCallback((value) => {
44
49
  setSelectedLang(value);
50
+ if (value !== "mermaid") setPreview(false);
45
51
  if (instanceRef.current) instanceRef.current.setOption("mode", value);
46
52
  editor.update(() => {
47
53
  node.setLang(value);
@@ -145,7 +151,9 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
145
151
  });
146
152
  });
147
153
  instance.on("change", () => {
148
- isEmptyRef.current = !instance.getValue().trim();
154
+ const currentValue = instance.getValue();
155
+ isEmptyRef.current = !currentValue.trim();
156
+ setCode(currentValue);
149
157
  });
150
158
  instance.on("change", debounce(() => {
151
159
  const currentValue = instance.getValue();
@@ -191,27 +199,42 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
191
199
  onMouseUp: (e) => e.stopPropagation(),
192
200
  onSelect: (e) => e.stopPropagation(),
193
201
  variant: "filled",
194
- children: [/* @__PURE__ */ jsx(Toolbar, {
195
- expand,
196
- labels,
197
- onClick: () => setExpand(!expand),
198
- onCopy: handleCopy,
199
- onLanguageChange: handleLanguageChange,
200
- onShowLineNumbersChange: handleShowLineNumbersChange,
201
- onTabSizeChange: handleTabSizeChange,
202
- onUseTabsChange: handleUseTabsChange,
203
- selectedLang,
204
- showLineNumbers,
205
- tabSize,
206
- toggleExpand: () => setExpand(!expand),
207
- useTabs
208
- }), /* @__PURE__ */ jsx("div", {
209
- className: cx("cm-container", !expand && "cm-container-collapsed"),
210
- children: /* @__PURE__ */ jsx("textarea", {
211
- className: "cm-textarea",
212
- ref
202
+ children: [
203
+ /* @__PURE__ */ jsx(Toolbar, {
204
+ expand,
205
+ extra: selectedLang === "mermaid" ? /* @__PURE__ */ jsx(ActionIcon, {
206
+ className: "cm-hidden-actions",
207
+ icon: preview ? CodeXml : Eye,
208
+ onClick: () => setPreview(!preview),
209
+ size: "small",
210
+ title: labels.preview
211
+ }) : void 0,
212
+ labels,
213
+ onClick: () => setExpand(!expand),
214
+ onCopy: handleCopy,
215
+ onLanguageChange: handleLanguageChange,
216
+ onShowLineNumbersChange: handleShowLineNumbersChange,
217
+ onTabSizeChange: handleTabSizeChange,
218
+ onUseTabsChange: handleUseTabsChange,
219
+ selectedLang,
220
+ showLineNumbers,
221
+ tabSize,
222
+ toggleExpand: () => setExpand(!expand),
223
+ useTabs
224
+ }),
225
+ /* @__PURE__ */ jsx("div", {
226
+ className: cx("cm-container", (!expand || preview && selectedLang === "mermaid") && "cm-container-collapsed"),
227
+ children: /* @__PURE__ */ jsx("textarea", {
228
+ className: "cm-textarea",
229
+ ref
230
+ })
231
+ }),
232
+ expand && preview && selectedLang === "mermaid" && /* @__PURE__ */ jsx("div", {
233
+ className: "cm-container",
234
+ onMouseDown: (e) => e.stopPropagation(),
235
+ children: /* @__PURE__ */ jsx(MermaidPreview, { code })
213
236
  })
214
- })]
237
+ ]
215
238
  });
216
239
  };
217
240
  ReactCodemirrorNode.displayName = "ReactCodemirrorNode";
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { Mermaid } from "@lobehub/ui";
3
+ import { useEffect, useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { cssVar } from "antd-style";
6
+ //#region src/plugins/codemirror-block/react/MermaidPreview.tsx
7
+ const MermaidPreview = ({ code }) => {
8
+ const [error, setError] = useState(null);
9
+ useEffect(() => {
10
+ if (!code.trim()) {
11
+ setError(null);
12
+ return;
13
+ }
14
+ let cancelled = false;
15
+ import("mermaid").then(async (mod) => {
16
+ try {
17
+ await mod.default.parse(code);
18
+ if (!cancelled) setError(null);
19
+ } catch (error_) {
20
+ if (!cancelled) setError(error_ instanceof Error ? error_.message : String(error_));
21
+ }
22
+ });
23
+ return () => {
24
+ cancelled = true;
25
+ };
26
+ }, [code]);
27
+ if (!code.trim()) return null;
28
+ if (error) return /* @__PURE__ */ jsx("div", {
29
+ style: {
30
+ color: cssVar.colorTextDescription,
31
+ fontFamily: cssVar.fontFamilyCode,
32
+ fontSize: 12,
33
+ padding: 16,
34
+ whiteSpace: "pre-wrap"
35
+ },
36
+ children: error
37
+ });
38
+ return /* @__PURE__ */ jsx(Mermaid, {
39
+ animated: false,
40
+ theme: "lobe-theme",
41
+ variant: "borderless",
42
+ children: code
43
+ });
44
+ };
45
+ MermaidPreview.displayName = "MermaidPreview";
46
+ //#endregion
47
+ export { MermaidPreview as default };
@@ -6,21 +6,36 @@ import { HIDE_TOOLBAR_COMMAND, registerToolbarCommand } from "../command/index.j
6
6
  import { getDOMRangeRect } from "../utils/getDOMRangeRect.js";
7
7
  import { setFloatingElemPosition } from "../utils/setFloatingElemPosition.js";
8
8
  import { styles } from "./style.js";
9
- import { Block } from "@lobehub/ui";
10
- import { useCallback, useRef } from "react";
9
+ import { Block, LOBE_THEME_APP_ID } from "@lobehub/ui";
10
+ import { useCallback, useRef, useState } from "react";
11
11
  import { jsx } from "react/jsx-runtime";
12
12
  import { cx, useThemeMode } from "antd-style";
13
13
  import { $getSelection, COMMAND_PRIORITY_LOW, SELECTION_CHANGE_COMMAND, getDOMSelection } from "lexical";
14
14
  import { mergeRegister } from "@lexical/utils";
15
+ import { createPortal } from "react-dom";
15
16
  //#region src/plugins/toolbar/react/index.tsx
16
17
  init_debug();
17
- const ReactToolbarPlugin = ({ className, children }) => {
18
+ const resolveDefaultPortalContainer = () => {
19
+ if (typeof document === "undefined") return null;
20
+ return document.getElementById(LOBE_THEME_APP_ID) ?? document.body;
21
+ };
22
+ const ReactToolbarPlugin = ({ className, children, getPopupContainer, usePortal = true, zIndex }) => {
18
23
  const popupCharStylesEditorRef = useRef(null);
19
24
  const anchorElemRef = useRef(null);
25
+ const [portalContainer, setPortalContainer] = useState(null);
20
26
  const [kernelEditor] = useLexicalComposerContext();
21
27
  const { isDarkMode } = useThemeMode();
22
28
  const isMouseDownRef = useRef(false);
23
29
  const logger = createDebugLogger("plugin", "toolbar");
30
+ const resolvePortalContainer = useCallback(() => {
31
+ if (!usePortal) return null;
32
+ if (getPopupContainer) return getPopupContainer();
33
+ return resolveDefaultPortalContainer();
34
+ }, [getPopupContainer, usePortal]);
35
+ const syncPortalContainer = useCallback(() => {
36
+ const nextContainer = resolvePortalContainer();
37
+ setPortalContainer((current) => current === nextContainer ? current : nextContainer);
38
+ }, [resolvePortalContainer]);
24
39
  const $updateTextFormatFloatingToolbar = useCallback((editor) => {
25
40
  if (!anchorElemRef.current) return;
26
41
  const selection = $getSelection();
@@ -65,6 +80,14 @@ const ReactToolbarPlugin = ({ className, children }) => {
65
80
  };
66
81
  }
67
82
  }, []);
83
+ useLexicalEditor((editor) => {
84
+ if (!usePortal) {
85
+ setPortalContainer(null);
86
+ return;
87
+ }
88
+ syncPortalContainer();
89
+ return editor.registerRootListener(syncPortalContainer);
90
+ }, [syncPortalContainer, usePortal]);
68
91
  useLexicalEditor((editor) => {
69
92
  const handleMouseDown = handleMouseDownFactory(() => {
70
93
  editor.dispatchCommand(HIDE_TOOLBAR_COMMAND, void 0);
@@ -75,10 +98,32 @@ const ReactToolbarPlugin = ({ className, children }) => {
75
98
  });
76
99
  });
77
100
  const rootElement = editor.getRootElement();
101
+ const editorWindow = editor._window ?? rootElement?.ownerDocument.defaultView ?? globalThis.window ?? null;
102
+ let animationFrameId = null;
103
+ const updateToolbarPosition = () => {
104
+ if (!editorWindow || isMouseDownRef.current || animationFrameId !== null) return;
105
+ animationFrameId = editorWindow.requestAnimationFrame(() => {
106
+ animationFrameId = null;
107
+ editor.getEditorState().read(() => {
108
+ $updateTextFormatFloatingToolbar(editor);
109
+ });
110
+ });
111
+ };
112
+ const scrollListenerOptions = {
113
+ capture: true,
114
+ passive: true
115
+ };
116
+ const scrollCleanupOptions = { capture: true };
117
+ const resizeListenerOptions = { passive: true };
78
118
  if (rootElement) {
79
119
  rootElement.addEventListener("mousedown", handleMouseDown);
80
120
  document.addEventListener("mouseup", handleMouseUp);
81
121
  }
122
+ if (editorWindow) {
123
+ editorWindow.addEventListener("scroll", updateToolbarPosition, scrollListenerOptions);
124
+ editorWindow.document.addEventListener("scroll", updateToolbarPosition, scrollListenerOptions);
125
+ editorWindow.addEventListener("resize", updateToolbarPosition, resizeListenerOptions);
126
+ }
82
127
  return mergeRegister(registerToolbarCommand(editor, { onHide: $hideFloatingToolbar }), editor.registerUpdateListener(({ editorState }) => {
83
128
  if (!isMouseDownRef.current) editorState.read(() => {
84
129
  $updateTextFormatFloatingToolbar(editor);
@@ -91,11 +136,18 @@ const ReactToolbarPlugin = ({ className, children }) => {
91
136
  rootElement.removeEventListener("mousedown", handleMouseDown);
92
137
  document.removeEventListener("mouseup", handleMouseUp);
93
138
  }
139
+ if (editorWindow) {
140
+ editorWindow.removeEventListener("scroll", updateToolbarPosition, scrollCleanupOptions);
141
+ editorWindow.document.removeEventListener("scroll", updateToolbarPosition, scrollCleanupOptions);
142
+ editorWindow.removeEventListener("resize", updateToolbarPosition);
143
+ if (animationFrameId !== null) editorWindow.cancelAnimationFrame(animationFrameId);
144
+ }
94
145
  });
95
146
  });
96
- return /* @__PURE__ */ jsx("div", {
97
- className: styles.anchor,
147
+ const toolbarNode = /* @__PURE__ */ jsx("div", {
148
+ className: usePortal ? styles.portalAnchor : styles.anchor,
98
149
  ref: anchorElemRef,
150
+ style: usePortal && zIndex !== void 0 ? { zIndex } : void 0,
99
151
  children: /* @__PURE__ */ jsx(Block, {
100
152
  className: cx(isDarkMode ? styles.toolbarDark : styles.toolbarLight, className),
101
153
  padding: 4,
@@ -104,6 +156,8 @@ const ReactToolbarPlugin = ({ className, children }) => {
104
156
  children
105
157
  })
106
158
  });
159
+ if (usePortal) return portalContainer ? createPortal(toolbarNode, portalContainer) : null;
160
+ return toolbarNode;
107
161
  };
108
162
  //#endregion
109
163
  export { ReactToolbarPlugin };
@@ -3,8 +3,15 @@ import { createStaticStyles } from "antd-style";
3
3
  const styles = createStaticStyles(({ css, cssVar }) => ({
4
4
  anchor: css`
5
5
  position: relative;
6
+ `,
7
+ portalAnchor: css`
8
+ pointer-events: none;
9
+ position: fixed;
10
+ z-index: 1100;
11
+ inset: 0;
6
12
  `,
7
13
  toolbarDark: css`
14
+ pointer-events: auto;
8
15
  will-change: transform;
9
16
 
10
17
  position: absolute;
@@ -28,6 +35,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
28
35
  transition: opacity 0.12s ${cssVar.motionEaseOut};
29
36
  `,
30
37
  toolbarLight: css`
38
+ pointer-events: auto;
31
39
  will-change: transform;
32
40
 
33
41
  position: absolute;
@@ -1,9 +1,12 @@
1
- import { ReactNode } from "react";
1
+ import { CSSProperties, ReactNode } from "react";
2
2
 
3
3
  //#region src/plugins/toolbar/react/type.d.ts
4
4
  interface ReactToolbarPluginProps {
5
5
  children?: ReactNode;
6
6
  className?: string;
7
+ getPopupContainer?: () => HTMLElement | null;
8
+ usePortal?: boolean;
9
+ zIndex?: CSSProperties['zIndex'];
7
10
  }
8
11
  //#endregion
9
12
  export { ReactToolbarPluginProps };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/editor",
3
- "version": "4.20.2",
3
+ "version": "4.21.0",
4
4
  "description": "A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.",
5
5
  "keywords": [
6
6
  "lobehub",