@liveblocks/node-lexical
Version:
A server-side utility that lets you modify lexical documents hosted in Liveblocks.
290 lines (285 loc) • 7.93 kB
JavaScript
// src/index.ts
import { createHeadlessEditor as createHeadlessEditor2 } from "@lexical/headless";
import { $convertToMarkdownString, TRANSFORMERS } from "@lexical/markdown";
import { createBinding as createBinding2 } from "@lexical/yjs";
import { detectDupes } from "@liveblocks/core";
import { $getRoot } from "lexical";
import { applyUpdate, Doc as Doc2, encodeStateAsUpdate, encodeStateVector } from "yjs";
// src/collab.ts
import { createHeadlessEditor } from "@lexical/headless";
import {
createBinding,
syncLexicalUpdateToYjs,
syncYjsChangesToLexical
} from "@lexical/yjs";
import { Doc } from "yjs";
function registerCollaborationListeners(editor, provider, binding) {
const unsubscribeUpdateListener = editor.registerUpdateListener(
({
dirtyElements,
dirtyLeaves,
editorState,
normalizedNodes,
prevEditorState,
tags
}) => {
if (tags.has("skip-collab") === false) {
syncLexicalUpdateToYjs(
binding,
provider,
prevEditorState,
editorState,
dirtyElements,
dirtyLeaves,
normalizedNodes,
tags
);
}
}
);
const observer = (events, transaction) => {
if (transaction.origin !== binding) {
syncYjsChangesToLexical(binding, provider, events, false);
}
};
binding.root.getSharedType().observeDeep(observer);
return () => {
unsubscribeUpdateListener();
binding.root.getSharedType().unobserveDeep(observer);
};
}
function createNoOpProvider() {
const emptyFunction = () => {
};
return {
awareness: {
getLocalState: () => null,
setLocalStateField: emptyFunction,
getStates: () => /* @__PURE__ */ new Map(),
off: emptyFunction,
on: emptyFunction,
setLocalState: emptyFunction
},
connect: emptyFunction,
disconnect: emptyFunction,
off: emptyFunction,
on: emptyFunction
};
}
// src/MentionNodeLite.ts
import { $applyNodeReplacement, DecoratorNode } from "lexical";
var MENTION_CHARACTER = "@";
var MentionNode = class _MentionNode extends DecoratorNode {
__id;
__userId;
constructor(id, userId, key) {
super(key);
this.__id = id;
this.__userId = userId;
}
static getType() {
return "lb-mention";
}
static clone(node) {
return new _MentionNode(node.__id, node.__userId);
}
static importJSON(serializedNode) {
const node = new _MentionNode(
serializedNode.value ?? serializedNode.id,
serializedNode.userId
);
return $applyNodeReplacement(node);
}
exportJSON() {
return {
id: this.getId(),
userId: this.getUserId(),
type: "lb-mention",
version: 1
};
}
getId() {
const self = this.getLatest();
return self.__id;
}
getUserId() {
const self = this.getLatest();
return self.__userId;
}
getTextContent() {
const userId = this.getUserId();
if (userId) {
return MENTION_CHARACTER + userId;
}
return this.getId();
}
decorate() {
return null;
}
};
// src/ThreadNodeLite.ts
import { $applyNodeReplacement as $applyNodeReplacement2, $isRangeSelection, ElementNode } from "lexical";
var ThreadMarkNode = class _ThreadMarkNode extends ElementNode {
/** @internal */
__ids;
// The ids of the threads that this mark is associated with
static getType() {
return "lb-thread-mark";
}
static clone(node) {
return new _ThreadMarkNode(Array.from(node.__ids), node.__key);
}
static importJSON(serializedNode) {
const node = $applyNodeReplacement2(
new _ThreadMarkNode(serializedNode.ids)
);
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);
return node;
}
exportJSON() {
return {
...super.exportJSON(),
ids: this.getIDs(),
type: "lb-thread-mark",
version: 1
};
}
getIDs() {
const self = this.getLatest();
return self instanceof _ThreadMarkNode ? self.__ids : [];
}
constructor(ids, key) {
super(key);
this.__ids = ids || [];
}
canInsertTextBefore() {
return false;
}
canInsertTextAfter() {
return false;
}
canBeEmpty() {
return false;
}
isInline() {
return true;
}
extractWithChild(_, selection, destination) {
if (!$isRangeSelection(selection) || destination === "html") {
return false;
}
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
const isBackward = selection.isBackward();
const selectionLength = isBackward ? anchor.offset - focus.offset : focus.offset - anchor.offset;
return this.isParentOf(anchorNode) && this.isParentOf(focusNode) && this.getTextContent().length === selectionLength;
}
excludeFromCopy(destination) {
return destination !== "clone";
}
};
// src/version.ts
var PKG_NAME = "@liveblocks/node-lexical";
var PKG_VERSION = "3.4.0";
var PKG_FORMAT = "esm";
// src/index.ts
import { $createParagraphNode, $createTextNode, $getRoot as $getRoot2 } from "lexical";
detectDupes(PKG_NAME, PKG_VERSION, PKG_FORMAT);
var LIVEBLOCKS_NODES = [ThreadMarkNode, MentionNode];
async function withLexicalDocument({ roomId, nodes, client }, callback) {
const update = new Uint8Array(
await client.getYjsDocumentAsBinaryUpdate(roomId)
);
const editor = createHeadlessEditor2({
nodes: [...LIVEBLOCKS_NODES, ...nodes ?? []]
});
const id = "root";
const doc = new Doc2();
const docMap = /* @__PURE__ */ new Map([[id, doc]]);
const provider = createNoOpProvider();
const binding = createBinding2(editor, provider, id, doc, docMap);
const unsubscribe = registerCollaborationListeners(editor, provider, binding);
applyUpdate(binding.doc, update);
editor.update(() => {
}, { discrete: true });
const val = await callback({
/**
* Fetches and resyncs the latest document with Liveblocks
*/
refresh: async () => {
const latest = new Uint8Array(
await client.getYjsDocumentAsBinaryUpdate(roomId)
);
applyUpdate(binding.doc, latest);
editor.update(() => {
}, { discrete: true });
},
/**
* Provide a callback to modify documetns with Lexical's standard api. All calls are discrete.
*/
update: async (modifyFn) => {
editor.update(() => {
}, { discrete: true });
const beforeVector = encodeStateVector(binding.doc);
editor.update(
() => {
modifyFn();
},
{ discrete: true }
);
const diffUpdate = encodeStateAsUpdate(binding.doc, beforeVector);
return client.sendYjsBinaryUpdate(roomId, diffUpdate);
},
/**
* Helper function to easily provide the text content from the root, i.e. `$getRoot().getTextContent()`
*/
getTextContent: () => {
let content = "";
editor.getEditorState().read(() => {
content = $getRoot().getTextContent();
});
return content;
},
/**
* Helper function to return editorState in JSON form
*/
toJSON: () => {
return editor.getEditorState().toJSON();
},
/**
* Helper function to return editor state as Markdown
*/
toMarkdown: () => {
let markdown = "";
editor.getEditorState().read(() => {
markdown = $convertToMarkdownString(TRANSFORMERS);
});
return markdown;
},
/**
* Helper function to return the editor's current state
*/
getEditorState: () => {
return editor.getEditorState();
},
/**
* Helper function to return the current headless editor instance
*/
getLexicalEditor: () => {
return editor;
}
});
unsubscribe();
return val;
}
export {
$createParagraphNode,
$createTextNode,
$getRoot2 as $getRoot,
withLexicalDocument
};
//# sourceMappingURL=index.js.map