diff --git a/packages/editor/core/src/hooks/use-editor.tsx b/packages/editor/core/src/hooks/use-editor.tsx index 647b79929..78d252c81 100644 --- a/packages/editor/core/src/hooks/use-editor.tsx +++ b/packages/editor/core/src/hooks/use-editor.tsx @@ -142,11 +142,11 @@ export const useEditor = ({ executeMenuItemCommand: (itemName: EditorMenuItemNames) => { const editorItems = getEditorMenuItems(editorRef.current, uploadFile); - const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.name === itemName); + const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName); const item = getEditorMenuItem(itemName); if (item) { - if (item.name === "image") { + if (item.key === "image") { item.command(savedSelection); } else { item.command(); @@ -158,7 +158,7 @@ export const useEditor = ({ isMenuItemActive: (itemName: EditorMenuItemNames): boolean => { const editorItems = getEditorMenuItems(editorRef.current, uploadFile); - const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.name === itemName); + const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName); const item = getEditorMenuItem(itemName); return item ? item.isActive() : false; }, diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index f0c6c85e0..ce2cf3ad6 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -4,6 +4,11 @@ import { findTableAncestor } from "src/lib/utils"; import { Selection } from "@tiptap/pm/state"; import { UploadImage } from "src/types/upload-image"; +export const setText = (editor: Editor, range?: Range) => { + if (range) editor.chain().focus().deleteRange(range).clearNodes().run(); + else editor.chain().focus().clearNodes().run(); +}; + export const toggleHeadingOne = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); else editor.chain().focus().toggleHeading({ level: 1 }).run(); @@ -19,6 +24,21 @@ export const toggleHeadingThree = (editor: Editor, range?: Range) => { else editor.chain().focus().toggleHeading({ level: 3 }).run(); }; +export const toggleHeadingFour = (editor: Editor, range?: Range) => { + if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 4 }).run(); + else editor.chain().focus().toggleHeading({ level: 4 }).run(); +}; + +export const toggleHeadingFive = (editor: Editor, range?: Range) => { + if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 5 }).run(); + else editor.chain().focus().toggleHeading({ level: 5 }).run(); +}; + +export const toggleHeadingSix = (editor: Editor, range?: Range) => { + if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 6 }).run(); + else editor.chain().focus().toggleHeading({ level: 6 }).run(); +}; + export const toggleBold = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).toggleBold().run(); else editor.chain().focus().toggleBold().run(); diff --git a/packages/editor/core/src/styles/editor.css b/packages/editor/core/src/styles/editor.css index 5868fce91..d602a1ddf 100644 --- a/packages/editor/core/src/styles/editor.css +++ b/packages/editor/core/src/styles/editor.css @@ -330,6 +330,29 @@ ul[data-type="taskList"] ul[data-type="taskList"] { line-height: 1.3; } +.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + margin-top: 1rem; + margin-bottom: 1px; + font-size: 1rem; + line-height: 1.5; +} + +.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + margin-top: 1rem; + margin-bottom: 1px; + font-size: 0.9rem; + font-weight: 600; + line-height: 1.5; +} + +.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + margin-top: 1rem; + margin-bottom: 1px; + font-size: 0.83rem; + font-weight: 600; + line-height: 1.5; +} + .prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 0.25rem; margin-bottom: 1px; diff --git a/packages/editor/core/src/ui/menus/menu-items/index.tsx b/packages/editor/core/src/ui/menus/menu-items/index.tsx index 66736e0ea..46b1ed92a 100644 --- a/packages/editor/core/src/ui/menus/menu-items/index.tsx +++ b/packages/editor/core/src/ui/menus/menu-items/index.tsx @@ -13,16 +13,24 @@ import { UnderlineIcon, StrikethroughIcon, CodeIcon, + Heading4, + Heading5, + Heading6, + CaseSensitive, } from "lucide-react"; import { Editor } from "@tiptap/react"; import { insertImageCommand, insertTableCommand, + setText, toggleBlockquote, toggleBold, toggleBulletList, toggleCodeBlock, + toggleHeadingFive, + toggleHeadingFour, toggleHeadingOne, + toggleHeadingSix, toggleHeadingThree, toggleHeadingTwo, toggleItalic, @@ -36,15 +44,26 @@ import { UploadImage } from "src/types/upload-image"; import { Selection } from "@tiptap/pm/state"; export interface EditorMenuItem { + key: string; name: string; isActive: () => boolean; command: () => void; icon: LucideIconType; } +export const TextItem = (editor: Editor) => + ({ + key: "text", + name: "Text", + isActive: () => editor.isActive("paragraph"), + command: () => setText(editor), + icon: CaseSensitive, + }) as const satisfies EditorMenuItem; + export const HeadingOneItem = (editor: Editor) => ({ - name: "H1", + key: "h1", + name: "Heading 1", isActive: () => editor.isActive("heading", { level: 1 }), command: () => toggleHeadingOne(editor), icon: Heading1, @@ -52,7 +71,8 @@ export const HeadingOneItem = (editor: Editor) => export const HeadingTwoItem = (editor: Editor) => ({ - name: "H2", + key: "h2", + name: "Heading 2", isActive: () => editor.isActive("heading", { level: 2 }), command: () => toggleHeadingTwo(editor), icon: Heading2, @@ -60,15 +80,44 @@ export const HeadingTwoItem = (editor: Editor) => export const HeadingThreeItem = (editor: Editor) => ({ - name: "H3", + key: "h3", + name: "Heading 3", isActive: () => editor.isActive("heading", { level: 3 }), command: () => toggleHeadingThree(editor), icon: Heading3, }) as const satisfies EditorMenuItem; +export const HeadingFourItem = (editor: Editor) => + ({ + key: "h4", + name: "Heading 4", + isActive: () => editor.isActive("heading", { level: 4 }), + command: () => toggleHeadingFour(editor), + icon: Heading4, + }) as const satisfies EditorMenuItem; + +export const HeadingFiveItem = (editor: Editor) => + ({ + key: "h5", + name: "Heading 5", + isActive: () => editor.isActive("heading", { level: 5 }), + command: () => toggleHeadingFive(editor), + icon: Heading5, + }) as const satisfies EditorMenuItem; + +export const HeadingSixItem = (editor: Editor) => + ({ + key: "h6", + name: "Heading 6", + isActive: () => editor.isActive("heading", { level: 6 }), + command: () => toggleHeadingSix(editor), + icon: Heading6, + }) as const satisfies EditorMenuItem; + export const BoldItem = (editor: Editor) => ({ - name: "bold", + key: "bold", + name: "Bold", isActive: () => editor?.isActive("bold"), command: () => toggleBold(editor), icon: BoldIcon, @@ -76,7 +125,8 @@ export const BoldItem = (editor: Editor) => export const ItalicItem = (editor: Editor) => ({ - name: "italic", + key: "italic", + name: "Italic", isActive: () => editor?.isActive("italic"), command: () => toggleItalic(editor), icon: ItalicIcon, @@ -84,7 +134,8 @@ export const ItalicItem = (editor: Editor) => export const UnderLineItem = (editor: Editor) => ({ - name: "underline", + key: "underline", + name: "Underline", isActive: () => editor?.isActive("underline"), command: () => toggleUnderline(editor), icon: UnderlineIcon, @@ -92,7 +143,8 @@ export const UnderLineItem = (editor: Editor) => export const StrikeThroughItem = (editor: Editor) => ({ - name: "strike", + key: "strikethrough", + name: "Strikethrough", isActive: () => editor?.isActive("strike"), command: () => toggleStrike(editor), icon: StrikethroughIcon, @@ -100,47 +152,53 @@ export const StrikeThroughItem = (editor: Editor) => export const BulletListItem = (editor: Editor) => ({ - name: "bullet-list", + key: "bulleted-list", + name: "Bulleted list", isActive: () => editor?.isActive("bulletList"), command: () => toggleBulletList(editor), icon: ListIcon, }) as const satisfies EditorMenuItem; -export const TodoListItem = (editor: Editor) => - ({ - name: "To-do List", - isActive: () => editor.isActive("taskItem"), - command: () => toggleTaskList(editor), - icon: CheckSquare, - }) as const satisfies EditorMenuItem; - -export const CodeItem = (editor: Editor) => - ({ - name: "code", - isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), - command: () => toggleCodeBlock(editor), - icon: CodeIcon, - }) as const satisfies EditorMenuItem; - export const NumberedListItem = (editor: Editor) => ({ - name: "ordered-list", + key: "numbered-list", + name: "Numbered list", isActive: () => editor?.isActive("orderedList"), command: () => toggleOrderedList(editor), icon: ListOrderedIcon, }) as const satisfies EditorMenuItem; +export const TodoListItem = (editor: Editor) => + ({ + key: "to-do-list", + name: "To-do list", + isActive: () => editor.isActive("taskItem"), + command: () => toggleTaskList(editor), + icon: CheckSquare, + }) as const satisfies EditorMenuItem; + export const QuoteItem = (editor: Editor) => ({ - name: "quote", + key: "quote", + name: "Quote", isActive: () => editor?.isActive("blockquote"), command: () => toggleBlockquote(editor), icon: QuoteIcon, }) as const satisfies EditorMenuItem; +export const CodeItem = (editor: Editor) => + ({ + key: "code", + name: "Code", + isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), + command: () => toggleCodeBlock(editor), + icon: CodeIcon, + }) as const satisfies EditorMenuItem; + export const TableItem = (editor: Editor) => ({ - name: "table", + key: "table", + name: "Table", isActive: () => editor?.isActive("table"), command: () => insertTableCommand(editor), icon: TableIcon, @@ -148,7 +206,8 @@ export const TableItem = (editor: Editor) => export const ImageItem = (editor: Editor, uploadFile: UploadImage) => ({ - name: "image", + key: "image", + name: "Image", isActive: () => editor?.isActive("image"), command: (savedSelection: Selection | null) => insertImageCommand(editor, uploadFile, savedSelection), icon: ImageIcon, @@ -159,9 +218,13 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag return []; } return [ + TextItem(editor), HeadingOneItem(editor), HeadingTwoItem(editor), HeadingThreeItem(editor), + HeadingFourItem(editor), + HeadingFiveItem(editor), + HeadingSixItem(editor), BoldItem(editor), ItalicItem(editor), UnderLineItem(editor), @@ -177,7 +240,7 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag } export type EditorMenuItemNames = ReturnType extends (infer U)[] - ? U extends { name: infer N } + ? U extends { key: infer N } ? N : never : never; diff --git a/packages/editor/document-editor/src/ui/menu/block-menu.tsx b/packages/editor/document-editor/src/ui/menu/block-menu.tsx index 6fc9a87fe..8a303809c 100644 --- a/packages/editor/document-editor/src/ui/menu/block-menu.tsx +++ b/packages/editor/document-editor/src/ui/menu/block-menu.tsx @@ -67,11 +67,13 @@ export default function BlockMenu(props: BlockMenuProps) { popup.current?.hide(); }; document.addEventListener("click", handleClickDragHandle); + document.addEventListener("contextmenu", handleClickDragHandle); document.addEventListener("keydown", handleKeyDown); document.addEventListener("scroll", handleScroll, true); // Using capture phase return () => { document.removeEventListener("click", handleClickDragHandle); + document.removeEventListener("contextmenu", handleClickDragHandle); document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("scroll", handleScroll, true); }; diff --git a/packages/editor/extensions/src/extensions/drag-drop.tsx b/packages/editor/extensions/src/extensions/drag-drop.tsx index e9ef9c06e..ab2df31ad 100644 --- a/packages/editor/extensions/src/extensions/drag-drop.tsx +++ b/packages/editor/extensions/src/extensions/drag-drop.tsx @@ -225,6 +225,9 @@ function DragHandle(options: DragHandleOptions) { dragHandleElement.addEventListener("click", (e) => { handleClick(e, view); }); + dragHandleElement.addEventListener("contextmenu", (e) => { + handleClick(e, view); + }); dragHandleElement.addEventListener("drag", (e) => { hideDragHandle(); diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx index 2dbc86cec..c11f0593d 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx @@ -15,6 +15,7 @@ import { } from "@plane/editor-core"; export interface BubbleMenuItem { + key: string; name: string; isActive: () => boolean; command: () => void; diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx index 1bb8c38bd..5c1c8479f 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx @@ -8,9 +8,13 @@ import { QuoteItem, CodeItem, TodoListItem, + TextItem, + HeadingFourItem, + HeadingFiveItem, + HeadingSixItem, } from "@plane/editor-core"; import { Editor } from "@tiptap/react"; -import { Check, ChevronDown, TextIcon } from "lucide-react"; +import { Check, ChevronDown } from "lucide-react"; import { Dispatch, FC, SetStateAction } from "react"; import { BubbleMenuItem } from "."; @@ -23,18 +27,16 @@ interface NodeSelectorProps { export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { const items: BubbleMenuItem[] = [ - { - name: "Text", - icon: TextIcon, - command: () => editor.chain().focus().clearNodes().run(), - isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"), - }, + TextItem(editor), HeadingOneItem(editor), HeadingTwoItem(editor), HeadingThreeItem(editor), - TodoListItem(editor), + HeadingFourItem(editor), + HeadingFiveItem(editor), + HeadingSixItem(editor), BulletListItem(editor), NumberedListItem(editor), + TodoListItem(editor), QuoteItem(editor), CodeItem(editor), ]; @@ -58,7 +60,7 @@ export const NodeSelector: FC = ({ editor, isOpen, setIsOpen {isOpen && ( -
+
{items.map((item) => ( ))}
diff --git a/space/constants/editor.ts b/space/constants/editor.ts index bdd07f0c5..eb8b99495 100644 --- a/space/constants/editor.ts +++ b/space/constants/editor.ts @@ -4,6 +4,9 @@ import { Heading1, Heading2, Heading3, + Heading4, + Heading5, + Heading6, Image, Italic, List, @@ -29,14 +32,17 @@ export type ToolbarMenuItem = { }; export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ - { key: "H1", name: "Heading 1", icon: Heading1, editors: ["document"] }, - { key: "H2", name: "Heading 2", icon: Heading2, editors: ["document"] }, - { key: "H3", name: "Heading 3", icon: Heading3, editors: ["document"] }, + { key: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] }, + { key: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] }, + { key: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] }, + { key: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] }, + { key: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] }, + { key: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] }, { key: "bold", name: "Bold", icon: Bold, shortcut: ["Cmd", "B"], editors: ["lite", "document"] }, { key: "italic", name: "Italic", icon: Italic, shortcut: ["Cmd", "I"], editors: ["lite", "document"] }, { key: "underline", name: "Underline", icon: Underline, shortcut: ["Cmd", "U"], editors: ["lite", "document"] }, { - key: "strike", + key: "strikethrough", name: "Strikethrough", icon: Strikethrough, shortcut: ["Cmd", "Shift", "S"], @@ -46,21 +52,21 @@ export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ export const LIST_ITEMS: ToolbarMenuItem[] = [ { - key: "bullet-list", + key: "bulleted-list", name: "Bulleted list", icon: List, shortcut: ["Cmd", "Shift", "7"], editors: ["lite", "document"], }, { - key: "ordered-list", + key: "numbered-list", name: "Numbered list", icon: ListOrdered, shortcut: ["Cmd", "Shift", "8"], editors: ["lite", "document"], }, { - key: "To-do List", + key: "to-do-list", name: "To-do list", icon: ListTodo, shortcut: ["Cmd", "Shift", "9"], diff --git a/web/components/pages/editor/header/toolbar.tsx b/web/components/pages/editor/header/toolbar.tsx index a23d57e77..b92e1eac9 100644 --- a/web/components/pages/editor/header/toolbar.tsx +++ b/web/components/pages/editor/header/toolbar.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState, useCallback } from "react"; +import { Check, ChevronDown } from "lucide-react"; // editor import { EditorMenuItemNames, EditorRefApi } from "@plane/document-editor"; // ui -import { Tooltip } from "@plane/ui"; +import { CustomMenu, Tooltip } from "@plane/ui"; // constants -import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor"; +import { TOOLBAR_ITEMS, TYPOGRAPHY_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers import { cn } from "@/helpers/common.helper"; @@ -34,12 +35,12 @@ const ToolbarButton: React.FC = React.memo((props) => { key={item.key} type="button" onClick={() => executeCommand(item.key)} - className={cn("grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80", { + className={cn("grid size-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80", { "bg-custom-background-80 text-custom-text-100": isActive, })} > @@ -71,8 +72,36 @@ export const PageToolbar: React.FC = ({ editorRef }) => { return () => unsubscribe(); }, [editorRef, updateActiveStates]); + const activeTypography = TYPOGRAPHY_ITEMS.find((item) => editorRef.isMenuItemActive(item.key)); + return (
+ + {activeTypography?.name || "Text"} + + + } + className="pr-2" + placement="bottom-start" + closeOnSelect + maxHeight="lg" + > + {TYPOGRAPHY_ITEMS.map((item) => ( + editorRef.executeMenuItemCommand(item.key)} + > + + + {item.name} + + {activeTypography?.key === item.key && } + + ))} + {Object.keys(toolbarItems).map((key) => (
{toolbarItems[key].map((item) => ( diff --git a/web/constants/editor.ts b/web/constants/editor.ts index d80da201c..2247d9f5c 100644 --- a/web/constants/editor.ts +++ b/web/constants/editor.ts @@ -1,9 +1,13 @@ import { Bold, + CaseSensitive, Code2, Heading1, Heading2, Heading3, + Heading4, + Heading5, + Heading6, Image, Italic, List, @@ -28,15 +32,22 @@ export type ToolbarMenuItem = { editors: TEditorTypes[]; }; -export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ - { key: "H1", name: "Heading 1", icon: Heading1, editors: ["document"] }, - { key: "H2", name: "Heading 2", icon: Heading2, editors: ["document"] }, - { key: "H3", name: "Heading 3", icon: Heading3, editors: ["document"] }, +export const TYPOGRAPHY_ITEMS: ToolbarMenuItem[] = [ + { key: "text", name: "Text", icon: CaseSensitive, editors: ["document"] }, + { key: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] }, + { key: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] }, + { key: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] }, + { key: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] }, + { key: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] }, + { key: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] }, +]; + +const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ { key: "bold", name: "Bold", icon: Bold, shortcut: ["Cmd", "B"], editors: ["lite", "document"] }, { key: "italic", name: "Italic", icon: Italic, shortcut: ["Cmd", "I"], editors: ["lite", "document"] }, { key: "underline", name: "Underline", icon: Underline, shortcut: ["Cmd", "U"], editors: ["lite", "document"] }, { - key: "strike", + key: "strikethrough", name: "Strikethrough", icon: Strikethrough, shortcut: ["Cmd", "Shift", "S"], @@ -44,23 +55,23 @@ export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ }, ]; -export const LIST_ITEMS: ToolbarMenuItem[] = [ +const LIST_ITEMS: ToolbarMenuItem[] = [ { - key: "bullet-list", + key: "bulleted-list", name: "Bulleted list", icon: List, shortcut: ["Cmd", "Shift", "7"], editors: ["lite", "document"], }, { - key: "ordered-list", + key: "numbered-list", name: "Numbered list", icon: ListOrdered, shortcut: ["Cmd", "Shift", "8"], editors: ["lite", "document"], }, { - key: "To-do List", + key: "to-do-list", name: "To-do list", icon: ListTodo, shortcut: ["Cmd", "Shift", "9"], @@ -68,12 +79,12 @@ export const LIST_ITEMS: ToolbarMenuItem[] = [ }, ]; -export const USER_ACTION_ITEMS: ToolbarMenuItem[] = [ +const USER_ACTION_ITEMS: ToolbarMenuItem[] = [ { key: "quote", name: "Quote", icon: Quote, editors: ["lite", "document"] }, { key: "code", name: "Code", icon: Code2, editors: ["lite", "document"] }, ]; -export const COMPLEX_ITEMS: ToolbarMenuItem[] = [ +const COMPLEX_ITEMS: ToolbarMenuItem[] = [ { key: "table", name: "Table", icon: Table, editors: ["document"] }, { key: "image", name: "Image", icon: Image, editors: ["lite", "document"] }, ];