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;
},
},
},
});
};
@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