@lobehub/editor 4.24.1 → 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.
Files changed (148) hide show
  1. package/es/codemirror/style.js +42 -0
  2. package/es/editor-kernel/kernel.d.ts +9 -0
  3. package/es/editor-kernel/kernel.js +74 -11
  4. package/es/editor-kernel/react/useDecorators.js +2 -4
  5. package/es/headless/collapsible-plugin.js +74 -0
  6. package/es/headless/index.js +2 -0
  7. package/es/headless.d.ts +30 -0
  8. package/es/headless.js +2618 -139
  9. package/es/index.d.ts +26 -3
  10. package/es/index.js +17 -2
  11. package/es/locale/index.d.ts +14 -0
  12. package/es/locale/index.js +14 -0
  13. package/es/plugins/block/command/index.js +24 -0
  14. package/es/plugins/block/plugin/index.js +7 -5
  15. package/es/plugins/block/react/ReactBlockPlugin.js +43 -1
  16. package/es/plugins/block/react/core/menu-position.js +20 -0
  17. package/es/plugins/block/react/drag/drag-session.js +3 -2
  18. package/es/plugins/block/react/drag/drag-utils.js +13 -1
  19. package/es/plugins/codemirror-block/index.d.ts +2 -1
  20. package/es/plugins/codemirror-block/plugin/index.js +2 -0
  21. package/es/plugins/codemirror-block/react/CodemirrorNode.js +95 -5
  22. package/es/plugins/codemirror-block/react/useCodemirrorEditLock.js +33 -0
  23. package/es/plugins/codemirror-block/service/index.d.ts +40 -0
  24. package/es/plugins/codemirror-block/service/index.js +59 -0
  25. package/es/plugins/collapsible/command/index.d.ts +17 -0
  26. package/es/plugins/collapsible/command/index.js +72 -0
  27. package/es/plugins/collapsible/index.d.ts +7 -0
  28. package/es/plugins/collapsible/node/CollapsibleNode.d.ts +30 -0
  29. package/es/plugins/collapsible/node/CollapsibleNode.js +138 -0
  30. package/es/plugins/collapsible/plugin/index.d.ts +11 -0
  31. package/es/plugins/collapsible/plugin/index.js +535 -0
  32. package/es/plugins/collapsible/react/ReactCollapsiblePlugin.d.ts +6 -0
  33. package/es/plugins/collapsible/react/ReactCollapsiblePlugin.js +19 -0
  34. package/es/plugins/collapsible/react/index.d.ts +3 -0
  35. package/es/plugins/collapsible/react/style.js +67 -0
  36. package/es/plugins/collapsible/react/type.d.ts +6 -0
  37. package/es/plugins/common/plugin/index.js +14 -4
  38. package/es/plugins/common/plugin/progressive-select-all.js +81 -0
  39. package/es/plugins/common/plugin/register.js +73 -4
  40. package/es/plugins/common/react/ReactPlainText.js +27 -9
  41. package/es/plugins/link/command/index.js +26 -3
  42. package/es/plugins/link/conversion/index.js +237 -0
  43. package/es/plugins/link/index.d.ts +5 -3
  44. package/es/plugins/link/node/LinkBlockCardNode.d.ts +16 -0
  45. package/es/plugins/link/node/LinkBlockCardNode.js +66 -0
  46. package/es/plugins/link/node/LinkCardNode.d.ts +42 -0
  47. package/es/plugins/link/node/LinkCardNode.js +161 -0
  48. package/es/plugins/link/node/LinkIframeNode.d.ts +33 -0
  49. package/es/plugins/link/node/LinkIframeNode.js +131 -0
  50. package/es/plugins/link/node/LinkNode.d.ts +48 -2
  51. package/es/plugins/link/node/LinkNode.js +14 -6
  52. package/es/plugins/link/node/SchemaNode.d.ts +36 -0
  53. package/es/plugins/link/node/SchemaNode.js +149 -0
  54. package/es/plugins/link/normalization/index.d.ts +7 -0
  55. package/es/plugins/link/normalization/index.js +38 -0
  56. package/es/plugins/link/plugin/index.d.ts +26 -1
  57. package/es/plugins/link/plugin/index.js +124 -4
  58. package/es/plugins/link/react/ReactLinkPlugin.js +165 -7
  59. package/es/plugins/link/react/components/LinkCard.js +235 -0
  60. package/es/plugins/link/react/components/LinkEdit.js +93 -14
  61. package/es/plugins/link/react/components/LinkIframe.js +128 -0
  62. package/es/plugins/link/react/components/LinkToolbar.js +440 -111
  63. package/es/plugins/link/react/components/SchemaLink.js +49 -0
  64. package/es/plugins/link/react/index.d.ts +2 -2
  65. package/es/plugins/link/react/renderer-registry.d.ts +81 -0
  66. package/es/plugins/link/react/renderer-registry.js +80 -0
  67. package/es/plugins/link/react/style.js +63 -1
  68. package/es/plugins/link/react/type.d.ts +24 -1
  69. package/es/plugins/link/service/i-link-service.d.ts +156 -1
  70. package/es/plugins/link/service/i-link-service.js +258 -5
  71. package/es/plugins/list/plugin/index.js +9 -2
  72. package/es/plugins/litexml/command/diffCommand.js +94 -14
  73. package/es/plugins/litexml/command/index.js +187 -6
  74. package/es/plugins/litexml/diff-validation.js +237 -0
  75. package/es/plugins/litexml/node/DiffContentNode.js +67 -0
  76. package/es/plugins/litexml/node/DiffNode.d.ts +1 -1
  77. package/es/plugins/litexml/node/DiffNode.js +5 -2
  78. package/es/plugins/litexml/node/TableCellDiffNode.js +77 -0
  79. package/es/plugins/litexml/node/TableRowDiffNode.js +73 -0
  80. package/es/plugins/litexml/plugin/index.d.ts +2 -0
  81. package/es/plugins/litexml/plugin/index.js +62 -9
  82. package/es/plugins/litexml/react/DiffNodeToolbar/index.js +10 -12
  83. package/es/plugins/litexml/react/TableRowDiffToolbar.js +123 -0
  84. package/es/plugins/litexml/react/hooks/useHasDiffNode.js +5 -2
  85. package/es/plugins/litexml/react/index.js +6 -2
  86. package/es/plugins/litexml/react/style.js +18 -5
  87. package/es/plugins/litexml/table-cell-diff.js +153 -0
  88. package/es/plugins/litexml/table-row-diff.js +82 -0
  89. package/es/plugins/markdown/data-source/markdown/parse.js +25 -1
  90. package/es/plugins/markdown/plugin/index.js +5 -3
  91. package/es/plugins/slash/react/ReactSlashPlugin.js +22 -43
  92. package/es/plugins/slash/react/components/DefaultSlashMenu.js +76 -16
  93. package/es/plugins/slash/react/menu-utils.js +84 -0
  94. package/es/plugins/slash/service/i-slash-service.d.ts +3 -0
  95. package/es/plugins/table/plugin/index.js +4 -1
  96. package/es/plugins/table/plugin/single-cell-paste.js +79 -0
  97. package/es/plugins/table/react/TableController/hooks.js +7 -4
  98. package/es/plugins/table/react/hooks.js +2 -2
  99. package/es/plugins/table/react/index.js +2 -0
  100. package/es/plugins/table/react/style.js +15 -5
  101. package/es/plugins/table/react/useScrollFocusedTableCellIntoView.js +46 -0
  102. package/es/plugins/toc/index.d.ts +10 -0
  103. package/es/plugins/toc/plugin/index.d.ts +8 -0
  104. package/es/plugins/toc/plugin/index.js +56 -0
  105. package/es/plugins/toc/react/ReactTocPlugin.d.ts +6 -0
  106. package/es/plugins/toc/react/ReactTocPlugin.js +36 -0
  107. package/es/plugins/toc/react/getNearestScrollContainer.d.ts +6 -0
  108. package/es/plugins/toc/react/getNearestScrollContainer.js +18 -0
  109. package/es/plugins/toc/react/hooks/useActiveHeading.js +59 -0
  110. package/es/plugins/toc/react/hooks/useTocItems.js +50 -0
  111. package/es/plugins/toc/react/index.d.ts +5 -0
  112. package/es/plugins/toc/react/type.d.ts +30 -0
  113. package/es/plugins/toc/react/useToc.d.ts +5 -0
  114. package/es/plugins/toc/react/useToc.js +41 -0
  115. package/es/plugins/toc/service/i-toc-service.d.ts +39 -0
  116. package/es/plugins/toc/service/i-toc-service.js +125 -0
  117. package/es/plugins/toc/service/index.d.ts +3 -0
  118. package/es/plugins/toc/types.d.ts +23 -0
  119. package/es/plugins/toc/utils/index.d.ts +6 -0
  120. package/es/plugins/toc/utils/index.js +30 -0
  121. package/es/plugins/toolbar/react/index.js +26 -12
  122. package/es/plugins/toolbar/react/style.js +3 -2
  123. package/es/plugins/virtual-block/plugin/register.js +34 -12
  124. package/es/plugins/yjs/index.d.ts +6 -0
  125. package/es/plugins/yjs/plugin/index.d.ts +7 -0
  126. package/es/plugins/yjs/plugin/index.js +191 -0
  127. package/es/plugins/yjs/plugin/types.d.ts +16 -0
  128. package/es/plugins/yjs/plugin/utils/awareness.js +9 -0
  129. package/es/plugins/yjs/plugin/utils/editor-state.js +47 -0
  130. package/es/plugins/yjs/plugin/utils/history.js +31 -0
  131. package/es/plugins/yjs/plugin/utils/node-properties.js +39 -0
  132. package/es/plugins/yjs/plugin/utils/sync.js +24 -0
  133. package/es/plugins/yjs/react/codemirrorEditLockProvider.js +59 -0
  134. package/es/plugins/yjs/react/index.d.ts +23 -0
  135. package/es/plugins/yjs/react/index.js +200 -0
  136. package/es/plugins/yjs/service/index.d.ts +47 -0
  137. package/es/plugins/yjs/service/index.js +84 -0
  138. package/es/plugins/yjs/websocket-provider.d.ts +78 -0
  139. package/es/plugins/yjs/websocket-provider.js +263 -0
  140. package/es/react/hooks/useEditor.d.ts +24 -2
  141. package/es/react/hooks/useEditor.js +13 -3
  142. package/es/react.d.ts +2 -2
  143. package/es/renderer/LexicalRenderer.js +9 -2
  144. package/es/types/kernel.d.ts +16 -0
  145. package/es/utils/updatePosition.js +5 -2
  146. package/package.json +3 -1
  147. package/patches/@lexical__yjs@0.42.0.patch +42 -0
  148. package/scripts/postinstall-lexical-patch.cjs +62 -36
@@ -0,0 +1,47 @@
1
+ import { IServiceID } from "../../../types/kernel.js";
2
+ import "../../../types/index.js";
3
+ import { Doc } from "yjs";
4
+ import { Binding, Provider, UserState } from "@lexical/yjs";
5
+ //#region src/plugins/yjs/service/index.d.ts
6
+ interface YjsAwarenessUser {
7
+ clientId: number;
8
+ state: UserState;
9
+ }
10
+ interface YjsPluginState {
11
+ binding: Binding;
12
+ doc: Doc | undefined;
13
+ docMap: Map<string, Doc>;
14
+ id: string;
15
+ provider: Provider;
16
+ }
17
+ type YjsPluginStateListener = (state: YjsPluginState | null) => void;
18
+ type YjsAwarenessUsersListener = (users: YjsAwarenessUser[]) => void;
19
+ declare class YjsService {
20
+ private awarenessUsers;
21
+ private awarenessUsersListeners;
22
+ private listeners;
23
+ private state;
24
+ getAwarenessUsers(): YjsAwarenessUser[];
25
+ getState(): YjsPluginState | null;
26
+ /**
27
+ * Apply a server-produced editor snapshot to the shared document exactly once.
28
+ *
29
+ * A normal `IEditor.setDocument` call replaces the local Lexical state without
30
+ * knowing about the Yjs binding. When the binding sees that state it can treat
31
+ * every imported node as a new local node and append a second copy to the
32
+ * shared tree. This path first hydrates from the shared tree, then applies the
33
+ * snapshot with the collaboration-skip tag and replaces the shared root in a
34
+ * single Yjs transaction. A second client receiving the same snapshot
35
+ * therefore becomes a no-op.
36
+ *
37
+ * @returns whether the shared document was changed
38
+ */
39
+ applyExternalEditorData(editorData: Record<string, unknown>): boolean;
40
+ setAwarenessUsers(users: YjsAwarenessUser[]): void;
41
+ setState(state: YjsPluginState | null): void;
42
+ subscribeAwarenessUsers(listener: YjsAwarenessUsersListener): () => void;
43
+ subscribe(listener: YjsPluginStateListener): () => void;
44
+ }
45
+ declare const IYjsService: IServiceID<YjsService>;
46
+ //#endregion
47
+ export { IYjsService, YjsAwarenessUser, YjsPluginState, YjsService };
@@ -0,0 +1,84 @@
1
+ import { createEmptyPreviousEditorState } from "../plugin/utils/editor-state.js";
2
+ import { hydrateLexicalFromYjsState, syncCurrentEditorStateToYjs } from "../plugin/utils/sync.js";
3
+ import { SKIP_COLLAB_TAG } from "lexical";
4
+ //#region src/plugins/yjs/service/index.ts
5
+ const serializeComparableEditorState = (state) => {
6
+ const serialized = state.toJSON();
7
+ if (serialized.root?.direction === "ltr") serialized.root.direction = null;
8
+ return JSON.stringify(serialized);
9
+ };
10
+ var YjsService = class {
11
+ constructor() {
12
+ this.awarenessUsers = [];
13
+ this.awarenessUsersListeners = /* @__PURE__ */ new Set();
14
+ this.listeners = /* @__PURE__ */ new Set();
15
+ this.state = null;
16
+ }
17
+ getAwarenessUsers() {
18
+ return this.awarenessUsers;
19
+ }
20
+ getState() {
21
+ return this.state;
22
+ }
23
+ /**
24
+ * Apply a server-produced editor snapshot to the shared document exactly once.
25
+ *
26
+ * A normal `IEditor.setDocument` call replaces the local Lexical state without
27
+ * knowing about the Yjs binding. When the binding sees that state it can treat
28
+ * every imported node as a new local node and append a second copy to the
29
+ * shared tree. This path first hydrates from the shared tree, then applies the
30
+ * snapshot with the collaboration-skip tag and replaces the shared root in a
31
+ * single Yjs transaction. A second client receiving the same snapshot
32
+ * therefore becomes a no-op.
33
+ *
34
+ * @returns whether the shared document was changed
35
+ */
36
+ applyExternalEditorData(editorData) {
37
+ const state = this.state;
38
+ if (!state) return false;
39
+ const { binding, provider } = state;
40
+ const hasSharedState = !binding.root.isEmpty() || binding.root._xmlText._length > 0;
41
+ if (hasSharedState && binding.root.isEmpty()) hydrateLexicalFromYjsState(binding, { discrete: true });
42
+ const previousEditorState = binding.editor.getEditorState();
43
+ const nextEditorState = binding.editor.parseEditorState(JSON.stringify(editorData));
44
+ const isSameState = serializeComparableEditorState(previousEditorState) === serializeComparableEditorState(nextEditorState);
45
+ if (hasSharedState && isSameState) return false;
46
+ binding.editor.setEditorState(nextEditorState, { tag: SKIP_COLLAB_TAG });
47
+ const replaceSharedState = () => {
48
+ const collabRoot = binding.root;
49
+ const sharedType = binding.root.getSharedType();
50
+ if (sharedType.length > 0) sharedType.delete(0, sharedType.length);
51
+ collabRoot._children.length = 0;
52
+ binding.collabNodeMap.clear();
53
+ syncCurrentEditorStateToYjs(binding, provider, createEmptyPreviousEditorState(binding.editor));
54
+ };
55
+ if (state.doc) state.doc.transact(replaceSharedState, binding);
56
+ else replaceSharedState();
57
+ return true;
58
+ }
59
+ setAwarenessUsers(users) {
60
+ this.awarenessUsers = users;
61
+ this.awarenessUsersListeners.forEach((listener) => listener(users));
62
+ }
63
+ setState(state) {
64
+ this.state = state;
65
+ this.listeners.forEach((listener) => listener(state));
66
+ }
67
+ subscribeAwarenessUsers(listener) {
68
+ this.awarenessUsersListeners.add(listener);
69
+ listener(this.awarenessUsers);
70
+ return () => {
71
+ this.awarenessUsersListeners.delete(listener);
72
+ };
73
+ }
74
+ subscribe(listener) {
75
+ this.listeners.add(listener);
76
+ listener(this.state);
77
+ return () => {
78
+ this.listeners.delete(listener);
79
+ };
80
+ }
81
+ };
82
+ const IYjsService = { __serviceId: "YjsService" };
83
+ //#endregion
84
+ export { IYjsService, YjsService };
@@ -0,0 +1,78 @@
1
+ import { Doc } from "yjs";
2
+ import { Provider, ProviderAwareness, UserState } from "@lexical/yjs";
3
+ //#region src/plugins/yjs/websocket-provider.d.ts
4
+ interface CreateWebSocketYjsProviderOptions {
5
+ wsBaseUrl?: string;
6
+ }
7
+ type WebSocketYjsProviderStatus = 'connected' | 'connecting' | 'disconnected' | 'reconnecting';
8
+ type ProviderEventMap = {
9
+ reload: (doc: Doc) => void;
10
+ status: (event: {
11
+ status: WebSocketYjsProviderStatus;
12
+ }) => void;
13
+ sync: (isSynced: boolean) => void;
14
+ update: (event: unknown) => void;
15
+ };
16
+ type AwarenessMessage = {
17
+ sender: number;
18
+ state: UserState | null;
19
+ type: 'awareness';
20
+ };
21
+ type UpdateMessage = {
22
+ sender: number;
23
+ type: 'update';
24
+ update: string;
25
+ };
26
+ type ClientWebSocketMessage = AwarenessMessage | UpdateMessage | {
27
+ stateVector: string;
28
+ type: 'sync-request';
29
+ };
30
+ declare class WebSocketAwareness implements ProviderAwareness {
31
+ private clientId;
32
+ private send;
33
+ private listeners;
34
+ private localState;
35
+ private states;
36
+ constructor(clientId: number, send: (message: ClientWebSocketMessage) => void);
37
+ getLocalState(): UserState | null;
38
+ getStates(): Map<number, UserState>;
39
+ off(type: 'update', cb: () => void): void;
40
+ on(type: 'update', cb: () => void): void;
41
+ setLocalState(state: UserState | null): void;
42
+ setLocalStateField(field: string, value: unknown): void;
43
+ updateRemoteState(clientId: number, state: UserState | null): void;
44
+ private emitUpdate;
45
+ }
46
+ declare class WebSocketYjsProvider implements Provider {
47
+ private id;
48
+ private doc;
49
+ private wsBaseUrl;
50
+ awareness: WebSocketAwareness;
51
+ private listeners;
52
+ private reconnectAttempt;
53
+ private reconnectTimer;
54
+ private shouldConnect;
55
+ private socket;
56
+ private updateHandler;
57
+ constructor(id: string, doc: Doc, wsBaseUrl?: string);
58
+ connect(): void;
59
+ disconnect(): void;
60
+ off<T extends keyof ProviderEventMap>(type: T, cb: ProviderEventMap[T]): void;
61
+ on<T extends keyof ProviderEventMap>(type: T, cb: ProviderEventMap[T]): void;
62
+ private clearReconnectTimer;
63
+ private emit;
64
+ private getReconnectDelay;
65
+ private openSocket;
66
+ private publishLocalDocumentState;
67
+ private requestServerDocumentState;
68
+ private scheduleReconnect;
69
+ private handleClose;
70
+ private handleMessage;
71
+ private send;
72
+ }
73
+ declare function createWebSocketYjsProvider(id: string, yjsDocMap: Map<string, Doc>, options?: CreateWebSocketYjsProviderOptions): WebSocketYjsProvider;
74
+ declare function fetchWebSocketDemoDocument(id: string): Promise<unknown>;
75
+ declare function saveWebSocketDemoDocument(id: string, content: unknown): Promise<void>;
76
+ declare function snapshotWebSocketDemoDocument(id: string, content: unknown): void;
77
+ //#endregion
78
+ export { CreateWebSocketYjsProviderOptions, WebSocketYjsProvider, WebSocketYjsProviderStatus, createWebSocketYjsProvider, fetchWebSocketDemoDocument, saveWebSocketDemoDocument, snapshotWebSocketDemoDocument };
@@ -0,0 +1,263 @@
1
+ import { Doc, applyUpdate, encodeStateAsUpdate, encodeStateVector } from "yjs";
2
+ //#region src/plugins/yjs/websocket-provider.ts
3
+ const DEFAULT_HTTP_BASE_URL = "http://localhost:12345";
4
+ const DEFAULT_WS_BASE_URL = "ws://localhost:12345";
5
+ const MAX_RECONNECT_DELAY_MS = 1e4;
6
+ const MIN_RECONNECT_DELAY_MS = 500;
7
+ function encodeUint8Array(update) {
8
+ let binary = "";
9
+ const chunkSize = 8192;
10
+ for (let index = 0; index < update.length; index += chunkSize) binary += String.fromCharCode(...update.slice(index, index + chunkSize));
11
+ return window.btoa(binary);
12
+ }
13
+ function decodeUint8Array(value) {
14
+ const binary = window.atob(value);
15
+ const update = new Uint8Array(binary.length);
16
+ for (let index = 0; index < binary.length; index += 1) update[index] = binary.charCodeAt(index);
17
+ return update;
18
+ }
19
+ var WebSocketAwareness = class {
20
+ constructor(clientId, send) {
21
+ this.clientId = clientId;
22
+ this.send = send;
23
+ this.listeners = /* @__PURE__ */ new Set();
24
+ this.localState = null;
25
+ this.states = /* @__PURE__ */ new Map();
26
+ }
27
+ getLocalState() {
28
+ return this.localState;
29
+ }
30
+ getStates() {
31
+ return this.states;
32
+ }
33
+ off(type, cb) {
34
+ if (type === "update") this.listeners.delete(cb);
35
+ }
36
+ on(type, cb) {
37
+ if (type === "update") this.listeners.add(cb);
38
+ }
39
+ setLocalState(state) {
40
+ this.localState = state;
41
+ if (state) this.states.set(this.clientId, state);
42
+ else this.states.delete(this.clientId);
43
+ this.emitUpdate();
44
+ this.send({
45
+ sender: this.clientId,
46
+ state,
47
+ type: "awareness"
48
+ });
49
+ }
50
+ setLocalStateField(field, value) {
51
+ const nextState = {
52
+ ...this.localState,
53
+ [field]: value
54
+ };
55
+ this.setLocalState(nextState);
56
+ }
57
+ updateRemoteState(clientId, state) {
58
+ if (clientId === this.clientId) return;
59
+ if (state) this.states.set(clientId, state);
60
+ else this.states.delete(clientId);
61
+ this.emitUpdate();
62
+ }
63
+ emitUpdate() {
64
+ this.listeners.forEach((listener) => listener());
65
+ }
66
+ };
67
+ var WebSocketYjsProvider = class {
68
+ constructor(id, doc, wsBaseUrl = DEFAULT_WS_BASE_URL) {
69
+ this.id = id;
70
+ this.doc = doc;
71
+ this.wsBaseUrl = wsBaseUrl;
72
+ this.listeners = {
73
+ reload: /* @__PURE__ */ new Set(),
74
+ status: /* @__PURE__ */ new Set(),
75
+ sync: /* @__PURE__ */ new Set(),
76
+ update: /* @__PURE__ */ new Set()
77
+ };
78
+ this.reconnectAttempt = 0;
79
+ this.reconnectTimer = null;
80
+ this.shouldConnect = false;
81
+ this.socket = null;
82
+ this.updateHandler = (update, origin) => {
83
+ if (origin === this) return;
84
+ this.send({
85
+ sender: this.doc.clientID,
86
+ type: "update",
87
+ update: encodeUint8Array(update)
88
+ });
89
+ };
90
+ this.awareness = new WebSocketAwareness(this.doc.clientID, (message) => this.send(message));
91
+ }
92
+ connect() {
93
+ this.shouldConnect = true;
94
+ if (this.socket || this.reconnectTimer) return;
95
+ this.openSocket("connecting");
96
+ }
97
+ disconnect() {
98
+ this.shouldConnect = false;
99
+ this.clearReconnectTimer();
100
+ if (!this.socket) return;
101
+ this.awareness.setLocalState(null);
102
+ this.doc.off("update", this.updateHandler);
103
+ this.socket.close();
104
+ this.socket = null;
105
+ this.emit("sync", false);
106
+ this.emit("status", { status: "disconnected" });
107
+ }
108
+ off(type, cb) {
109
+ this.listeners[type].delete(cb);
110
+ }
111
+ on(type, cb) {
112
+ this.listeners[type].add(cb);
113
+ }
114
+ clearReconnectTimer() {
115
+ if (!this.reconnectTimer) return;
116
+ clearTimeout(this.reconnectTimer);
117
+ this.reconnectTimer = null;
118
+ }
119
+ emit(type, ...args) {
120
+ this.listeners[type].forEach((listener) => {
121
+ listener(...args);
122
+ });
123
+ }
124
+ getReconnectDelay() {
125
+ const exponentialDelay = MIN_RECONNECT_DELAY_MS * 2 ** this.reconnectAttempt;
126
+ const jitter = Math.floor(Math.random() * MIN_RECONNECT_DELAY_MS);
127
+ return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY_MS);
128
+ }
129
+ openSocket(status) {
130
+ const socket = new WebSocket(`${this.wsBaseUrl}/collaboration/${encodeURIComponent(this.id)}?clientId=${this.doc.clientID}`);
131
+ this.socket = socket;
132
+ socket.binaryType = "arraybuffer";
133
+ this.emit("status", { status });
134
+ socket.addEventListener("open", () => {
135
+ if (this.socket !== socket) {
136
+ socket.close();
137
+ return;
138
+ }
139
+ this.reconnectAttempt = 0;
140
+ this.doc.on("update", this.updateHandler);
141
+ this.emit("status", { status: "connected" });
142
+ this.requestServerDocumentState();
143
+ const localState = this.awareness.getLocalState();
144
+ if (localState) this.send({
145
+ sender: this.doc.clientID,
146
+ state: localState,
147
+ type: "awareness"
148
+ });
149
+ });
150
+ socket.addEventListener("message", (event) => this.handleMessage(socket, event));
151
+ socket.addEventListener("close", () => this.handleClose(socket));
152
+ socket.addEventListener("error", () => {
153
+ this.emit("status", { status: "disconnected" });
154
+ });
155
+ }
156
+ publishLocalDocumentState() {
157
+ this.send({
158
+ sender: this.doc.clientID,
159
+ type: "update",
160
+ update: encodeUint8Array(encodeStateAsUpdate(this.doc))
161
+ });
162
+ }
163
+ requestServerDocumentState() {
164
+ this.send({
165
+ stateVector: encodeUint8Array(encodeStateVector(this.doc)),
166
+ type: "sync-request"
167
+ });
168
+ }
169
+ scheduleReconnect() {
170
+ if (!this.shouldConnect || this.reconnectTimer) return;
171
+ const delay = this.getReconnectDelay();
172
+ this.reconnectAttempt += 1;
173
+ this.emit("status", { status: "reconnecting" });
174
+ this.reconnectTimer = setTimeout(() => {
175
+ this.reconnectTimer = null;
176
+ this.openSocket("reconnecting");
177
+ }, delay);
178
+ }
179
+ handleClose(socket) {
180
+ if (this.socket !== socket) return;
181
+ this.doc.off("update", this.updateHandler);
182
+ this.socket = null;
183
+ this.emit("sync", false);
184
+ this.emit("status", { status: "disconnected" });
185
+ this.scheduleReconnect();
186
+ }
187
+ handleMessage(socket, event) {
188
+ if (this.socket !== socket) return;
189
+ let message;
190
+ try {
191
+ message = JSON.parse(event.data);
192
+ } catch {
193
+ this.emit("status", { status: "disconnected" });
194
+ return;
195
+ }
196
+ if (message.type === "awareness") {
197
+ this.awareness.updateRemoteState(message.sender, message.state);
198
+ return;
199
+ }
200
+ if (message.type === "sync") {
201
+ try {
202
+ applyUpdate(this.doc, decodeUint8Array(message.update), this);
203
+ } catch {
204
+ this.emit("status", { status: "disconnected" });
205
+ return;
206
+ }
207
+ for (const awareness of message.awareness) this.awareness.updateRemoteState(awareness.clientId, awareness.state);
208
+ this.emit("sync", true);
209
+ this.publishLocalDocumentState();
210
+ return;
211
+ }
212
+ if (message.sender === this.doc.clientID) return;
213
+ try {
214
+ const update = decodeUint8Array(message.update);
215
+ applyUpdate(this.doc, update, this);
216
+ this.emit("update", update);
217
+ } catch {
218
+ this.emit("status", { status: "disconnected" });
219
+ }
220
+ }
221
+ send(message) {
222
+ if (this.socket?.readyState !== WebSocket.OPEN) return;
223
+ this.socket.send(JSON.stringify(message));
224
+ }
225
+ };
226
+ function createWebSocketYjsProvider(id, yjsDocMap, options = {}) {
227
+ const doc = yjsDocMap.get(id) || new Doc();
228
+ yjsDocMap.set(id, doc);
229
+ return new WebSocketYjsProvider(id, doc, options.wsBaseUrl);
230
+ }
231
+ async function fetchWebSocketDemoDocument(id) {
232
+ const response = await fetch(`${DEFAULT_HTTP_BASE_URL}/documents/${encodeURIComponent(id)}`);
233
+ if (!response.ok) throw new Error(`Failed to load document: ${response.status}`);
234
+ const { content } = await response.json();
235
+ return content;
236
+ }
237
+ async function postWebSocketDemoDocument(id, path, content) {
238
+ const response = await fetch(`${DEFAULT_HTTP_BASE_URL}/documents/${encodeURIComponent(id)}/${path}`, {
239
+ body: JSON.stringify({ content }),
240
+ headers: { "Content-Type": "application/json" },
241
+ method: "POST"
242
+ });
243
+ if (!response.ok) throw new Error(`Failed to ${path} document: ${response.status}`);
244
+ }
245
+ async function saveWebSocketDemoDocument(id, content) {
246
+ await postWebSocketDemoDocument(id, "save", content);
247
+ }
248
+ function snapshotWebSocketDemoDocument(id, content) {
249
+ const url = `${DEFAULT_HTTP_BASE_URL}/documents/${encodeURIComponent(id)}/snapshot`;
250
+ const body = JSON.stringify({ content });
251
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
252
+ const blob = new Blob([body], { type: "text/plain;charset=UTF-8" });
253
+ if (navigator.sendBeacon(url, blob)) return;
254
+ }
255
+ fetch(url, {
256
+ body,
257
+ headers: { "Content-Type": "application/json" },
258
+ keepalive: true,
259
+ method: "POST"
260
+ }).catch(() => {});
261
+ }
262
+ //#endregion
263
+ export { WebSocketYjsProvider, createWebSocketYjsProvider, fetchWebSocketDemoDocument, saveWebSocketDemoDocument, snapshotWebSocketDemoDocument };
@@ -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 };
@@ -2,11 +2,14 @@ import { getCSSVariables, getRendererClassName } from "./style.js";
2
2
  import { renderNode } from "./engine/render-tree.js";
3
3
  import { rendererNodes } from "./nodes/index.js";
4
4
  import { createDefaultRenderers } from "./renderers/index.js";
5
- import { createElement, useMemo } from "react";
5
+ import { MotionComponent, MotionProvider } from "@lobehub/ui";
6
+ import { createElement, use, useMemo } from "react";
6
7
  import { createHeadlessEditor } from "@lexical/headless";
7
8
  import { $getRoot } from "lexical";
9
+ import { motion } from "motion/react";
8
10
  //#region src/renderer/LexicalRenderer.tsx
9
11
  function LexicalRenderer({ value, overrides, extraNodes, renderContext, as: Tag = "div", className, style, variant }) {
12
+ const inheritedMotion = use(MotionComponent);
10
13
  const content = useMemo(() => {
11
14
  const nodes = extraNodes ? [...rendererNodes, ...extraNodes] : rendererNodes;
12
15
  const registry = createDefaultRenderers();
@@ -36,13 +39,17 @@ function LexicalRenderer({ value, overrides, extraNodes, renderContext, as: Tag
36
39
  variant
37
40
  ]);
38
41
  const cssVars = getCSSVariables(variant);
39
- return createElement(Tag, {
42
+ const tree = createElement(Tag, {
40
43
  className: getRendererClassName(className),
41
44
  style: {
42
45
  ...cssVars,
43
46
  ...style
44
47
  }
45
48
  }, createElement("div", null, content));
49
+ return inheritedMotion ? tree : createElement(MotionProvider, {
50
+ children: tree,
51
+ motion
52
+ });
46
53
  }
47
54
  //#endregion
48
55
  export { LexicalRenderer };
@@ -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
@@ -340,6 +352,10 @@ interface IEditorPlugin<IConfig> {
340
352
  * Editor destruction
341
353
  */
342
354
  destroy(): void;
355
+ /**
356
+ * After editor document content is written by setDocument
357
+ */
358
+ onDocumentChange?(type: string, content: any, options?: IDocumentOptions): void;
343
359
  /**
344
360
  * After Lexical editor instantiation
345
361
  * @param editor Lexical editor instance
@@ -12,8 +12,11 @@ const updatePosition = ({ floating, placement = "bottom-start", reference, offse
12
12
  }).then((props) => {
13
13
  if (!floating) return false;
14
14
  const { x, y } = props;
15
- floating.style.left = `${x}px`;
16
- floating.style.top = `${y}px`;
15
+ const viewportPadding = 8;
16
+ const viewportMinX = window.scrollX + viewportPadding;
17
+ const viewportMinY = window.scrollY + viewportPadding;
18
+ floating.style.left = `${Math.max(x, viewportMinX)}px`;
19
+ floating.style.top = `${Math.max(y, viewportMinY)}px`;
17
20
  callback?.(props);
18
21
  });
19
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/editor",
3
- "version": "4.24.1",
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",
@@ -62,6 +62,7 @@
62
62
  "build:watch": "tsdown --watch",
63
63
  "ci": "npm run lint:circular && npm run lint && npm run type-check",
64
64
  "clean": "rm -r es lib dist coverage .react-router .eslintcache node_modules/.cache",
65
+ "demo:yjs-server": "node ./scripts/yjs-websocket-demo-server.cjs",
65
66
  "dev": "lobedocs dev",
66
67
  "docs:build": "lobedocs build",
67
68
  "docs:build-analyze": "ANALYZE=1 lobedocs build",
@@ -138,6 +139,7 @@
138
139
  "chalk": "^4.1.2"
139
140
  },
140
141
  "patchedDependencies": {
142
+ "@lexical/yjs@0.42.0": "patches/@lexical__yjs@0.42.0.patch",
141
143
  "lexical@0.42.0": "patches/lexical@0.42.0.patch"
142
144
  }
143
145
  }