Skip to content

Instantly share code, notes, and snippets.

@bluebeel
Forked from slava-vishnyakov/readme.md
Created June 30, 2021 18:14
Show Gist options
  • Save bluebeel/af49ad6d96287f846fc5d5d9b9bea1d1 to your computer and use it in GitHub Desktop.
Save bluebeel/af49ad6d96287f846fc5d5d9b9bea1d1 to your computer and use it in GitHub Desktop.
How to upload images with TipTap editor
  1. Create a file Image.js from the source below (it is almost a copy of Image.js from tiptap-extensions except that it has a constructor that accepts uploadFunc (function to be called with image being uploaded) and additional logic if(upload) { ... } else { ... previous base64 logic .. } in the new Plugin section.
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)
                                }
                            })
                        },
                    },
                },
            }),
        ]
    }

}
  1. Import it:
import Image from './Image';

async function upload(file) {
...
}

new Editor({
  extensions: [
    ...
    new Image(null, null, upload),
    ...
  1. Implement the upload function:
async function 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 POSTs using axios to /upload and expects a JSON back of this form:

{"src": "https://yoursite.com/images/uploadedimage.jpg"}
  1. Implement server-side logic for /upload

  2. If you want to support pasting of images, modify Image.js starting at props:, ending at handleDOMEvents (re-factor common parts if you want to)

props: {
    handlePaste(view, event, slice) {
        const items = (event.clipboardData  || event.originalEvent.clipboardData).items;
        for (const item of items) {
            if (item.type.indexOf("image") === 0) {
                event.preventDefault();
                const { schema } = view.state;

                const image = item.getAsFile();

                if(upload) {
                    upload(image).then(src => {
                        const node = schema.nodes.image.create({
                            src: src,
                        });
                        const transaction = view.state.tr.replaceSelectionWith(node);
                        view.dispatch(transaction)
                    });

                } else {
                    const reader = new FileReader();
                    reader.onload = readerEvent => {
                        const node = schema.nodes.image.create({
                            src: readerEvent.target.result,
                        });
                        const transaction = view.state.tr.replaceSelectionWith(node);
                        view.dispatch(transaction)
                    };
                    reader.readAsDataURL(image)
                }

            }
        }
        return false;
    },
    handleDOMEvents: {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment