@lobehub/editor 4.24.0 → 4.25.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/codemirror/style.js +42 -0
- package/es/editor-kernel/kernel.js +3 -0
- package/es/editor-kernel/react/useDecorators.js +2 -4
- package/es/headless/collapsible-plugin.js +74 -0
- package/es/headless/index.js +2 -0
- package/es/headless.d.ts +18 -0
- package/es/headless.js +2479 -71
- package/es/index.d.ts +26 -3
- package/es/index.js +17 -2
- package/es/locale/index.d.ts +14 -0
- package/es/locale/index.js +14 -0
- package/es/plugins/block/command/index.js +24 -0
- package/es/plugins/block/plugin/index.js +7 -5
- package/es/plugins/block/react/ReactBlockPlugin.js +43 -1
- package/es/plugins/block/react/core/menu-position.js +20 -0
- package/es/plugins/block/react/drag/drag-session.js +3 -2
- package/es/plugins/block/react/drag/drag-utils.js +13 -1
- package/es/plugins/codemirror-block/index.d.ts +2 -1
- package/es/plugins/codemirror-block/plugin/index.js +2 -0
- package/es/plugins/codemirror-block/react/CodemirrorNode.js +95 -5
- package/es/plugins/codemirror-block/react/useCodemirrorEditLock.js +33 -0
- package/es/plugins/codemirror-block/service/index.d.ts +40 -0
- package/es/plugins/codemirror-block/service/index.js +59 -0
- package/es/plugins/collapsible/command/index.d.ts +17 -0
- package/es/plugins/collapsible/command/index.js +72 -0
- package/es/plugins/collapsible/index.d.ts +7 -0
- package/es/plugins/collapsible/node/CollapsibleNode.d.ts +30 -0
- package/es/plugins/collapsible/node/CollapsibleNode.js +138 -0
- package/es/plugins/collapsible/plugin/index.d.ts +11 -0
- package/es/plugins/collapsible/plugin/index.js +535 -0
- package/es/plugins/collapsible/react/ReactCollapsiblePlugin.d.ts +6 -0
- package/es/plugins/collapsible/react/ReactCollapsiblePlugin.js +19 -0
- package/es/plugins/collapsible/react/index.d.ts +3 -0
- package/es/plugins/collapsible/react/style.js +67 -0
- package/es/plugins/collapsible/react/type.d.ts +6 -0
- package/es/plugins/common/plugin/index.js +3 -2
- package/es/plugins/common/plugin/progressive-select-all.js +81 -0
- package/es/plugins/common/plugin/register.js +73 -4
- package/es/plugins/link/command/index.js +26 -3
- package/es/plugins/link/conversion/index.js +237 -0
- package/es/plugins/link/index.d.ts +5 -3
- package/es/plugins/link/node/LinkBlockCardNode.d.ts +16 -0
- package/es/plugins/link/node/LinkBlockCardNode.js +66 -0
- package/es/plugins/link/node/LinkCardNode.d.ts +42 -0
- package/es/plugins/link/node/LinkCardNode.js +161 -0
- package/es/plugins/link/node/LinkIframeNode.d.ts +33 -0
- package/es/plugins/link/node/LinkIframeNode.js +131 -0
- package/es/plugins/link/node/LinkNode.d.ts +48 -2
- package/es/plugins/link/node/LinkNode.js +14 -6
- package/es/plugins/link/node/SchemaNode.d.ts +36 -0
- package/es/plugins/link/node/SchemaNode.js +149 -0
- package/es/plugins/link/normalization/index.d.ts +7 -0
- package/es/plugins/link/normalization/index.js +38 -0
- package/es/plugins/link/plugin/index.d.ts +26 -1
- package/es/plugins/link/plugin/index.js +124 -4
- package/es/plugins/link/react/ReactLinkPlugin.js +165 -7
- package/es/plugins/link/react/components/LinkCard.js +235 -0
- package/es/plugins/link/react/components/LinkEdit.js +93 -14
- package/es/plugins/link/react/components/LinkIframe.js +128 -0
- package/es/plugins/link/react/components/LinkToolbar.js +440 -111
- package/es/plugins/link/react/components/SchemaLink.js +49 -0
- package/es/plugins/link/react/index.d.ts +2 -2
- package/es/plugins/link/react/renderer-registry.d.ts +81 -0
- package/es/plugins/link/react/renderer-registry.js +80 -0
- package/es/plugins/link/react/style.js +63 -1
- package/es/plugins/link/react/type.d.ts +24 -1
- package/es/plugins/link/service/i-link-service.d.ts +156 -1
- package/es/plugins/link/service/i-link-service.js +258 -5
- package/es/plugins/list/plugin/index.js +9 -2
- package/es/plugins/litexml/command/diffCommand.js +94 -14
- package/es/plugins/litexml/command/index.js +187 -6
- package/es/plugins/litexml/diff-validation.js +237 -0
- package/es/plugins/litexml/node/DiffContentNode.js +67 -0
- package/es/plugins/litexml/node/DiffNode.d.ts +1 -1
- package/es/plugins/litexml/node/DiffNode.js +5 -2
- package/es/plugins/litexml/node/TableCellDiffNode.js +77 -0
- package/es/plugins/litexml/node/TableRowDiffNode.js +73 -0
- package/es/plugins/litexml/plugin/index.d.ts +2 -0
- package/es/plugins/litexml/plugin/index.js +62 -9
- package/es/plugins/litexml/react/DiffNodeToolbar/index.js +10 -12
- package/es/plugins/litexml/react/TableRowDiffToolbar.js +123 -0
- package/es/plugins/litexml/react/hooks/useHasDiffNode.js +5 -2
- package/es/plugins/litexml/react/index.js +6 -2
- package/es/plugins/litexml/react/style.js +18 -5
- package/es/plugins/litexml/table-cell-diff.js +153 -0
- package/es/plugins/litexml/table-row-diff.js +82 -0
- package/es/plugins/markdown/data-source/markdown/parse.js +25 -1
- package/es/plugins/markdown/plugin/index.js +5 -3
- package/es/plugins/slash/react/ReactSlashPlugin.js +22 -43
- package/es/plugins/slash/react/components/DefaultSlashMenu.js +76 -16
- package/es/plugins/slash/react/menu-utils.js +84 -0
- package/es/plugins/slash/service/i-slash-service.d.ts +3 -0
- package/es/plugins/table/plugin/index.js +4 -1
- package/es/plugins/table/plugin/single-cell-paste.js +79 -0
- package/es/plugins/table/react/TableController/hooks.js +7 -4
- package/es/plugins/table/react/hooks.js +2 -2
- package/es/plugins/table/react/index.js +2 -0
- package/es/plugins/table/react/style.js +15 -5
- package/es/plugins/table/react/useScrollFocusedTableCellIntoView.js +46 -0
- package/es/plugins/toc/index.d.ts +10 -0
- package/es/plugins/toc/plugin/index.d.ts +8 -0
- package/es/plugins/toc/plugin/index.js +56 -0
- package/es/plugins/toc/react/ReactTocPlugin.d.ts +6 -0
- package/es/plugins/toc/react/ReactTocPlugin.js +36 -0
- package/es/plugins/toc/react/getNearestScrollContainer.d.ts +6 -0
- package/es/plugins/toc/react/getNearestScrollContainer.js +18 -0
- package/es/plugins/toc/react/hooks/useActiveHeading.js +59 -0
- package/es/plugins/toc/react/hooks/useTocItems.js +50 -0
- package/es/plugins/toc/react/index.d.ts +5 -0
- package/es/plugins/toc/react/type.d.ts +30 -0
- package/es/plugins/toc/react/useToc.d.ts +5 -0
- package/es/plugins/toc/react/useToc.js +41 -0
- package/es/plugins/toc/service/i-toc-service.d.ts +39 -0
- package/es/plugins/toc/service/i-toc-service.js +125 -0
- package/es/plugins/toc/service/index.d.ts +3 -0
- package/es/plugins/toc/types.d.ts +23 -0
- package/es/plugins/toc/utils/index.d.ts +6 -0
- package/es/plugins/toc/utils/index.js +30 -0
- package/es/plugins/toolbar/react/index.js +26 -12
- package/es/plugins/toolbar/react/style.js +3 -2
- package/es/plugins/virtual-block/plugin/register.js +34 -12
- package/es/plugins/yjs/index.d.ts +6 -0
- package/es/plugins/yjs/plugin/index.d.ts +7 -0
- package/es/plugins/yjs/plugin/index.js +191 -0
- package/es/plugins/yjs/plugin/types.d.ts +16 -0
- package/es/plugins/yjs/plugin/utils/awareness.js +9 -0
- package/es/plugins/yjs/plugin/utils/editor-state.js +47 -0
- package/es/plugins/yjs/plugin/utils/history.js +31 -0
- package/es/plugins/yjs/plugin/utils/node-properties.js +39 -0
- package/es/plugins/yjs/plugin/utils/sync.js +24 -0
- package/es/plugins/yjs/react/codemirrorEditLockProvider.js +59 -0
- package/es/plugins/yjs/react/index.d.ts +23 -0
- package/es/plugins/yjs/react/index.js +200 -0
- package/es/plugins/yjs/service/index.d.ts +47 -0
- package/es/plugins/yjs/service/index.js +84 -0
- package/es/plugins/yjs/websocket-provider.d.ts +78 -0
- package/es/plugins/yjs/websocket-provider.js +263 -0
- package/es/renderer/LexicalRenderer.js +9 -2
- package/es/renderer/engine/render-builtin-node.js +17 -8
- package/es/types/kernel.d.ts +4 -0
- package/es/utils/updatePosition.js +5 -2
- package/package.json +3 -1
- package/patches/@lexical__yjs@0.42.0.patch +42 -0
- 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 };
|
|
@@ -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 {
|
|
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
|
-
|
|
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 };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getListClassName, getTableWrapperClassName } from "../style.js";
|
|
2
2
|
import { parseCSSText } from "./utils.js";
|
|
3
3
|
import { createElement } from "react";
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/renderer/engine/render-builtin-node.tsx
|
|
6
6
|
const SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set([
|
|
7
7
|
"http:",
|
|
@@ -31,6 +31,11 @@ function formatLinkHighlightUrl(url) {
|
|
|
31
31
|
function textToSlug(text) {
|
|
32
32
|
return text.toLowerCase().trim().replaceAll(/[^\s\w\u3000-\u9FFF\uAC00-\uD7AF\uFF00-\uFFEF-]/g, "").replaceAll(/[\s_]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
33
33
|
}
|
|
34
|
+
const FLUID_TABLE_STYLE = { width: "100%" };
|
|
35
|
+
function toTableColWidths(value) {
|
|
36
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
37
|
+
return value.every((width) => Number.isFinite(width) && width > 0) ? value : null;
|
|
38
|
+
}
|
|
34
39
|
const DIFF_ROOT_STYLE = { position: "relative" };
|
|
35
40
|
const DIFF_LIST_ITEM_ROOT_STYLE = {
|
|
36
41
|
display: "inline-block",
|
|
@@ -157,13 +162,17 @@ function renderBuiltinNode(node, key, children, headingSlugs, textContent) {
|
|
|
157
162
|
children
|
|
158
163
|
}, key);
|
|
159
164
|
}
|
|
160
|
-
case "table":
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
className:
|
|
164
|
-
children: /* @__PURE__ */
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
case "table": {
|
|
166
|
+
const colWidths = toTableColWidths(node.colWidths);
|
|
167
|
+
return /* @__PURE__ */ jsx("div", {
|
|
168
|
+
className: getTableWrapperClassName(),
|
|
169
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
170
|
+
className: "editor_table",
|
|
171
|
+
style: colWidths ? void 0 : FLUID_TABLE_STYLE,
|
|
172
|
+
children: [colWidths && /* @__PURE__ */ jsx("colgroup", { children: colWidths.map((width, index) => /* @__PURE__ */ jsx("col", { style: { width } }, index)) }), /* @__PURE__ */ jsx("tbody", { children })]
|
|
173
|
+
})
|
|
174
|
+
}, key);
|
|
175
|
+
}
|
|
167
176
|
case "tablerow": return /* @__PURE__ */ jsx("tr", { children }, key);
|
|
168
177
|
case "tablecell": {
|
|
169
178
|
const isHeader = !!node.headerState;
|
package/es/types/kernel.d.ts
CHANGED
|
@@ -340,6 +340,10 @@ interface IEditorPlugin<IConfig> {
|
|
|
340
340
|
* Editor destruction
|
|
341
341
|
*/
|
|
342
342
|
destroy(): void;
|
|
343
|
+
/**
|
|
344
|
+
* After editor document content is written by setDocument
|
|
345
|
+
*/
|
|
346
|
+
onDocumentChange?(type: string, content: any, options?: IDocumentOptions): void;
|
|
343
347
|
/**
|
|
344
348
|
* After Lexical editor instantiation
|
|
345
349
|
* @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
|
-
|
|
16
|
-
|
|
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.
|
|
3
|
+
"version": "4.25.0",
|
|
4
4
|
"description": "A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lobehub",
|
|
@@ -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
|
}
|