1) Create a file Image.js (it is almost a copy of [Image.js from tiptap-extensions](https://github.com/scrumpy/tiptap/blob/master/packages/tiptap-extensions/src/nodes/Image.js) except that it has a constructor that accepts `uploadFunc` (function to be called with `image` being uploaded). ```js import {Node, Plugin} from 'tiptap' import {nodeInputRule} from 'tiptap-commands' /** * Matches following attributes in Markdown-typed image: [, alt, src, title] * * Example: * ![Lorem](image.jpg) -> [, "Lorem", "image.jpg"] * ![](image.jpg "Ipsum") -> [, "", "image.jpg", "Ipsum"] * ![Lorem](image.jpg "Ipsum") -> [, "Lorem", "image.jpg", "Ipsum"] */ const IMAGE_INPUT_REGEX = /!\[(.+|:?)\]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/; export default class Image extends Node { constructor(name, parent, uploadFunc = null) { super(name, parent); this.uploadFunc = uploadFunc; } get name() { return 'image' } get schema() { return { inline: true, attrs: { src: {}, alt: { default: null, }, title: { default: null, }, }, group: 'inline', draggable: true, parseDOM: [ { tag: 'img[src]', getAttrs: dom => ({ src: dom.getAttribute('src'), title: dom.getAttribute('title'), alt: dom.getAttribute('alt'), }), }, ], toDOM: node => ['img', node.attrs], } } commands({ type }) { return attrs => (state, dispatch) => { 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 }) { return [ nodeInputRule(IMAGE_INPUT_REGEX, type, match => { const [, alt, src, title] = match; return { src, alt, title, } }), ] } get plugins() { const upload = this.uploadFunc; return [ new Plugin({ props: { handleDOMEvents: { drop(view, event) { const hasFiles = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length; if (!hasFiles) { return } const images = Array .from(event.dataTransfer.files) .filter(file => (/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(async image => { const reader = new FileReader(); if(upload) { const node = schema.nodes.image.create({ src: await upload(image), }); const transaction = view.state.tr.insert(coordinates.pos, node); view.dispatch(transaction) } else { reader.onload = readerEvent => { 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) } }) }, }, }, }), ] } } ``` 2) Import it: ``` import Image from './Image'; new Editor({ extensions: [ ... new Image(null, null, upload), ... ``` 3) Create `upload` function: ```js async upload(file) { let formData = new FormData(); formData.append('file', file); const headers = {'Content-Type': 'multipart/form-data'}; const response = await axios.post('/upload', formData, {headers: headers} ); return response.data.src; }, ``` This `POST`s using `axios` to `/upload` and expects a JSON back of this form: ``` {"src": "https://yoursite.com/images/uploadedimage.jpg"} ``` 5) Implement server-side logic for `/upload`