Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/tiptap/src/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -22,6 +23,7 @@ interface EditorProps {
setContentFromOutside?: boolean;
mentionConfig: MentionConfig;
placeholderComponent?: PlaceholderFunction;
fileHandlerConfig?: FileHandlerConfig;
}

const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>(
Expand All @@ -33,6 +35,7 @@ const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>(
setContentFromOutside = false,
mentionConfig,
placeholderComponent,
fileHandlerConfig,
},
ref,
) => {
Expand All @@ -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<typeof useEditor>[0]["editorProps"] = useMemo(
Expand Down Expand Up @@ -155,9 +158,7 @@ const Editor = forwardRef<{ editor: TiptapEditor | null }, EditorProps>(
}, [editor, editable]);

return (
<div role="textbox">
<EditorContent editor={editor} />
</div>
<EditorContent editor={editor} className="tiptap-root" role="textbox" />
);
},
);
Expand Down
101 changes: 99 additions & 2 deletions packages/tiptap/src/shared/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,15 +17,45 @@ 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] },
underline: false,
link: false,
listKeymap: false,
}),
Image,
AttachmentImage.configure({
inline: false,
allowBase64: true,
HTMLAttributes: { class: "tiptap-image" },
}),
Underline,
Placeholder.configure({
placeholder:
Expand Down Expand Up @@ -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();
18 changes: 18 additions & 0 deletions packages/tiptap/src/styles/base.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
.tiptap-root {
height: 100%;
display: flex;
flex-direction: column;
}

.tiptap-root .tiptap {
flex: 1 1 auto;
min-height: 100%;
}
Comment on lines +1 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Confirm .tiptap-root / inner editor structure so flex+min-height actually apply

The layout assumes a .tiptap-root container with a nested .tiptap element, and .tiptap-normal inside with min-height: 100%. Please double‑check the actual DOM that EditorContent + editorProps.attributes.class = "tiptap-normal" produce; if there isn’t a real .tiptap-root .tiptap descendant chain, the flex growth and min-height rules here won’t take effect and you may want to target .tiptap-root and/or .tiptap-normal directly instead.

Also applies to: 19-19


🌐 Web query:

Check how `@tiptap/react`’s `EditorContent` applies `className` and `editorProps.attributes.class` in version 3.10.7. Specifically: does passing `className="tiptap-root"` plus `editorProps.attributes.class = "tiptap-normal"` result in a `.tiptap-root .tiptap-normal` structure, or just a single root element with both classes?

💡 Result:

Short answer: a single root element with both classes (not nested). EditorContent renders one element and applies your component className together with the editor's editorProps.attributes.class to that same root, so you'll see one element with both classes (e.g. class="tiptap-root tiptap-normal"). Source: Tiptap React docs and the Editor API note on editorProps/attributes. [1][2]

Sources

  • React integration / EditorContent usage (EditorContent is the React wrapper for the editor DOM). [1]
  • Editor API — editorProps.attributes.class (how the editor's attributes/classes are applied). [2]

Fix CSS selectors: .tiptap-root .tiptap descendant rule won't apply (dead code)

Based on @tiptap/react's EditorContent implementation, both className="tiptap-root" and editorProps.attributes.class = "tiptap-normal" are applied to a single root element (e.g., class="tiptap-root tiptap-normal"), not nested elements. The CSS rule .tiptap-root .tiptap on lines 7–10 is unreachable dead code because there is no .tiptap child element inside .tiptap-root.

To fix: either target .tiptap-root directly with the flex and min-height rules, or adjust selectors to match your actual DOM structure. If .tiptap-normal is on the root, use .tiptap-root.tiptap-normal or simplify to .tiptap-normal.

🤖 Prompt for AI Agents
packages/tiptap/src/styles/base.css lines 1-10: the descendant selector
`.tiptap-root .tiptap` is dead because EditorContent applies both classes on the
same root element (e.g., `class="tiptap-root tiptap-normal"`); replace the
unreachable selector with one that matches the actual DOM (e.g., target
`.tiptap-root` directly for flex/min-height, or use the combined selector
`.tiptap-root.tiptap-normal` or `.tiptap-normal`) so the flex layout and
min-height rules apply to the root element.


.tiptap-normal {
word-break: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
hyphens: auto;
padding-bottom: 24px;
caret-color: #374151;
min-height: 100%;

:first-child {
margin-top: 0;
Expand All @@ -19,4 +31,10 @@
p {
margin-bottom: 0.25rem;
}

.tiptap-image {
max-width: 240px;
height: auto;
display: block;
}
}
Loading