yjs / y-prosemirror

ProseMirror editor binding for Yjs
https://demos.yjs.dev/prosemirror/prosemirror.html
MIT License
328 stars 115 forks source link

Issue with ProseMirror Integration: Warning When Initializing with y-prosemirror Plugins #149

Open totorofly opened 9 months ago

totorofly commented 9 months ago

Please save me some time and use the following template. In 90% of all issues I can't reproduce the problem because I don't know what exactly you are doing, in which environment, or which y-* version is responsible. Just use the following template even if you think the problem is obvious.

Checklist

Describe the bug I am experiencing a warning issue in a ProseMirror-based editor integrated with y-prosemirror plugins. The warning occurs when initializing the editor with the following y-prosemirror plugins: ySyncPlugin, yCursorPlugin, and yUndoPlugin. The exact warning message is as follows:

index.js:221 TextSelection endpoint not pointing into a node with inline content (doc).

image

To Reproduce Steps to reproduce the behavior:

  1. Initialize a ProseMirror editor with a custom schema (yschema). page.tsx:
    
    "use client";
    import { EditorState } from "prosemirror-state";
    import "prosemirror-view/style/prosemirror.css";
    import React, { useRef, useEffect } from "react";
    import { exampleSetup } from "prosemirror-example-setup";
    import { EditorView } from "prosemirror-view";
    import { toggleMark } from "prosemirror-commands";
    import * as Y from "yjs";
    import { WebsocketProvider } from "y-websocket";
    import {
    ySyncPlugin,
    yCursorPlugin,
    yUndoPlugin,
    undo,
    redo,
    } from "y-prosemirror";
    import { schema as yschema } from "./yschema";
    import { keymap } from "prosemirror-keymap";
    import "./ystyle.css";

const toggleBold = toggleMark(yschema.marks.strong);

const ydoc = new Y.Doc(); const provider = new WebsocketProvider( "ws://localhost:1234", "prosemirror", ydoc ); const type = ydoc.getXmlFragment("prosemirror");

export default function Home() { const editorRef = useRef<HTMLDivElement | null>(null); const editorViewRef = useRef<EditorView | null>(null);

useEffect(() => { editorViewRef.current = new EditorView(editorRef.current, { state: EditorState.create({ schema: yschema, plugins: exampleSetup({ schema: yschema }).concat([ ySyncPlugin(type), yCursorPlugin(provider.awareness), yUndoPlugin(), keymap({ "Mod-z": undo, "Mod-y": redo, "Mod-Shift-z": redo, }), ]), }), });

return () => {
  editorViewRef.current?.destroy();
};

}, []);

const printContentAsJSON = () => { const editorView = editorViewRef.current; if (editorView) { const content = editorView.state.doc.toJSON(); console.log(content); } };

const applyBold = () => { const editorView = editorViewRef.current; if (editorView) { toggleBold(editorView.state, editorView.dispatch, editorView); editorView.focus(); } };

return (

React ProseMirror Demo

); }

yschema.ts:

import { Schema } from "prosemirror-model";

const brDOM = ["br"];

const calcYchangeDomAttrs = (attrs, domAttrs = {}) => { domAttrs = Object.assign({}, domAttrs); if (attrs.ychange !== null) { domAttrs.ychange_user = attrs.ychange.user; domAttrs.ychange_state = attrs.ychange.state; } return domAttrs; };

// :: Object // Specs for the nodes defined in this schema. export const nodes = { // :: NodeSpec The top level document node. doc: { content: "block+", },

// :: NodeSpec A plain paragraph textblock. Represented in the DOM // as a <p> element. paragraph: { attrs: { ychange: { default: null } }, content: "inline*", group: "block", parseDOM: [{ tag: "p" }], toDOM(node) { return ["p", calcYchangeDomAttrs(node.attrs), 0]; }, },

// :: NodeSpec A blockquote (<blockquote>) wrapping one or more blocks. blockquote: { attrs: { ychange: { default: null } }, content: "block+", group: "block", defining: true, parseDOM: [{ tag: "blockquote" }], toDOM(node) { return ["blockquote", calcYchangeDomAttrs(node.attrs), 0]; }, },

// :: NodeSpec A horizontal rule (<hr>). horizontal_rule: { attrs: { ychange: { default: null } }, group: "block", parseDOM: [{ tag: "hr" }], toDOM(node) { return ["hr", calcYchangeDomAttrs(node.attrs)]; }, },

// :: NodeSpec A heading textblock, with a level attribute that // should hold the number 1 to 6. Parsed and serialized as <h1> to // <h6> elements. heading: { attrs: { level: { default: 1 }, ychange: { default: null }, }, content: "inline*", group: "block", defining: true, parseDOM: [ { tag: "h1", attrs: { level: 1 } }, { tag: "h2", attrs: { level: 2 } }, { tag: "h3", attrs: { level: 3 } }, { tag: "h4", attrs: { level: 4 } }, { tag: "h5", attrs: { level: 5 } }, { tag: "h6", attrs: { level: 6 } }, ], toDOM(node) { return ["h" + node.attrs.level, calcYchangeDomAttrs(node.attrs), 0]; }, },

// :: NodeSpec A code listing. Disallows marks or non-text inline // nodes by default. Represented as a <pre> element with a // <code> element inside of it. code_block: { attrs: { ychange: { default: null } }, content: "text*", marks: "", group: "block", code: true, defining: true, parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], toDOM(node) { return ["pre", calcYchangeDomAttrs(node.attrs), ["code", 0]]; }, },

// :: NodeSpec The text node. text: { group: "inline", },

// :: NodeSpec An inline image (<img>) node. Supports src, // alt, and href attributes. The latter two default to the empty // string. image: { inline: true, attrs: { ychange: { default: null }, src: {}, alt: { default: null }, title: { default: null }, }, group: "inline", draggable: true, parseDOM: [ { tag: "img[src]", getAttrs(dom) { return { src: dom.getAttribute("src"), title: dom.getAttribute("title"), alt: dom.getAttribute("alt"), }; }, }, ], toDOM(node) { const domAttrs = { src: node.attrs.src, title: node.attrs.title, alt: node.attrs.alt, }; return ["img", calcYchangeDomAttrs(node.attrs, domAttrs)]; }, },

// :: NodeSpec A hard line break, represented in the DOM as <br>. hard_break: { inline: true, group: "inline", selectable: false, parseDOM: [{ tag: "br" }], toDOM() { return brDOM; }, }, };

const emDOM = ["em", 0]; const strongDOM = ["strong", 0]; const codeDOM = ["code", 0];

// :: Object Specs for the marks in the schema. export const marks = { // :: MarkSpec A link. Has href and title attributes. title // defaults to the empty string. Rendered and parsed as an <a> // element. link: { attrs: { href: {}, title: { default: null }, }, inclusive: false, parseDOM: [ { tag: "a[href]", getAttrs(dom) { return { href: dom.getAttribute("href"), title: dom.getAttribute("title"), }; }, }, ], toDOM(node) { return ["a", node.attrs, 0]; }, },

// :: MarkSpec An emphasis mark. Rendered as an <em> element. // Has parse rules that also match <i> and font-style: italic. em: { parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], toDOM() { return emDOM; }, },

// :: MarkSpec A strong mark. Rendered as <strong>, parse rules // also match <b> and font-weight: bold. strong: { parseDOM: [ { tag: "strong" }, // This works around a Google Docs misbehavior where // pasted content will be inexplicably wrapped in <b> // tags with a font-weight normal. { tag: "b", getAttrs: (node) => node.style.fontWeight !== "normal" && null, }, { style: "font-weight", getAttrs: (value) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null, }, ], toDOM() { return strongDOM; }, },

// :: MarkSpec Code font mark. Represented as a <code> element. code: { parseDOM: [{ tag: "code" }], toDOM() { return codeDOM; }, }, ychange: { attrs: { user: { default: null }, state: { default: null }, }, inclusive: false, parseDOM: [{ tag: "ychange" }], toDOM(node) { return [ "ychange", { ychange_user: node.attrs.user, ychange_state: node.attrs.state }, 0, ]; }, }, };

// :: Schema // This schema rougly corresponds to the document schema used by // CommonMark, minus the list elements, // which are defined in the prosemirror-schema-list // module. // // To reuse elements from this schema, extend or read from its // spec.nodes and spec.marks properties. export const schema = new Schema({ nodes, marks });


3. Include ySyncPlugin, yCursorPlugin, and yUndoPlugin from y-prosemirror.
4. Observe the warning in the console when the editor is initialized.
5. The warning appears whenever any of the mentioned y-prosemirror plugins are used. If I remove all three plugins, the warning disappears. However, removing just one or two of the plugins does not resolve the issue.

**Expected behavior**
The editor should initialize without any warnings, and the y-prosemirror plugins should integrate seamlessly with ProseMirror.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment Information**
 - Browser / Node.js [e.g. Chrome, Firefox, Node.js]
 - Yjs version and the versions of the y-* modules you are using [e.g. yjs v13.0.1, y-webrtc v1.2.1]. Use `npm ls yjs` to find out the exact version you are using.
<img width="722" alt="image" src="https://github.com/yjs/y-prosemirror/assets/41875799/675b756c-98ab-4634-9f81-0ca3f91aa4ef">

**Additional context**
Add any other context about the problem here.