@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.
- package/es/codemirror/style.js +42 -0
- package/es/editor-kernel/kernel.d.ts +9 -0
- package/es/editor-kernel/kernel.js +74 -11
- 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 +30 -0
- package/es/headless.js +2618 -139
- 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 +14 -4
- package/es/plugins/common/plugin/progressive-select-all.js +81 -0
- package/es/plugins/common/plugin/register.js +73 -4
- package/es/plugins/common/react/ReactPlainText.js +27 -9
- 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/react/hooks/useEditor.d.ts +24 -2
- package/es/react/hooks/useEditor.js +13 -3
- package/es/react.d.ts +2 -2
- package/es/renderer/LexicalRenderer.js +9 -2
- package/es/types/kernel.d.ts +16 -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,191 @@
|
|
|
1
|
+
import { KernelPlugin, init_plugin } from "../../../editor-kernel/plugin.js";
|
|
2
|
+
import { clearEditorSkipCollab, initializeEditor } from "./utils/editor-state.js";
|
|
3
|
+
import { ensureYjsNodePropertiesFromEditorState } from "./utils/node-properties.js";
|
|
4
|
+
import { hydrateLexicalFromYjsState, syncCurrentEditorStateToYjs } from "./utils/sync.js";
|
|
5
|
+
import { IYjsService, YjsService } from "../service/index.js";
|
|
6
|
+
import { getAwarenessUsers } from "./utils/awareness.js";
|
|
7
|
+
import { registerYjsHistory } from "./utils/history.js";
|
|
8
|
+
import { $getRoot, COLLABORATION_TAG, COMMAND_PRIORITY_CRITICAL, SKIP_COLLAB_TAG } from "lexical";
|
|
9
|
+
import { UndoManager } from "yjs";
|
|
10
|
+
import { CONNECTED_COMMAND, TOGGLE_CONNECT_COMMAND, createBinding, syncLexicalUpdateToYjs, syncYjsChangesToLexical } from "@lexical/yjs";
|
|
11
|
+
//#region src/plugins/yjs/plugin/index.ts
|
|
12
|
+
init_plugin();
|
|
13
|
+
const YjsPlugin = class extends KernelPlugin {
|
|
14
|
+
static {
|
|
15
|
+
this.pluginName = "YjsPlugin";
|
|
16
|
+
}
|
|
17
|
+
constructor(kernel, config) {
|
|
18
|
+
super();
|
|
19
|
+
this.kernel = kernel;
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.bootstrapCurrentEditorState = null;
|
|
22
|
+
this.docMap = /* @__PURE__ */ new Map();
|
|
23
|
+
this.hasInitialized = false;
|
|
24
|
+
this.isReloadingDoc = false;
|
|
25
|
+
this.markDocumentChanged = null;
|
|
26
|
+
this.service = new YjsService();
|
|
27
|
+
kernel.registerServiceHotReload(IYjsService, this.service);
|
|
28
|
+
}
|
|
29
|
+
destroy() {
|
|
30
|
+
super.destroy();
|
|
31
|
+
this.bootstrapCurrentEditorState = null;
|
|
32
|
+
this.markDocumentChanged = null;
|
|
33
|
+
this.service.setState(null);
|
|
34
|
+
}
|
|
35
|
+
onDocumentChange() {
|
|
36
|
+
this.markDocumentChanged?.();
|
|
37
|
+
}
|
|
38
|
+
connectProvider(provider, connectionState) {
|
|
39
|
+
if (connectionState.hasConnected) return;
|
|
40
|
+
connectionState.hasConnected = true;
|
|
41
|
+
connectionState.connection = provider.connect();
|
|
42
|
+
}
|
|
43
|
+
registerAwareness(provider) {
|
|
44
|
+
this.service.setAwarenessUsers(getAwarenessUsers(provider));
|
|
45
|
+
const updateAwarenessUsers = () => {
|
|
46
|
+
this.service.setAwarenessUsers(getAwarenessUsers(provider));
|
|
47
|
+
};
|
|
48
|
+
provider.awareness.on("update", updateAwarenessUsers);
|
|
49
|
+
this.register(() => {
|
|
50
|
+
provider.awareness.off("update", updateAwarenessUsers);
|
|
51
|
+
this.service.setAwarenessUsers([]);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
registerEditorSync(editor, binding, provider, shouldBootstrap, syncState) {
|
|
55
|
+
this.register(editor.registerUpdateListener(({ dirtyElements, dirtyLeaves, editorState, normalizedNodes, prevEditorState, tags }) => {
|
|
56
|
+
if (tags.has(SKIP_COLLAB_TAG) || tags.has(COLLABORATION_TAG)) return;
|
|
57
|
+
if (!syncState.providerHasSynced) return;
|
|
58
|
+
ensureYjsNodePropertiesFromEditorState(binding, editorState);
|
|
59
|
+
if (shouldBootstrap && syncState.providerHasSynced && binding.root.isEmpty() && binding.root._xmlText._length === 0) {
|
|
60
|
+
if (!editorState.read(() => $getRoot().isEmpty())) {
|
|
61
|
+
syncCurrentEditorStateToYjs(binding, provider);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
syncLexicalUpdateToYjs(binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags);
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
registerProviderEvents(editor, binding, provider, id, syncState) {
|
|
69
|
+
const onProviderDocReload = (doc) => {
|
|
70
|
+
clearEditorSkipCollab(editor);
|
|
71
|
+
this.docMap.set(id, doc);
|
|
72
|
+
this.isReloadingDoc = true;
|
|
73
|
+
this.service.setState({
|
|
74
|
+
binding,
|
|
75
|
+
doc,
|
|
76
|
+
docMap: this.docMap,
|
|
77
|
+
id,
|
|
78
|
+
provider
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const onStatus = ({ status }) => {
|
|
82
|
+
editor.dispatchCommand(CONNECTED_COMMAND, status === "connected");
|
|
83
|
+
};
|
|
84
|
+
const onSync = (isSynced) => {
|
|
85
|
+
if (!isSynced) return;
|
|
86
|
+
syncState.providerHasSynced = true;
|
|
87
|
+
if (this.isReloadingDoc) {
|
|
88
|
+
this.isReloadingDoc = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (binding.root.isEmpty() && binding.root._xmlText._length > 0) {
|
|
92
|
+
hydrateLexicalFromYjsState(binding);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.bootstrapCurrentEditorState?.();
|
|
96
|
+
};
|
|
97
|
+
provider.on("reload", onProviderDocReload);
|
|
98
|
+
provider.on("status", onStatus);
|
|
99
|
+
provider.on("sync", onSync);
|
|
100
|
+
this.register(() => {
|
|
101
|
+
provider.off("reload", onProviderDocReload);
|
|
102
|
+
provider.off("status", onStatus);
|
|
103
|
+
provider.off("sync", onSync);
|
|
104
|
+
this.bootstrapCurrentEditorState = null;
|
|
105
|
+
this.markDocumentChanged = null;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
registerProviderToggleCommand(editor, provider) {
|
|
109
|
+
this.register(editor.registerCommand(TOGGLE_CONNECT_COMMAND, (shouldConnect) => {
|
|
110
|
+
if (shouldConnect) provider.connect();
|
|
111
|
+
else provider.disconnect();
|
|
112
|
+
return true;
|
|
113
|
+
}, COMMAND_PRIORITY_CRITICAL));
|
|
114
|
+
}
|
|
115
|
+
registerProviderTeardown(binding, provider, connectionState) {
|
|
116
|
+
this.register(() => {
|
|
117
|
+
if (connectionState.connection) connectionState.connection.then(() => provider.disconnect());
|
|
118
|
+
else if (connectionState.hasConnected) provider.disconnect();
|
|
119
|
+
binding.root.destroy(binding);
|
|
120
|
+
this.service.setState(null);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
registerYjsTreeSync(binding, provider) {
|
|
124
|
+
const onYjsTreeChanges = (events, transaction) => {
|
|
125
|
+
if (transaction.origin === binding) return;
|
|
126
|
+
syncYjsChangesToLexical(binding, provider, events, transaction.origin instanceof UndoManager, () => void 0);
|
|
127
|
+
};
|
|
128
|
+
binding.root.getSharedType().observeDeep(onYjsTreeChanges);
|
|
129
|
+
this.register(() => {
|
|
130
|
+
binding.root.getSharedType().unobserveDeep(onYjsTreeChanges);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
setBootstrapCurrentEditorState(editor, binding, provider, initialEditorState, shouldBootstrap, syncState) {
|
|
134
|
+
this.bootstrapCurrentEditorState = () => {
|
|
135
|
+
if (!shouldBootstrap || !syncState.providerHasSynced || !syncState.documentHasChanged && !initialEditorState || !binding.root.isEmpty() || binding.root._xmlText._length !== 0) return;
|
|
136
|
+
if (editor.getEditorState().read(() => $getRoot().isEmpty()) && !initialEditorState) return;
|
|
137
|
+
if (initialEditorState) {
|
|
138
|
+
clearEditorSkipCollab(editor);
|
|
139
|
+
initializeEditor(editor, initialEditorState, {
|
|
140
|
+
discrete: true,
|
|
141
|
+
skipIfNotEmpty: false,
|
|
142
|
+
tag: void 0
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
syncCurrentEditorStateToYjs(binding, provider);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
setServiceState(binding, id, provider, doc) {
|
|
149
|
+
this.service.setState({
|
|
150
|
+
binding,
|
|
151
|
+
doc,
|
|
152
|
+
docMap: this.docMap,
|
|
153
|
+
id,
|
|
154
|
+
provider
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
onInit(editor) {
|
|
158
|
+
const { excludedProperties, id, initialEditorState, providerFactory, shouldBootstrap = true, yjsDoc } = this.config || {};
|
|
159
|
+
if (!id || !providerFactory) throw new Error("YjsPlugin requires both \"id\" and \"providerFactory\".");
|
|
160
|
+
if (this.hasInitialized) return;
|
|
161
|
+
this.hasInitialized = true;
|
|
162
|
+
if (yjsDoc) this.docMap.set(id, yjsDoc);
|
|
163
|
+
const provider = providerFactory(id, this.docMap);
|
|
164
|
+
const connectionState = {
|
|
165
|
+
connection: void 0,
|
|
166
|
+
hasConnected: false
|
|
167
|
+
};
|
|
168
|
+
const syncState = {
|
|
169
|
+
documentHasChanged: false,
|
|
170
|
+
providerHasSynced: false
|
|
171
|
+
};
|
|
172
|
+
const binding = createBinding(editor, provider, id, this.docMap.get(id), this.docMap, excludedProperties);
|
|
173
|
+
this.setServiceState(binding, id, provider, this.docMap.get(id));
|
|
174
|
+
this.registerAwareness(provider);
|
|
175
|
+
this.registerYjsTreeSync(binding, provider);
|
|
176
|
+
this.registerEditorSync(editor, binding, provider, shouldBootstrap, syncState);
|
|
177
|
+
this.setBootstrapCurrentEditorState(editor, binding, provider, initialEditorState, shouldBootstrap, syncState);
|
|
178
|
+
this.markDocumentChanged = () => {
|
|
179
|
+
syncState.documentHasChanged = true;
|
|
180
|
+
this.connectProvider(provider, connectionState);
|
|
181
|
+
this.bootstrapCurrentEditorState?.();
|
|
182
|
+
};
|
|
183
|
+
this.registerProviderEvents(editor, binding, provider, id, syncState);
|
|
184
|
+
this.registerProviderToggleCommand(editor, provider);
|
|
185
|
+
this.register(registerYjsHistory(editor, binding));
|
|
186
|
+
if (initialEditorState) this.connectProvider(provider, connectionState);
|
|
187
|
+
this.registerProviderTeardown(binding, provider, connectionState);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
//#endregion
|
|
191
|
+
export { YjsPlugin };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EditorState, LexicalEditor } from "lexical";
|
|
2
|
+
import { Doc } from "yjs";
|
|
3
|
+
import { ExcludedProperties, Provider } from "@lexical/yjs";
|
|
4
|
+
//#region src/plugins/yjs/plugin/types.d.ts
|
|
5
|
+
type YjsProviderFactory = (id: string, yjsDocMap: Map<string, Doc>) => Provider;
|
|
6
|
+
type YjsInitialEditorState = EditorState | ((editor: LexicalEditor) => void) | null | string;
|
|
7
|
+
interface YjsPluginOptions {
|
|
8
|
+
excludedProperties?: ExcludedProperties;
|
|
9
|
+
id: string;
|
|
10
|
+
initialEditorState?: YjsInitialEditorState;
|
|
11
|
+
providerFactory: YjsProviderFactory;
|
|
12
|
+
shouldBootstrap?: boolean;
|
|
13
|
+
yjsDoc?: Doc;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { YjsInitialEditorState, YjsPluginOptions, YjsProviderFactory };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { $createParagraphNode, $getRoot, HISTORY_MERGE_TAG, SKIP_COLLAB_TAG } from "lexical";
|
|
2
|
+
//#region src/plugins/yjs/plugin/utils/editor-state.ts
|
|
3
|
+
function initializeEditor(editor, initialEditorState, options = {}) {
|
|
4
|
+
const { discrete, skipIfNotEmpty = true, tag = HISTORY_MERGE_TAG } = options;
|
|
5
|
+
const updateOptions = Object.keys({
|
|
6
|
+
...discrete ? { discrete: true } : {},
|
|
7
|
+
...tag ? { tag } : {}
|
|
8
|
+
}).length ? {
|
|
9
|
+
...discrete ? { discrete: true } : {},
|
|
10
|
+
...tag ? { tag } : {}
|
|
11
|
+
} : void 0;
|
|
12
|
+
if (initialEditorState && typeof initialEditorState !== "function") {
|
|
13
|
+
if (editor.getEditorState().read(() => {
|
|
14
|
+
return skipIfNotEmpty && !$getRoot().isEmpty();
|
|
15
|
+
})) return;
|
|
16
|
+
editor.setEditorState(typeof initialEditorState === "string" ? editor.parseEditorState(initialEditorState) : initialEditorState, updateOptions);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
editor.update(() => {
|
|
20
|
+
const root = $getRoot();
|
|
21
|
+
if (skipIfNotEmpty && !root.isEmpty()) return;
|
|
22
|
+
if (typeof initialEditorState === "function") {
|
|
23
|
+
initialEditorState(editor);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
root.append($createParagraphNode());
|
|
27
|
+
}, updateOptions);
|
|
28
|
+
}
|
|
29
|
+
function createEmptyPreviousEditorState(editor) {
|
|
30
|
+
return editor.parseEditorState(JSON.stringify({ root: {
|
|
31
|
+
children: [],
|
|
32
|
+
direction: null,
|
|
33
|
+
format: "",
|
|
34
|
+
indent: 0,
|
|
35
|
+
type: "root",
|
|
36
|
+
version: 1
|
|
37
|
+
} }));
|
|
38
|
+
}
|
|
39
|
+
function clearEditorSkipCollab(editor) {
|
|
40
|
+
editor.update(() => {
|
|
41
|
+
const root = $getRoot();
|
|
42
|
+
root.clear();
|
|
43
|
+
root.select();
|
|
44
|
+
}, { tag: SKIP_COLLAB_TAG });
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { clearEditorSkipCollab, createEmptyPreviousEditorState, initializeEditor };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_CRITICAL, REDO_COMMAND, UNDO_COMMAND } from "lexical";
|
|
2
|
+
import { createUndoManager } from "@lexical/yjs";
|
|
3
|
+
//#region src/plugins/yjs/plugin/utils/history.ts
|
|
4
|
+
function registerYjsHistory(editor, binding) {
|
|
5
|
+
const undoManager = createUndoManager(binding, binding.root.getSharedType());
|
|
6
|
+
const updateUndoRedoState = () => {
|
|
7
|
+
editor.dispatchCommand(CAN_UNDO_COMMAND, undoManager.undoStack.length > 0);
|
|
8
|
+
editor.dispatchCommand(CAN_REDO_COMMAND, undoManager.redoStack.length > 0);
|
|
9
|
+
};
|
|
10
|
+
undoManager.on("stack-item-added", updateUndoRedoState);
|
|
11
|
+
undoManager.on("stack-item-popped", updateUndoRedoState);
|
|
12
|
+
undoManager.on("stack-cleared", updateUndoRedoState);
|
|
13
|
+
const unregisterUndo = editor.registerCommand(UNDO_COMMAND, () => {
|
|
14
|
+
undoManager.undo();
|
|
15
|
+
return true;
|
|
16
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
17
|
+
const unregisterRedo = editor.registerCommand(REDO_COMMAND, () => {
|
|
18
|
+
undoManager.redo();
|
|
19
|
+
return true;
|
|
20
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
21
|
+
return () => {
|
|
22
|
+
unregisterUndo();
|
|
23
|
+
unregisterRedo();
|
|
24
|
+
undoManager.off("stack-item-added", updateUndoRedoState);
|
|
25
|
+
undoManager.off("stack-item-popped", updateUndoRedoState);
|
|
26
|
+
undoManager.off("stack-cleared", updateUndoRedoState);
|
|
27
|
+
undoManager.destroy();
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { registerYjsHistory };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/plugins/yjs/plugin/utils/node-properties.ts
|
|
2
|
+
const ignoredNodeProperties = /* @__PURE__ */ new Set([
|
|
3
|
+
"__cachedText",
|
|
4
|
+
"__first",
|
|
5
|
+
"__key",
|
|
6
|
+
"__last",
|
|
7
|
+
"__next",
|
|
8
|
+
"__parent",
|
|
9
|
+
"__prev",
|
|
10
|
+
"__size",
|
|
11
|
+
"__state",
|
|
12
|
+
"__text"
|
|
13
|
+
]);
|
|
14
|
+
function shouldIgnoreNodeProperty(property, node, binding) {
|
|
15
|
+
if (ignoredNodeProperties.has(property) || typeof node[property] === "function") return true;
|
|
16
|
+
const excludedProperties = binding.excludedProperties.get(node.constructor);
|
|
17
|
+
return Boolean(excludedProperties?.has(property));
|
|
18
|
+
}
|
|
19
|
+
function ensureYjsNodeProperties(binding, node) {
|
|
20
|
+
if (binding.nodeProperties.has(node.__type)) return;
|
|
21
|
+
let defaultNode = node;
|
|
22
|
+
try {
|
|
23
|
+
defaultNode = new node.constructor();
|
|
24
|
+
} catch {
|
|
25
|
+
defaultNode = node;
|
|
26
|
+
}
|
|
27
|
+
const defaultProperties = {};
|
|
28
|
+
for (const [property, value] of Object.entries(defaultNode)) if (!shouldIgnoreNodeProperty(property, defaultNode, binding)) defaultProperties[property] = value;
|
|
29
|
+
binding.nodeProperties.set(node.__type, Object.freeze(defaultProperties));
|
|
30
|
+
}
|
|
31
|
+
function ensureYjsNodePropertiesFromEditorState(binding, editorState) {
|
|
32
|
+
editorState.read(() => {
|
|
33
|
+
editorState._nodeMap.forEach((node) => {
|
|
34
|
+
ensureYjsNodeProperties(binding, node);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { ensureYjsNodePropertiesFromEditorState };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createEmptyPreviousEditorState } from "./editor-state.js";
|
|
2
|
+
import { ensureYjsNodePropertiesFromEditorState } from "./node-properties.js";
|
|
3
|
+
import { COLLABORATION_TAG } from "lexical";
|
|
4
|
+
import { syncLexicalUpdateToYjs } from "@lexical/yjs";
|
|
5
|
+
//#region src/plugins/yjs/plugin/utils/sync.ts
|
|
6
|
+
function syncCurrentEditorStateToYjs(binding, provider, prevEditorState = createEmptyPreviousEditorState(binding.editor)) {
|
|
7
|
+
const editorState = binding.editor.getEditorState();
|
|
8
|
+
ensureYjsNodePropertiesFromEditorState(binding, editorState);
|
|
9
|
+
syncLexicalUpdateToYjs(binding, provider, prevEditorState, editorState, /* @__PURE__ */ new Map([["root", true]]), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
10
|
+
}
|
|
11
|
+
function hydrateLexicalFromYjsState(binding, options = {}) {
|
|
12
|
+
const root = binding.root;
|
|
13
|
+
const sharedType = root.getSharedType();
|
|
14
|
+
binding.editor.update(() => {
|
|
15
|
+
root.applyChildrenYjsDelta(binding, sharedType.toDelta());
|
|
16
|
+
root.syncChildrenFromYjs(binding);
|
|
17
|
+
}, {
|
|
18
|
+
discrete: options.discrete ? true : void 0,
|
|
19
|
+
skipTransforms: true,
|
|
20
|
+
tag: COLLABORATION_TAG
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { hydrateLexicalFromYjsState, syncCurrentEditorStateToYjs };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/plugins/yjs/react/codemirrorEditLockProvider.ts
|
|
2
|
+
function getAwarenessData(state) {
|
|
3
|
+
const awarenessData = state?.awarenessData;
|
|
4
|
+
if (!awarenessData || typeof awarenessData !== "object") return {};
|
|
5
|
+
return awarenessData;
|
|
6
|
+
}
|
|
7
|
+
function getEditingBlock(state) {
|
|
8
|
+
const editingBlock = getAwarenessData(state).editingBlock;
|
|
9
|
+
return editingBlock && typeof editingBlock.key === "string" ? editingBlock : null;
|
|
10
|
+
}
|
|
11
|
+
function updateLocalLock(state, key, nextLock) {
|
|
12
|
+
const localState = state.provider.awareness.getLocalState();
|
|
13
|
+
if (!localState) return;
|
|
14
|
+
const nextAwarenessData = { ...getAwarenessData(localState) };
|
|
15
|
+
if (nextLock) nextAwarenessData.editingBlock = nextLock;
|
|
16
|
+
else if (nextAwarenessData.editingBlock?.key === key) delete nextAwarenessData.editingBlock;
|
|
17
|
+
state.provider.awareness.setLocalState({
|
|
18
|
+
...localState,
|
|
19
|
+
awarenessData: nextAwarenessData
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function createCodemirrorEditLockProvider(state) {
|
|
23
|
+
const localClientId = state.binding.clientID;
|
|
24
|
+
const getRemoteLockOwner = (key) => {
|
|
25
|
+
for (const [clientId, userState] of state.provider.awareness.getStates()) {
|
|
26
|
+
if (clientId === localClientId) continue;
|
|
27
|
+
const lock = getEditingBlock(userState);
|
|
28
|
+
if (lock?.key === key) return {
|
|
29
|
+
id: clientId,
|
|
30
|
+
lock,
|
|
31
|
+
name: userState.name
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
acquireLock: (key, label) => {
|
|
38
|
+
if (getRemoteLockOwner(key)) return false;
|
|
39
|
+
updateLocalLock(state, key, {
|
|
40
|
+
key,
|
|
41
|
+
label,
|
|
42
|
+
updatedAt: Date.now()
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
getRemoteLockOwner,
|
|
47
|
+
releaseLock: (key) => {
|
|
48
|
+
updateLocalLock(state, key, null);
|
|
49
|
+
},
|
|
50
|
+
subscribe: (listener) => {
|
|
51
|
+
state.provider.awareness.on("update", listener);
|
|
52
|
+
return () => {
|
|
53
|
+
state.provider.awareness.off("update", listener);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { createCodemirrorEditLockProvider };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { YjsInitialEditorState, YjsProviderFactory } from "../plugin/types.js";
|
|
2
|
+
import "../plugin/index.js";
|
|
3
|
+
import { FC, RefObject } from "react";
|
|
4
|
+
import { Doc, Doc as Doc$1 } from "yjs";
|
|
5
|
+
import { ExcludedProperties, SyncCursorPositionsFn } from "@lexical/yjs";
|
|
6
|
+
//#region src/plugins/yjs/react/index.d.ts
|
|
7
|
+
interface ReactYjsPluginProps {
|
|
8
|
+
awarenessData?: object;
|
|
9
|
+
cursorColor?: string;
|
|
10
|
+
cursorsContainerRef?: RefObject<HTMLElement | null>;
|
|
11
|
+
excludedProperties?: ExcludedProperties;
|
|
12
|
+
id: string;
|
|
13
|
+
initialEditorState?: YjsInitialEditorState;
|
|
14
|
+
persistCursorOnBlur?: boolean;
|
|
15
|
+
providerFactory: YjsProviderFactory;
|
|
16
|
+
shouldBootstrap?: boolean;
|
|
17
|
+
syncCursorPositionsFn?: SyncCursorPositionsFn;
|
|
18
|
+
username?: string;
|
|
19
|
+
yjsDoc?: Doc;
|
|
20
|
+
}
|
|
21
|
+
declare const ReactYjsPlugin: FC<ReactYjsPluginProps>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { type Doc$1 as Doc, ReactYjsPlugin, ReactYjsPluginProps };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLexicalComposerContext } from "../../../editor-kernel/react/react-context.js";
|
|
3
|
+
import { useLexicalEditor } from "../../../editor-kernel/react/useLexicalEditor.js";
|
|
4
|
+
import { ICodemirrorEditLockService } from "../../codemirror-block/service/index.js";
|
|
5
|
+
import { IYjsService } from "../service/index.js";
|
|
6
|
+
import { YjsPlugin } from "../plugin/index.js";
|
|
7
|
+
import { createCodemirrorEditLockProvider } from "./codemirrorEditLockProvider.js";
|
|
8
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
import { BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, FOCUS_COMMAND } from "lexical";
|
|
11
|
+
import { createPortal } from "react-dom";
|
|
12
|
+
import { initLocalState, setLocalStateFocus, syncCursorPositions } from "@lexical/yjs";
|
|
13
|
+
//#region src/plugins/yjs/react/index.tsx
|
|
14
|
+
const DEFAULT_CURSOR_COLOR = "#2563eb";
|
|
15
|
+
const DEFAULT_USERNAME = "Anonymous";
|
|
16
|
+
const CURSOR_OVERLAY_Z_INDEX = 1;
|
|
17
|
+
function useYjsState(lexicalEditor, pluginRegisteredSignal) {
|
|
18
|
+
const [editor] = useLexicalComposerContext();
|
|
19
|
+
const [state, setState] = useState(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!lexicalEditor) return;
|
|
22
|
+
return editor.requireService(IYjsService)?.subscribe(setState);
|
|
23
|
+
}, [
|
|
24
|
+
editor,
|
|
25
|
+
lexicalEditor,
|
|
26
|
+
pluginRegisteredSignal
|
|
27
|
+
]);
|
|
28
|
+
return state;
|
|
29
|
+
}
|
|
30
|
+
function YjsCursors({ cursorsContainerRef, lexicalEditor, syncCursorPositionsFn, state }) {
|
|
31
|
+
const [defaultContainer, setDefaultContainer] = useState(null);
|
|
32
|
+
const cursorsContainer = cursorsContainerRef?.current || defaultContainer || document.body;
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
if (cursorsContainerRef?.current) return;
|
|
35
|
+
const rootElement = lexicalEditor.getRootElement();
|
|
36
|
+
setDefaultContainer(rootElement?.parentElement || rootElement || document.body);
|
|
37
|
+
}, [cursorsContainerRef, lexicalEditor]);
|
|
38
|
+
return useMemo(() => {
|
|
39
|
+
const ref = (element) => {
|
|
40
|
+
state.binding.cursorsContainer = element;
|
|
41
|
+
if (element) syncCursorPositionsFn(state.binding, state.provider);
|
|
42
|
+
};
|
|
43
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
44
|
+
ref,
|
|
45
|
+
style: {
|
|
46
|
+
height: 0,
|
|
47
|
+
left: 0,
|
|
48
|
+
pointerEvents: "none",
|
|
49
|
+
position: "absolute",
|
|
50
|
+
top: 0,
|
|
51
|
+
width: 0,
|
|
52
|
+
zIndex: CURSOR_OVERLAY_Z_INDEX
|
|
53
|
+
}
|
|
54
|
+
}), cursorsContainer);
|
|
55
|
+
}, [
|
|
56
|
+
cursorsContainer,
|
|
57
|
+
state.binding,
|
|
58
|
+
state.provider,
|
|
59
|
+
syncCursorPositionsFn
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
const ReactYjsPlugin = ({ awarenessData = {}, cursorColor = DEFAULT_CURSOR_COLOR, cursorsContainerRef, excludedProperties, id, initialEditorState, persistCursorOnBlur = true, providerFactory, shouldBootstrap, syncCursorPositionsFn = syncCursorPositions, username = DEFAULT_USERNAME, yjsDoc }) => {
|
|
63
|
+
const [editor] = useLexicalComposerContext();
|
|
64
|
+
const [lexicalEditor, setLexicalEditor] = useState(null);
|
|
65
|
+
const [pluginRegisteredSignal, setPluginRegisteredSignal] = useState(0);
|
|
66
|
+
const lastLocalSelectionRef = useRef(null);
|
|
67
|
+
const state = useYjsState(lexicalEditor, pluginRegisteredSignal);
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
editor.registerPlugin(YjsPlugin, {
|
|
70
|
+
excludedProperties,
|
|
71
|
+
id,
|
|
72
|
+
initialEditorState,
|
|
73
|
+
providerFactory,
|
|
74
|
+
shouldBootstrap,
|
|
75
|
+
yjsDoc
|
|
76
|
+
});
|
|
77
|
+
setPluginRegisteredSignal((value) => value + 1);
|
|
78
|
+
}, [
|
|
79
|
+
editor,
|
|
80
|
+
excludedProperties,
|
|
81
|
+
id,
|
|
82
|
+
initialEditorState,
|
|
83
|
+
providerFactory,
|
|
84
|
+
shouldBootstrap,
|
|
85
|
+
yjsDoc
|
|
86
|
+
]);
|
|
87
|
+
useLexicalEditor((activeEditor) => {
|
|
88
|
+
setLexicalEditor(activeEditor);
|
|
89
|
+
return () => {
|
|
90
|
+
setLexicalEditor(null);
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!lexicalEditor || !state) return;
|
|
95
|
+
const awareness = state.provider.awareness;
|
|
96
|
+
const setLocalState = awareness.setLocalState.bind(awareness);
|
|
97
|
+
const getLocalStateWithPreservedSelection = (localState) => {
|
|
98
|
+
if (!localState) {
|
|
99
|
+
lastLocalSelectionRef.current = null;
|
|
100
|
+
return localState;
|
|
101
|
+
}
|
|
102
|
+
if (localState.anchorPos && localState.focusPos) {
|
|
103
|
+
lastLocalSelectionRef.current = {
|
|
104
|
+
anchorPos: localState.anchorPos,
|
|
105
|
+
focusPos: localState.focusPos
|
|
106
|
+
};
|
|
107
|
+
return localState;
|
|
108
|
+
}
|
|
109
|
+
if (!persistCursorOnBlur || !localState.focusing || !lastLocalSelectionRef.current) return localState;
|
|
110
|
+
return {
|
|
111
|
+
...localState,
|
|
112
|
+
anchorPos: lastLocalSelectionRef.current.anchorPos,
|
|
113
|
+
focusPos: lastLocalSelectionRef.current.focusPos
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
awareness.setLocalState = (localState) => {
|
|
117
|
+
setLocalState(getLocalStateWithPreservedSelection(localState));
|
|
118
|
+
};
|
|
119
|
+
initLocalState(state.provider, username, cursorColor, persistCursorOnBlur || document.activeElement === lexicalEditor.getRootElement(), awarenessData);
|
|
120
|
+
const updateAwareness = () => {
|
|
121
|
+
const localState = awareness.getLocalState();
|
|
122
|
+
const nextLocalState = getLocalStateWithPreservedSelection(localState);
|
|
123
|
+
if (nextLocalState !== localState) setLocalState(nextLocalState);
|
|
124
|
+
syncCursorPositionsFn(state.binding, state.provider);
|
|
125
|
+
};
|
|
126
|
+
awareness.on("update", updateAwareness);
|
|
127
|
+
const unregisterUpdate = lexicalEditor.registerUpdateListener(updateAwareness);
|
|
128
|
+
let animationFrame = 0;
|
|
129
|
+
const renderCursorPositions = () => {
|
|
130
|
+
updateAwareness();
|
|
131
|
+
animationFrame = window.requestAnimationFrame(renderCursorPositions);
|
|
132
|
+
};
|
|
133
|
+
animationFrame = window.requestAnimationFrame(renderCursorPositions);
|
|
134
|
+
return () => {
|
|
135
|
+
awareness.setLocalState = setLocalState;
|
|
136
|
+
awareness.off("update", updateAwareness);
|
|
137
|
+
unregisterUpdate();
|
|
138
|
+
window.cancelAnimationFrame(animationFrame);
|
|
139
|
+
};
|
|
140
|
+
}, [
|
|
141
|
+
awarenessData,
|
|
142
|
+
cursorColor,
|
|
143
|
+
lexicalEditor,
|
|
144
|
+
persistCursorOnBlur,
|
|
145
|
+
state,
|
|
146
|
+
syncCursorPositionsFn,
|
|
147
|
+
username
|
|
148
|
+
]);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!lexicalEditor || !state) return;
|
|
151
|
+
return editor.requireService(ICodemirrorEditLockService)?.registerProvider(createCodemirrorEditLockProvider(state));
|
|
152
|
+
}, [
|
|
153
|
+
editor,
|
|
154
|
+
lexicalEditor,
|
|
155
|
+
state
|
|
156
|
+
]);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!lexicalEditor || !state) return;
|
|
159
|
+
const setFocus = (focusing) => {
|
|
160
|
+
if (!focusing && persistCursorOnBlur) return false;
|
|
161
|
+
setLocalStateFocus(state.provider, username, cursorColor, focusing, awarenessData);
|
|
162
|
+
return false;
|
|
163
|
+
};
|
|
164
|
+
const unregisterFocus = lexicalEditor.registerCommand(FOCUS_COMMAND, () => setFocus(true), COMMAND_PRIORITY_EDITOR);
|
|
165
|
+
const unregisterBlur = lexicalEditor.registerCommand(BLUR_COMMAND, () => setFocus(false), COMMAND_PRIORITY_EDITOR);
|
|
166
|
+
return () => {
|
|
167
|
+
unregisterFocus();
|
|
168
|
+
unregisterBlur();
|
|
169
|
+
};
|
|
170
|
+
}, [
|
|
171
|
+
awarenessData,
|
|
172
|
+
cursorColor,
|
|
173
|
+
lexicalEditor,
|
|
174
|
+
persistCursorOnBlur,
|
|
175
|
+
state,
|
|
176
|
+
username
|
|
177
|
+
]);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!state) return;
|
|
180
|
+
const clearAwarenessState = () => {
|
|
181
|
+
state.provider.awareness.setLocalState(null);
|
|
182
|
+
};
|
|
183
|
+
window.addEventListener("beforeunload", clearAwarenessState);
|
|
184
|
+
window.addEventListener("pagehide", clearAwarenessState);
|
|
185
|
+
return () => {
|
|
186
|
+
window.removeEventListener("beforeunload", clearAwarenessState);
|
|
187
|
+
window.removeEventListener("pagehide", clearAwarenessState);
|
|
188
|
+
clearAwarenessState();
|
|
189
|
+
};
|
|
190
|
+
}, [state]);
|
|
191
|
+
if (!lexicalEditor || !state) return null;
|
|
192
|
+
return /* @__PURE__ */ jsx(YjsCursors, {
|
|
193
|
+
cursorsContainerRef,
|
|
194
|
+
lexicalEditor,
|
|
195
|
+
state,
|
|
196
|
+
syncCursorPositionsFn
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
//#endregion
|
|
200
|
+
export { ReactYjsPlugin };
|