diff --git a/packages/tiptap/src/editor/index.tsx b/packages/tiptap/src/editor/index.tsx index 2efd0e7cb9..852a10a346 100644 --- a/packages/tiptap/src/editor/index.tsx +++ b/packages/tiptap/src/editor/index.tsx @@ -10,6 +10,7 @@ import { useDebounceCallback } from "usehooks-ts"; import "../../styles.css"; import * as shared from "../shared"; +import type { FileHandlerConfig } from "../shared/extensions"; import type { PlaceholderFunction } from "../shared/extensions/placeholder"; import { mention, type MentionConfig } from "./mention"; @@ -22,6 +23,7 @@ interface EditorProps { setContentFromOutside?: boolean; mentionConfig: MentionConfig; placeholderComponent?: PlaceholderFunction; + fileHandlerConfig?: FileHandlerConfig; } const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>( @@ -33,6 +35,7 @@ const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>( setContentFromOutside = false, mentionConfig, placeholderComponent, + fileHandlerConfig, }, ref, ) => { @@ -54,10 +57,10 @@ const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>( const extensions = useMemo( () => [ - ...shared.getExtensions(placeholderComponent), + ...shared.getExtensions(placeholderComponent, fileHandlerConfig), mention(mentionConfig), ], - [mentionConfig, placeholderComponent], + [mentionConfig, placeholderComponent, fileHandlerConfig], ); const editorProps: Parameters[0]["editorProps"] = useMemo( @@ -155,9 +158,7 @@ const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>( }, [editor, editable]); return ( -
- -
+ ); }, ); diff --git a/packages/tiptap/src/shared/extensions/index.ts b/packages/tiptap/src/shared/extensions/index.ts index a71f50558e..71f78861fb 100644 --- a/packages/tiptap/src/shared/extensions/index.ts +++ b/packages/tiptap/src/shared/extensions/index.ts @@ -1,3 +1,4 @@ +import FileHandler from "@tiptap/extension-file-handler"; import Highlight from "@tiptap/extension-highlight"; import Image from "@tiptap/extension-image"; import Link from "@tiptap/extension-link"; @@ -16,7 +17,33 @@ import { SearchAndReplace } from "./search-and-replace"; export type { PlaceholderFunction }; -export const getExtensions = (placeholderComponent?: PlaceholderFunction) => [ +export type FileHandlerConfig = { + onDrop?: (files: File[], editor: any, position?: number) => boolean | void; + onPaste?: (files: File[], editor: any) => boolean | void; +}; + +const AttachmentImage = Image.extend({ + addAttributes() { + return { + ...this.parent?.(), + attachmentId: { + default: null, + parseHTML: (element) => element.getAttribute("data-attachment-id"), + renderHTML: (attributes) => { + if (!attributes.attachmentId) { + return {}; + } + return { "data-attachment-id": attributes.attachmentId }; + }, + }, + }; + }, +}); + +export const getExtensions = ( + placeholderComponent?: PlaceholderFunction, + fileHandlerConfig?: FileHandlerConfig, +) => [ // https://tiptap.dev/docs/editor/extensions/functionality/starterkit StarterKit.configure({ heading: { levels: [1, 2, 3] }, @@ -24,7 +51,11 @@ export const getExtensions = (placeholderComponent?: PlaceholderFunction) => [ link: false, listKeymap: false, }), - Image, + AttachmentImage.configure({ + inline: false, + allowBase64: true, + HTMLAttributes: { class: "tiptap-image" }, + }), Underline, Placeholder.configure({ placeholder: @@ -86,6 +117,72 @@ export const getExtensions = (placeholderComponent?: PlaceholderFunction) => [ searchResultClass: "search-result", disableRegex: true, }), + ...(fileHandlerConfig + ? [ + FileHandler.configure({ + allowedMimeTypes: [ + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + ], + onDrop: (currentEditor, files, pos) => { + if (fileHandlerConfig.onDrop) { + const result = fileHandlerConfig.onDrop( + files, + currentEditor, + pos, + ); + if (result === false) return false; + } + + files.forEach((file) => { + const fileReader = new FileReader(); + + fileReader.readAsDataURL(file); + fileReader.onload = () => { + currentEditor + .chain() + .insertContentAt(pos, { + type: "image", + attrs: { + src: fileReader.result, + }, + }) + .focus() + .run(); + }; + }); + + return true; + }, + onPaste: (currentEditor, files) => { + if (fileHandlerConfig.onPaste) { + const result = fileHandlerConfig.onPaste(files, currentEditor); + if (result === false) return false; + } + + files.forEach((file) => { + const fileReader = new FileReader(); + + fileReader.readAsDataURL(file); + fileReader.onload = () => { + const imageNode = { + type: "image", + attrs: { + src: fileReader.result, + }, + }; + + currentEditor.chain().focus().insertContent(imageNode).run(); + }; + }); + + return true; + }, + }), + ] + : []), ]; export const extensions = getExtensions(); diff --git a/packages/tiptap/src/styles/base.css b/packages/tiptap/src/styles/base.css index 8acaeff47c..74ae9a4122 100644 --- a/packages/tiptap/src/styles/base.css +++ b/packages/tiptap/src/styles/base.css @@ -1,3 +1,14 @@ +.tiptap-root { + height: 100%; + display: flex; + flex-direction: column; +} + +.tiptap-root .tiptap { + flex: 1 1 auto; + min-height: 100%; +} + .tiptap-normal { word-break: break-word; overflow-wrap: break-word; @@ -5,6 +16,7 @@ hyphens: auto; padding-bottom: 24px; caret-color: #374151; + min-height: 100%; :first-child { margin-top: 0; @@ -19,4 +31,10 @@ p { margin-bottom: 0.25rem; } + + .tiptap-image { + max-width: 240px; + height: auto; + display: block; + } }