@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.
- package/es/editor-kernel/kernel.d.ts +9 -0
- package/es/editor-kernel/kernel.js +71 -11
- package/es/headless.d.ts +13 -0
- package/es/headless.js +84 -12
- package/es/locale/index.d.ts +1 -0
- package/es/locale/index.js +1 -0
- package/es/plugins/common/plugin/index.js +12 -3
- package/es/plugins/common/react/ReactPlainText.js +27 -9
- package/es/plugins/image/react/components/Image.js +31 -5
- package/es/plugins/image/react/components/style.js +26 -0
- package/es/react/hooks/useEditor.d.ts +24 -2
- package/es/react/hooks/useEditor.js +13 -3
- package/es/react.d.ts +2 -2
- package/es/types/kernel.d.ts +12 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
112
|
+
this.unregisterRootLifecycle?.();
|
|
113
|
+
this.unregisterRootLifecycle = void 0;
|
|
114
|
+
let destroyError;
|
|
106
115
|
this.pluginsInstances.forEach((plugin) => {
|
|
107
|
-
|
|
116
|
+
try {
|
|
117
|
+
plugin.destroy?.();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
destroyError ??= error;
|
|
120
|
+
}
|
|
108
121
|
});
|
|
109
122
|
this.pluginsInstances = [];
|
|
110
|
-
this.
|
|
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.
|
|
115
|
-
this.
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
836
|
+
this.unregisterRootLifecycle?.();
|
|
837
|
+
this.unregisterRootLifecycle = void 0;
|
|
838
|
+
let destroyError;
|
|
829
839
|
this.pluginsInstances.forEach((plugin) => {
|
|
830
|
-
|
|
840
|
+
try {
|
|
841
|
+
plugin.destroy?.();
|
|
842
|
+
} catch (error) {
|
|
843
|
+
destroyError ??= error;
|
|
844
|
+
}
|
|
831
845
|
});
|
|
832
846
|
this.pluginsInstances = [];
|
|
833
|
-
this.
|
|
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.
|
|
838
|
-
this.
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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);
|
package/es/locale/index.d.ts
CHANGED
package/es/locale/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
editor.
|
|
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
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/es/types/kernel.d.ts
CHANGED
|
@@ -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