Best practice for creating nodes


#1

Hello,

I am using the image placeholder plugin, however, instead of replacing the placeholder with an image, I want to create an image wrapped in nodes as follows:

<section>
   <imageblock>
      <img>
      <p.caption>

What is the best way to create nodes like this all at once? What is the best way to resolve position when creating?

This is the original code:

    view.dispatch(view.state.tr
                  .replaceWith(pos, pos, mySchema.nodes.image.create({src: url}))
                  .setMeta(placeholderPlugin, {remove: {id}}))

While below worked for testing, I know this is not the way to go:

    view.dispatch(view.state.tr
                  .delete(pos, pos) // Delete placeholder
                  .setMeta(placeholderPlugin, {remove: {id}}))
    // Create section
    view.dispatch(view.state.tr.insert(pos, mySchema.nodes.section.create()))
    // Add image block inside section in node before
    view.dispatch(view.state.tr.insert(view.state.selection.$cursor.pos-3, mySchema.nodes.image_block.create()))
   // Add image inside image block
    view.dispatch(view.state.tr.insert(view.state.selection.$cursor.pos-4, mySchema.nodes.image.create({src: url})))

Thanks! Bahadir


#2

Definitely don’t dispatch a separate transaction for each change—put them all in a single transaction. And in this case, it seems you can make do with a single insert call. The delete call doesn’t do anything (it deletes an empty region), and you can build up a piece of document structure before you insert it, something like

let section = mySchema.nodes.section.create(null,
  mySchema.nodes.image_block.create(null,
    mySchema.nodes.image.create({src: url})))
view.dispatch(view.state.tr.insert(pos, section).setMeta(placeholderPlugin, {remove: {id}}))

#3

Thank you, this worked great. I have one more problem with deleting nodes.

My current document structure:

<section>
  <p>text

<image_section>
  <image_block>
    <img>
  <caption>

<section>
  <p>

When using backspace in last <p> node, it used to join the <p> backwards into image_section as described here. so I constrained image_section content to be:

  image_section: {
    content: "(image_block){1}(caption){1}",
    group: "container",
    defining: true, /* Don't know what this is. */
    parseDOM: [{tag: "div.section_content.editor-image"}],
    toDOM(node) { return ["div", {class: "section_content editor-image"}, 0] }
  },

I also modified image node to be not inline and as a leaf node, as I did not want it to be treated as text.

If I press backspace, the cursor moves back into caption as expected. When caption is empty, it deletes the <img> but not image_section, image_block or caption nodes. These nodes cannot be deleted at the moment.

How would you define your nodes to delete whole <image_section> with backspace, when caption is empty?

My nodes for reference:

  doc: {
    content: "(section|image_section)+"
  },

  image_section: {
    content: "(image_block){1}(caption){1}",
    group: "container",
    defining: true, /* Don't know what this is. */
    parseDOM: [{tag: "div.section_content.editor-image"}],
    toDOM(node) { return ["div", {class: "section_content editor-image"}, 0] }
  },

  // :: NodeSpec Wrapper tag around images.
  image_block: {
    content: "image{1}",
    group: "block",
    defining: true,
    parseDOM: [{ tag: "div.imageblock" }],
    toDOM() { return ["div", { class: "imageblock"}, 0] }
  },

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

 // A special paragraph that goes under images only.
  caption: {
    content: "inline*",
    group: "block",
    attrs: { class: {default: "caption"}},
    parseDOM: [{tag: "p.caption"}],
    toDOM(node) { 
      node.attrs["data-placeholder"] = "Add caption here.";
      return ["p", node.attrs, 0] 
    }
  },

Alternatively I could define a tooltip on the image and a special delete command, but I’d love to have your advice first.

Thank you, Bahadir


#4

Why are you putting an image_block around the image when it can only ever have an image as child? (The {1} parts in the content strings are unnecessary, by the way—just "image_block caption" works.) In fact, why don’t you make the whole image section a single node, with inline content for the caption, and the image data stored in its attributes? I think that’ll make this all easier to work with. (You can still render a single document node with a more complex DOM structure, if the DOM structure is important to you.)


#5

@marijn Thank you. Your suggestion worked great. Here is what I did:

imagesection: {
   group: "container",
   //inline: true,
   isLeaf: true,
   content: "text*",
   //defining: true,
   inlineContent: true,
   attrs: {
     src: { default: null},
     alt: {default: null},
     title: {default: null},
   },
   parseDOM: [{tag: "div.section_content editor-image"}],
   toDOM(node) {
     return ["div", {class: "section_content editor-image"}, ["div", { class: "imageblock" }, ["img", node.attrs]], ["p", {class: "caption"}, 0]]
   }
 },

The position of 0 matters, it needs to point at the position where the caption text starts.

Here is what I used in the placeholderPlugin to instantiate this:

  let imagesection = mySchema.nodes.imagesection.create({src: url})
  view.dispatch(view.state.tr.insert(pos, imagesection).setMeta(placeholderPlugin, {remove: {id}}))

I will need to work on parseDOM some more.


#6

Hello,

How do I write a parseDOM function for a node with multiple nested tags and read attributes in an inner tag? I haven’t found any examples. I tried using the top-level tag but this did not work:

parseDOM: [{tag: "div.section_content editor-image", getAttrs(dom) {
  return {
    src: dom.getAttribute("src"),
    title: dom.getAttribute("title"),
    alt: dom.getAttribute("alt")
  }
}}],

I need to parse:

<div class="section_content editor-image">
    <div class="imageblock">
        <img src="">
    </div>
    <p class="caption"><br/></p>
</div>

Thank you,

Bahadir


#7

Something like…

parseDOM: [{tag: "div.section_content.editor-image", getAttrs(dom) {
  let img = dom.querySelector("img")
  return !img ? false : {
    src: img.getAttribute("src"),
    title: img.getAttribute("title"),
    alt: img.getAttribute("alt")
  }
}}],