@lobehub/editor 4.25.0 → 4.25.1

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
@@ -357,6 +357,7 @@ interface IEditor {
357
357
  * @param dom
358
358
  */
359
359
  setRootElement(dom: HTMLElement, editable?: boolean): LexicalEditor;
360
+ setRootElement(dom: null, editable?: boolean): LexicalEditor | null;
360
361
  /**
361
362
  * set editor selection
362
363
  * @param selection
@@ -435,6 +436,17 @@ interface IEditorKernel extends IEditor {
435
436
  * @returns unregister function
436
437
  */
437
438
  registerRootClassName(className: string): () => void;
439
+ /**
440
+ * Register a callback for root attach/detach events.
441
+ *
442
+ * Call this after the kernel's Lexical editor has been initialized (usually
443
+ * from plugin onInit). Lexical immediately invokes a newly registered root
444
+ * listener with the current root, so listeners registered after the initial
445
+ * setRootElement still receive the first root attach. Headless kernels
446
+ * return a no-op cleanup because headless Lexical editors do not support
447
+ * root listeners.
448
+ */
449
+ registerRootListener(listener: (rootElement: HTMLElement | null) => void): () => void;
438
450
  /**
439
451
  * Register service
440
452
  * @param serviceId
package/es/headless.js CHANGED
@@ -818,26 +818,46 @@ var Kernel = class Kernel extends EventEmitter {
818
818
  return this.editor || null;
819
819
  }
820
820
  destroy() {
821
- unregisterEditorKernel(this.themes[EDITOR_THEME_KEY]);
821
+ const editor = this.editor;
822
+ const editorId = this.getEditorId();
822
823
  this.logger.info(`🗑️ Destroying editor with ${this.pluginsInstances.length} plugins`);
824
+ if (editor) try {
825
+ editor.setRootElement(null);
826
+ } catch (error) {
827
+ this.logger.warn("Failed to detach editor root during destroy:", error);
828
+ }
829
+ unregisterEditorKernel(editorId);
823
830
  try {
824
- this.editor?.setEditorState(createEmptyEditorState());
831
+ editor?.setEditorState(createEmptyEditorState());
825
832
  } catch (error) {
826
833
  this.logger.warn("Failed to reset editor state during destroy:", error);
827
834
  }
828
- this.dataTypeMap.clear();
835
+ this.unregisterRootLifecycle?.();
836
+ this.unregisterRootLifecycle = void 0;
837
+ let destroyError;
829
838
  this.pluginsInstances.forEach((plugin) => {
830
- if (plugin.destroy) plugin.destroy();
839
+ try {
840
+ plugin.destroy?.();
841
+ } catch (error) {
842
+ destroyError ??= error;
843
+ }
831
844
  });
832
845
  this.pluginsInstances = [];
833
- this.beforeEditorInitHooks = [];
846
+ this.dataTypeMap.clear();
847
+ this.decorators = {};
834
848
  this.nodeTransforms = [];
849
+ this.nodes = [];
850
+ this.beforeEditorInitHooks = [];
835
851
  this.rootClassNames.clear();
836
852
  this.serviceMap.clear();
837
- this.decorators = {};
838
- this.themes = {};
853
+ this.themes = { [EDITOR_THEME_KEY]: generateEditorId() };
854
+ this.historyState = createEmptyHistoryState();
855
+ this._commands.clear();
856
+ this._commandsClean.clear();
857
+ this.editor = void 0;
839
858
  this.headlessEditor = false;
840
859
  this.logger.info("✅ Editor destroyed");
860
+ if (destroyError) throw destroyError;
841
861
  }
842
862
  getRootElement() {
843
863
  if (this.headlessEditor) return null;
@@ -845,12 +865,23 @@ var Kernel = class Kernel extends EventEmitter {
845
865
  }
846
866
  setRootElement(dom, editable = true) {
847
867
  if (this.editor) {
848
- if (this.headlessEditor) throw new Error("Headless editor cannot be attached to a root element.");
868
+ if (this.headlessEditor) {
869
+ if (dom === null) return this.editor;
870
+ throw new Error("Headless editor cannot be attached to a root element.");
871
+ }
849
872
  this.logger.warn("[Editor] Editor is already initialized, updating root element only");
873
+ if (dom) registerEditorKernel(this.getEditorId(), this);
850
874
  this.editor.setRootElement(dom);
851
- this.applyRootClassNames(dom);
875
+ if (dom) {
876
+ this.editor.setEditable(editable);
877
+ this.applyRootClassNames(dom);
878
+ }
852
879
  return this.editor;
853
880
  }
881
+ if (dom === null) {
882
+ unregisterEditorKernel(this.getEditorId());
883
+ return null;
884
+ }
854
885
  if (this.pluginsInstances.length === 0) {
855
886
  this.logger.info(`🔌 Initializing ${this.plugins.length} plugins`);
856
887
  for (const plugin of this.plugins) {
@@ -861,7 +892,7 @@ var Kernel = class Kernel extends EventEmitter {
861
892
  this.runBeforeEditorInitLifecycle();
862
893
  const resolvedNodes = this.resolveNodesForInitialization();
863
894
  this.logger.info(`📝 Creating editor with ${resolvedNodes.length} nodes`);
864
- registerEditorKernel(this.themes[EDITOR_THEME_KEY], this);
895
+ registerEditorKernel(this.getEditorId(), this);
865
896
  const editor = this.editor = createEditor({
866
897
  __kernel: this,
867
898
  editable,
@@ -874,9 +905,9 @@ var Kernel = class Kernel extends EventEmitter {
874
905
  theme: this.themes
875
906
  });
876
907
  this.headlessEditor = false;
908
+ this.registerRootLifecycle(editor);
877
909
  this.editor.setRootElement(dom);
878
910
  this.applyRootClassNames(dom);
879
- registerEvent(editor, dom);
880
911
  this.pluginsInstances.forEach((plugin) => {
881
912
  plugin.onInit?.(editor);
882
913
  });
@@ -916,6 +947,7 @@ var Kernel = class Kernel extends EventEmitter {
916
947
  theme: this.themes
917
948
  });
918
949
  this.headlessEditor = false;
950
+ this.registerRootLifecycle(editor);
919
951
  this.pluginsInstances.forEach((plugin) => {
920
952
  plugin.onInit?.(editor);
921
953
  });
@@ -1236,6 +1268,34 @@ var Kernel = class Kernel extends EventEmitter {
1236
1268
  if (currentRootElement) currentRootElement.classList.remove(...classNames);
1237
1269
  };
1238
1270
  }
1271
+ registerRootListener(listener) {
1272
+ if (!this.editor || this.headlessEditor) return noop;
1273
+ return this.editor.registerRootListener(listener);
1274
+ }
1275
+ getEditorId() {
1276
+ return this.themes[EDITOR_THEME_KEY];
1277
+ }
1278
+ registerRootLifecycle(editor) {
1279
+ this.unregisterRootLifecycle?.();
1280
+ let rootEventCleanup;
1281
+ const clearRootEvent = () => {
1282
+ rootEventCleanup?.();
1283
+ rootEventCleanup = void 0;
1284
+ };
1285
+ const unregisterRootListener = editor.registerRootListener((rootElement) => {
1286
+ clearRootEvent();
1287
+ if (!rootElement) {
1288
+ unregisterEditorKernel(this.getEditorId());
1289
+ return;
1290
+ }
1291
+ registerEditorKernel(this.getEditorId(), this);
1292
+ rootEventCleanup = registerEvent(editor, rootElement);
1293
+ });
1294
+ this.unregisterRootLifecycle = () => {
1295
+ clearRootEvent();
1296
+ unregisterRootListener();
1297
+ };
1298
+ }
1239
1299
  runBeforeEditorInitLifecycle() {
1240
1300
  for (const hook of this.beforeEditorInitHooks) hook();
1241
1301
  }
@@ -6076,7 +6136,18 @@ const CommonPlugin = class extends KernelPlugin {
6076
6136
  if (this.config?.pasteAsPlainText) return runPasteHandlers(ctx, [handleFilePaste, handlePlainTextPaste]);
6077
6137
  return false;
6078
6138
  }, 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, () => {
6139
+ let dragonCleanup;
6140
+ const clearDragon = () => {
6141
+ dragonCleanup?.();
6142
+ dragonCleanup = void 0;
6143
+ };
6144
+ this.register(this.kernel.registerRootListener((rootElement) => {
6145
+ clearDragon();
6146
+ if (!rootElement || !CAN_USE_DOM) return;
6147
+ dragonCleanup = registerDragonSupport(editor);
6148
+ }));
6149
+ this.register(clearDragon);
6150
+ 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
6151
  const selection = $getSelection();
6081
6152
  if (!$isRangeSelection(selection)) return false;
6082
6153
  const { inCell, inTable } = $isCursorInTable(selection);
@@ -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,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.25.1",
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",