Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
2 tasks done
totorofly opened this issue Nov 11, 2023 · 0 comments
Open
2 tasks done
Assignees
Labels
bug Something isn't working

Comments

@totorofly
Copy link

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

  • Are you reporting a bug? Use github issues for bug reports and feature requests. For general questions, please use https://discuss.yjs.dev/
  • Try to report your issue in the correct repository. Yjs consists of many modules. When in doubt, report it to https://github.com/yjs/yjs/issues/

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 (
    <main className="flex min-h-screen flex-col items-center justify-start p-24 border border-black gap-4">
      <h1>React ProseMirror Demo</h1>
      <button onClick={applyBold}>Bold</button>
      <div ref={editorRef} className="w-4/5" />
      <button onClick={printContentAsJSON}>Print JSON</button>
    </main>
  );
}

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](#model.NodeSpec) 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](#model.MarkSpec) 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](http://commonmark.org/), minus the list elements,
// which are defined in the [`prosemirror-schema-list`](#schema-list)
// module.
//
// To reuse elements from this schema, extend or read from its
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
export const schema = new Schema({ nodes, marks });

  1. Include ySyncPlugin, yCursorPlugin, and yUndoPlugin from y-prosemirror.
  2. Observe the warning in the console when the editor is initialized.
  3. 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.
image

Additional context
Add any other context about the problem here.

@totorofly totorofly added the bug Something isn't working label Nov 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants