Skip to content

Commit

Permalink
✨ fully rework menu and inputs (#2)
Browse files Browse the repository at this point in the history
✨ fully rework menu and inputs
  • Loading branch information
renardeinside authored Nov 23, 2024
2 parents f0cb526 + 768399e commit 53e88f8
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 74 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.2",
"@tailwindcss/typography": "^0.5.15",
Expand All @@ -32,7 +33,8 @@
"@tiptap/extension-placeholder": "^2.9.1",
"@tiptap/extension-task-item": "^2.9.1",
"@tiptap/extension-task-list": "^2.9.1",
"@tiptap/extension-underline": "^2.9.1",
"@tiptap/extension-text-align": "^2.10.2",
"@tiptap/extension-underline": "^2.10.2",
"@tiptap/pm": "^2.9.1",
"@tiptap/react": "^2.9.1",
"@tiptap/starter-kit": "^2.9.1",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const Editor = () => {
{loaded ? (
editor && (
<div className="flex px-8 pt-4 justify-center">
<EditorContent editor={editor} className="w-4/5" />
<EditorContent editor={editor} className="w-3/5" />
<EditorMenu editor={editor} />
</div>
)
Expand Down
95 changes: 53 additions & 42 deletions src/components/EditorMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,71 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlignCenter,
AlignJustify,
AlignLeft,
AlignRight,
Bold,
Edit3,
Italic,
Link,
Strikethrough,
Underline,
} from "lucide-react";
import { Editor } from "@tiptap/core";
import LinkInput from "./LinkInput";
import { Separator } from "@/components/ui/separator";

export default function EditorMenu({ editor }: { editor: Editor }) {
const [isOpen, setIsOpen] = useState(false);
const [openLinkInput, setOpenLinkInput] = useState(false);

const alignOptions = [
{
icon: AlignLeft,
title: "Align Left",
action: () => editor.chain().focus().setTextAlign("left").run(),
},
{
icon: AlignCenter,
title: "Align Center",
action: () => editor.chain().focus().setTextAlign("center").run(),
},
{
icon: AlignRight,
title: "Align Right",
action: () => editor.chain().focus().setTextAlign("right").run(),
},
{
icon: AlignJustify,
title: "Align Justify",
action: () => editor.chain().focus().setTextAlign("justify").run(),
},
];

const formatOptions = [
{
icon: Bold,
title: "Bold",
action: () => editor.chain().focus().toggleBold().run(),
variant: editor.isActive("bold") ? "default" : "ghost",
},
{
icon: Italic,
title: "Italic",
action: () => editor.chain().focus().toggleItalic().run(),
variant: editor.isActive("italic") ? "default" : "ghost",
},
{
icon: Strikethrough,
title: "Strikethrough",
action: () => editor.chain().focus().toggleStrike().run(),
variant: editor.isActive("strike") ? "default" : "ghost",
},
{
icon: Underline,
title: "Underline",
// @ts-expect-error - underline is not in the core
action: () => editor.chain().focus().toggleUnderline().run(),
variant: editor.isActive("underline") ? "default" : "ghost",
},
{
icon: Link,
title: "Link",
action: () => setOpenLinkInput(true),
variant: editor.isActive("link") ? "default" : "ghost",
},
];

Expand All @@ -61,38 +75,35 @@ export default function EditorMenu({ editor }: { editor: Editor }) {
<LinkInput setIsOpen={setOpenLinkInput} editor={editor} />
)}
<div className="fixed bottom-4 right-4 z-50">
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<div className="flex flex-col">
{alignOptions.map((option) => (
<Button
key={option.title}
variant="ghost"
size="icon"
className="h-10 w-10 rounded-sm"
title={option.title}
onClick={option.action}
>
<option.icon className="h-4 w-4" />
<span className="sr-only">{option.title}</span>
</Button>
))}
<Separator />
{formatOptions.map((option) => (
<Button
variant="outline"
key={option.title}
variant="ghost"
size="icon"
className="h-11 w-11 rounded-sm shadow-lg text-primary border-primary"
className="h-10 w-10 rounded-sm"
title={option.title}
onClick={option.action}
>
<Edit3 className="h-6 w-6" />
<span className="sr-only">Open text formatter</span>
<option.icon className="h-4 w-4" />
<span className="sr-only">{option.title}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
className="w-12 min-w-0 bg-background rounded-sm shadow-xl"
>
{formatOptions.map((option) => (
<DropdownMenuItem key={option.title} asChild>
<Button
// @ts-expect-error - string doesn't match enum
variant={option.variant}
size="icon"
className="h-10 w-10 rounded-sm"
title={option.title}
onClick={option.action}
>
<option.icon className="h-4 w-4" />
<span className="sr-only">{option.title}</span>
</Button>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
))}
</div>
</div>
</>
);
Expand Down
45 changes: 24 additions & 21 deletions src/components/LinkInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { toast } from "sonner";
import { Editor } from "@tiptap/core";

const formSchema = z.object({
link: z.string().url("Please enter a valid URL"),
link: z.string().url("Please enter a valid URL").or(z.literal("")),
text: z.string().min(1, "Please enter a valid text"),
});

Expand Down Expand Up @@ -57,22 +57,28 @@ export default function LinkInput({
setFormData({ link: "", text: "" });
setErrors({});

editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: validatedData.link })
.insertContentAt(
{
from: editor.state.selection.from,
to: editor.state.selection.to,
},
validatedData.text
)
.run();
if (!validatedData.link) {
editor.chain().focus().unsetLink().run();
toast.success("Link removed successfully");
} else {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: validatedData.link })
.insertContentAt(
{
from: editor.state.selection.from,
to: editor.state.selection.to,
},
validatedData.text
)
.run();

toast.success("Link added successfully");
toast.success("Link added successfully");
}
} catch (error) {
console.error(error);
if (error instanceof z.ZodError) {
setErrors(error.flatten().fieldErrors);
}
Expand Down Expand Up @@ -134,12 +140,9 @@ export default function LinkInput({
</div>
</div>
<div className="ml-auto space-x-2">
{formData.link && (
<Button variant={"destructive"} type="reset">
Remove Link
</Button>
)}
<Button type="submit">Save</Button>
<Button type="submit" variant={"outline"}>
Save
</Button>
</div>
</form>
</DialogContent>
Expand Down
29 changes: 29 additions & 0 deletions src/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

import { cn } from "@/lib/utils"

const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName

export { Separator }
15 changes: 10 additions & 5 deletions src/lib/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import Image from "@tiptap/extension-image";
import { nodePasteRule, type PasteRuleFinder } from "@tiptap/core";
import * as Y from "yjs";
import Collaboration from "@tiptap/extension-collaboration";
export const loadExtensions = (
doc: Y.Doc
) => {
import Underline from "@tiptap/extension-underline";
import TextAlign from '@tiptap/extension-text-align'

export const loadExtensions = (doc: Y.Doc) => {
const ImageFinder: PasteRuleFinder = /data:image\//g;

const ImageExtended = Image.extend({
Expand All @@ -37,9 +38,9 @@ export const loadExtensions = (

// define your extension array
const extensions = [
Underline,
StarterKit.configure({
codeBlock: false,
history: false,
}),
CodeBlockLowlight.configure({
lowlight,
Expand All @@ -51,10 +52,14 @@ export const loadExtensions = (
nested: true,
}),
ImageExtended,
// although we don't really use collaboration, it's just for Yjs to work
Collaboration.configure({
document: doc, // Configure Y.Doc for collaboration
}),
TextAlign.configure({
types: ['heading', 'paragraph']
})
];

return extensions;
};
};
Loading

0 comments on commit 53e88f8

Please sign in to comment.