Adding drag and drop image uploads to Tiptap

Need to be able to drag and drop image files into your Tiptap WYSIWYG editor? Tiptap is highly customisable, so let's add some drag-and-drop magic to the Image extension.

If you don't already know, Tiptap is a headless WYSIWYG editor that uses ProseMirror under the hood. I love how easy it is to build/add just the features you need and create a totally custom editing experience for your users.

Although Tiptap comes with an image extension, it doesn't have drag-and-drop functionality out of the box (at the time of writing). But like every other part of Tiptap, it's fairly straightforward to add this extra feature - and that's what we will do in this blog post!

Here's what we will build:

First things first, you're going to need Tiptap installed, and you will also need to use the Image extension.

I'm going to assume you already have Tiptap installed, but here's the bare bones of it.

import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image'; new Editor({ element: document.querySelector('.editor'), extensions: [ StarterKit, Image ], content: '<p>Hello World!</p>',
});
<div class="editor"></div>

There are instructions in the Tiptap documentation for ReactJS (my framework of choice), Vue, and other options.

I've created a Tiptap CodePen if you want to follow along without the setup.

Add the Dropcursor extension

I'm going to highly recommend the Tiptap Dropcursor extension if you have the image extension installed.

Without it, you can still move your images around within the editor, but you can't necessarily see where they will end up!

If you have the StarterKit extension installed like in the code example above, you already have the Dropcursor extension. But if you didn't use the Starterkit extension, you can install the Dropcursor extension now.

Image drag in Tiptap

OK, now you have the image extension, and you can display images in your editor.

import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image'; new Editor({ element: document.querySelector('.editor'), extensions: [ StarterKit, Image ], content: ` <p>Hello World!</p> <img src="https://source.unsplash.com/8xznAGy4HcY/800x400" /> `,
});

And you can drag and drop images around in the editor, this all works out of the box, thanks to Tiptap.

dragging image in tiptap gif

But if you're reading this post, I'm guessing you want to drag and drop images from outside of the editor (e.g. an image file on the desktop) into the editor.

Image file drag and drop in Tiptap

Under the hood, Tiptap uses ProseMirror. ProseMirror has a handleDrop function that's "called when something is dropped on the editor.". This sounds exactly like what we need for our drag-and-drop images.

We can access ProseMirror props through editorProps in Tiptap - let's get that set up before we write the function.

new Editor({ element: document.querySelector('.editor'), extensions: [ StarterKit, Image, ], editorProps: { handleDrop: function(view, event, slice, moved) { return false; } }, content: ` <p>Hello World!</p> <img src="https://source.unsplash.com/8xznAGy4HcY/800x400" /> `,
});

This is the bit we are working on:

handleDrop: function(view, event, slice, moved) { return false; }

We'll start by just returning false from the handleDrop function. Since this function covers all drop events, we only want to change the behaviour if a new image is dragged in from outside the editor (since the existing functionality for dragging an image around inside the editor works great).

Let's update the function to handle any new files dragged into the editor, by checking that the drop event isn't moving something and that it is transferring a new file.

handleDrop: function(view, event, slice, moved) { if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { return true; } return false; }

Now our drop function checks for a new file and prevents the default behaviour if the user is dropping a file. But we are not taking any old files, images only, please!

Handle the image

Now you need to handle the image, here's the function that I use, but you might want to make some changes. For example, I'm only allowing png and jpeg files under 10MB. They also must be under 5000px in width and height. You can adjust these settings depending on the file size and dimensions you want to allow.

handleDrop: function(view, event, slice, moved) { if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { let file = event.dataTransfer.files[0]; let filesize = ((file.size/1024)/1024).toFixed(4); if ((file.type === "image/jpeg" || file.type === "image/png") && filesize < 10) { let _URL = window.URL || window.webkitURL; let img = new Image(); img.src = _URL.createObjectURL(file); img.onload = function () { if (this.width > 5000 || this.height > 5000) { window.alert("Your images need to be less than 5000 pixels in height and width."); } else { uploadImage(file).then(function(response) { }).catch(function(error) { if (error) { window.alert("There was a problem uploading your image, please try again."); } }); } }; } else { window.alert("Images need to be in jpg or png format and less than 10mb in size."); } return true; } return false; }

Sending the image to your server

You might have noticed a warning on the Tiptap page for the Image extension.

This extension does only the rendering of images. It doesn’t upload images to your server, that’s a whole different story.

- Tiptap Image

If you are letting people drag and drop images into the editor, you will need to save them somewhere so that you can display them. This might be an s3 bucket or some other object storage.

The uploadImage function will probably be an API call to your server to handle this. If you use axios it might look something like this.

function uploadImage(file) {
  const data = new FormData();
  data.append('file', file);
  return axios.post('/documents/image/upload', data);
};

I have a blog post for handling images on the server with code examples. For this post, I will keep the focus on the Tiptap side of the code.

Displaying the uploaded image in the editor

Now you have saved the image somewhere (hopefully), you should get a response with the URL to the saved file. Now you can display it back in the editor.

In the code below, I've added some code that creates the image element from the image URL that gets returned from the server and places it in the editor where it was dropped.

handleDrop: function(view, event, slice, moved) { if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { let file = event.dataTransfer.files[0]; let filesize = ((file.size/1024)/1024).toFixed(4); if ((file.type === "image/jpeg" || file.type === "image/png") && filesize < 10) { let _URL = window.URL || window.webkitURL; let img = new Image(); img.src = _URL.createObjectURL(file); img.onload = function () { if (this.width > 5000 || this.height > 5000) { window.alert("Your images need to be less than 5000 pixels in height and width."); } else { uploadImage(file).then(function(response) { const { schema } = view.state; const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }); const node = schema.nodes.image.create({ src: response }); const transaction = view.state.tr.insert(coordinates.pos, node); return view.dispatch(transaction); }).catch(function(error) { if (error) { window.alert("There was a problem uploading your image, please try again."); } }); } }; } else { window.alert("Images need to be in jpg or png format and less than 10mb in size."); } return true; } return false; }

Preventing a delay between response and display

You might find, especially if you display a loading spinner* while the image uploads, that there is a delay between your response and when the image is displayed in the editor. That's because the browser might take a moment to get the image from the URL.

*For example, if you set a loading state to true when a file is dropped, and then set it to false when the response arrives. We've not added a loading spinner in this tutorial, but maybe that's something for another time!

To get around that you can pre-load the image before responding.

uploadImage(file).then(function(response) { let image = new Image(); image.src = response; image.onload = function() { const { schema } = view.state; const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }); const node = schema.nodes.image.create({ src: response }); const transaction = view.state.tr.insert(coordinates.pos, node); return view.dispatch(transaction); }
}).catch(function(error) { if (error) { window.alert("There was a problem uploading your image, please try again."); }
});

The finished code

We made it! Here's the final code.

import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image'; new Editor({ element: document.querySelector('.editor'), extensions: [ StarterKit, Image, ], editorProps: { handleDrop: function(view, event, slice, moved) { if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { let file = event.dataTransfer.files[0]; let filesize = ((file.size/1024)/1024).toFixed(4); if ((file.type === "image/jpeg" || file.type === "image/png") && filesize < 10) { let _URL = window.URL || window.webkitURL; let img = new Image(); img.src = _URL.createObjectURL(file); img.onload = function () { if (this.width > 5000 || this.height > 5000) { window.alert("Your images need to be less than 5000 pixels in height and width."); } else { uploadImage(file).then(function(response) { let image = new Image(); image.src = response; image.onload = function() { const { schema } = view.state; const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }); const node = schema.nodes.image.create({ src: response }); const transaction = view.state.tr.insert(coordinates.pos, node); return view.dispatch(transaction); } }).catch(function(error) { if (error) { window.alert("There was a problem uploading your image, please try again."); } }); } }; } else { window.alert("Images need to be in jpg or png format and less than 10mb in size."); } return true; } return false; } }, content: ` <p>Hello World!</p> <img src="https://source.unsplash.com/8xznAGy4HcY/800x400" /> `,
});

Home - Wiki
Copyright © 2011-2024 iteam. Current version is 2.129.0. UTC+08:00, 2024-07-02 13:43
浙ICP备14020137号-1 $Map of visitor$