Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jamespantalones/7daecacf2e1365441b01eaead8526f24 to your computer and use it in GitHub Desktop.
Save jamespantalones/7daecacf2e1365441b01eaead8526f24 to your computer and use it in GitHub Desktop.

Revisions

  1. @fantactuka fantactuka created this gist Nov 8, 2022.
    106 changes: 106 additions & 0 deletions createHeadlessCollaborativeEditor.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,106 @@
    "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,
    };
    }