@lobehub/editor 4.25.0 → 4.26.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.
@@ -24,6 +24,11 @@ declare class Kernel extends EventEmitter implements IEditorKernel {
24
24
  private hotReloadMode;
25
25
  private logger;
26
26
  private historyState;
27
+ /**
28
+ * Root-scoped resources are attached to the Lexical editor's root listener
29
+ * so direct Lexical setRootElement(null) calls are safe as well.
30
+ */
31
+ private unregisterRootLifecycle?;
27
32
  private editor?;
28
33
  private headlessEditor;
29
34
  constructor();
@@ -44,6 +49,7 @@ declare class Kernel extends EventEmitter implements IEditorKernel {
44
49
  destroy(): void;
45
50
  getRootElement(): HTMLElement | null;
46
51
  setRootElement(dom: HTMLElement, editable?: boolean): LexicalEditor;
52
+ setRootElement(dom: null, editable?: boolean): LexicalEditor | null;
47
53
  setEditable(editable: boolean): void;
48
54
  initNodeEditor(): LexicalEditor;
49
55
  initHeadlessEditor(): LexicalEditor;
@@ -80,6 +86,9 @@ declare class Kernel extends EventEmitter implements IEditorKernel {
80
86
  registerNodeTransform(transform: INodeRegistrationTransform): () => void;
81
87
  registerBeforeEditorInit(hook: IBeforeEditorInitLifecycle): () => void;
82
88
  registerRootClassName(className: string): () => void;
89
+ registerRootListener(listener: (rootElement: HTMLElement | null) => void): () => void;
90
+ private getEditorId;
91
+ private registerRootLifecycle;
83
92
  private runBeforeEditorInitLifecycle;
84
93
  private applyRootClassNames;
85
94
  private resolveNodesForInitialization;
@@ -95,26 +95,46 @@ var init_kernel = __esmMin((() => {
95
95
  return this.editor || null;
96
96
  }
97
97
  destroy() {
98
- unregisterEditorKernel(this.themes[EDITOR_THEME_KEY]);
98
+ const editor = this.editor;
99
+ const editorId = this.getEditorId();
99
100
  this.logger.info(`🗑️ Destroying editor with ${this.pluginsInstances.length} plugins`);
101
+ if (editor) try {
102
+ editor.setRootElement(null);
103
+ } catch (error) {
104
+ this.logger.warn("Failed to detach editor root during destroy:", error);
105
+ }
106
+ unregisterEditorKernel(editorId);
100
107
  try {
101
- this.editor?.setEditorState(createEmptyEditorState());
108
+ editor?.setEditorState(createEmptyEditorState());
102
109
  } catch (error) {
103
110
  this.logger.warn("Failed to reset editor state during destroy:", error);
104
111
  }
105
- this.dataTypeMap.clear();
112
+ this.unregisterRootLifecycle?.();
113
+ this.unregisterRootLifecycle = void 0;
114
+ let destroyError;
106
115
  this.pluginsInstances.forEach((plugin) => {
107
- if (plugin.destroy) plugin.destroy();
116
+ try {
117
+ plugin.destroy?.();
118
+ } catch (error) {
119
+ destroyError ??= error;
120
+ }
108
121
  });
109
122
  this.pluginsInstances = [];
110
- this.beforeEditorInitHooks = [];
123
+ this.dataTypeMap.clear();
124
+ this.decorators = {};
111
125
  this.nodeTransforms = [];
126
+ this.nodes = [];
127
+ this.beforeEditorInitHooks = [];
112
128
  this.rootClassNames.clear();
113
129
  this.serviceMap.clear();
114
- this.decorators = {};
115
- this.themes = {};
130
+ this.themes = { [EDITOR_THEME_KEY]: generateEditorId() };
131
+ this.historyState = createEmptyHistoryState();
132
+ this._commands.clear();
133
+ this._commandsClean.clear();
134
+ this.editor = void 0;
116
135
  this.headlessEditor = false;
117
136
  this.logger.info("✅ Editor destroyed");
137
+ if (destroyError) throw destroyError;
118
138
  }
119
139
  getRootElement() {
120
140
  if (this.headlessEditor) return null;
@@ -122,12 +142,23 @@ var init_kernel = __esmMin((() => {
122
142
  }
123
143
  setRootElement(dom, editable = true) {
124
144
  if (this.editor) {
125
- if (this.headlessEditor) throw new Error("Headless editor cannot be attached to a root element.");
145
+ if (this.headlessEditor) {
146
+ if (dom === null) return this.editor;
147
+ throw new Error("Headless editor cannot be attached to a root element.");
148
+ }
126
149
  this.logger.warn("[Editor] Editor is already initialized, updating root element only");
150
+ if (dom) registerEditorKernel(this.getEditorId(), this);
127
151
  this.editor.setRootElement(dom);
128
- this.applyRootClassNames(dom);
152
+ if (dom) {
153
+ this.editor.setEditable(editable);
154
+ this.applyRootClassNames(dom);
155
+ }
129
156
  return this.editor;
130
157
  }
158
+ if (dom === null) {
159
+ unregisterEditorKernel(this.getEditorId());
160
+ return null;
161
+ }
131
162
  if (this.pluginsInstances.length === 0) {
132
163
  this.logger.info(`🔌 Initializing ${this.plugins.length} plugins`);
133
164
  for (const plugin of this.plugins) {
@@ -138,7 +169,7 @@ var init_kernel = __esmMin((() => {
138
169
  this.runBeforeEditorInitLifecycle();
139
170
  const resolvedNodes = this.resolveNodesForInitialization();
140
171
  this.logger.info(`📝 Creating editor with ${resolvedNodes.length} nodes`);
141
- registerEditorKernel(this.themes[EDITOR_THEME_KEY], this);
172
+ registerEditorKernel(this.getEditorId(), this);
142
173
  const editor = this.editor = createEditor({
143
174
  __kernel: this,
144
175
  editable,
@@ -151,9 +182,9 @@ var init_kernel = __esmMin((() => {
151
182
  theme: this.themes
152
183
  });
153
184
  this.headlessEditor = false;
185
+ this.registerRootLifecycle(editor);
154
186
  this.editor.setRootElement(dom);
155
187
  this.applyRootClassNames(dom);
156
- registerEvent(editor, dom);
157
188
  this.pluginsInstances.forEach((plugin) => {
158
189
  plugin.onInit?.(editor);
159
190
  });
@@ -193,6 +224,7 @@ var init_kernel = __esmMin((() => {
193
224
  theme: this.themes
194
225
  });
195
226
  this.headlessEditor = false;
227
+ this.registerRootLifecycle(editor);
196
228
  this.pluginsInstances.forEach((plugin) => {
197
229
  plugin.onInit?.(editor);
198
230
  });
@@ -513,6 +545,34 @@ var init_kernel = __esmMin((() => {
513
545
  if (currentRootElement) currentRootElement.classList.remove(...classNames);
514
546
  };
515
547
  }
548
+ registerRootListener(listener) {
549
+ if (!this.editor || this.headlessEditor) return noop;
550
+ return this.editor.registerRootListener(listener);
551
+ }
552
+ getEditorId() {
553
+ return this.themes[EDITOR_THEME_KEY];
554
+ }
555
+ registerRootLifecycle(editor) {
556
+ this.unregisterRootLifecycle?.();
557
+ let rootEventCleanup;
558
+ const clearRootEvent = () => {
559
+ rootEventCleanup?.();
560
+ rootEventCleanup = void 0;
561
+ };
562
+ const unregisterRootListener = editor.registerRootListener((rootElement) => {
563
+ clearRootEvent();
564
+ if (!rootElement) {
565
+ unregisterEditorKernel(this.getEditorId());
566
+ return;
567
+ }
568
+ registerEditorKernel(this.getEditorId(), this);
569
+ rootEventCleanup = registerEvent(editor, rootElement);
570
+ });
571
+ this.unregisterRootLifecycle = () => {
572
+ clearRootEvent();
573
+ unregisterRootListener();
574
+ };
575
+ }
516
576
  runBeforeEditorInitLifecycle() {
517
577
  for (const hook of this.beforeEditorInitHooks) hook();
518
578
  }
package/es/headless.d.ts CHANGED
@@ -76,6 +76,7 @@ declare const _default: {
76
76
  };
77
77
  image: {
78
78
  broken: string;
79
+ preview: string;
79
80
  replace: string;
80
81
  };
81
82
  link: {
@@ -357,6 +358,7 @@ interface IEditor {
357
358
  * @param dom
358
359
  */
359
360
  setRootElement(dom: HTMLElement, editable?: boolean): LexicalEditor;
361
+ setRootElement(dom: null, editable?: boolean): LexicalEditor | null;
360
362
  /**
361
363
  * set editor selection
362
364
  * @param selection
@@ -435,6 +437,17 @@ interface IEditorKernel extends IEditor {
435
437
  * @returns unregister function
436
438
  */
437
439
  registerRootClassName(className: string): () => void;
440
+ /**
441
+ * Register a callback for root attach/detach events.
442
+ *
443
+ * Call this after the kernel's Lexical editor has been initialized (usually
444
+ * from plugin onInit). Lexical immediately invokes a newly registered root
445
+ * listener with the current root, so listeners registered after the initial
446
+ * setRootElement still receive the first root attach. Headless kernels
447
+ * return a no-op cleanup because headless Lexical editors do not support
448
+ * root listeners.
449
+ */
450
+ registerRootListener(listener: (rootElement: HTMLElement | null) => void): () => void;
438
451
  /**
439
452
  * Register service
440
453
  * @param serviceId
package/es/headless.js CHANGED
@@ -43,6 +43,7 @@ var locale_default = {
43
43
  },
44
44
  image: {
45
45
  broken: "Broken image",
46
+ preview: "Preview image",
46
47
  replace: "Replace"
47
48
  },
48
49
  link: {
@@ -818,26 +819,46 @@ var Kernel = class Kernel extends EventEmitter {
818
819
  return this.editor || null;
819
820
  }
820
821
  destroy() {
821
- unregisterEditorKernel(this.themes[EDITOR_THEME_KEY]);
822
+ const editor = this.editor;
823
+ const editorId = this.getEditorId();
822
824
  this.logger.info(`🗑️ Destroying editor with ${this.pluginsInstances.length} plugins`);
825
+ if (editor) try {
826
+ editor.setRootElement(null);
827
+ } catch (error) {
828
+ this.logger.warn("Failed to detach editor root during destroy:", error);
829
+ }
830
+ unregisterEditorKernel(editorId);
823
831
  try {
824
- this.editor?.setEditorState(createEmptyEditorState());
832
+ editor?.setEditorState(createEmptyEditorState());
825
833
  } catch (error) {
826
834
  this.logger.warn("Failed to reset editor state during destroy:", error);
827
835
  }
828
- this.dataTypeMap.clear();
836
+ this.unregisterRootLifecycle?.();
837
+ this.unregisterRootLifecycle = void 0;
838
+ let destroyError;
829
839
  this.pluginsInstances.forEach((plugin) => {
830
- if (plugin.destroy) plugin.destroy();
840
+ try {
841
+ plugin.destroy?.();
842
+ } catch (error) {
843
+ destroyError ??= error;
844
+ }
831
845
  });
832
846
  this.pluginsInstances = [];
833
- this.beforeEditorInitHooks = [];
847
+ this.dataTypeMap.clear();
848
+ this.decorators = {};
834
849
  this.nodeTransforms = [];
850
+ this.nodes = [];
851
+ this.beforeEditorInitHooks = [];
835
852
  this.rootClassNames.clear();
836
853
  this.serviceMap.clear();
837
- this.decorators = {};
838
- this.themes = {};
854
+ this.themes = { [EDITOR_THEME_KEY]: generateEditorId() };
855
+ this.historyState = createEmptyHistoryState();
856
+ this._commands.clear();
857
+ this._commandsClean.clear();
858
+ this.editor = void 0;
839
859
  this.headlessEditor = false;
840
860
  this.logger.info("✅ Editor destroyed");
861
+ if (destroyError) throw destroyError;
841
862
  }
842
863
  getRootElement() {
843
864
  if (this.headlessEditor) return null;
@@ -845,12 +866,23 @@ var Kernel = class Kernel extends EventEmitter {
845
866
  }
846
867
  setRootElement(dom, editable = true) {
847
868
  if (this.editor) {
848
- if (this.headlessEditor) throw new Error("Headless editor cannot be attached to a root element.");
869
+ if (this.headlessEditor) {
870
+ if (dom === null) return this.editor;
871
+ throw new Error("Headless editor cannot be attached to a root element.");
872
+ }
849
873
  this.logger.warn("[Editor] Editor is already initialized, updating root element only");
874
+ if (dom) registerEditorKernel(this.getEditorId(), this);
850
875
  this.editor.setRootElement(dom);
851
- this.applyRootClassNames(dom);
876
+ if (dom) {
877
+ this.editor.setEditable(editable);
878
+ this.applyRootClassNames(dom);
879
+ }
852
880
  return this.editor;
853
881
  }
882
+ if (dom === null) {
883
+ unregisterEditorKernel(this.getEditorId());
884
+ return null;
885
+ }
854
886
  if (this.pluginsInstances.length === 0) {
855
887
  this.logger.info(`🔌 Initializing ${this.plugins.length} plugins`);
856
888
  for (const plugin of this.plugins) {
@@ -861,7 +893,7 @@ var Kernel = class Kernel extends EventEmitter {
861
893
  this.runBeforeEditorInitLifecycle();
862
894
  const resolvedNodes = this.resolveNodesForInitialization();
863
895
  this.logger.info(`📝 Creating editor with ${resolvedNodes.length} nodes`);
864
- registerEditorKernel(this.themes[EDITOR_THEME_KEY], this);
896
+ registerEditorKernel(this.getEditorId(), this);
865
897
  const editor = this.editor = createEditor({
866
898
  __kernel: this,
867
899
  editable,
@@ -874,9 +906,9 @@ var Kernel = class Kernel extends EventEmitter {
874
906
  theme: this.themes
875
907
  });
876
908
  this.headlessEditor = false;
909
+ this.registerRootLifecycle(editor);
877
910
  this.editor.setRootElement(dom);
878
911
  this.applyRootClassNames(dom);
879
- registerEvent(editor, dom);
880
912
  this.pluginsInstances.forEach((plugin) => {
881
913
  plugin.onInit?.(editor);
882
914
  });
@@ -916,6 +948,7 @@ var Kernel = class Kernel extends EventEmitter {
916
948
  theme: this.themes
917
949
  });
918
950
  this.headlessEditor = false;
951
+ this.registerRootLifecycle(editor);
919
952
  this.pluginsInstances.forEach((plugin) => {
920
953
  plugin.onInit?.(editor);
921
954
  });
@@ -1236,6 +1269,34 @@ var Kernel = class Kernel extends EventEmitter {
1236
1269
  if (currentRootElement) currentRootElement.classList.remove(...classNames);
1237
1270
  };
1238
1271
  }
1272
+ registerRootListener(listener) {
1273
+ if (!this.editor || this.headlessEditor) return noop;
1274
+ return this.editor.registerRootListener(listener);
1275
+ }
1276
+ getEditorId() {
1277
+ return this.themes[EDITOR_THEME_KEY];
1278
+ }
1279
+ registerRootLifecycle(editor) {
1280
+ this.unregisterRootLifecycle?.();
1281
+ let rootEventCleanup;
1282
+ const clearRootEvent = () => {
1283
+ rootEventCleanup?.();
1284
+ rootEventCleanup = void 0;
1285
+ };
1286
+ const unregisterRootListener = editor.registerRootListener((rootElement) => {
1287
+ clearRootEvent();
1288
+ if (!rootElement) {
1289
+ unregisterEditorKernel(this.getEditorId());
1290
+ return;
1291
+ }
1292
+ registerEditorKernel(this.getEditorId(), this);
1293
+ rootEventCleanup = registerEvent(editor, rootElement);
1294
+ });
1295
+ this.unregisterRootLifecycle = () => {
1296
+ clearRootEvent();
1297
+ unregisterRootListener();
1298
+ };
1299
+ }
1239
1300
  runBeforeEditorInitLifecycle() {
1240
1301
  for (const hook of this.beforeEditorInitHooks) hook();
1241
1302
  }
@@ -6076,7 +6137,18 @@ const CommonPlugin = class extends KernelPlugin {
6076
6137
  if (this.config?.pasteAsPlainText) return runPasteHandlers(ctx, [handleFilePaste, handlePlainTextPaste]);
6077
6138
  return false;
6078
6139
  }, COMMAND_PRIORITY_CRITICAL));
6079
- this.registerClears(registerRichText(editor), registerProgressiveSelectAll(editor), CAN_USE_DOM ? registerDragonSupport(editor) : noop, registerHistory(editor, this.kernel.getHistoryState(), 300), registerBlockBackspace(editor), registerRichKeydown(editor, this.kernel, { enableHotkey: this.config?.enableHotkey }), registerCommands(editor), registerBreakLineClick(editor), registerCursorNode(editor), registerLastElement(editor), editor.registerCommand(INSERT_LINE_BREAK_COMMAND, () => {
6140
+ let dragonCleanup;
6141
+ const clearDragon = () => {
6142
+ dragonCleanup?.();
6143
+ dragonCleanup = void 0;
6144
+ };
6145
+ this.register(this.kernel.registerRootListener((rootElement) => {
6146
+ clearDragon();
6147
+ if (!rootElement || !CAN_USE_DOM) return;
6148
+ dragonCleanup = registerDragonSupport(editor);
6149
+ }));
6150
+ this.register(clearDragon);
6151
+ this.registerClears(registerRichText(editor), registerProgressiveSelectAll(editor), registerHistory(editor, this.kernel.getHistoryState(), 300), registerBlockBackspace(editor), registerRichKeydown(editor, this.kernel, { enableHotkey: this.config?.enableHotkey }), registerCommands(editor), registerBreakLineClick(editor), registerCursorNode(editor), registerLastElement(editor), editor.registerCommand(INSERT_LINE_BREAK_COMMAND, () => {
6080
6152
  const selection = $getSelection();
6081
6153
  if (!$isRangeSelection(selection)) return false;
6082
6154
  const { inCell, inTable } = $isCursorInTable(selection);
@@ -23,6 +23,7 @@ declare const _default: {
23
23
  };
24
24
  image: {
25
25
  broken: string;
26
+ preview: string;
26
27
  replace: string;
27
28
  };
28
29
  link: {
@@ -26,6 +26,7 @@ var init_locale = __esmMin((() => {
26
26
  },
27
27
  image: {
28
28
  broken: "Broken image",
29
+ preview: "Preview image",
29
30
  replace: "Replace"
30
31
  },
31
32
  link: {
@@ -1,7 +1,6 @@
1
1
  import { INodeHelper, init_helper } from "../../../editor-kernel/inode/helper.js";
2
2
  import { $isCursorInQuote, $isCursorInTable, createBlockNode, init_utils, sampleReader } from "../utils/index.js";
3
3
  import { KernelPlugin, init_plugin } from "../../../editor-kernel/plugin.js";
4
- import { init_utils as init_utils$1, noop } from "../../../editor-kernel/utils.js";
5
4
  import { registerCommands } from "../command/index.js";
6
5
  import { CursorNode, registerCursorNode } from "../node/cursor.js";
7
6
  import { ILitexmlService } from "../../litexml/service/litexml-service.js";
@@ -20,7 +19,6 @@ import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, Hea
20
19
  import { CAN_USE_DOM } from "@lexical/utils";
21
20
  import { registerDragonSupport } from "@lexical/dragon";
22
21
  //#region src/plugins/common/plugin/index.ts
23
- init_utils$1();
24
22
  init_helper();
25
23
  init_plugin();
26
24
  init_utils();
@@ -232,7 +230,18 @@ const CommonPlugin = class extends KernelPlugin {
232
230
  if (this.config?.pasteAsPlainText) return runPasteHandlers(ctx, [handleFilePaste, handlePlainTextPaste]);
233
231
  return false;
234
232
  }, COMMAND_PRIORITY_CRITICAL));
235
- this.registerClears(registerRichText(editor), registerProgressiveSelectAll(editor), CAN_USE_DOM ? registerDragonSupport(editor) : noop, registerHistory(editor, this.kernel.getHistoryState(), 300), registerBlockBackspace(editor), registerRichKeydown(editor, this.kernel, { enableHotkey: this.config?.enableHotkey }), registerCommands(editor), registerBreakLineClick(editor), registerCursorNode(editor), registerLastElement(editor), editor.registerCommand(INSERT_LINE_BREAK_COMMAND, () => {
233
+ let dragonCleanup;
234
+ const clearDragon = () => {
235
+ dragonCleanup?.();
236
+ dragonCleanup = void 0;
237
+ };
238
+ this.register(this.kernel.registerRootListener((rootElement) => {
239
+ clearDragon();
240
+ if (!rootElement || !CAN_USE_DOM) return;
241
+ dragonCleanup = registerDragonSupport(editor);
242
+ }));
243
+ this.register(clearDragon);
244
+ this.registerClears(registerRichText(editor), registerProgressiveSelectAll(editor), registerHistory(editor, this.kernel.getHistoryState(), 300), registerBlockBackspace(editor), registerRichKeydown(editor, this.kernel, { enableHotkey: this.config?.enableHotkey }), registerCommands(editor), registerBreakLineClick(editor), registerCursorNode(editor), registerLastElement(editor), editor.registerCommand(INSERT_LINE_BREAK_COMMAND, () => {
236
245
  const selection = $getSelection();
237
246
  if (!$isRangeSelection(selection)) return false;
238
247
  const { inCell, inTable } = $isCursorInTable(selection);
@@ -46,6 +46,12 @@ const ReactPlainText = memo(({ style, children, theme = {}, onChange, editable,
46
46
  }, [markdownOption]);
47
47
  const [isInitialized, setIsInitialized] = useState(false);
48
48
  const { props: { type, content, placeholder, lineEmptyPlaceholder } } = Children.only(children);
49
+ const initialDocumentRef = useRef({
50
+ content,
51
+ type
52
+ });
53
+ const editableRef = useRef(editable);
54
+ editableRef.current = editable;
49
55
  useLayoutEffect(() => {
50
56
  editor.registerPlugin(MarkdownPlugin, {
51
57
  autoFormatMarkdown,
@@ -76,13 +82,22 @@ const ReactPlainText = memo(({ style, children, theme = {}, onChange, editable,
76
82
  ]);
77
83
  useEffect(() => {
78
84
  const container = editorContainerRef.current;
79
- if (container && !isInitialized) {
80
- editor.setRootElement(container, editable);
81
- editor.setDocument(type, content);
85
+ if (!container) return;
86
+ if (!editor.getLexicalEditor()) {
87
+ editor.setRootElement(container, editableRef.current);
88
+ editor.setDocument(initialDocumentRef.current.type, initialDocumentRef.current.content);
82
89
  setIsInitialized(true);
83
- }
90
+ } else if (editor.getRootElement() !== container) editor.setRootElement(container, editableRef.current);
91
+ return () => {
92
+ if (editor.getRootElement() === container) editor.setRootElement(null);
93
+ };
94
+ }, [editor]);
95
+ useEffect(() => {
96
+ if (!isInitialized) return;
97
+ const lexicalEditor = editor.getLexicalEditor();
98
+ if (!lexicalEditor) return;
84
99
  let previousContent;
85
- return editor.getLexicalEditor()?.registerUpdateListener(({ dirtyElements, dirtyLeaves }) => {
100
+ return lexicalEditor.registerUpdateListener(({ dirtyElements, dirtyLeaves }) => {
86
101
  onChange?.(editor);
87
102
  if (onTextChange && (dirtyElements.size > 0 || dirtyLeaves.size > 0)) {
88
103
  const currentContent = JSON.stringify(editor.getDocument(type));
@@ -95,16 +110,19 @@ const ReactPlainText = memo(({ style, children, theme = {}, onChange, editable,
95
110
  });
96
111
  }, [
97
112
  editor,
113
+ isInitialized,
98
114
  type,
99
- content,
100
115
  onChange,
101
- onTextChange,
102
- isInitialized
116
+ onTextChange
103
117
  ]);
104
118
  useEffect(() => {
105
119
  if (!isInitialized) return;
106
120
  if (typeof editable === "boolean") editor.setEditable(editable);
107
- }, [isInitialized, editable]);
121
+ }, [
122
+ editor,
123
+ isInitialized,
124
+ editable
125
+ ]);
108
126
  useEffect(() => {
109
127
  if (editor && onPressEnter) return editor.registerHighCommand(KEY_DOWN_COMMAND, (event) => {
110
128
  if (event.key === "Enter" && !event.isComposing && onPressEnter({
@@ -1,4 +1,5 @@
1
1
  import { useLexicalEditor } from "../../../../editor-kernel/react/useLexicalEditor.js";
2
+ import { useTranslation } from "../../../../editor-kernel/react/useTranslation.js";
2
3
  import { $isBlockImageNode } from "../../node/block-image-node.js";
3
4
  import { useLexicalNodeSelection } from "../../../../editor-kernel/react/useLexicalNodeSelection.js";
4
5
  import { styles } from "./style.js";
@@ -6,20 +7,22 @@ import BrokenImage from "./BrokenImage.js";
6
7
  import ImageEditPopover from "./ImageEditPopover.js";
7
8
  import LazyImage from "./LazyImage.js";
8
9
  import { ResizeHandle } from "./ResizeHandle.js";
9
- import { Icon } from "@lobehub/ui";
10
- import { LoaderCircleIcon } from "lucide-react";
10
+ import { ActionIcon, Icon } from "@lobehub/ui";
11
+ import { LoaderCircleIcon, ZoomInIcon } from "lucide-react";
11
12
  import { Suspense, memo, useCallback, useMemo, useRef, useState } from "react";
12
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
14
  import { cx } from "antd-style";
15
+ import { Image } from "antd";
14
16
  import { COMMAND_PRIORITY_LOW, SELECTION_CHANGE_COMMAND } from "lexical";
15
17
  //#region src/plugins/image/react/components/Image.tsx
16
18
  function getResizedImageWidth(startWidth, deltaX, maxWidth) {
17
19
  const adjustedDeltaX = deltaX * 2;
18
20
  return Math.max(50, Math.min(startWidth + adjustedDeltaX, maxWidth));
19
21
  }
20
- const Image = memo(({ node, className, showScaleInfo = false, handleUpload, onPickFile }) => {
22
+ const Image$1 = memo(({ node, className, showScaleInfo = false, handleUpload, onPickFile }) => {
21
23
  const [isSelected, setSelected] = useLexicalNodeSelection(node.getKey());
22
24
  const [isHovered, setIsHovered] = useState(false);
25
+ const [previewOpen, setPreviewOpen] = useState(false);
23
26
  const [scale, setScale] = useState(1);
24
27
  const [size, setSize] = useState({
25
28
  height: 0,
@@ -34,6 +37,7 @@ const Image = memo(({ node, className, showScaleInfo = false, handleUpload, onPi
34
37
  const editorRef = useRef(null);
35
38
  const startWidthRef = useRef(0);
36
39
  const lastLoadedSrcRef = useRef(null);
40
+ const t = useTranslation();
37
41
  const isBlock = useMemo(() => {
38
42
  return $isBlockImageNode(node);
39
43
  }, [node]);
@@ -92,6 +96,11 @@ const Image = memo(({ node, className, showScaleInfo = false, handleUpload, onPi
92
96
  const handleMouseLeave = useCallback(() => {
93
97
  setIsHovered(false);
94
98
  }, []);
99
+ const handlePreview = useCallback((e) => {
100
+ e.preventDefault();
101
+ e.stopPropagation();
102
+ setPreviewOpen(true);
103
+ }, []);
95
104
  const children = useMemo(() => {
96
105
  switch (node.status) {
97
106
  case "error": return /* @__PURE__ */ jsx(BrokenImage, {});
@@ -167,6 +176,23 @@ const Image = memo(({ node, className, showScaleInfo = false, handleUpload, onPi
167
176
  spin: true
168
177
  })
169
178
  }),
179
+ (isHovered || isSelected) && node.status === "uploaded" && /* @__PURE__ */ jsx(ActionIcon, {
180
+ "aria-label": t("image.preview"),
181
+ glass: true,
182
+ className: styles.previewAction,
183
+ icon: ZoomInIcon,
184
+ onClick: handlePreview,
185
+ size: "small",
186
+ title: t("image.preview"),
187
+ variant: "filled"
188
+ }),
189
+ /* @__PURE__ */ jsx(Image.PreviewGroup, {
190
+ items: [node.src],
191
+ preview: {
192
+ open: previewOpen,
193
+ onOpenChange: setPreviewOpen
194
+ }
195
+ }),
170
196
  showScaleInfo && isSelected && scale !== 1 && /* @__PURE__ */ jsxs("div", {
171
197
  className: styles.scaleInfo,
172
198
  children: [Math.round(scale * 100), "%"]
@@ -190,6 +216,6 @@ const Image = memo(({ node, className, showScaleInfo = false, handleUpload, onPi
190
216
  })
191
217
  });
192
218
  });
193
- Image.displayName = "Image";
219
+ Image$1.displayName = "Image";
194
220
  //#endregion
195
- export { Image as default };
221
+ export { Image$1 as default };
@@ -78,6 +78,32 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
78
78
  justify-content: center;
79
79
 
80
80
  color: ${cssVar.colorTextSecondary};
81
+ `,
82
+ previewAction: css`
83
+ position: absolute;
84
+ z-index: 10000;
85
+ inset-block-start: 8px;
86
+ inset-inline-end: 8px;
87
+
88
+ && {
89
+ border: 1px solid rgb(255 255 255 / 24%);
90
+
91
+ color: rgb(255 255 255 / 96%);
92
+
93
+ background: rgb(0 0 0 / 58%);
94
+ backdrop-filter: saturate(120%) blur(12px);
95
+ box-shadow: 0 2px 8px rgb(0 0 0 / 36%);
96
+ }
97
+
98
+ &&:hover {
99
+ color: rgb(255 255 255);
100
+ background: rgb(0 0 0 / 72%);
101
+ }
102
+
103
+ &&:active {
104
+ color: rgb(255 255 255);
105
+ background: rgb(0 0 0 / 80%);
106
+ }
81
107
  `,
82
108
  resizeHandle: css`
83
109
  pointer-events: auto;
@@ -1,6 +1,28 @@
1
1
  import { IEditor } from "../../types/kernel.js";
2
2
  import "../../types/index.js";
3
3
  //#region src/react/hooks/useEditor.d.ts
4
- declare const useEditor: () => IEditor;
4
+ interface UseEditorOptions {
5
+ /**
6
+ * Destroy the kernel when the owning component is cleaned up.
7
+ *
8
+ * The default is false because React Activity also runs effect cleanup while
9
+ * hiding a subtree. Detaching the root keeps the editor reversible and lets
10
+ * the same kernel be attached again when the subtree becomes visible.
11
+ *
12
+ * `autoDestroy: true` is mutually exclusive with React Activity: Activity
13
+ * cannot distinguish a hidden subtree from a permanently unmounted owner,
14
+ * so its cleanup would destroy the editor during a hide. Use the default
15
+ * reversible detach with Activity, and call `destroy()` (or unmount the
16
+ * owner with `autoDestroy`) only when the instance is no longer needed.
17
+ *
18
+ * Root detach releases the kernel from the library's global root registry
19
+ * and DOM/dragon listeners. It does not guarantee garbage collection when
20
+ * application code keeps the `IEditor` in a store, provider, or other
21
+ * long-lived reference. Remove that reference, unmount the owner, or use
22
+ * `autoDestroy: true`/`destroy()` to release the editor's runtime resources.
23
+ */
24
+ autoDestroy?: boolean;
25
+ }
26
+ declare const useEditor: ({ autoDestroy }?: UseEditorOptions) => IEditor;
5
27
  //#endregion
6
- export { useEditor };
28
+ export { UseEditorOptions, useEditor };
@@ -1,8 +1,18 @@
1
1
  import Editor from "../../editor-kernel/index.js";
2
- import { useMemo } from "react";
2
+ import { useEffect, useMemo } from "react";
3
3
  //#region src/react/hooks/useEditor.ts
4
- const useEditor = () => {
5
- return useMemo(() => Editor.createEditor(), []);
4
+ const useEditor = ({ autoDestroy = false } = {}) => {
5
+ const editor = useMemo(() => Editor.createEditor(), []);
6
+ useEffect(() => {
7
+ return () => {
8
+ if (autoDestroy) {
9
+ editor.destroy();
10
+ return;
11
+ }
12
+ if (editor.getLexicalEditor()) editor.setRootElement(null);
13
+ };
14
+ }, [autoDestroy, editor]);
15
+ return editor;
6
16
  };
7
17
  //#endregion
8
18
  export { useEditor };
package/es/react.d.ts CHANGED
@@ -13,7 +13,7 @@ import "./react/ChatInputActions/index.js";
13
13
  import { CodeLanguageSelectProps } from "./react/CodeLanguageSelect/type.js";
14
14
  import CodeLanguageSelect from "./react/CodeLanguageSelect/CodeLanguageSelect.js";
15
15
  import "./react/CodeLanguageSelect/index.js";
16
- import { useEditor } from "./react/hooks/useEditor.js";
16
+ import { UseEditorOptions, useEditor } from "./react/hooks/useEditor.js";
17
17
  import { EditorState, useEditorState } from "./react/hooks/useEditorState/index.js";
18
18
  import { EditorProps } from "./react/Editor/type.js";
19
19
  import { withProps } from "./react/Editor/utils.js";
@@ -25,4 +25,4 @@ import "./react/FloatActions/index.js";
25
25
  import { SendButtonProps } from "./react/SendButton/type.js";
26
26
  import SendButton from "./react/SendButton/SendButton.js";
27
27
  import "./react/SendButton/index.js";
28
- export { ANCHOR_PADDING_CSS_VAR, ChatInput, ChatInputActionBar, type ChatInputActionBarProps, type ChatInputActionEvent, ChatInputActions, type ChatInputActionsProps, type ChatInputProps, CodeLanguageSelect, type CodeLanguageSelectProps, type CollaborationProviderFactory, DEFAULT_BLOCK_ANCHOR_PADDING, Editor, type EditorCollaborationConfig, type EditorProps, EditorProvider, type EditorProviderConfig, type EditorProviderProps, type EditorState, FloatActions, type FloatActionsProps, SendButton, type SendButtonProps, useEditor, useEditorContent, useEditorState, withProps };
28
+ export { ANCHOR_PADDING_CSS_VAR, ChatInput, ChatInputActionBar, type ChatInputActionBarProps, type ChatInputActionEvent, ChatInputActions, type ChatInputActionsProps, type ChatInputProps, CodeLanguageSelect, type CodeLanguageSelectProps, type CollaborationProviderFactory, DEFAULT_BLOCK_ANCHOR_PADDING, Editor, type EditorCollaborationConfig, type EditorProps, EditorProvider, type EditorProviderConfig, type EditorProviderProps, type EditorState, FloatActions, type FloatActionsProps, SendButton, type SendButtonProps, type UseEditorOptions, useEditor, useEditorContent, useEditorState, withProps };
@@ -225,6 +225,7 @@ interface IEditor {
225
225
  * @param dom
226
226
  */
227
227
  setRootElement(dom: HTMLElement, editable?: boolean): LexicalEditor;
228
+ setRootElement(dom: null, editable?: boolean): LexicalEditor | null;
228
229
  /**
229
230
  * set editor selection
230
231
  * @param selection
@@ -303,6 +304,17 @@ interface IEditorKernel extends IEditor {
303
304
  * @returns unregister function
304
305
  */
305
306
  registerRootClassName(className: string): () => void;
307
+ /**
308
+ * Register a callback for root attach/detach events.
309
+ *
310
+ * Call this after the kernel's Lexical editor has been initialized (usually
311
+ * from plugin onInit). Lexical immediately invokes a newly registered root
312
+ * listener with the current root, so listeners registered after the initial
313
+ * setRootElement still receive the first root attach. Headless kernels
314
+ * return a no-op cleanup because headless Lexical editors do not support
315
+ * root listeners.
316
+ */
317
+ registerRootListener(listener: (rootElement: HTMLElement | null) => void): () => void;
306
318
  /**
307
319
  * Register service
308
320
  * @param serviceId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/editor",
3
- "version": "4.25.0",
3
+ "version": "4.26.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",