Reference to this.dom in the NodeView is outdated

I created a schema for image with caption.

<figure>
  <figcontent>
    <img>
    <figcaption>
  </figcontent>
<figure>
  figure: {
    content: 'figcontent',
    group: 'block',
    attrs: {
      style: {
        default: 'display:flex;justify-content:center;',
      },
    },
    marks: '',
    draggable: true,
    selectable: true,
    parseDOM: [{tag: 'figure'}],
    toDOM(node) {
      return ['figure', {style: 'display:flex;justify-content:center;'}, 0]
    },
  },

  figcontent: {
    content: 'image figcaption',
    attrs: {
      class: {default: MEDISTREAM_EDITOR_CLASS + '__figcontent'},
    },
    group: 'figure',
    marks: '',
    selectable: false,
    parseDOM: [{tag: 'div'}],
    toDOM: node => ['div', {class: node.attrs.class}, 0],
  },

  figcaption: {
    content: 'inline*',
    attrs: {
      class: {default: MEDISTREAM_EDITOR_CLASS + '__figcaption'},
    },
    group: 'figcontent',
    marks: 'strong link',
    parseDOM: [{tag: 'figcaption'}],
    toDOM(node) {
      return ['figcaption', {class: node.attrs.class}, 0]
    },
  },

  image: {
    attrs: {
      src: {
        default:
          'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII',
      },
      alt: {default: null},
      title: {default: null},
      width: {default: '200px'},
      height: {default: 'auto'},
      class: {default: MEDISTREAM_EDITOR_CLASS + '__img'},
    },
    group: 'figcontent',
    draggable: false,
    selectable: false,
    toDOM(node) {
      const {src, alt, title, width, height} = node.attrs
      return [
        'img',
        {
          src,
          alt,
          title,
          width,
          height,
          class: node.attrs.class,
        },
      ]
    },
    parseDOM: [
      {
        tag: 'img[src]',
        /**
         * @param {HTMLElement} dom
         */
        getAttrs: dom => ({
          src: dom.getAttribute('src'),
          title: dom.getAttribute('title'),
          alt: dom.getAttribute('alt'),
          width: dom.getAttribute('width'),
          height: dom.getAttribute('height'),
        }),
      },
    ],
  },

From here, I created a NodeView for figure node so I can show user the image editing controllers such as resize handle, align buttons and etc.

class FigureNodeView {
  /**
   *
   * @param {Node} node
   * @param {EditorView} view
   * @param {() => number} getPos
   */
  constructor(node, view, getPos) {
    this.node = node
    this.view = view
    this.getPos = getPos

    this.dom = document.createElement('figure')
    this.dom.className = MEDISTREAM_EDITOR_CLASS + '__figure'

    this.contentDOM = document.createElement('span')
    this.contentDOM.style.position = 'relative'

    this.img = document.createElement('img')
    this.img.className = MEDISTREAM_EDITOR_CLASS + '__img'
    this.node.descendants(node => {
      if (node.type.name === 'image') {
        this.img.src = node.attrs.src
      }
    })

    this.figcaption = document.createElement('figcaption')
    this.figcaption.className = MEDISTREAM_EDITOR_CLASS + '__figcaption'

    this.dom.append(this.contentDOM)
    this.contentDOM.append(this.img)
    this.contentDOM.append(this.figcaption)
  }

  selectNode() {
    this.dom.classList.add('ProseMirror-selectednode')
    this._showImageControls()
  }

  deselectNode() {
    this.dom.classList.remove('ProseMirror-selectednode')
    this._hideImageControls()
  }

  _showImageControls() {
    const resizeHandle = document.createElement('span')
    resizeHandle.className = MEDISTREAM_EDITOR_CLASS + '__image-resize-handle'
    resizeHandle.style.display = 'inline'
    resizeHandle.onmousedown = this._resizeHandleMouseDown.bind(this)
    this.resizeHandle = resizeHandle
    this.contentDOM.append(resizeHandle)

    const optionPanel = document.createElement('div')

    const alignLeftButton = document.createElement('div')
    alignLeftButton.onclick = () => {
      console.log(this.dom)               <-------------- this.dom is not what's on the view...
    }

    const alignRightButton = document.createElement('div')
    
    const alignCenterButton = document.createElement('div')

    const stretchImageButton = document.createElement('div')

    optionPanel.append(alignLeftButton)
    optionPanel.append(alignCenterButton)
    optionPanel.append(alignRightButton)
    optionPanel.append(stretchImageButton)
    this.contentDOM.append(optionPanel)
  }

this.dom in the _showImageControls is not the same node that is in the view. So I can’t manipulate its attributes to align differently…

Definitely don’t just append stuff to this.contentDOM in a node view—that will cause the editor to treat your widget as part of the actual document content, which will cause problems.

The library never reassigns NodeView.dom, so most likely you are ending up with a widget not being cleaned up when the node view is replaced, or something like that.

1 Like