From 3a1ccab9505cda7135e06bd87e00a0e0a72ff81f Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Fri, 24 Jan 2025 11:22:19 +0100 Subject: [PATCH 1/2] feat: add checklist support in RichText editor and viewer components --- apps/web/app/components/RichText/Editor.tsx | 16 ++++++++++- apps/web/app/components/RichText/Viever.tsx | 28 +++++++++++++++++-- .../RichText/toolbar/EditorToolbar.tsx | 17 +++++++++++ apps/web/package.json | 2 ++ pnpm-lock.yaml | 26 +++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/apps/web/app/components/RichText/Editor.tsx b/apps/web/app/components/RichText/Editor.tsx index fdf3f8305..adb9cae58 100644 --- a/apps/web/app/components/RichText/Editor.tsx +++ b/apps/web/app/components/RichText/Editor.tsx @@ -1,3 +1,5 @@ +import { TaskItem } from "@tiptap/extension-task-item"; +import { TaskList } from "@tiptap/extension-task-list"; import { EditorContent, useEditor } from "@tiptap/react"; // eslint-disable-next-line import/no-named-as-default import StarterKit from "@tiptap/starter-kit"; @@ -16,7 +18,19 @@ type EditorProps = { const Editor = ({ content, placeholder, onChange, id, className }: EditorProps) => { const editor = useEditor({ - extensions: [StarterKit], + extensions: [ + StarterKit, + TaskList, + TaskItem.configure({ + nested: true, + HTMLAttributes: { + class: "flex items-start gap-2 [&_p]:inline [&_p]:m-0", + }, + onReadOnlyChecked: (_node, _checked) => { + return true; + }, + }), + ], content: content, onUpdate: ({ editor }) => { onChange(editor.getHTML()); diff --git a/apps/web/app/components/RichText/Viever.tsx b/apps/web/app/components/RichText/Viever.tsx index db56e8a57..c8529f656 100644 --- a/apps/web/app/components/RichText/Viever.tsx +++ b/apps/web/app/components/RichText/Viever.tsx @@ -1,3 +1,5 @@ +import { TaskItem } from "@tiptap/extension-task-item"; +import { TaskList } from "@tiptap/extension-task-list"; import { EditorContent, useEditor } from "@tiptap/react"; // eslint-disable-next-line import/no-named-as-default import StarterKit from "@tiptap/starter-kit"; @@ -12,8 +14,9 @@ type ViewerProps = { }; const defaultClasses = { - ul: "[&>div>ul]:list-disc [&>div>ul]:list-inside [&>div>ul>li>p]:inline", + ul: "[&>div>ul]:list-disc [&>div>ul]:list-inside [&>div>ul>li>p]:inline [&_ul]:list-disc [&_ul]:ml-6 [&_ul>li>p]:inline", ol: "[&>div>ol]:list-decimal [&>div>ol]:list-inside [&>div>ol>li>p]:inline", + taskList: "[&_[data-type='taskList']]:list-none [&_[data-type='taskList']]:pl-0", }; const lessonVariantClasses = { @@ -26,7 +29,21 @@ const lessonVariantClasses = { const Viewer = ({ content, style, className, variant = "default" }: ViewerProps) => { const editor = useEditor({ - extensions: [StarterKit], + extensions: [ + StarterKit, + TaskList.configure({ + HTMLAttributes: { + class: "list-none", + }, + }), + TaskItem.configure({ + nested: true, + HTMLAttributes: { + class: "flex items-start gap-2", + }, + onReadOnlyChecked: (_node, _checked) => true, + }), + ], content: content, editable: false, }); @@ -49,7 +66,12 @@ const Viewer = ({ content, style, className, variant = "default" }: ViewerProps) ] : []; - const editorClasses = cn(defaultClasses.ul, defaultClasses.ol, ...variantClasses); + const editorClasses = cn( + defaultClasses.ul, + defaultClasses.ol, + defaultClasses.taskList, + ...variantClasses, + ); return (
diff --git a/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx b/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx index 180d98dbe..209fef853 100644 --- a/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx +++ b/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx @@ -9,6 +9,7 @@ import { Redo, Strikethrough, Undo, + CheckSquare, } from "lucide-react"; import { Button } from "~/components/ui/button"; @@ -135,6 +136,22 @@ const EditorToolbar = ({ editor }: EditorToolbarProps) => { Horizontal Rule: Inserts a horizontal line + + + + + + Checkbox: Adds a checklist + + Date: Fri, 24 Jan 2025 16:55:10 +0100 Subject: [PATCH 2/2] refactor: consolidate rich text editor and viewer configurations through plugins and default styles --- apps/web/app/components/RichText/Editor.tsx | 29 +++++++------------ apps/web/app/components/RichText/Viever.tsx | 29 +++---------------- apps/web/app/components/RichText/plugins.ts | 19 ++++++++++++ apps/web/app/components/RichText/styles.ts | 25 ++++++++++++++++ .../RichText/toolbar/EditorToolbar.tsx | 8 +++-- 5 files changed, 64 insertions(+), 46 deletions(-) create mode 100644 apps/web/app/components/RichText/plugins.ts create mode 100644 apps/web/app/components/RichText/styles.ts diff --git a/apps/web/app/components/RichText/Editor.tsx b/apps/web/app/components/RichText/Editor.tsx index adb9cae58..b7ce866c3 100644 --- a/apps/web/app/components/RichText/Editor.tsx +++ b/apps/web/app/components/RichText/Editor.tsx @@ -1,11 +1,9 @@ -import { TaskItem } from "@tiptap/extension-task-item"; -import { TaskList } from "@tiptap/extension-task-list"; import { EditorContent, useEditor } from "@tiptap/react"; -// eslint-disable-next-line import/no-named-as-default -import StarterKit from "@tiptap/starter-kit"; import { cn } from "~/lib/utils"; +import { plugins } from "./plugins"; +import { defaultClasses } from "./styles"; import EditorToolbar from "./toolbar/EditorToolbar"; type EditorProps = { @@ -18,19 +16,7 @@ type EditorProps = { const Editor = ({ content, placeholder, onChange, id, className }: EditorProps) => { const editor = useEditor({ - extensions: [ - StarterKit, - TaskList, - TaskItem.configure({ - nested: true, - HTMLAttributes: { - class: "flex items-start gap-2 [&_p]:inline [&_p]:m-0", - }, - onReadOnlyChecked: (_node, _checked) => { - return true; - }, - }), - ], + extensions: [...plugins], content: content, onUpdate: ({ editor }) => { onChange(editor.getHTML()); @@ -44,6 +30,8 @@ const Editor = ({ content, placeholder, onChange, id, className }: EditorProps) if (!editor) return <>; + const editorClasses = cn("h-full", defaultClasses.ul, defaultClasses.ol, defaultClasses.taskList); + return (
@@ -53,7 +41,12 @@ const Editor = ({ content, placeholder, onChange, id, className }: EditorProps) className, )} > - +
); diff --git a/apps/web/app/components/RichText/Viever.tsx b/apps/web/app/components/RichText/Viever.tsx index c8529f656..b580d0ec7 100644 --- a/apps/web/app/components/RichText/Viever.tsx +++ b/apps/web/app/components/RichText/Viever.tsx @@ -1,11 +1,10 @@ -import { TaskItem } from "@tiptap/extension-task-item"; -import { TaskList } from "@tiptap/extension-task-list"; import { EditorContent, useEditor } from "@tiptap/react"; -// eslint-disable-next-line import/no-named-as-default -import StarterKit from "@tiptap/starter-kit"; import { cn } from "~/lib/utils"; +import { plugins } from "./plugins"; +import { defaultClasses } from "./styles"; + type ViewerProps = { content: string; style?: "default" | "prose"; @@ -13,12 +12,6 @@ type ViewerProps = { variant?: "default" | "lesson"; }; -const defaultClasses = { - ul: "[&>div>ul]:list-disc [&>div>ul]:list-inside [&>div>ul>li>p]:inline [&_ul]:list-disc [&_ul]:ml-6 [&_ul>li>p]:inline", - ol: "[&>div>ol]:list-decimal [&>div>ol]:list-inside [&>div>ol>li>p]:inline", - taskList: "[&_[data-type='taskList']]:list-none [&_[data-type='taskList']]:pl-0", -}; - const lessonVariantClasses = { layout: "[&>div]:flex [&>div]:flex-col [&>div]:gap-y-6", h2: "[&>div>h2]:h6 [&>div>h2]:text-neutral-950", @@ -29,21 +22,7 @@ const lessonVariantClasses = { const Viewer = ({ content, style, className, variant = "default" }: ViewerProps) => { const editor = useEditor({ - extensions: [ - StarterKit, - TaskList.configure({ - HTMLAttributes: { - class: "list-none", - }, - }), - TaskItem.configure({ - nested: true, - HTMLAttributes: { - class: "flex items-start gap-2", - }, - onReadOnlyChecked: (_node, _checked) => true, - }), - ], + extensions: [...plugins], content: content, editable: false, }); diff --git a/apps/web/app/components/RichText/plugins.ts b/apps/web/app/components/RichText/plugins.ts new file mode 100644 index 000000000..993ee4000 --- /dev/null +++ b/apps/web/app/components/RichText/plugins.ts @@ -0,0 +1,19 @@ +import { TaskItem } from "@tiptap/extension-task-item"; +import { TaskList } from "@tiptap/extension-task-list"; +import { StarterKit } from "@tiptap/starter-kit"; + +export const plugins = [ + StarterKit, + TaskList.configure({ + HTMLAttributes: { + class: "list-none", + }, + }), + TaskItem.configure({ + nested: true, + HTMLAttributes: { + class: "flex items-start gap-2 [&_p]:inline [&_p]:m-0", + }, + onReadOnlyChecked: (_node, _checked) => true, + }), +]; diff --git a/apps/web/app/components/RichText/styles.ts b/apps/web/app/components/RichText/styles.ts new file mode 100644 index 000000000..da2f60f12 --- /dev/null +++ b/apps/web/app/components/RichText/styles.ts @@ -0,0 +1,25 @@ +export const defaultClasses = { + ul: ` + [&>div>ul]:list-disc + [&>div>ul]:pl-5 + [&>div>ul>li>p]:inline + [&>div>ul>li>p]:text-neutral-900 + + [&_ul]:list-disc + [&_[contenteditable='true']>ul>li]:pl-0 + [&_[contenteditable='true']>ul>li_ul_li]:pl-0 + [&_[contenteditable='false']>ul>li]:pl-4 + [&_[contenteditable='false']>ul>li_ul_li]:pl-4 + [&_ul>li]:marker:text-neutral-400 + [&_ul>li>p]:inline + [&_ul>li>p]:text-neutral-900 + `, + ol: ` + [&>div>ol]:list-decimal + [&>div>ol]:list-inside + [&>div>ol>li>p]:inline + [&>div>ol>li>ol]:pl-4 + [&_ol>li>ol]:pl-4 + `, + taskList: "[&_[data-type='taskList']]:list-none [&_[data-type='taskList']]:pl-0", +}; diff --git a/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx b/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx index 209fef853..b92e9ed54 100644 --- a/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx +++ b/apps/web/app/components/RichText/toolbar/EditorToolbar.tsx @@ -15,6 +15,7 @@ import { import { Button } from "~/components/ui/button"; import { ToggleGroup, Toolbar } from "~/components/ui/toolbar"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; +import { cn } from "~/lib/utils"; import { FormatType } from "./FormatType"; @@ -141,9 +142,10 @@ const EditorToolbar = ({ editor }: EditorToolbarProps) => {