mirror of
https://github.com/docmost/docmost.git
synced 2026-05-20 08:34:04 +08:00
resolve comment, delete comment
This commit is contained in:
@@ -209,3 +209,82 @@ function applyMarkToYFragment(
|
|||||||
if (!processItem(fragment.get(i))) break;
|
if (!processItem(fragment.get(i))) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a mark from all text in the fragment that has the specified attribute value.
|
||||||
|
* Useful for deleting comments by commentId.
|
||||||
|
*/
|
||||||
|
export function removeYjsMarkByAttribute(
|
||||||
|
fragment: Y.XmlFragment,
|
||||||
|
markName: string,
|
||||||
|
attributeName: string,
|
||||||
|
attributeValue: string,
|
||||||
|
) {
|
||||||
|
const processItem = (item: any) => {
|
||||||
|
if (item instanceof Y.XmlText) {
|
||||||
|
// Get all formatting deltas to find ranges with this mark
|
||||||
|
const deltas = item.toDelta();
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const delta of deltas) {
|
||||||
|
const length = delta.insert?.length ?? 0;
|
||||||
|
const attributes = delta.attributes ?? {};
|
||||||
|
const markAttr = attributes[markName];
|
||||||
|
|
||||||
|
if (markAttr && markAttr[attributeName] === attributeValue) {
|
||||||
|
// Remove the mark by setting it to null
|
||||||
|
item.format(offset, length, { [markName]: null });
|
||||||
|
}
|
||||||
|
offset += length;
|
||||||
|
}
|
||||||
|
} else if (item instanceof Y.XmlElement) {
|
||||||
|
for (let i = 0; i < item.length; i++) {
|
||||||
|
processItem(item.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < fragment.length; i++) {
|
||||||
|
processItem(fragment.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a mark's attributes for all text that has the specified attribute value.
|
||||||
|
* Useful for resolving/unresolving comments by commentId.
|
||||||
|
*/
|
||||||
|
export function updateYjsMarkAttribute(
|
||||||
|
fragment: Y.XmlFragment,
|
||||||
|
markName: string,
|
||||||
|
findByAttribute: { name: string; value: string },
|
||||||
|
newAttributes: Record<string, any>,
|
||||||
|
) {
|
||||||
|
const processItem = (item: any) => {
|
||||||
|
if (item instanceof Y.XmlText) {
|
||||||
|
const deltas = item.toDelta();
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const delta of deltas) {
|
||||||
|
const length = delta.insert?.length ?? 0;
|
||||||
|
const attributes = delta.attributes ?? {};
|
||||||
|
const markAttr = attributes[markName];
|
||||||
|
|
||||||
|
if (markAttr && markAttr[findByAttribute.name] === findByAttribute.value) {
|
||||||
|
// Update the mark with new attributes (merge with existing)
|
||||||
|
item.format(offset, length, {
|
||||||
|
[markName]: { ...markAttr, ...newAttributes },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
offset += length;
|
||||||
|
}
|
||||||
|
} else if (item instanceof Y.XmlElement) {
|
||||||
|
for (let i = 0; i < item.length; i++) {
|
||||||
|
processItem(item.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < fragment.length; i++) {
|
||||||
|
processItem(fragment.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,349 +0,0 @@
|
|||||||
import { TiptapTransformer } from "@hocuspocus/transformer";
|
|
||||||
import test from "ava";
|
|
||||||
import * as Y from "yjs";
|
|
||||||
import { newHocuspocus, newHocuspocusProvider, sleep } from "../utils/index.ts";
|
|
||||||
|
|
||||||
test("direct connection prevents document from being removed from memory", async (t) => {
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
const server = await newHocuspocus();
|
|
||||||
|
|
||||||
await server.openDirectConnection("hocuspocus-test");
|
|
||||||
|
|
||||||
const provider = newHocuspocusProvider(server, {
|
|
||||||
onSynced() {
|
|
||||||
provider.configuration.websocketProvider.destroy();
|
|
||||||
provider.destroy();
|
|
||||||
|
|
||||||
sleep(server.configuration.debounce + 50).then(() => {
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
resolve("done");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test("direct connection works even if provider is connected", async (t) => {
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
const server = await newHocuspocus();
|
|
||||||
|
|
||||||
const provider = newHocuspocusProvider(server, {
|
|
||||||
onSynced() {
|
|
||||||
provider.document.getMap("config").set("a", "valueFromProvider");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
const directConnection =
|
|
||||||
await server.openDirectConnection("hocuspocus-test");
|
|
||||||
await directConnection.transact((doc) => {
|
|
||||||
t.is("valueFromProvider", String(doc.getMap("config").get("a")));
|
|
||||||
doc.getMap("config").set("b", "valueFromServerDirectConnection");
|
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(100);
|
|
||||||
t.is(
|
|
||||||
"valueFromServerDirectConnection",
|
|
||||||
String(provider.document.getMap("config").get("b")),
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve(1);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("direct connection can apply yjsUpdate", async (t) => {
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
const server = await newHocuspocus();
|
|
||||||
|
|
||||||
const provider = newHocuspocusProvider(server);
|
|
||||||
|
|
||||||
t.is("", provider.document.getXmlFragment("default").toJSON());
|
|
||||||
|
|
||||||
const directConnection =
|
|
||||||
await server.openDirectConnection("hocuspocus-test");
|
|
||||||
await directConnection.transact((doc) => {
|
|
||||||
Y.applyUpdate(
|
|
||||||
doc,
|
|
||||||
Y.encodeStateAsUpdate(
|
|
||||||
TiptapTransformer.toYdoc({
|
|
||||||
type: "doc",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: "Example Paragraph",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(100);
|
|
||||||
|
|
||||||
t.is(
|
|
||||||
"<paragraph>Example Paragraph</paragraph>",
|
|
||||||
provider.document.getXmlFragment("default").toJSON(),
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve(1);
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("direct connection can transact", async (t) => {
|
|
||||||
const server = await newHocuspocus();
|
|
||||||
|
|
||||||
const direct = await server.openDirectConnection("hocuspocus-test");
|
|
||||||
|
|
||||||
await direct.transact((document) => {
|
|
||||||
document.getArray("test").insert(0, ["value"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
t.is(direct.document?.getArray("test").toJSON()[0], "value");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("direct connection cannot transact once closed", async (t) => {
|
|
||||||
const server = await newHocuspocus();
|
|
||||||
|
|
||||||
const direct = await server.openDirectConnection("hocuspocus-test");
|
|
||||||
await direct.disconnect();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await direct.transact((document) => {
|
|
||||||
document.getArray("test").insert(0, ["value"]);
|
|
||||||
});
|
|
||||||
t.fail(
|
|
||||||
"DirectConnection should throw an error when transacting on closed connection",
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error && err.message === "direct connection closed") {
|
|
||||||
t.pass();
|
|
||||||
} else {
|
|
||||||
t.fail("unknown error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("if a direct connection closes, the document should be unloaded if there is no other connection left", async (t) => {
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
const server = await newHocuspocus();
|
|
||||||
|
|
||||||
const direct = await server.openDirectConnection("hocuspocus-test1");
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 1);
|
|
||||||
|
|
||||||
await direct.transact((document) => {
|
|
||||||
document.getArray("test").insert(0, ["value"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
await direct.disconnect();
|
|
||||||
|
|
||||||
t.is(server.getConnectionsCount(), 0);
|
|
||||||
t.is(server.getDocumentsCount(), 0);
|
|
||||||
resolve("done");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("direct connection transact awaits until onStoreDocument has finished", async (t) => {
|
|
||||||
let onStoreDocumentFinished = false;
|
|
||||||
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
const server = await newHocuspocus({
|
|
||||||
onStoreDocument: async () => {
|
|
||||||
onStoreDocumentFinished = false;
|
|
||||||
await sleep(200);
|
|
||||||
onStoreDocumentFinished = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const direct = await server.openDirectConnection("hocuspocus-test2");
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 1);
|
|
||||||
|
|
||||||
t.is(onStoreDocumentFinished, false);
|
|
||||||
await direct.transact((document) => {
|
|
||||||
document.getArray("test").insert(0, ["value"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
await direct.disconnect();
|
|
||||||
t.is(onStoreDocumentFinished, true);
|
|
||||||
|
|
||||||
t.is(server.getConnectionsCount(), 0);
|
|
||||||
t.is(server.getDocumentsCount(), 0);
|
|
||||||
t.is(onStoreDocumentFinished, true);
|
|
||||||
resolve("done");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("direct connection transact awaits until onStoreDocument has finished, even if unloadImmediately=false", async (t) => {
|
|
||||||
let onStoreDocumentFinished = false;
|
|
||||||
let directConnDisconnecting = false;
|
|
||||||
let storedAfterDisconnect = false;
|
|
||||||
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
const server = await newHocuspocus({
|
|
||||||
unloadImmediately: false,
|
|
||||||
onStoreDocument: async () => {
|
|
||||||
onStoreDocumentFinished = false;
|
|
||||||
await sleep(200);
|
|
||||||
onStoreDocumentFinished = true;
|
|
||||||
|
|
||||||
if (directConnDisconnecting) {
|
|
||||||
storedAfterDisconnect = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
afterUnloadDocument: async (data) => {
|
|
||||||
if (!storedAfterDisconnect) {
|
|
||||||
t.fail("this shouldnt be called");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const direct = await server.openDirectConnection("hocuspocus-test");
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 1);
|
|
||||||
|
|
||||||
t.is(onStoreDocumentFinished, false);
|
|
||||||
await direct.transact((document) => {
|
|
||||||
document.getArray("test").insert(0, ["value"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const provider = newHocuspocusProvider(server);
|
|
||||||
provider.document.getMap("aaa").set("bb", "b");
|
|
||||||
provider.disconnect();
|
|
||||||
provider.configuration.websocketProvider.disconnect();
|
|
||||||
|
|
||||||
await sleep(100);
|
|
||||||
|
|
||||||
directConnDisconnecting = true;
|
|
||||||
await direct.disconnect();
|
|
||||||
t.is(onStoreDocumentFinished, true);
|
|
||||||
|
|
||||||
t.is(server.getConnectionsCount(), 0);
|
|
||||||
|
|
||||||
t.is(storedAfterDisconnect, true);
|
|
||||||
|
|
||||||
resolve("done");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not unload document if an earlierly started onStoreDocument is still running", async (t) => {
|
|
||||||
let onStoreDocumentStarted = 0;
|
|
||||||
let onStoreDocumentFinished = 0;
|
|
||||||
|
|
||||||
const server = await newHocuspocus({
|
|
||||||
unloadImmediately: false,
|
|
||||||
debounce: 100,
|
|
||||||
onStoreDocument: async () => {
|
|
||||||
onStoreDocumentStarted++;
|
|
||||||
if (onStoreDocumentStarted === 1) {
|
|
||||||
// Simulate a long running onStoreDocument for the first debounced save
|
|
||||||
await sleep(500);
|
|
||||||
}
|
|
||||||
onStoreDocumentFinished++;
|
|
||||||
},
|
|
||||||
afterUnloadDocument: async (data) => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Trigger a change, which will start a debounced onStoreDocument after 100ms
|
|
||||||
const provider = newHocuspocusProvider(server);
|
|
||||||
provider.document.getMap("aaa").set("bb", "b");
|
|
||||||
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
provider.on("synced", resolve);
|
|
||||||
|
|
||||||
if (!provider.unsyncedChanges) resolve("");
|
|
||||||
});
|
|
||||||
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 1);
|
|
||||||
|
|
||||||
// Wait for the debounced onStoreDocument to start
|
|
||||||
await sleep(110);
|
|
||||||
t.is(onStoreDocumentStarted, 1);
|
|
||||||
t.is(onStoreDocumentFinished, 0);
|
|
||||||
|
|
||||||
// Open direct connection to prevent document from being unloaded
|
|
||||||
const direct = await server.openDirectConnection("hocuspocus-test");
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 2);
|
|
||||||
|
|
||||||
// Close the websocket client
|
|
||||||
provider.disconnect();
|
|
||||||
provider.configuration.websocketProvider.disconnect();
|
|
||||||
await sleep(50);
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 1);
|
|
||||||
t.is(onStoreDocumentStarted, 1);
|
|
||||||
t.is(onStoreDocumentFinished, 0);
|
|
||||||
|
|
||||||
direct.disconnect();
|
|
||||||
await sleep(50);
|
|
||||||
// Another save must not start before the first one has finished
|
|
||||||
t.is(onStoreDocumentStarted, 1);
|
|
||||||
t.is(onStoreDocumentFinished, 0);
|
|
||||||
// Document must not be unloaded yet, because the first onStoreDocument is still running
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 0);
|
|
||||||
|
|
||||||
// Wait enough time to be sure the onStoreDocument has finished and ensure that the document was eventually unloaded
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
// The second onStoreDocument triggered by direct.disconnect must have started and finished now
|
|
||||||
t.is(onStoreDocumentStarted, 2);
|
|
||||||
t.is(onStoreDocumentFinished, 2);
|
|
||||||
// The document must have been unloaded now as well
|
|
||||||
t.is(server.getDocumentsCount(), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("creating a websocket connection after transact but before debounce interval doesnt create different docs", async (t) => {
|
|
||||||
let onStoreDocumentFinished = false;
|
|
||||||
let disconnected = false;
|
|
||||||
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
const server = await newHocuspocus({
|
|
||||||
onStoreDocument: async () => {
|
|
||||||
onStoreDocumentFinished = false;
|
|
||||||
await sleep(200);
|
|
||||||
onStoreDocumentFinished = true;
|
|
||||||
},
|
|
||||||
async afterUnloadDocument(data) {
|
|
||||||
console.log("called");
|
|
||||||
if (disconnected) {
|
|
||||||
t.fail("must not be called");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const direct = await server.openDirectConnection("hocuspocus-test");
|
|
||||||
t.is(server.getDocumentsCount(), 1);
|
|
||||||
t.is(server.getConnectionsCount(), 1);
|
|
||||||
|
|
||||||
t.is(onStoreDocumentFinished, false);
|
|
||||||
await direct.transact((document) => {
|
|
||||||
document.transact(() => {
|
|
||||||
document.getArray("test").insert(0, ["value"]);
|
|
||||||
}, "testOrigin");
|
|
||||||
});
|
|
||||||
|
|
||||||
await direct.disconnect();
|
|
||||||
t.is(onStoreDocumentFinished, true);
|
|
||||||
disconnected = true;
|
|
||||||
|
|
||||||
t.is(server.getConnectionsCount(), 0);
|
|
||||||
t.is(server.getDocumentsCount(), 0);
|
|
||||||
t.is(onStoreDocumentFinished, true);
|
|
||||||
|
|
||||||
const provider = newHocuspocusProvider(server);
|
|
||||||
|
|
||||||
await sleep(server.configuration.debounce * 2);
|
|
||||||
|
|
||||||
resolve("done");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+1
-1
Submodule apps/server/src/ee updated: fce3e9e945...1d1ab6cf81
@@ -1,181 +0,0 @@
|
|||||||
async createThread(options: {
|
|
||||||
initialComment: { body: CommentBody; metadata?: any };
|
|
||||||
metadata?: any;
|
|
||||||
}) {
|
|
||||||
const thread = await threadStore.createThread(options);
|
|
||||||
if (threadStore.addThreadToDocument) {
|
|
||||||
const view = editor.prosemirrorView!;
|
|
||||||
const pmSelection = view.state.selection;
|
|
||||||
const ystate = ySyncPluginKey.getState(view.state);
|
|
||||||
const selection = {
|
|
||||||
prosemirror: {
|
|
||||||
head: pmSelection.head,
|
|
||||||
anchor: pmSelection.anchor,
|
|
||||||
},
|
|
||||||
yjs: ystate
|
|
||||||
? getRelativeSelection(ystate.binding, view.state)
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
await threadStore.addThreadToDocument({
|
|
||||||
threadId: thread.id,
|
|
||||||
selection,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
(editor as any)._tiptapEditor.commands.setMark(markType, {
|
|
||||||
orphan: false,
|
|
||||||
threadId: thread.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userStore,
|
|
||||||
commentEditorSchema,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
public addThreadToDocument = async (options: {
|
|
||||||
threadId: string;
|
|
||||||
selection: {
|
|
||||||
prosemirror: {
|
|
||||||
head: number;
|
|
||||||
anchor: number;
|
|
||||||
};
|
|
||||||
yjs: {
|
|
||||||
head: any;
|
|
||||||
anchor: any;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}) => {
|
|
||||||
const { threadId, ...rest } = options;
|
|
||||||
return this.doRequest(`/${threadId}/addToDocument`, "POST", rest);
|
|
||||||
};
|
|
||||||
-----
|
|
||||||
// addToDocument
|
|
||||||
router.post("/:threadId/addToDocument", async (c) => {
|
|
||||||
const json = await c.req.json();
|
|
||||||
// TODO: you'd probably validate the request json here
|
|
||||||
|
|
||||||
const doc = c.get("document");
|
|
||||||
const fragment = doc.getXmlFragment("doc");
|
|
||||||
|
|
||||||
setMark(doc, fragment, json.selection.yjs, "comment", {
|
|
||||||
orphan: false,
|
|
||||||
threadId: c.req.param("threadId"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ message: "Thread added to document" });
|
|
||||||
});
|
|
||||||
----
|
|
||||||
import { ServerBlockNoteEditor } from "@blocknote/server-util";
|
|
||||||
import { Document } from "@hocuspocus/server";
|
|
||||||
import { EditorState, TextSelection } from "prosemirror-state";
|
|
||||||
import {
|
|
||||||
initProseMirrorDoc,
|
|
||||||
relativePositionToAbsolutePosition,
|
|
||||||
updateYFragment,
|
|
||||||
} from "y-prosemirror";
|
|
||||||
import * as Y from "yjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a mark in the yjs document based on a yjs selection
|
|
||||||
*/
|
|
||||||
export function setMark(
|
|
||||||
doc: Document,
|
|
||||||
fragment: Y.XmlFragment,
|
|
||||||
yjsSelection: {
|
|
||||||
anchor: any;
|
|
||||||
head: any;
|
|
||||||
},
|
|
||||||
markName: string,
|
|
||||||
markAttributes: any
|
|
||||||
) {
|
|
||||||
// needed to get the pmSchema
|
|
||||||
// if you use a BlockNote custom schema, make sure to pass it to the create options
|
|
||||||
const editor = ServerBlockNoteEditor.create();
|
|
||||||
|
|
||||||
// get the prosemirror document
|
|
||||||
const { doc: pNode, mapping } = initProseMirrorDoc(
|
|
||||||
fragment,
|
|
||||||
editor.editor.pmSchema as any
|
|
||||||
);
|
|
||||||
|
|
||||||
// get the prosemirror positions based on the yjs positions
|
|
||||||
// we need to get this from yjs because other users might have made changes in between
|
|
||||||
const anchor = relativePositionToAbsolutePosition(
|
|
||||||
doc,
|
|
||||||
fragment,
|
|
||||||
yjsSelection.anchor,
|
|
||||||
mapping
|
|
||||||
);
|
|
||||||
const head = relativePositionToAbsolutePosition(
|
|
||||||
doc,
|
|
||||||
fragment,
|
|
||||||
yjsSelection.head,
|
|
||||||
mapping
|
|
||||||
);
|
|
||||||
|
|
||||||
// now, let's create the mark in the prosemirror document
|
|
||||||
const state = EditorState.create({
|
|
||||||
doc: pNode,
|
|
||||||
schema: editor.editor.pmSchema as any,
|
|
||||||
selection: TextSelection.create(pNode, anchor!, head!),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tr = setMarkInProsemirror(
|
|
||||||
editor.editor.pmSchema.marks[markName],
|
|
||||||
markAttributes,
|
|
||||||
state
|
|
||||||
);
|
|
||||||
|
|
||||||
// finally, update the yjs document with the new prosemirror document
|
|
||||||
updateYFragment(doc, fragment, tr.doc, mapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on https://github.com/ueberdosis/tiptap/blob/f3258d9ee5fb7979102fe63434f6ea4120507311/packages/core/src/commands/setMark.ts#L66
|
|
||||||
export const setMarkInProsemirror = (
|
|
||||||
type: any,
|
|
||||||
attributes = {},
|
|
||||||
state: EditorState
|
|
||||||
) => {
|
|
||||||
let tr = state.tr;
|
|
||||||
const { selection } = state;
|
|
||||||
const { ranges } = selection;
|
|
||||||
|
|
||||||
ranges.forEach((range) => {
|
|
||||||
const from = range.$from.pos;
|
|
||||||
const to = range.$to.pos;
|
|
||||||
|
|
||||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
||||||
const trimmedFrom = Math.max(pos, from);
|
|
||||||
const trimmedTo = Math.min(pos + node.nodeSize, to);
|
|
||||||
const someHasMark = node.marks.find((mark) => mark.type === type);
|
|
||||||
|
|
||||||
// if there is already a mark of this type
|
|
||||||
// we know that we have to merge its attributes
|
|
||||||
// otherwise we add a fresh new mark
|
|
||||||
if (someHasMark) {
|
|
||||||
node.marks.forEach((mark) => {
|
|
||||||
if (type === mark.type) {
|
|
||||||
tr = tr.addMark(
|
|
||||||
trimmedFrom,
|
|
||||||
trimmedTo,
|
|
||||||
type.create({
|
|
||||||
...mark.attrs,
|
|
||||||
...attributes,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tr = tr.addMark(trimmedFrom, trimmedTo, type.create(attributes));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return tr;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user