Skip to content

Instantly share code, notes, and snippets.

@waptik
Created July 12, 2021 16:48
Show Gist options
  • Save waptik/f44b0d3c803fade75456817b1b1df6b4 to your computer and use it in GitHub Desktop.
Save waptik/f44b0d3c803fade75456817b1b1df6b4 to your computer and use it in GitHub Desktop.
A custom extension for tiptap v2 to for image upload
import { Node, nodeInputRule } from "@tiptap/core";
import { mergeAttributes } from "@tiptap/react";
import { uploadImagePlugin, UploadFn } from "./upload_image";
/**
* Tiptap Extension to upload images
* @see https://gist.github.com/slava-vishnyakov/16076dff1a77ddaca93c4bccd4ec4521#gistcomment-3744392
* @since 7th July 2021
*
* 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"]
*/
interface ImageOptions {
inline: boolean;
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
image: {
/**
* Add an image
*/
setImage: (options: { src: string; alt?: string; title?: string }) => ReturnType;
};
}
}
const IMAGE_INPUT_REGEX = /!\[(.+|:?)\]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
export const TipTapCustomImage = (uploadFn: UploadFn) => {
return Node.create<ImageOptions>({
name: "image",
defaultOptions: {
inline: false,
HTMLAttributes: {},
},
inline() {
return this.options.inline;
},
group() {
return this.options.inline ? "inline" : "block";
},
draggable: true,
addAttributes() {
return {
src: {
default: null,
},
alt: {
default: null,
},
title: {
default: null,
},
};
},
parseHTML: () => [
{
tag: "img[src]",
getAttrs: dom => {
if (typeof dom === "string") return {};
const element = dom as HTMLImageElement;
const obj = {
src: element.getAttribute("src"),
title: element.getAttribute("title"),
alt: element.getAttribute("alt"),
};
return obj;
},
},
],
renderHTML: ({ HTMLAttributes }) => ["img", mergeAttributes(HTMLAttributes)],
addCommands() {
return {
setImage:
attrs =>
({ state, dispatch }) => {
const { selection } = state;
const position = selection.$head ? selection.$head.pos : selection.$to.pos;
const node = this.type.create(attrs);
const transaction = state.tr.insert(position, node);
return dispatch?.(transaction);
},
};
},
addInputRules() {
return [
nodeInputRule(IMAGE_INPUT_REGEX, this.type, match => {
const [, alt, src, title] = match;
return {
src,
alt,
title,
};
}),
];
},
addProseMirrorPlugins() {
return [uploadImagePlugin(uploadFn)];
},
});
};
import { Plugin } from "prosemirror-state";
/**
* function for image drag n drop(for tiptap)
* @see https://gist.github.com/slava-vishnyakov/16076dff1a77ddaca93c4bccd4ec4521#gistcomment-3744392
*/
export type UploadFn = (image: File) => Promise<string>;
export const uploadImagePlugin = (upload: UploadFn) => {
return new Plugin({
props: {
handlePaste(view, event) {
console.log("----onhandlePaste image---");
const items = Array.from(event.clipboardData?.items || []);
const { schema } = view.state;
console.log({ items });
items.forEach(item => {
const image = item.getAsFile();
console.log({ image, item });
if (item.type.indexOf("image") === 0) {
console.log("item is an image");
event.preventDefault();
if (upload && image) {
upload(image).then(src => {
const node = schema.nodes.image.create({
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);
};
if (!image) return;
reader.readAsDataURL(image);
}
});
return false;
},
handleDOMEvents: {
drop(view, event) {
console.log("----handleDom.onDrop----");
const hasFiles = event.dataTransfer?.files?.length;
if (!hasFiles) {
return false;
}
const images = Array.from(event!.dataTransfer!.files).filter(file => /image/i.test(file.type));
if (images.length === 0) {
return false;
}
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);
}
});
return false;
},
},
},
});
};
@varun-raj
Copy link

Is there any ways we can add a loader while when the image is getting uploaded and then replace the loader with the actual image once the upload is done? this way we can give more context to the users.

@cesc1989
Copy link

cesc1989 commented Jan 6, 2024

For Tiptap version 2.1.3 I had to made the change commented by @jqhoogland for the nodeInputRule. Otherwise the editor breaks when trying to type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment