-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: rich text editor #892
Closed
Closed
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
c62a4b2
set up text editor for web
in-mai-space 8efb639
add some components to support editor
in-mai-space 3e7d434
restart on the editor
in-mai-space 2069262
feat: rich text editor for mobile and web
in-mai-space ac2d230
tentap editor
in-mai-space 13c02b9
fix: link input for dashboard
in-mai-space dc971df
Merge branch 'main' into rich-text-editor
in-mai-space 5a3be5b
minor merge conflicts resolved
in-mai-space 9190719
yarn lock
in-mai-space 460bad5
minor fix
in-mai-space 9d7759d
Merge branch 'main' into rich-text-editor
in-mai-space 57eef7a
Merge branch 'main' into rich-text-editor
garrettladley ef01034
Revert "Merge branch 'main' into rich-text-editor"
garrettladley 1eb3f75
Revert "Revert "Merge branch 'main' into rich-text-editor""
garrettladley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import {BubbleMenu} from '@tiptap/react'; | ||
import { Editor } from '@tiptap/react' | ||
import { Bold, Heading1, Heading2, Italic, ListIcon, ListOrdered, UnderlineIcon } from 'lucide-react'; | ||
Check notice Code scanning / CodeQL Unused variable, import, function or class Note
Unused imports ListOrdered, UnderlineIcon.
|
||
|
||
interface BubbleMenuProps { | ||
editor: Editor | null; | ||
} | ||
|
||
export const EditorBubbleMenu: React.FC<BubbleMenuProps> = ({ editor }) => { | ||
if (!editor) { | ||
return null | ||
} | ||
|
||
return ( | ||
<BubbleMenu className="flex items-center flex-row bg-gray-300 rounded-md space-x-1 p-2" editor={editor} tippyOptions={{ duration: 100 }}> | ||
<div className={editor.isActive('bold') ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'} | ||
onClick={() => editor.chain().focus().toggleBold().run()}> | ||
<Bold size={20} /> | ||
</div> | ||
<div className={editor.isActive('italic') ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'} | ||
onClick={() => editor.chain().focus().toggleItalic().run()}> | ||
<Italic size={20}/> | ||
</div> | ||
<div className={editor.isActive('heading', { level: 1 }) ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'} | ||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}> | ||
<Heading1 size={20} /> | ||
</div> | ||
<div className={editor.isActive('heading', { level: 2 }) ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'} | ||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}> | ||
<Heading2 size={20}/> | ||
</div> | ||
<div className={editor.isActive('bulletList') ? 'bg-gray-400 rounded-md p-1.5' : 'p-1.5'} | ||
onClick={() => editor.chain().focus().toggleBulletList().run()}> | ||
<ListIcon size={20}/> | ||
</div> | ||
</BubbleMenu> | ||
) | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const Divider = () => { | ||
return ( | ||
<div className="bg-black h-5 w-0.5 mx-5" /> | ||
) | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
"use client" | ||
import { useEditor, EditorContent } from '@tiptap/react' | ||
import StarterKit from '@tiptap/starter-kit' | ||
import { Toolbar } from './toolbar' | ||
import Underline from '@tiptap/extension-underline'; | ||
import { Link } from '@tiptap/extension-link'; | ||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'; | ||
import { EditorBubbleMenu } from './bubbleMenu'; | ||
import React from 'react'; | ||
import './styles.css'; | ||
|
||
const Tiptap = () => { | ||
const editor = useEditor({ | ||
autofocus: true, | ||
editable: false, | ||
extensions: [ | ||
StarterKit, Underline, | ||
BubbleMenu, | ||
Link.configure({ | ||
openOnClick: true, | ||
autolink: true, | ||
}), | ||
], | ||
content: {"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Hello World"}]},{"type":"heading","attrs":{"level":2},"content":[{"type":"text","marks":[{"type":"bold"},{"type":"italic"},{"type":"strike"},{"type":"underline"}],"text":"This is generate"}]},{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 1"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 2"}]}]}]},{"type":"orderedList","attrs":{"start":1},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 3"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Task 4"}]}]}]},{"type":"paragraph","content":[{"type":"text","marks":[{"type":"link","attrs":{"href":"https://generatenu.com","target":"_blank","rel":"noopener noreferrer nofollow","class":null}}],"text":"link"}]}]}, | ||
}) | ||
|
||
return ( | ||
<> | ||
<Toolbar editor={editor}/> | ||
{editor && <EditorBubbleMenu editor={editor}/>} | ||
<EditorContent className="h-15 pl-10 p-5 rounded-md bg-slate-200" editor={editor} /> | ||
</> | ||
) | ||
} | ||
|
||
export default Tiptap; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { Button } from "@/components/ui/button"; | ||
import { Input } from "@/components/ui/input"; | ||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; | ||
import { Editor } from "@tiptap/react"; | ||
import { Link } from "lucide-react"; | ||
import { useEffect } from "react"; | ||
import { z } from "zod"; | ||
import { useForm, Controller } from "react-hook-form"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
|
||
interface HyperLinkProps { | ||
editor: Editor | null; | ||
disabled: boolean; | ||
} | ||
|
||
const linkSchema = z.object({ | ||
link: z.string().url("Invalid URL").optional(), | ||
}); | ||
|
||
type LinkData = z.infer<typeof linkSchema>; | ||
|
||
export const HyperlinkButton: React.FC<HyperLinkProps> = ({ editor, disabled }) => { | ||
const { | ||
control, | ||
handleSubmit, | ||
setValue, | ||
formState: { errors }, | ||
} = useForm<LinkData>({ | ||
resolver: zodResolver(linkSchema), | ||
defaultValues: { | ||
link: editor?.getAttributes("link").href || "", // Use optional chaining here | ||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
if (!editor) return; | ||
|
||
const updateLink = () => { | ||
const currentLink = editor.getAttributes("link").href; | ||
setValue("link", currentLink || ""); | ||
}; | ||
|
||
editor.on("transaction", updateLink); | ||
return () => { | ||
editor.off("transaction", updateLink); | ||
}; | ||
}, [editor, setValue]); | ||
|
||
const setLinkInEditor = (data: LinkData) => { | ||
const url = data.link; | ||
if (!url) { | ||
editor?.chain().focus().unsetLink().run(); | ||
return; | ||
} | ||
editor?.chain().focus().extendMarkRange("link").setLink({ href: url }).run(); | ||
}; | ||
|
||
const onSubmit = handleSubmit((data) => { | ||
setLinkInEditor(data); | ||
}); | ||
|
||
return ( | ||
<Popover> | ||
<PopoverTrigger asChild> | ||
<Button | ||
variant="link" | ||
className={editor?.isActive("link") && editor?.isEditable ? "bg-slate-300 rounded-md p-1.5" : "p-1.5"} | ||
size="link" | ||
disabled={disabled} | ||
> | ||
<Link /> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent className="w-76"> | ||
<form onSubmit={onSubmit} className="flex flex-row items-center space-x-5"> | ||
<div className="flex flex-row space-x-2"> | ||
<div className="flex flex-col items-top"> | ||
<Controller | ||
name="link" | ||
control={control} | ||
render={({ field }) => ( | ||
<Input | ||
id="link" | ||
placeholder="Enter a link" | ||
{...field} | ||
onKeyDown={(e) => { | ||
if (e.key === "Enter") { | ||
onSubmit(); | ||
} | ||
}} | ||
/> | ||
)} | ||
/> | ||
{errors.link && ( | ||
<p className="text-sm pt-1 text-red-400">{errors.link.message}</p> | ||
)} | ||
</div> | ||
<Button type="submit">Submit</Button> | ||
</div> | ||
</form> | ||
</PopoverContent> | ||
</Popover> | ||
); | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
.tiptap { | ||
> * + * { | ||
margin-top: 0.75em; | ||
} | ||
|
||
a { | ||
color: revert; | ||
text-decoration: revert; | ||
} | ||
|
||
ul, ol { | ||
list-style: revert; | ||
padding-left: revert; | ||
} | ||
|
||
h1, h2 { | ||
font-size: revert; | ||
font-weight: revert; | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { Editor } from '@tiptap/react' | ||
import {Bold, Italic, Strikethrough, | ||
Heading1, Heading2, ListOrdered, | ||
Undo, Redo, Unlink, | ||
ListIcon, UnderlineIcon} from 'lucide-react'; | ||
import { useState } from 'react'; | ||
import { Divider } from './divider'; | ||
import { HyperlinkButton } from './hyperlink'; | ||
import { Button } from "@/components/ui/button"; | ||
|
||
interface ToolbarProps { | ||
editor: Editor | null; | ||
} | ||
|
||
export const Toolbar: React.FC<ToolbarProps> = ({ editor }) => { | ||
const [json, setJSON] = useState("") | ||
Check notice Code scanning / CodeQL Unused variable, import, function or class Note
Unused variable json.
|
||
|
||
if (!editor) { | ||
return null | ||
} | ||
|
||
return ( | ||
<div className="flex flex-row bg-slate-200 p-2 rounded-md items-center mb-2 justify-between flex-wrap"> | ||
<div className="ml-1 flex flex-row items-center flex-wrap space-x-1"> | ||
<Button onClick={() => editor.chain().focus().toggleBold().run()} | ||
disabled={!editor.isEditable} | ||
variant="link" | ||
size="link" | ||
className={editor.isActive('bold') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<Bold /> | ||
</Button> | ||
<Button disabled={!editor.isEditable} | ||
variant="link" | ||
size="link" | ||
onClick={() => editor.chain().focus().toggleItalic().run()} | ||
className={editor.isActive('italic') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<Italic /> | ||
</Button> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleStrike().run()} | ||
className={editor.isActive('strike') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<Strikethrough /> | ||
</Button> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleUnderline().run()} | ||
className={editor.isActive('underline') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<UnderlineIcon /> | ||
</Button> | ||
<Divider /> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} | ||
className={editor.isActive('heading', { level: 1 }) && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<Heading1 /> | ||
</Button> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} | ||
className={editor.isActive('heading', { level: 2 }) && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<Heading2 /> | ||
</Button> | ||
<Divider /> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleBulletList().run()} | ||
className={editor.isActive('bulletList') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<ListIcon /> | ||
</Button> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} onClick={() => editor.chain().focus().toggleOrderedList().run()} | ||
className={editor.isActive('orderedList') && editor.isEditable ? 'bg-slate-300 rounded-md p-1.5' : 'p-1.5'}> | ||
<ListOrdered /> | ||
</Button> | ||
<HyperlinkButton disabled={!editor.isEditable} editor={editor}/> | ||
<Button | ||
variant="link" | ||
size="link" | ||
onClick={() => { | ||
editor.chain().focus().unsetLink().run(); | ||
}} | ||
disabled={!editor.isActive('link') || !editor.isEditable} | ||
className="p-1.5" | ||
> | ||
<Unlink /> | ||
</Button> | ||
<Divider /> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} className="p-1.5" onClick={() => editor.chain().focus().undo().run()}> | ||
<Undo /> | ||
</Button> | ||
<Button variant="link" | ||
size="link" disabled={!editor.isEditable} className="p-1.5" onClick={() => editor.chain().focus().redo().run()}> | ||
<Redo /> | ||
</Button> | ||
</div> | ||
|
||
<Button onClick={() => { | ||
setJSON(JSON.stringify(editor.getJSON())); | ||
editor.setEditable(!editor.isEditable); | ||
}} | ||
className="px-4 py-1 bg-black rounded-md text-white text-sm">{editor.isEditable ? "Save" : "Edit"}</Button> | ||
</div> | ||
) | ||
} |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
"use client" | ||
|
||
import * as React from "react" | ||
import * as PopoverPrimitive from "@radix-ui/react-popover" | ||
|
||
import { cn } from "@/lib/utils" | ||
|
||
const Popover = PopoverPrimitive.Root | ||
|
||
const PopoverTrigger = PopoverPrimitive.Trigger | ||
|
||
const PopoverContent = React.forwardRef< | ||
React.ElementRef<typeof PopoverPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> | ||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( | ||
<PopoverPrimitive.Portal> | ||
<PopoverPrimitive.Content | ||
ref={ref} | ||
align={align} | ||
sideOffset={sideOffset} | ||
className={cn( | ||
"z-50 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
</PopoverPrimitive.Portal> | ||
)) | ||
PopoverContent.displayName = PopoverPrimitive.Content.displayName | ||
|
||
export { Popover, PopoverTrigger, PopoverContent } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add this line back in?