@milkdown/plugin-collab
Version:
Collaborative editing support for [milkdown](https://milkdown.dev/).
178 lines (177 loc) • 5.35 kB
JavaScript
import { schemaCtx, parserCtx, getDoc, prosePluginsCtx, editorViewCtx, EditorViewReady } from "@milkdown/core";
import { createSlice, createTimer } from "@milkdown/ctx";
import { ctxNotBind, missingYjsDoc } from "@milkdown/exception";
import { keydownHandler } from "@milkdown/prose/keymap";
import { Plugin, PluginKey } from "@milkdown/prose/state";
import { ySyncPlugin, yUndoPlugin, redo, undo, yCursorPlugin, yXmlFragmentToProseMirrorRootNode, prosemirrorToYDoc, ySyncPluginKey, yCursorPluginKey, yUndoPluginKey } from "y-prosemirror";
import { encodeStateAsUpdate, applyUpdate } from "yjs";
const CollabKeymapPluginKey = new PluginKey("MILKDOWN_COLLAB_KEYMAP");
const collabPluginKeys = [
CollabKeymapPluginKey,
ySyncPluginKey,
yCursorPluginKey,
yUndoPluginKey
];
class CollabService {
/// @internal
#options = {};
/// @internal
#xmlFragment = null;
/// @internal
#awareness = null;
/// @internal
#ctx = null;
/// @internal
#connected = false;
/// @internal
#valueToNode(value) {
if (!this.#ctx) throw ctxNotBind();
const schema = this.#ctx.get(schemaCtx);
const parser = this.#ctx.get(parserCtx);
const doc = getDoc(value, parser, schema);
return doc;
}
/// @internal
#createPlugins() {
if (!this.#xmlFragment) throw missingYjsDoc();
const { ySyncOpts, yUndoOpts } = this.#options;
const plugins = [
ySyncPlugin(this.#xmlFragment, ySyncOpts),
yUndoPlugin(yUndoOpts),
new Plugin({
key: CollabKeymapPluginKey,
props: {
handleKeyDown: keydownHandler({
"Mod-z": undo,
"Mod-y": redo,
"Mod-Shift-z": redo
})
}
})
];
if (this.#awareness) {
const { yCursorOpts, yCursorStateField } = this.#options;
plugins.push(
yCursorPlugin(
this.#awareness,
yCursorOpts,
yCursorStateField
)
);
}
return plugins;
}
/// @internal
#flushEditor(plugins) {
if (!this.#ctx) throw ctxNotBind();
this.#ctx.set(prosePluginsCtx, plugins);
const view = this.#ctx.get(editorViewCtx);
const newState = view.state.reconfigure({ plugins });
view.updateState(newState);
}
/// Bind the context to the service.
bindCtx(ctx) {
this.#ctx = ctx;
return this;
}
/// Bind the document to the service.
bindDoc(doc) {
this.#xmlFragment = doc.getXmlFragment("prosemirror");
return this;
}
/// Bind the Yjs XmlFragment to the service.
bindXmlFragment(xmlFragment) {
this.#xmlFragment = xmlFragment;
return this;
}
/// Set the options of the service.
setOptions(options) {
this.#options = options;
return this;
}
/// Merge some options to the service.
/// The options will be merged to the existing options.
/// THe options should be partial of the `CollabServiceOptions`.
mergeOptions(options) {
Object.assign(this.#options, options);
return this;
}
/// Set the awareness of the service.
setAwareness(awareness) {
this.#awareness = awareness;
return this;
}
/// Apply the template to the document.
applyTemplate(template, condition) {
if (!this.#ctx) throw ctxNotBind();
if (!this.#xmlFragment) throw missingYjsDoc();
const conditionFn = condition || ((yDocNode2) => yDocNode2.textContent.length === 0);
const node = this.#valueToNode(template);
const schema = this.#ctx.get(schemaCtx);
const yDocNode = yXmlFragmentToProseMirrorRootNode(
this.#xmlFragment,
schema
);
if (node && conditionFn(yDocNode, node)) {
const fragment = this.#xmlFragment;
fragment.delete(0, fragment.length);
const templateDoc = prosemirrorToYDoc(node);
const template2 = encodeStateAsUpdate(templateDoc);
if (fragment.doc) applyUpdate(fragment.doc, template2);
templateDoc.destroy();
}
return this;
}
/// Connect the service.
connect() {
if (!this.#ctx) throw ctxNotBind();
if (this.#connected) return;
const prosePlugins = this.#ctx.get(prosePluginsCtx);
const collabPlugins = this.#createPlugins();
const plugins = prosePlugins.concat(collabPlugins);
this.#flushEditor(plugins);
this.#connected = true;
return this;
}
/// Disconnect the service.
disconnect() {
if (!this.#ctx) throw ctxNotBind();
if (!this.#connected) return this;
const prosePlugins = this.#ctx.get(prosePluginsCtx);
const plugins = prosePlugins.filter(
(plugin) => !plugin.spec.key || !collabPluginKeys.includes(plugin.spec.key)
);
this.#flushEditor(plugins);
this.#connected = false;
return this;
}
}
const collabServiceCtx = createSlice(
new CollabService(),
"collabServiceCtx"
);
const CollabReady = createTimer("CollabReady");
const collab = (ctx) => {
const collabService = new CollabService();
ctx.inject(collabServiceCtx, collabService).record(CollabReady);
return async () => {
await ctx.wait(EditorViewReady);
collabService.bindCtx(ctx);
ctx.done(CollabReady);
return () => {
ctx.remove(collabServiceCtx).clearTimer(CollabReady);
};
};
};
collab.meta = {
package: "@milkdown/plugin-collab",
displayName: "Collab"
};
export {
CollabKeymapPluginKey,
CollabReady,
CollabService,
collab,
collabServiceCtx
};
//# sourceMappingURL=index.js.map