Tiptap has a GitHub issue with the list of all the community extensions in the repo where people share extensions that they create. Someone mentioned that they needed Commenting-feature in Tiptap that is similar to the commenting-feature in Google Docs.
So here's how I had my chance of creating the same.
sereneinserenade / tiptap-comment-extension
Google-Docs 📄🔥 like commenting 💬 solution for Tiptap 2(https://tiptap.dev)
Tiptap Comment Extension:
DM Me on Discord - sereneinserenade#4869
Tiptap Extension for having Google-Docs like pro-commenting in Tiptap.
A ⭐️ to the repo if you 👍 / ❤️ what I'm doing would be much appreciated. If you're using this extension and making money from it, it'd be very kind of you to ❤️ Sponsor me. If you're looking for a dev to work you on your project's Rich Text Editor with or as a frontend developer, DM me on Discord/Twitter/LinkedIn👨💻🤩.
I've made a bunch of extensions for Tiptap 2, some of them are Resiable Images And Videos, Search and Replace, LanguageTool integration with tiptap. You can check it our here https://github.com/sereneinserenade#a-glance-of-my-projects.
Demo:
Try live demo: https://sereneinserenade.github.io/tiptap-comment-extension/
tiptap-comment-extension.mp4
How to use
npm i @sereneinserenade/tiptap-comment-extension
import StarterKit from "@tiptap/starter-kit";
import CommentExtension from "@sereneinserenade/tiptap-comment-extension";
/* or
import { CommentExtension } from "@sereneinserenade/tiptap-comment-extension";
*/
const extensions
…Initially I created it just for Vue3 since it's only supposed to be an example implementation of a framework agnostic Tiptap extension. And by the request of community there's also an implementation in React. If you want to check it out before we jump into the code, here's a demo.
Okay, enough context, let's dive into how it's made.
When I started working on it, the first thing I did was to Google whether there were any discussions around it, and I found this thread, which at the time I couldn't comprehend fully because of my limited knowledge of Prosemirror. I got to know pros and cons of implementing comments as marks and the other solution, which mentioned storing ranges of comments in an external data structure, and highlighting the comments in the doc with Prosemirror decorations, but then we'd have to figure out how to transfer comments when copying the content from one doc and pasting to another doc.
After all, I decided to give a shot to Comments as marks since I was already familiar with marks. So I created a new Mark, here's the file.
export interface CommentOptions {
HTMLAttributes: Record<string, any>,
}
export const Comment = Mark.create<CommentOptions>({
name: 'comment',
addOptions() {
return {
HTMLAttributes: {},
};
},
parseHTML() {
return [
{
tag: 'span',
},
];
},
renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
});
Right now, we have created a comment mark, that'll just render a span, but we need to store the comments somewhere. That's when the data-comment
attribute comes in.
export const Comment = Mark.create<CommentOptions>({
// ... rest of code
addAttributes() {
return {
comment: {
default: null,
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('data-comment'),
renderHTML: (attrs) => ({ 'data-comment': attrs.comment }),
},
};
},
parseHTML() {
return [
{
tag: 'span[data-comment]',
getAttrs: (el) => !!(el as HTMLSpanElement).getAttribute('data-comment')?.trim() && null,
},
];
},
// ... rest of code
})
Now, it would render a data-comment
attribute with span element, and adding a Tiptap-Attribute will allow us to define the comment
property of mark so we can access it programmatically instead of getting data-comment
attribute by searching through DOM.
We need commands to be able to set comment mark.
// ... rest of code
addCommands() {
return {
setComment: (comment: string) => ({ commands }) => commands.setMark('comment', { comment }),
toggleComment: () => ({ commands }) => commands.toggleMark('comment'),
unsetComment: () => ({ commands }) => commands.unsetMark('comment'),
};
},
// ... rest of code
That's it for the extension part. Now we look at how we're going to structure the comments and store them, which leads us to the Vue part of things, which is implemented here.
interface CommentInstance {
uuid?: string
comments?: any[]
}
// to store currently active instance
const activeCommentsInstance = ref<CommentInstance>({})
const allComments = ref<any[]>([]) // to store all comments
Loading all the comments when editor is created and updated, setting currently active comment when editor is updated.
const findCommentsAndStoreValues = (editor: Editor) => {
const tempComments: any[] = []
// to get the comments from comment mark.
editor.state.doc.descendants((node, pos) => {
const { marks } = node
marks.forEach((mark) => {
if (mark.type.name === 'comment') {
const markComments = mark.attrs.comment; // accessing the comment attribute programmatically as mentioned before
const jsonComments = markComments ? JSON.parse(markComments) : null;
if (jsonComments !== null) {
tempComments.push({
node,
jsonComments,
from: pos,
to: pos + (node.text?.length || 0),
text: node.text,
});
}
}
})
})
allComments.value = tempComments
}
const setCurrentComment = (editor: Editor) => {
const newVal = editor.isActive('comment')
if (newVal) {
setTimeout(() => (showCommentMenu.value = newVal), 50)
showAddCommentSection.value = !editor.state.selection.empty
const parsedComment = JSON.parse(editor.getAttributes('comment').comment)
parsedComment.comment = typeof parsedComment.comments === 'string' ? JSON.parse(parsedComment.comments) : parsedComment.comments
activeCommentsInstance.value = parsedComment
} else {
activeCommentsInstance.value = {}
}
}
const tiptapEditor = useEditor({
content: `
<p> Comment here. </p>
`,
extensions: [StarterKit, Comment],
onUpdate({ editor }) {
findCommentsAndStoreValues(editor)
setCurrentComment(editor)
},
onSelectionUpdate({ editor }) {
setCurrentComment(editor)
isTextSelected.value = !!editor.state.selection.content().size
},
onCreate({ editor }) {
findCommentsAndStoreValues(editor)
},
editorProps: {
attributes: {
spellcheck: 'false',
},
},
})
That was the core logic of how to get comments from Tiptap and store then in Vue so it can be use outside of the scope of editor.
Here's the structure of the comments that made most sense to me. However, since it's just going to be a string in the data-comment
property, you can have any JSON or XML or YAML or whatever format you want, just make sure you parse it right.
{
"uuid": "d1858137-e0d8-48ac-9f38-ae778b56c719",
"comments": [
{
"userName": "sereneinserenade",
"time": 1648338852939,
"content": "First comment"
},
{
"userName": "sereneinserenade",
"time": 1648338857073,
"content": "Following Comment"
}
]
}
The other part of logic(which I consider to be not-core logic) , i.e. creating a new comment, toggling comment mode on/off and showing the comments outside can be found in the repo.
Enjoy the days, and if you have any questions/suggestions, I'll be in the comment section.
Top comments (9)
Awesome work, thank you!
Any ideas on how to show the respective comment box on the same height as the line where the comment appears? Currently have tried getting the coordinate for the starting position and adding that to position the element, without much success.
const startPos = editor.view.posAtDOM(commentNode, 0);
const coords = editor.view.coordsAtPos(startPos);
comment.from = startPos;
Hi Jeet! many thanks!
How do you set the Mark' style?
giving it an attribute and styling it works just fine github.com/sereneinserenade/tiptap...
Got it! Thank you so much!
Hi Jeet, I am using Tiptap. The editor contain some existing text and images I want to style it . How can I do that?
Hey @rini001, please have a look here at how tiptap styles it, it should be able to do something similar
styles.css
Thanks for writing this Jeet, it was super helpful for me!
Glad it was helpful. Always welcome!
Hi Jeet, I was working on something similar with Tiptap and React. Is there a way to focus on and select the text in the editor when the user clicks a comment from the side panel?