Created
July 12, 2021 16:48
-
-
Save waptik/f44b0d3c803fade75456817b1b1df6b4 to your computer and use it in GitHub Desktop.
A custom extension for tiptap v2 to for image upload
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)]; | |
}, | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}, | |
}, | |
}, | |
}); | |
}; |
In order to get this working, I needed to update the nodeInputRule
in index@103
:
nodeInputRule({
find: IMAGE_INPUT_REGEX,
type: this.type,
getAttributes: (match) => {
const [, alt, src, title] = match;
return {
src,
alt,
title,
};
},
}),
IMAGE_INPUT_REGEX
Ah, good for you then.
You might have been using the latest version of tiptap because the it works with the version i am using
Just trying to be helpful for future stumblers! Thanks for the gist!
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.
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
Yes, you pass it to the extensions array. As you can see in https://github.com/waptik/tiptap-react/blob/main/src/components/editor/extensions/index.ts#L37, I conditionally add it to the array depending on the type of editor I'm building. See how it's being used