title: Tiptap extension for images and editable captions
body: <p><a href="https://i.stack.imgur.com/4h160.gif" rel="nofollow noreferrer">enter image description here</a></p>
  
  <p>I want to implement <strong>image and editable caption with tiptap extension</strong></p>
  
  <p><a href="https://discuss.prosemirror.net/t/figure-and-editable-caption/462" rel="nofollow noreferrer">https://discuss.prosemirror.net/t/figure-and-editable-caption/462</a><br />
  There was a very good example with ProseMirror, but is it difficult to achieve with tiptap?<br />
  Please tell me what code you should write if possible.</p>
  
  <p>I attach the code that I wrote below.<br />
  The image and caption have been successfully added, but the caption cannot be edited yet.</p>
  
  <pre><code>// ./CustomImage.ts
  // @ts-ignore
  import { Node, Plugin } from 'tiptap'
  // @ts-ignore
  import { nodeInputRule } from 'tiptap-commands'
  
  const IMAGE_INPUT_REGEX = /!\[(.+|:?)\]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
  
  export default class CustomImage extends Node {
    get name () {
      return 'customImage'
    }
  
    get schema () {
      return {
        attrs: {
          src: {
            default: null
          },
          alt: {
            default: null
          },
          title: {
            default: null
          },
          caption: {
            default: null
          }
        },
        group: 'block',
        selectable: false,
        draggable: true,
        parseDOM: [
          {
            tag: 'figure'
          },
          [
            {
              tag: 'img[src]',
              getAttrs: (dom: any) =&gt; ({
                src: dom.getAttribute('src'),
                title: dom.getAttribute('title'),
                alt: dom.getAttribute('alt')
              })
            },
            {
              tag: 'figcaption'
            }
          ]
        ],
        toDOM: (node: any) =&gt; [
          'figure',
          [
            'img',
            {
              src: node.attrs.src,
              title: node.attrs.title,
              alt: node.attrs.alt
            }
          ],
          [
            'figcaption',
            {
              contenteditable: 'true'
            },
            node.attrs.caption
          ]
        ]
      }
    }
  
    commands ({ type }: any) {
      return (attrs: any) =&gt; (state: any, dispatch: any) =&gt; {
        const { selection } = state
        const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
        const node = type.create(attrs)
        const transaction = state.tr.insert(position, node)
        dispatch(transaction)
      }
    }
  
    inputRules ({ type }: any) {
      return [
        nodeInputRule(IMAGE_INPUT_REGEX, type, (match: any) =&gt; {
          const [, alt, src, title] = match
          return {
            src,
            alt,
            title
          }
        })
      ]
    }
  
    get plugins () {
      return [
        new Plugin({
          props: {
            handleDOMEvents: {
              drop (view: any, event: any) {
                const hasFiles = event.dataTransfer &amp;&amp;
                event.dataTransfer.files &amp;&amp;
                event.dataTransfer.files.length
  
                if (!hasFiles) {
                  return
                }
  
                const images = Array
                  .from(event.dataTransfer.files)
                  .filter((file: any) =&gt; (/image/i).test(file.type))
  
                if (images.length === 0) {
                  return
                }
  
                event.preventDefault()
  
                const { schema } = view.state
                const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
  
                images.forEach((image: any) =&gt; {
                  const reader = new FileReader()
  
                  reader.onload = (readerEvent: any) =&gt; {
                    const node = schema.nodes.image.create({
                      src: readerEvent.target.result
                    })
                    const transaction = view.state.tr.insert(coordinates.pos, node)
                    view.dispatch(transaction)
                  }
                  reader.readAsDataURL(image)
                })
              }
            }
          }
        })
      ]
    }
  }
  
  </code></pre>