Skip to content

Instantly share code, notes, and snippets.

@fantactuka
Created November 8, 2022 18:25
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save fantactuka/ee8ef8ac1c51d179c7a9199262444b04 to your computer and use it in GitHub Desktop.
Save fantactuka/ee8ef8ac1c51d179c7a9199262444b04 to your computer and use it in GitHub Desktop.
"use strict";
import type { LexicalEditor, LexicalNode } from "Lexical";
import type { Binding, Provider } from "LexicalYjs";
import {
createBinding,
syncLexicalUpdateToYjs,
syncYjsChangesToLexical,
} from "LexicalYjs";
import { Doc, encodeStateAsUpdate } from "yjs";
export default function headlessConvertYDocStateToLexicalJSON(
nodes: Array<Class<LexicalNode>>,
yDocState: Uint8Array
): SerializedEditorState {
const { binding, editor } = createHeadlessCollaborationEditor(nodes);
applyUpdate(binding.doc, yDocState, { isUpdateRemote: true });
editor.update(() => {}, { discrete: true });
return editor.getEditorState().toJSON();
}
/**
* Creates headless collaboration editor with no-op provider (since it won't
* connect to message distribution infra) and binding. It also sets up
* bi-directional synchronization between yDoc and editor
*/
function createHeadlessCollaborationEditor(nodes: Array<Class<LexicalNode>>): {
editor: LexicalEditor,
provider: Provider,
binding: Binding,
} {
const editor = createEditor({
headless: true,
namespace: "headless",
nodes,
onError: (error) => {
throw error;
},
});
const id = "main";
const doc = new Doc();
const docMap = new Map([[id, doc]]);
const provider = createNoOpProvider();
const binding = createBinding(editor, provider, id, doc, docMap);
registerCollaborationListeners(editor, provider, binding);
return {
binding,
editor,
provider,
};
}
function registerCollaborationListeners(
editor: LexicalEditor,
provider: Provider,
binding: Binding
): void {
editor.registerUpdateListener(
({
dirtyElements,
dirtyLeaves,
editorState,
normalizedNodes,
prevEditorState,
tags,
}) => {
if (tags.has("skip-collab") === false) {
syncLexicalUpdateToYjs(
binding,
provider,
prevEditorState,
editorState,
dirtyElements,
dirtyLeaves,
normalizedNodes,
tags
);
}
}
);
binding.root.getSharedType().observeDeep((events, transaction) => {
if (transaction?.origin !== binding) {
syncYjsChangesToLexical(binding, provider, events);
}
});
}
function createNoOpProvider(): Provider {
const emptyFunction = () => {};
return {
awareness: {
getLocalState: () => null,
getStates: () => new Map(),
off: emptyFunction,
on: emptyFunction,
setLocalState: emptyFunction,
},
connect: emptyFunction,
disconnect: emptyFunction,
off: emptyFunction,
on: emptyFunction,
};
}
@jamespantalones
Copy link

@fantactuka this is great. Thank you. I'm running into an issue where I have yjs buffers stored in a db. I'm able to successfully pull them out and load them into Lexical on the client, but in headless mode, I'm getting an empty array of children in the Lexical JSON. any ideas?

@jvandenaardweg
Copy link

@jamespantalones have you found a solution for this? I'm experiencing the same

@jamespantalones
Copy link

jamespantalones commented May 24, 2023 via email

@jvandenaardweg
Copy link

@jamespantalones thanks for the reply!

I think I was having a different issue, but it was also related to ESM and CommonJS issues. Got it worked out by your hint on ESM/CommonJS. Thanks for that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment