Getting a feel for NodeView

I’ve been playing with NodeView today to try to understand how it would work in our editor (we have a few things that would be considered NodeViews I guess: signature, payment etc).

The example I was playing with is the figure block, a picture with a caption that can be edited with two states:

  • empty: shows a dropzone and can upload a file on click/drop
  • active: has an active image, an editable caption without markup and some actions on the picture

What I got so far is the following:

The schema part:

figure: {
    content: "inline*",
    marks: "",
    group: "block",
    defining: true,
    draggable: true,
    selectable: true,
    attrs: {
      src: {default: null},
      caption: {default: ""},
    },
    parseDOM: [
      {
        tag: "figure",
        contentElement: "figcaption",
        getAttrs(dom: any) {
          let img = dom.querySelector("img");
          return {src: img && img.parentNode === dom ? img.src : null};
        }
      },
      {
        tag: "img[src]",
        getAttrs(dom: any) {
          return {src: dom.src, caption: dom.alt};
        }
      }
    ],
    toDOM(node: any) {
      return ["figure", ["img", {src: node.attrs.src, alt: node.attrs.caption}], ["figcaption", 0]]
    }
  }

A basic version of the NodeView that allows “uploading” and shows caption:

import {Node} from "prosemirror-model";
import {EditorView, NodeView} from "prosemirror-view";


export default class FigureView implements NodeView {
  dom: HTMLElement;
  input: HTMLInputElement | null = null;
  node: Node;
  view: EditorView;
  getPos: () => number;
  src: string | null;
  caption: string;

  constructor(node: Node, view: EditorView, getPos: () => number) {
    this.src = node.attrs.src;
    this.caption = node.attrs.caption;
    this.node = node;
    this.view = view;
    this.getPos = getPos;

    this.dom = document.createElement("figure");
    this.dom.draggable = true;
    this.dom.classList.add("editor-figure");

    if (!!this.src) {
      this.createViewWithImage();
    } else {
      this.createEmptyView();
    }
  }

  private createViewWithImage() {
    this.dom.innerHTML = "";
    this.dom.classList.remove("editor-figure--empty");

    if (!this.src) {
      return;
    }

    const img = document.createElement("img");
    img.src = this.src;
    this.dom.appendChild(img);

    const caption = document.createElement("figcaption");
    caption.innerText = this.caption;
    caption.contentEditable = "true";
    this.dom.appendChild(caption);
  }

  private createEmptyView() {
    this.dom.innerHTML = "";

    this.dom.classList.add("editor-figure--empty");
    this.input = document.createElement("input");
    this.input.type = "file";

    let image = null;

    this.input.onclick = () => {
      image = null;
    };

    this.input.onchange = (e: any) => {
      if (this.input && this.input.files && this.input.files[0]) {
        const reader = new FileReader();

        reader.onload = (e: any) => {
          this.input = null;
          this.src = e.target.result;
          this.updateAttrs();
          this.createViewWithImage();
          this.uploadPic(this.src!);
        };

        reader.readAsDataURL(this.input.files[0]);
      }
    };

    this.dom.addEventListener("click", (e: Event) => {
      e.preventDefault();
      if (this.input) {
        this.input.click();
      }
    });
  }

  private updateAttrs() {
    this.view.dispatch(
      this.view.state.tr.setNodeMarkup(
        this.getPos(),
        undefined,
        {src: this.src, caption: this.caption}
        )
    );
  }

  private uploadPic(file: string) {
    // upload the picture using whatever lib and update the src
    // to be the url instead of the base64
    console.log("Uploading pic!");
    // this.src = my_href
    // document.querySelector("img").src = this.src;
  }

  stopEvent() {
    return true;
  }
}

This kinda works but there are some issues:

1/ Is there a way to make the figcaption automatically part of the prosemirror editor somehow or is the recommended way to embed another instance of PM like in http://prosemirror.net/examples/footnote/ to avoid selection/undo/redo working? 2/ I set the figure to be draggable in the schema and in the dom but I can’t move the node around in the editor. Did I miss something?

Yes, contentDOM is what you’d use for that.

That sounds like this thread. I haven’t been able to reproduce it, but if you can distill it down to simple code I can take a look.

I’ve added this.contentDom = caption; after creating the figcaption node but nothing happens, how would I debug a NodeView?

The code in the initial post should be a reproduction, do you have a repo with an editor setup (similar to example-setup but with a html page etc) so we can clone and just the reproduction code? I’ll publish a repo when I get a bit more time.

I ended up pushing the whole project at https://github.com/Keats/editor2 if you want to have a look for now. Run yarn and yarn dev to get the editor up and running

That’s still 1500 lines of code containing various plumbing that isn’t necessary for the issue at hand, and which fills up my dev console with error messages about sockets, which isn’t great, but I’ll try to take a look.

Also, if I copy in the current prosemirror-view code and change the stopEvent handler from return true to return !/drag/.test(e.type), dragging seems to work.

Sorry about that, I finally got some time to make a smaller version: GitHub - Keats/prosemirror-example-setup: An example of how to set up a ProseMirror editor

npm install && npm build and then run a server in the directory, like python3 -m http.server

It actually allows dragging (I’ve added this.dom.draggable = true otherwise it doesn’t seem like PM will drag the whole nodeview) but dropping doesn’t happen. I’m wondering if it’s because it’s somehow trying to drop into a paragraph.

Also contentDOM works as expected but is there a way to set a node value when there is no node yet? For example in the repo above, there is the following code Simple nodeview version · Keats/prosemirror-example-setup@b3e5c60 · GitHub to try to parse the caption from the alt attribute but the getContent needs to return a Fragment which wouldn’t exist at this point.

Are you, by any chance, not using the current (not on NPM yet) prosemirror-view code when testing? Because copying that in fixes the issue for me.

That code seems very confused (you can’t create a ProseMirror fragment from a DOM node), so I’m not really sure what you’re trying to do there.

Damn sorry I missed that in your message above. If i depend on master for prosemirror-view, it does work as expected!

EDIT: actually after playing a bit more, it duplicates the pictures on drop sometimes. I’ll try to see if I can reproduce it consistently. I still haven’t figured out a pattern but it does happen frequently. Here’s a screenshot of the dev tools showing the transaction that resulted in 2 figures if that can be helpful:

It definitely is confused :stuck_out_tongue: What I’m trying to do is to have the content of the figure node in the schema be the caption so I can use contentDOM. without having to create a new EditorView like in ProseMirror footnote example. The issue is that I’m trying to use getContent to parse the alt attribute of an image in parseDOM as its content but it expects a Fragment and I can’t see how I could return one. Should I just use the node attrs instead and skip contentDOM in that case?

Something like Fragment.from(schema.text(domNode.alt)) would do it, I think.

1 Like

So I finally had time to look a bit into the duplication issue and I found the following.

In some cases, the selection is empty for some reason and https://github.com/ProseMirror/prosemirror-view/blob/master/src/input.js#L507 will thus not do anything. It looks like the selection can be changed/removed while dragging things move around which is probably the cause of the issue: it gets duplicated when tr.selection is a TextSelection instead of a NodeSelection. Should changing selection be disabled while dragging something and force it to be the node being dragged?

If you run the code at https://github.com/Keats/prosemirror-example-setup (yarn && yarn build and run python3 -m http.server in the folder with the HTML file).

Note that the drag&drop only works in Chrome, looks like Firefox requires a ondragstart event to be set like in https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations Should those browsers inconsistencies be documented somewhere to make it easy to look them up?

Do you have a specific cause for this selection change, or is it more of a general observation? It is certainly possible to change the selection during dragging, and the result is indeed problematic, but I haven’t run into this so far because, for the user, it’s pretty hard and non-obvious to cause this situation.

In principle, that would be the right thing, but it’s a little hard to do—the dragging state lives purely in the view, which has no control (and should have no control) over what the state does.

I just tried again the repository in my post above after upgrading deps and here’s what happens in Chrome: Imgur: The magic of the Internet

It might be that the NodeView is wrong (prosemirror-example-setup/src/nodeview.js at master · Keats/prosemirror-example-setup · GitHub) but I’m not using ProseMirror right now so I didn’t bother looking into it.