From 23cfb36ee7c144d8ca1a9183344848743d2707f4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 19 Sep 2024 01:48:26 +0530 Subject: [PATCH 1/6] feat: add text color and highlight options to pages --- packages/editor/package.json | 4 +- .../src/ce/extensions/document-extensions.tsx | 4 +- .../components/editors/rich-text/editor.tsx | 4 +- .../menus/bubble-menu/node-selector.tsx | 6 +- .../components/menus/bubble-menu/root.tsx | 18 +- .../src/core/components/menus/menu-items.ts | 49 +- packages/editor/src/core/constants/common.ts | 51 +++ .../src/core/extensions/core-without-props.ts | 6 + .../editor/src/core/extensions/extensions.tsx | 6 + packages/editor/src/core/extensions/index.ts | 2 +- .../core/extensions/read-only-extensions.tsx | 6 + .../src/core/extensions/slash-commands.tsx | 423 ------------------ .../slash-commands/command-items-list.tsx | 294 ++++++++++++ .../slash-commands/command-menu-item.tsx | 37 ++ .../slash-commands/command-menu.tsx | 144 ++++++ .../core/extensions/slash-commands/index.ts | 1 + .../core/extensions/slash-commands/root.tsx | 114 +++++ .../src/core/helpers/editor-commands.ts | 39 ++ packages/editor/src/core/hooks/use-editor.ts | 20 +- packages/editor/src/core/types/editor.ts | 25 +- .../core/types/slash-commands-suggestion.ts | 15 +- packages/editor/src/index.ts | 3 + .../components/editor/lite-text-editor.tsx | 6 +- space/core/components/editor/toolbar.tsx | 6 +- .../lite-text-editor/lite-text-editor.tsx | 6 +- .../editor/lite-text-editor/toolbar.tsx | 6 +- .../pages/editor/header/color-dropdown.tsx | 116 +++++ .../components/pages/editor/header/index.ts | 1 + .../pages/editor/header/toolbar.tsx | 42 +- yarn.lock | 51 +-- 30 files changed, 991 insertions(+), 514 deletions(-) create mode 100644 packages/editor/src/core/constants/common.ts delete mode 100644 packages/editor/src/core/extensions/slash-commands.tsx create mode 100644 packages/editor/src/core/extensions/slash-commands/command-items-list.tsx create mode 100644 packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx create mode 100644 packages/editor/src/core/extensions/slash-commands/command-menu.tsx create mode 100644 packages/editor/src/core/extensions/slash-commands/index.ts create mode 100644 packages/editor/src/core/extensions/slash-commands/root.tsx create mode 100644 web/core/components/pages/editor/header/color-dropdown.tsx diff --git a/packages/editor/package.json b/packages/editor/package.json index b92336ddde1..3201c14f562 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -41,13 +41,15 @@ "@tiptap/extension-blockquote": "^2.1.13", "@tiptap/extension-character-count": "^2.6.5", "@tiptap/extension-collaboration": "^2.3.2", + "@tiptap/extension-color": "^2.7.1", + "@tiptap/extension-highlight": "^2.7.1", "@tiptap/extension-image": "^2.1.13", "@tiptap/extension-list-item": "^2.1.13", "@tiptap/extension-mention": "^2.1.13", "@tiptap/extension-placeholder": "^2.3.0", "@tiptap/extension-task-item": "^2.1.13", "@tiptap/extension-task-list": "^2.1.13", - "@tiptap/extension-text-style": "^2.1.13", + "@tiptap/extension-text-style": "^2.7.1", "@tiptap/extension-underline": "^2.1.13", "@tiptap/pm": "^2.1.13", "@tiptap/react": "^2.1.13", diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 93900700b27..2809fcee4ef 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -1,6 +1,6 @@ import { HocuspocusProvider } from "@hocuspocus/provider"; import { Extensions } from "@tiptap/core"; -import { SlashCommand } from "@/extensions"; +import { SlashCommands } from "@/extensions"; // plane editor types import { TIssueEmbedConfig } from "@/plane-editor/types"; // types @@ -14,7 +14,7 @@ type Props = { }; export const DocumentEditorAdditionalExtensions = (_props: Props) => { - const extensions: Extensions = [SlashCommand()]; + const extensions: Extensions = [SlashCommands()]; return extensions; }; diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index fe4d2d51373..53f766ee21a 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -3,7 +3,7 @@ import { forwardRef, useCallback } from "react"; import { EditorWrapper } from "@/components/editors"; import { EditorBubbleMenu } from "@/components/menus"; // extensions -import { SideMenuExtension, SlashCommand } from "@/extensions"; +import { SideMenuExtension, SlashCommands } from "@/extensions"; // types import { EditorRefApi, IRichTextEditor } from "@/types"; @@ -11,7 +11,7 @@ const RichTextEditor = (props: IRichTextEditor) => { const { dragDropEnabled } = props; const getExtensions = useCallback(() => { - const extensions = [SlashCommand()]; + const extensions = [SlashCommands()]; extensions.push( SideMenuExtension({ diff --git a/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx index 466d07c0db9..0c36942be2a 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx @@ -15,7 +15,7 @@ import { HeadingFourItem, HeadingFiveItem, HeadingSixItem, - BubbleMenuItem, + EditorMenuItem, } from "@/components/menus"; // helpers import { cn } from "@/helpers/common"; @@ -27,7 +27,7 @@ type Props = { }; export const BubbleMenuNodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { - const items: BubbleMenuItem[] = [ + const items: EditorMenuItem[] = [ TextItem(editor), HeadingOneItem(editor), HeadingTwoItem(editor), @@ -42,7 +42,7 @@ export const BubbleMenuNodeSelector: FC = ({ editor, isOpen, setIsOpen }) CodeItem(editor), ]; - const activeItem = items.filter((item) => item.isActive()).pop() ?? { + const activeItem = items.filter((item) => item.isActive("")).pop() ?? { name: "Multiple", }; diff --git a/packages/editor/src/core/components/menus/bubble-menu/root.tsx b/packages/editor/src/core/components/menus/bubble-menu/root.tsx index ec72f154086..2fab2b14ecf 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/root.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/root.tsx @@ -1,12 +1,12 @@ import { FC, useEffect, useState } from "react"; import { BubbleMenu, BubbleMenuProps, isNodeSelection } from "@tiptap/react"; -import { LucideIcon } from "lucide-react"; // components import { BoldItem, BubbleMenuLinkSelector, BubbleMenuNodeSelector, CodeItem, + EditorMenuItem, ItalicItem, StrikeThroughItem, UnderLineItem, @@ -16,18 +16,10 @@ import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-sele // helpers import { cn } from "@/helpers/common"; -export interface BubbleMenuItem { - key: string; - name: string; - isActive: () => boolean; - command: () => void; - icon: LucideIcon; -} - type EditorBubbleMenuProps = Omit; export const EditorBubbleMenu: FC = (props: any) => { - const items: BubbleMenuItem[] = [ + const items: EditorMenuItem[] = [ ...(props.editor.isActive("code") ? [] : [ @@ -129,7 +121,7 @@ export const EditorBubbleMenu: FC = (props: any) => {
{items.map((item) => ( diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index b60196beacd..e860efbcddc 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -20,11 +20,13 @@ import { Heading6, CaseSensitive, LucideIcon, + Palette, } from "lucide-react"; // helpers import { insertTableCommand, setText, + toggleBackgroundColor, toggleBlockquote, toggleBold, toggleBulletList, @@ -39,18 +41,26 @@ import { toggleOrderedList, toggleStrike, toggleTaskList, + toggleTextColor, toggleUnderline, } from "@/helpers/editor-commands"; // types -import { TEditorCommands } from "@/types"; +import { TColorEditorCommands, TNonColorEditorCommands } from "@/types"; -export interface EditorMenuItem { - key: TEditorCommands; +export type EditorMenuItem = { name: string; - isActive: () => boolean; - command: () => void; + command: (...args: any) => void; icon: LucideIcon; -} +} & ( + | { + key: TNonColorEditorCommands; + isActive: () => boolean; + } + | { + key: TColorEditorCommands; + isActive: (color: string | undefined) => boolean; + } +); export const TextItem = (editor: Editor): EditorMenuItem => ({ key: "text", @@ -198,10 +208,25 @@ export const ImageItem = (editor: Editor) => icon: ImageIcon, }) as const; -export function getEditorMenuItems(editor: Editor | null) { - if (!editor) { - return []; - } +export const TextColorItem = (editor: Editor): EditorMenuItem => ({ + key: "text-color", + name: "Color", + isActive: (color) => editor.getAttributes("textStyle").color === color, + command: (color: string) => toggleTextColor(color, editor), + icon: Palette, +}); + +export const BackgroundColorItem = (editor: Editor): EditorMenuItem => ({ + key: "background-color", + name: "Background color", + isActive: (color) => editor.isActive("highlight", { color }), + command: (color: string) => toggleBackgroundColor(color, editor), + icon: Palette, +}); + +export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => { + if (!editor) return []; + return [ TextItem(editor), HeadingOneItem(editor), @@ -221,5 +246,7 @@ export function getEditorMenuItems(editor: Editor | null) { QuoteItem(editor), TableItem(editor), ImageItem(editor), + TextColorItem(editor), + BackgroundColorItem(editor), ]; -} +}; diff --git a/packages/editor/src/core/constants/common.ts b/packages/editor/src/core/constants/common.ts new file mode 100644 index 00000000000..1cba97e8d36 --- /dev/null +++ b/packages/editor/src/core/constants/common.ts @@ -0,0 +1,51 @@ +export const COLORS_LIST: { + backgroundColor: string; + textColor: string; + label: string; +}[] = [ + { + backgroundColor: "#1c202426", + textColor: "#1c2024", + label: "Black", + }, + { + backgroundColor: "#5c5e6326", + textColor: "#5c5e63", + label: "Gray", + }, + { + backgroundColor: "#ff5b5926", + textColor: "#ff5b59", + label: "Peach", + }, + { + backgroundColor: "#f6538526", + textColor: "#f65385", + label: "Pink", + }, + { + backgroundColor: "#fd903826", + textColor: "#fd9038", + label: "Orange", + }, + { + backgroundColor: "#0fc27b26", + textColor: "#0fc27b", + label: "Green", + }, + { + backgroundColor: "#17bee926", + textColor: "#17bee9", + label: "Light blue", + }, + { + backgroundColor: "#266df026", + textColor: "#266df0", + label: "Dark blue", + }, + { + backgroundColor: "#9162f926", + textColor: "#9162f9", + label: "Purple", + }, +]; diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index 1cedd513966..7fc0ae6d052 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -1,3 +1,5 @@ +import { Color } from "@tiptap/extension-color"; +import Highlight from "@tiptap/extension-highlight"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; @@ -83,6 +85,10 @@ export const CoreEditorExtensionsWithoutProps = [ TableCell, TableRow, CustomMentionWithoutProps(), + Color, + Highlight.configure({ + multicolor: true, + }), ]; export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()]; diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 1c2e1889112..d890a358935 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -1,4 +1,6 @@ import CharacterCount from "@tiptap/extension-character-count"; +import { Color } from "@tiptap/extension-color"; +import Highlight from "@tiptap/extension-highlight"; import Placeholder from "@tiptap/extension-placeholder"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; @@ -166,4 +168,8 @@ export const CoreEditorExtensions = ({ includeChildren: true, }), CharacterCount, + Color, + Highlight.configure({ + multicolor: true, + }), ]; diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index 658dd2f7997..f00aaaab66f 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -6,6 +6,7 @@ export * from "./custom-list-keymap"; export * from "./image"; export * from "./issue-embed"; export * from "./mentions"; +export * from "./slash-commands"; export * from "./table"; export * from "./typography"; export * from "./core-without-props"; @@ -18,4 +19,3 @@ export * from "./keymap"; export * from "./quote"; export * from "./read-only-extensions"; export * from "./side-menu"; -export * from "./slash-commands"; diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx index 2898b6cdc03..7e7f664eb23 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.tsx @@ -1,4 +1,6 @@ import CharacterCount from "@tiptap/extension-character-count"; +import { Color } from "@tiptap/extension-color"; +import Highlight from "@tiptap/extension-highlight"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; @@ -108,4 +110,8 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { readonly: true, }), CharacterCount, + Color, + Highlight.configure({ + multicolor: true, + }), ]; diff --git a/packages/editor/src/core/extensions/slash-commands.tsx b/packages/editor/src/core/extensions/slash-commands.tsx deleted file mode 100644 index 3b1789781cf..00000000000 --- a/packages/editor/src/core/extensions/slash-commands.tsx +++ /dev/null @@ -1,423 +0,0 @@ -import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react"; -import { Editor, Range, Extension } from "@tiptap/core"; -import { ReactRenderer } from "@tiptap/react"; -import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; -import tippy from "tippy.js"; -import { - CaseSensitive, - Code2, - Heading1, - Heading2, - Heading3, - Heading4, - Heading5, - Heading6, - ImageIcon, - List, - ListOrdered, - ListTodo, - MinusSquare, - Quote, - Table, -} from "lucide-react"; -// helpers -import { cn } from "@/helpers/common"; -import { - insertTableCommand, - toggleBlockquote, - toggleBulletList, - toggleOrderedList, - toggleTaskList, - toggleHeadingOne, - toggleHeadingTwo, - toggleHeadingThree, - toggleHeadingFour, - toggleHeadingFive, - toggleHeadingSix, -} from "@/helpers/editor-commands"; -// types -import { CommandProps, ISlashCommandItem } from "@/types"; - -interface CommandItemProps { - key: string; - title: string; - description: string; - icon: ReactNode; -} - -export type SlashCommandOptions = { - suggestion: Omit; -}; - -const Command = Extension.create({ - name: "slash-command", - addOptions() { - return { - suggestion: { - char: "/", - command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { - props.command({ editor, range }); - }, - allow({ editor }: { editor: Editor }) { - const { selection } = editor.state; - - const parentNode = selection.$from.node(selection.$from.depth); - const blockType = parentNode.type.name; - - if (blockType === "codeBlock") { - return false; - } - - if (editor.isActive("table")) { - return false; - } - - return true; - }, - }, - }; - }, - addProseMirrorPlugins() { - return [ - Suggestion({ - editor: this.editor, - ...this.options.suggestion, - }), - ]; - }, -}); - -const getSuggestionItems = - (additionalOptions?: Array) => - ({ query }: { query: string }) => { - let slashCommands: ISlashCommandItem[] = [ - { - key: "text", - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }: CommandProps) => { - if (range) { - editor.chain().focus().deleteRange(range).clearNodes().run(); - } - editor.chain().focus().clearNodes().run(); - }, - }, - { - key: "h1", - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingOne(editor, range); - }, - }, - { - key: "h2", - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingTwo(editor, range); - }, - }, - { - key: "h3", - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingThree(editor, range); - }, - }, - { - key: "h4", - title: "Heading 4", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingFour(editor, range); - }, - }, - { - key: "h5", - title: "Heading 5", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingFive(editor, range); - }, - }, - { - key: "h6", - title: "Heading 6", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingSix(editor, range); - }, - }, - { - key: "to-do-list", - title: "To do", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleTaskList(editor, range); - }, - }, - { - key: "bulleted-list", - title: "Bullet list", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleBulletList(editor, range); - }, - }, - { - key: "numbered-list", - title: "Numbered list", - description: "Create a list with numbering.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleOrderedList(editor, range); - }, - }, - { - key: "table", - title: "Table", - description: "Create a table", - searchTerms: ["table", "cell", "db", "data", "tabular"], - icon: , - command: ({ editor, range }: CommandProps) => { - insertTableCommand(editor, range); - }, - }, - { - key: "quote", - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range), - }, - { - key: "code", - title: "Code", - description: "Capture a code snippet.", - searchTerms: ["codeblock"], - icon: , - command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), - }, - { - key: "image", - title: "Image", - icon: , - description: "Insert an image", - searchTerms: ["img", "photo", "picture", "media", "upload"], - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setImageUpload({ event: "insert" }).run(); - }, - }, - { - key: "divider", - title: "Divider", - description: "Visually divide blocks.", - searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setHorizontalRule().run(); - }, - }, - ]; - - if (additionalOptions) { - additionalOptions.map((item) => { - slashCommands.push(item); - }); - } - - slashCommands = slashCommands.filter((item) => { - if (typeof query === "string" && query.length > 0) { - const search = query.toLowerCase(); - return ( - item.title.toLowerCase().includes(search) || - item.description.toLowerCase().includes(search) || - (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search))) - ); - } - return true; - }); - - return slashCommands; - }; - -export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { - const containerHeight = container.offsetHeight; - const itemHeight = item ? item.offsetHeight : 0; - - const top = item.offsetTop; - const bottom = top + itemHeight; - - if (top < container.scrollTop) { - container.scrollTop -= container.scrollTop - top + 5; - } else if (bottom > containerHeight + container.scrollTop) { - container.scrollTop += bottom - containerHeight - container.scrollTop + 5; - } -}; - -const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => { - // states - const [selectedIndex, setSelectedIndex] = useState(0); - // refs - const commandListContainer = useRef(null); - - const selectItem = useCallback( - (index: number) => { - const item = items[index]; - if (item) command(item); - }, - [command, items] - ); - - useEffect(() => { - const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; - const onKeyDown = (e: KeyboardEvent) => { - if (navigationKeys.includes(e.key)) { - e.preventDefault(); - if (e.key === "ArrowUp") { - setSelectedIndex((selectedIndex + items.length - 1) % items.length); - return true; - } - if (e.key === "ArrowDown") { - setSelectedIndex((selectedIndex + 1) % items.length); - return true; - } - if (e.key === "Enter") { - selectItem(selectedIndex); - return true; - } - return false; - } - }; - document.addEventListener("keydown", onKeyDown); - return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, [items, selectedIndex, setSelectedIndex, selectItem]); - - useEffect(() => { - setSelectedIndex(0); - }, [items]); - - useLayoutEffect(() => { - const container = commandListContainer?.current; - - const item = container?.children[selectedIndex] as HTMLElement; - - if (item && container) updateScrollView(container, item); - }, [selectedIndex]); - - if (items.length <= 0) return null; - - return ( -
- {items.map((item, index) => ( - - ))} -
- ); -}; - -interface CommandListInstance { - onKeyDown: (props: { event: KeyboardEvent }) => boolean; -} - -const renderItems = () => { - let component: ReactRenderer | null = null; - let popup: any | null = null; - return { - onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { - component = new ReactRenderer(CommandList, { - props, - editor: props.editor, - }); - - const tippyContainer = - document.querySelector(".active-editor") ?? document.querySelector('[id^="editor-container"]'); - - // @ts-expect-error Tippy overloads are messed up - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: tippyContainer, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); - }, - onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { - component?.updateProps(props); - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); - }, - onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key === "Escape") { - popup?.[0].hide(); - - return true; - } - - if (component?.ref?.onKeyDown(props)) { - return true; - } - return false; - }, - onExit: () => { - popup?.[0].destroy(); - component?.destroy(); - }, - }; -}; - -export const SlashCommand = (additionalOptions?: Array) => - Command.configure({ - suggestion: { - items: getSuggestionItems(additionalOptions), - render: renderItems, - }, - }); diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx new file mode 100644 index 00000000000..4b10b177a75 --- /dev/null +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -0,0 +1,294 @@ +import { + ALargeSmall, + CaseSensitive, + Code2, + Heading1, + Heading2, + Heading3, + Heading4, + Heading5, + Heading6, + ImageIcon, + List, + ListOrdered, + ListTodo, + MinusSquare, + Quote, + Table, +} from "lucide-react"; +// constants +import { COLORS_LIST } from "@/constants/common"; +// helpers +import { + insertTableCommand, + toggleBlockquote, + toggleBulletList, + toggleOrderedList, + toggleTaskList, + toggleHeadingOne, + toggleHeadingTwo, + toggleHeadingThree, + toggleHeadingFour, + toggleHeadingFive, + toggleHeadingSix, + toggleTextColor, + toggleBackgroundColor, +} from "@/helpers/editor-commands"; +// types +import { CommandProps, ISlashCommandItem } from "@/types"; + +export type TSlashCommandSection = { + key: string; + title?: string; + items: ISlashCommandItem[]; +}; + +export const getSlashCommandFilteredSections = + (additionalOptions?: ISlashCommandItem[]) => + ({ query }: { query: string }): TSlashCommandSection[] => { + const slashCommandSections: TSlashCommandSection[] = [ + { + key: "general", + items: [ + { + commandKey: "text", + key: "text", + title: "Text", + description: "Just start typing with plain text.", + searchTerms: ["p", "paragraph"], + icon: , + command: ({ editor, range }: CommandProps) => { + if (range) { + editor.chain().focus().deleteRange(range).clearNodes().run(); + } + editor.chain().focus().clearNodes().run(); + }, + }, + { + commandKey: "h1", + key: "h1", + title: "Heading 1", + description: "Big section heading.", + searchTerms: ["title", "big", "large"], + icon: , + command: ({ editor, range }) => toggleHeadingOne(editor, range), + }, + { + commandKey: "h2", + key: "h2", + title: "Heading 2", + description: "Medium section heading.", + searchTerms: ["subtitle", "medium"], + icon: , + command: ({ editor, range }) => toggleHeadingTwo(editor, range), + }, + { + commandKey: "h3", + key: "h3", + title: "Heading 3", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingThree(editor, range), + }, + { + commandKey: "h4", + key: "h4", + title: "Heading 4", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingFour(editor, range), + }, + { + commandKey: "h5", + key: "h5", + title: "Heading 5", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingFive(editor, range), + }, + { + commandKey: "h6", + key: "h6", + title: "Heading 6", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingSix(editor, range), + }, + { + commandKey: "to-do-list", + key: "to-do-list", + title: "To do", + description: "Track tasks with a to-do list.", + searchTerms: ["todo", "task", "list", "check", "checkbox"], + icon: , + command: ({ editor, range }) => toggleTaskList(editor, range), + }, + { + commandKey: "bulleted-list", + key: "bulleted-list", + title: "Bullet list", + description: "Create a simple bullet list.", + searchTerms: ["unordered", "point"], + icon: , + command: ({ editor, range }) => toggleBulletList(editor, range), + }, + { + commandKey: "numbered-list", + key: "numbered-list", + title: "Numbered list", + description: "Create a list with numbering.", + searchTerms: ["ordered"], + icon: , + command: ({ editor, range }) => toggleOrderedList(editor, range), + }, + { + commandKey: "table", + key: "table", + title: "Table", + description: "Create a table", + searchTerms: ["table", "cell", "db", "data", "tabular"], + icon:
, + command: ({ editor, range }) => insertTableCommand(editor, range), + }, + { + commandKey: "quote", + key: "quote", + title: "Quote", + description: "Capture a quote.", + searchTerms: ["blockquote"], + icon: , + command: ({ editor, range }) => toggleBlockquote(editor, range), + }, + { + commandKey: "code", + key: "code", + title: "Code", + description: "Capture a code snippet.", + searchTerms: ["codeblock"], + icon: , + command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), + }, + { + commandKey: "image", + key: "image", + title: "Image", + icon: , + description: "Insert an image", + searchTerms: ["img", "photo", "picture", "media", "upload"], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).setImageUpload({ event: "insert" }).run(), + }, + { + commandKey: "divider", + key: "divider", + title: "Divider", + description: "Visually divide blocks.", + searchTerms: ["line", "divider", "horizontal", "rule", "separate"], + icon: , + command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(), + }, + ], + }, + { + key: "text-color", + title: "Colors", + items: [ + ...COLORS_LIST.map( + (color) => + ({ + commandKey: "text-color", + key: `text-color-${color.textColor}`, + title: color.label, + description: "Change text color", + searchTerms: ["color", "text", color.label], + icon: ( + + ), + command: ({ editor, range }) => toggleTextColor(color.textColor, editor, range), + }) as ISlashCommandItem + ), + { + commandKey: "text-color", + key: "text-color-default", + title: "Default", + description: "Change text color", + searchTerms: ["color", "text", "default"], + icon: ( + + ), + command: ({ editor, range }) => toggleTextColor(undefined, editor, range), + }, + ], + }, + { + key: "background-color", + title: "Background colors", + items: [ + ...COLORS_LIST.map( + (color) => + ({ + commandKey: "background-color", + key: `background-color-${color.backgroundColor}`, + title: `${color.label} background`, + description: "Change background color", + searchTerms: ["color", "bg", "background", color.label], + icon: , + iconContainerStyle: { + borderRadius: "4px", + backgroundColor: color.backgroundColor, + }, + command: ({ editor, range }) => toggleBackgroundColor(color.backgroundColor, editor, range), + }) as ISlashCommandItem + ), + { + commandKey: "background-color", + key: "background-color-default", + title: "Default background", + description: "Change background color", + searchTerms: ["color", "bg", "background", "default"], + icon: , + iconContainerStyle: { + borderRadius: "4px", + backgroundColor: "rgba(var(--color-background-100))", + border: "1px solid rgba(var(--color-border-300))", + }, + command: ({ editor, range }) => toggleTextColor(undefined, editor, range), + }, + ], + }, + ]; + + if (additionalOptions) { + additionalOptions.map((item) => slashCommandSections?.[0]?.items.push(item)); + } + + const filteredSlashSections = slashCommandSections.map((section) => ({ + ...section, + items: section.items.filter((item) => { + if (typeof query !== "string") return; + + const lowercaseQuery = query.toLowerCase(); + return ( + item.title.toLowerCase().includes(lowercaseQuery) || + item.description.toLowerCase().includes(lowercaseQuery) || + item.searchTerms.some((t) => t.includes(lowercaseQuery)) + ); + }), + })); + + return filteredSlashSections; + }; diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx new file mode 100644 index 00000000000..3a03c3b6a70 --- /dev/null +++ b/packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx @@ -0,0 +1,37 @@ +// helpers +import { cn } from "@/helpers/common"; +// types +import { ISlashCommandItem } from "@/types"; + +type Props = { + isSelected: boolean; + item: ISlashCommandItem; + itemIndex: number; + onClick: (e: React.MouseEvent) => void; + onMouseEnter: () => void; + sectionIndex: number; +}; + +export const CommandMenuItem: React.FC = (props) => { + const { isSelected, item, itemIndex, onClick, onMouseEnter, sectionIndex } = props; + + return ( + + ); +}; diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx new file mode 100644 index 00000000000..9abc4ef6791 --- /dev/null +++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx @@ -0,0 +1,144 @@ +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; +// components +import { TSlashCommandSection } from "./command-items-list"; +import { CommandMenuItem } from "./command-menu-item"; + +type Props = { + items: TSlashCommandSection[]; + command: any; + editor: any; + range: any; +}; + +const updateScrollView = (container: HTMLElement, item: HTMLElement) => { + const containerHeight = container.offsetHeight; + const itemHeight = item ? item.offsetHeight : 0; + + const top = item.offsetTop; + const bottom = top + itemHeight; + + if (top < container.scrollTop) { + container.scrollTop -= container.scrollTop - top + 5; + } else if (bottom > containerHeight + container.scrollTop) { + container.scrollTop += bottom - containerHeight - container.scrollTop + 5; + } +}; + +export const SlashCommandsMenu = (props: Props) => { + const { items: sections, command } = props; + // states + const [selectedIndex, setSelectedIndex] = useState({ + section: 0, + item: 0, + }); + // refs + const commandListContainer = useRef(null); + + const selectItem = useCallback( + (sectionIndex: number, itemIndex: number) => { + const item = sections[sectionIndex].items[itemIndex]; + if (item) command(item); + }, + [command, sections] + ); + // handle arrow key navigation + useEffect(() => { + const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; + const onKeyDown = (e: KeyboardEvent) => { + if (navigationKeys.includes(e.key)) { + e.preventDefault(); + const currentSection = selectedIndex.section; + const currentItem = selectedIndex.item; + let nextSection = currentSection; + let nextItem = currentItem; + + if (e.key === "ArrowUp") { + nextItem = currentItem - 1; + if (nextItem < 0) { + nextSection = currentSection - 1; + if (nextSection < 0) nextSection = sections.length - 1; + nextItem = sections[nextSection].items.length - 1; + } + } + if (e.key === "ArrowDown") { + nextItem = currentItem + 1; + if (nextItem >= sections[currentSection].items.length) { + nextSection = currentSection + 1; + if (nextSection >= sections.length) nextSection = 0; + nextItem = 0; + } + } + if (e.key === "Enter") { + selectItem(currentSection, currentItem); + } + setSelectedIndex({ + section: nextSection, + item: nextItem, + }); + } + }; + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("keydown", onKeyDown); + }; + }, [sections, selectedIndex, setSelectedIndex, selectItem]); + // initialize the select index to 0 by default + useEffect(() => { + setSelectedIndex({ + section: 0, + item: 0, + }); + }, [sections]); + // scroll to the dropdown item when navigating via keyboard + useLayoutEffect(() => { + const container = commandListContainer?.current; + if (!container) return; + + const item = container.querySelector(`#item-${selectedIndex.section}-${selectedIndex.item}`) as HTMLElement; + + // use scroll into view to bring the item in view if it is not in view + item?.scrollIntoView({ block: "nearest" }); + }, [sections, selectedIndex]); + + const areSearchResultsEmpty = sections.map((s) => s.items.length).reduce((acc, curr) => acc + curr, 0) === 0; + + if (areSearchResultsEmpty) return null; + + return ( +
+ {sections.map((section, sectionIndex) => { + if (section.items.length === 0) return; + return ( +
+ {section.title &&
{section.title}
} +
+ {section.items.map((item, itemIndex) => ( + { + e.stopPropagation(); + selectItem(sectionIndex, itemIndex); + }} + onMouseEnter={() => + setSelectedIndex({ + section: sectionIndex, + item: itemIndex, + }) + } + sectionIndex={sectionIndex} + /> + ))} +
+
+ ); + })} +
+ ); +}; diff --git a/packages/editor/src/core/extensions/slash-commands/index.ts b/packages/editor/src/core/extensions/slash-commands/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/packages/editor/src/core/extensions/slash-commands/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx new file mode 100644 index 00000000000..09f148eade6 --- /dev/null +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -0,0 +1,114 @@ +import { Editor, Range, Extension } from "@tiptap/core"; +import { ReactRenderer } from "@tiptap/react"; +import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; +import tippy from "tippy.js"; +// types +import { ISlashCommandItem } from "@/types"; +// components +import { getSlashCommandFilteredSections } from "./command-items-list"; +import { SlashCommandsMenu } from "./command-menu"; + +export type SlashCommandOptions = { + suggestion: Omit; +}; + +const Command = Extension.create({ + name: "slash-command", + addOptions() { + return { + suggestion: { + char: "/", + command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { + props.command({ editor, range }); + }, + allow({ editor }: { editor: Editor }) { + const { selection } = editor.state; + + const parentNode = selection.$from.node(selection.$from.depth); + const blockType = parentNode.type.name; + + if (blockType === "codeBlock") { + return false; + } + + if (editor.isActive("table")) { + return false; + } + + return true; + }, + }, + }; + }, + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + ...this.options.suggestion, + }), + ]; + }, +}); + +interface CommandListInstance { + onKeyDown: (props: { event: KeyboardEvent }) => boolean; +} + +const renderItems = () => { + let component: ReactRenderer | null = null; + let popup: any | null = null; + return { + onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { + component = new ReactRenderer(SlashCommandsMenu, { + props, + editor: props.editor, + }); + + const tippyContainer = + document.querySelector(".active-editor") ?? document.querySelector('[id^="editor-container"]'); + + // @ts-expect-error Tippy overloads are messed up + popup = tippy("body", { + getReferenceClientRect: props.clientRect, + appendTo: tippyContainer, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: "manual", + placement: "bottom-start", + }); + }, + onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { + component?.updateProps(props); + + popup && + popup[0].setProps({ + getReferenceClientRect: props.clientRect, + }); + }, + onKeyDown: (props: { event: KeyboardEvent }) => { + if (props.event.key === "Escape") { + popup?.[0].hide(); + + return true; + } + + if (component?.ref?.onKeyDown(props)) { + return true; + } + return false; + }, + onExit: () => { + popup?.[0].destroy(); + component?.destroy(); + }, + }; +}; + +export const SlashCommands = (additionalOptions?: ISlashCommandItem[]) => + Command.configure({ + suggestion: { + items: getSlashCommandFilteredSections(additionalOptions), + render: renderItems, + }, + }); diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 7cf3e8d1f17..3cb716d0312 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -156,3 +156,42 @@ export const insertImageCommand = ( }; input.click(); }; + +export const toggleTextColor = (color: string | undefined, editor: Editor, range?: Range) => { + if (color) { + if (range) editor.chain().focus().deleteRange(range).setColor(color).run(); + else editor.chain().focus().setColor(color).run(); + } else { + if (range) editor.chain().focus().deleteRange(range).unsetColor().run(); + else editor.chain().focus().unsetColor().run(); + } +}; + +export const toggleBackgroundColor = (color: string | undefined, editor: Editor, range?: Range) => { + if (color) { + if (range) { + editor + .chain() + .focus() + .deleteRange(range) + .setHighlight({ + color, + }) + .run(); + } else { + editor + .chain() + .focus() + .setHighlight({ + color, + }) + .run(); + } + } else { + if (range) { + editor.chain().focus().deleteRange(range).unsetHighlight().run(); + } else { + editor.chain().focus().unsetHighlight().run(); + } + } +}; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 0542418e5d5..59178f2008d 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -131,7 +131,8 @@ export const useEditor = (props: CustomEditorProps) => { insertContentAtSavedSelection(editorRef, content, savedSelection); } }, - executeMenuItemCommand: (itemKey: TEditorCommands) => { + executeMenuItemCommand: (props) => { + const { itemKey } = props; const editorItems = getEditorMenuItems(editorRef.current); const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); @@ -140,6 +141,8 @@ export const useEditor = (props: CustomEditorProps) => { if (item) { if (item.key === "image") { item.command(savedSelectionRef.current); + } else if (itemKey === "text-color" || itemKey === "background-color") { + item.command(props.color); } else { item.command(); } @@ -147,12 +150,19 @@ export const useEditor = (props: CustomEditorProps) => { console.warn(`No command found for item: ${itemKey}`); } }, - isMenuItemActive: (itemName: TEditorCommands): boolean => { + isMenuItemActive: (props) => { + const { itemKey } = props; const editorItems = getEditorMenuItems(editorRef.current); - const getEditorMenuItem = (itemName: TEditorCommands) => editorItems.find((item) => item.key === itemName); - const item = getEditorMenuItem(itemName); - return item ? item.isActive() : false; + const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); + const item = getEditorMenuItem(itemKey); + if (!item) return false; + + if (itemKey === "text-color" || itemKey === "background-color") { + return item.isActive(props.color); + } else { + return item.isActive(""); + } }, onStateChange: (callback: () => void) => { // Subscribe to editor state changes diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index f6c790305f3..c8159d0679d 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -5,14 +5,15 @@ import { IMentionHighlight, IMentionSuggestion, TAIHandler, + TColorEditorCommands, TDisplayConfig, TEditorCommands, TEmbedConfig, TExtensions, TFileHandler, + TNonColorEditorCommands, TServerHandler, } from "@/types"; - // editor refs export type EditorReadOnlyRefApi = { getMarkDown: () => string; @@ -29,8 +30,26 @@ export type EditorReadOnlyRefApi = { export interface EditorRefApi extends EditorReadOnlyRefApi { setEditorValueAtCursorPosition: (content: string) => void; - executeMenuItemCommand: (itemKey: TEditorCommands) => void; - isMenuItemActive: (itemKey: TEditorCommands) => boolean; + executeMenuItemCommand: ( + props: + | { + itemKey: TNonColorEditorCommands; + } + | { + itemKey: TColorEditorCommands; + color: string | undefined; + } + ) => void; + isMenuItemActive: ( + props: + | { + itemKey: TNonColorEditorCommands; + } + | { + itemKey: TColorEditorCommands; + color: string | undefined; + } + ) => boolean; onStateChange: (callback: () => void) => () => void; setFocusAtPosition: (position: number) => void; isEditorReadyToDiscard: () => boolean; diff --git a/packages/editor/src/core/types/slash-commands-suggestion.ts b/packages/editor/src/core/types/slash-commands-suggestion.ts index 3cb9d76b0ea..ce3408a34f0 100644 --- a/packages/editor/src/core/types/slash-commands-suggestion.ts +++ b/packages/editor/src/core/types/slash-commands-suggestion.ts @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { CSSProperties } from "react"; import { Editor, Range } from "@tiptap/core"; export type TEditorCommands = @@ -21,7 +21,12 @@ export type TEditorCommands = | "table" | "image" | "divider" - | "issue-embed"; + | "issue-embed" + | "text-color" + | "background-color"; + +export type TColorEditorCommands = Extract; +export type TNonColorEditorCommands = Exclude; export type CommandProps = { editor: Editor; @@ -29,10 +34,12 @@ export type CommandProps = { }; export type ISlashCommandItem = { - key: TEditorCommands; + commandKey: TEditorCommands; + key: string; title: string; description: string; searchTerms: string[]; - icon: ReactNode; + icon: React.ReactNode; + iconContainerStyle?: CSSProperties; command: ({ editor, range }: CommandProps) => void; }; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 0ee93d4c411..93aa6c5a412 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -18,6 +18,9 @@ export { export { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; +// constants +export * from "@/constants/common"; + // helpers export * from "@/helpers/common"; export * from "@/helpers/editor-commands"; diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx index 186f44a1011..86b143f9ded 100644 --- a/space/core/components/editor/lite-text-editor.tsx +++ b/space/core/components/editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; // components import { IssueCommentToolbar } from "@/components/editor"; // helpers @@ -56,7 +56,9 @@ export const LiteTextEditor = React.forwardRef { if (isMutableRefObject(ref)) { - ref.current?.executeMenuItemCommand(key); + ref.current?.executeMenuItemCommand({ + itemKey: key as TNonColorEditorCommands, + }); } }} isSubmitting={isSubmitting} diff --git a/space/core/components/editor/toolbar.tsx b/space/core/components/editor/toolbar.tsx index 3b0942e17a5..edbd899c58b 100644 --- a/space/core/components/editor/toolbar.tsx +++ b/space/core/components/editor/toolbar.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from "react"; // editor -import { EditorRefApi, TEditorCommands } from "@plane/editor"; +import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants @@ -34,7 +34,9 @@ export const IssueCommentToolbar: React.FC = (props) => { .flat() .forEach((item) => { // Assert that editorRef.current is not null - newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive(item.key); + newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ + itemKey: item.key as TNonColorEditorCommands, + }); }); setActiveStates(newActiveStates); } diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index 8036e4c8d4e..f8e1f3bde22 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; // types import { IUserLite } from "@plane/types"; // components @@ -87,7 +87,9 @@ export const LiteTextEditor = React.forwardRef { if (isMutableRefObject(ref)) { - ref.current?.executeMenuItemCommand(key); + ref.current?.executeMenuItemCommand({ + itemKey: key as TNonColorEditorCommands, + }); } }} handleAccessChange={handleAccessChange} diff --git a/web/core/components/editor/lite-text-editor/toolbar.tsx b/web/core/components/editor/lite-text-editor/toolbar.tsx index 7576a109666..372a7f87f8e 100644 --- a/web/core/components/editor/lite-text-editor/toolbar.tsx +++ b/web/core/components/editor/lite-text-editor/toolbar.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from "react"; import { Globe2, Lock, LucideIcon } from "lucide-react"; // editor -import { EditorRefApi, TEditorCommands } from "@plane/editor"; +import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants @@ -69,7 +69,9 @@ export const IssueCommentToolbar: React.FC = (props) => { .flat() .forEach((item) => { // Assert that editorRef.current is not null - newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive(item.key); + newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ + itemKey: item.key as TNonColorEditorCommands, + }); }); setActiveStates(newActiveStates); } diff --git a/web/core/components/pages/editor/header/color-dropdown.tsx b/web/core/components/pages/editor/header/color-dropdown.tsx new file mode 100644 index 00000000000..4628a3f222e --- /dev/null +++ b/web/core/components/pages/editor/header/color-dropdown.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { memo } from "react"; +import { Popover } from "@headlessui/react"; +import { ALargeSmall, Ban } from "lucide-react"; +// plane editor +import { COLORS_LIST, TColorEditorCommands } from "@plane/editor"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + handleColorSelect: (key: TColorEditorCommands, color: string | undefined) => void; + isColorActive: (key: TColorEditorCommands, color: string | undefined) => boolean; +}; + +export const ColorDropdown: React.FC = memo((props) => { + const { handleColorSelect, isColorActive } = props; + + const activeTextColor = COLORS_LIST.find((c) => isColorActive("text-color", c.textColor)); + const activeBackgroundColor = COLORS_LIST.find((c) => isColorActive("background-color", c.backgroundColor)); + + return ( + + + {({ open }) => ( + + Color + + + + + )} + + +
+

Text colors

+
+ {COLORS_LIST.map((color) => ( + +
+
+
+

Background colors

+
+ {COLORS_LIST.map((color) => ( + +
+
+
+
+ ); +}); diff --git a/web/core/components/pages/editor/header/index.ts b/web/core/components/pages/editor/header/index.ts index 219ed44d87f..d87f5d11946 100644 --- a/web/core/components/pages/editor/header/index.ts +++ b/web/core/components/pages/editor/header/index.ts @@ -1,3 +1,4 @@ +export * from "./color-dropdown"; export * from "./extra-options"; export * from "./info-popover"; export * from "./options-dropdown"; diff --git a/web/core/components/pages/editor/header/toolbar.tsx b/web/core/components/pages/editor/header/toolbar.tsx index 65d484ef152..726094521dd 100644 --- a/web/core/components/pages/editor/header/toolbar.tsx +++ b/web/core/components/pages/editor/header/toolbar.tsx @@ -3,9 +3,11 @@ import React, { useEffect, useState, useCallback } from "react"; import { Check, ChevronDown } from "lucide-react"; // editor -import { EditorRefApi, TEditorCommands } from "@plane/editor"; +import { EditorRefApi, TNonColorEditorCommands } from "@plane/editor"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; +// components +import { ColorDropdown } from "@/components/pages"; // constants import { TOOLBAR_ITEMS, TYPOGRAPHY_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers @@ -18,7 +20,7 @@ type Props = { type ToolbarButtonProps = { item: ToolbarMenuItem; isActive: boolean; - executeCommand: (commandKey: TEditorCommands) => void; + executeCommand: EditorRefApi["executeMenuItemCommand"]; }; const ToolbarButton: React.FC = React.memo((props) => { @@ -36,7 +38,11 @@ const ToolbarButton: React.FC = React.memo((props) => { + {isOpen && ( +
+
+

Text colors

+
+ {COLORS_LIST.map((color) => ( + +
+
+
+

Background colors

+
+ {COLORS_LIST.map((color) => ( + +
+
+
+ )} + + ); +}; diff --git a/packages/editor/src/core/components/menus/bubble-menu/index.ts b/packages/editor/src/core/components/menus/bubble-menu/index.ts index 71a98bada08..526feed3d1e 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/index.ts +++ b/packages/editor/src/core/components/menus/bubble-menu/index.ts @@ -1,3 +1,4 @@ +export * from "./color-selector"; export * from "./link-selector"; export * from "./node-selector"; export * from "./root"; diff --git a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx index 20335e8abb6..eaa20ed26bb 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx @@ -1,6 +1,6 @@ import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; import { Editor } from "@tiptap/core"; -import { Check, Trash } from "lucide-react"; +import { Check, Link, Trash } from "lucide-react"; // helpers import { cn, isValidHttpUrl } from "@/helpers/common"; import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands"; @@ -11,7 +11,9 @@ type Props = { setIsOpen: Dispatch>; }; -export const BubbleMenuLinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { +export const BubbleMenuLinkSelector: FC = (props) => { + const { editor, isOpen, setIsOpen } = props; + // refs const inputRef = useRef(null); const onLinkSubmit = useCallback(() => { @@ -28,26 +30,23 @@ export const BubbleMenuLinkSelector: FC = ({ editor, isOpen, setIsOpen }) }); return ( -
+
{isOpen && (
>; }; -export const BubbleMenuNodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { +export const BubbleMenuNodeSelector: FC = (props) => { + const { editor, isOpen, setIsOpen } = props; + const items: EditorMenuItem[] = [ TextItem(editor), HeadingOneItem(editor), @@ -54,12 +56,11 @@ export const BubbleMenuNodeSelector: FC = ({ editor, isOpen, setIsOpen }) setIsOpen(!isOpen); e.stopPropagation(); }} - className="flex h-full items-center gap-1 whitespace-nowrap p-2 text-sm font-medium text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5" + className="flex items-center gap-1 h-full whitespace-nowrap px-3 text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 rounded transition-colors" > {activeItem?.name} - + - {isOpen && (
{items.map((item) => ( diff --git a/packages/editor/src/core/components/menus/bubble-menu/root.tsx b/packages/editor/src/core/components/menus/bubble-menu/root.tsx index 2fab2b14ecf..0f789dd8a1b 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/root.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/root.tsx @@ -3,6 +3,7 @@ import { BubbleMenu, BubbleMenuProps, isNodeSelection } from "@tiptap/react"; // components import { BoldItem, + BubbleMenuColorSelector, BubbleMenuLinkSelector, BubbleMenuNodeSelector, CodeItem, @@ -19,23 +20,20 @@ import { cn } from "@/helpers/common"; type EditorBubbleMenuProps = Omit; export const EditorBubbleMenu: FC = (props: any) => { - const items: EditorMenuItem[] = [ - ...(props.editor.isActive("code") - ? [] - : [ - BoldItem(props.editor), - ItalicItem(props.editor), - UnderLineItem(props.editor), - StrikeThroughItem(props.editor), - ]), - CodeItem(props.editor), - ]; + // states + const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); + const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); + const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); + const [isSelecting, setIsSelecting] = useState(false); + + const items: EditorMenuItem[] = props.editor.isActive("code") + ? [CodeItem(props.editor)] + : [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)]; const bubbleMenuProps: EditorBubbleMenuProps = { ...props, shouldShow: ({ state, editor }) => { const { selection } = state; - const { empty } = selection; if ( @@ -55,15 +53,11 @@ export const EditorBubbleMenu: FC = (props: any) => { onHidden: () => { setIsNodeSelectorOpen(false); setIsLinkSelectorOpen(false); + setIsColorSelectorOpen(false); }, }, }; - const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); - const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); - - const [isSelecting, setIsSelecting] = useState(false); - useEffect(() => { function handleMouseDown() { function handleMouseMove() { @@ -94,31 +88,50 @@ export const EditorBubbleMenu: FC = (props: any) => { return ( - {isSelecting ? null : ( + {!isSelecting && ( <> - {!props.editor.isActive("table") && ( - { - setIsNodeSelectorOpen(!isNodeSelectorOpen); - setIsLinkSelectorOpen(false); - }} - /> - )} - {!props.editor.isActive("code") && ( - { - setIsLinkSelectorOpen(!isLinkSelectorOpen); - setIsNodeSelectorOpen(false); - }} - /> - )} -
+
+ {!props.editor.isActive("table") && ( + { + setIsNodeSelectorOpen((prev) => !prev); + setIsLinkSelectorOpen(false); + setIsColorSelectorOpen(false); + }} + /> + )} +
+
+ {!props.editor.isActive("code") && ( + { + setIsLinkSelectorOpen((prev) => !prev); + setIsNodeSelectorOpen(false); + setIsColorSelectorOpen(false); + }} + /> + )} +
+
+ {!props.editor.isActive("code") && ( + { + setIsColorSelectorOpen((prev) => !prev); + setIsNodeSelectorOpen(false); + setIsLinkSelectorOpen(false); + }} + /> + )} +
+
{items.map((item) => ( ))}
diff --git a/web/core/components/pages/editor/header/color-dropdown.tsx b/web/core/components/pages/editor/header/color-dropdown.tsx index 4628a3f222e..33d7c534fb9 100644 --- a/web/core/components/pages/editor/header/color-dropdown.tsx +++ b/web/core/components/pages/editor/header/color-dropdown.tsx @@ -21,21 +21,32 @@ export const ColorDropdown: React.FC = memo((props) => { return ( - + + cn("h-full", { + "outline-none": open, + }) + } + > {({ open }) => ( Color = ({ editorRef }) => { + // states const [activeStates, setActiveStates] = useState>({}); const updateActiveStates = useCallback(() => { From 85824bdd201c29437cfa9b6893c0783fcbf0f7f0 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 19 Sep 2024 14:19:05 +0530 Subject: [PATCH 3/6] chore: remove unused function --- .../extensions/slash-commands/command-menu.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx index 9abc4ef6791..fb4a45f6aa2 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx @@ -10,20 +10,6 @@ type Props = { range: any; }; -const updateScrollView = (container: HTMLElement, item: HTMLElement) => { - const containerHeight = container.offsetHeight; - const itemHeight = item ? item.offsetHeight : 0; - - const top = item.offsetTop; - const bottom = top + itemHeight; - - if (top < container.scrollTop) { - container.scrollTop -= container.scrollTop - top + 5; - } else if (bottom > containerHeight + container.scrollTop) { - container.scrollTop += bottom - containerHeight - container.scrollTop + 5; - } -}; - export const SlashCommandsMenu = (props: Props) => { const { items: sections, command } = props; // states From 4e978a7eff96beaa067234f50b7d97f12e70783b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 19 Sep 2024 15:43:53 +0530 Subject: [PATCH 4/6] refactor: slash command components --- .../slash-commands/command-items-list.tsx | 420 +++++++++--------- .../core/extensions/slash-commands/root.tsx | 7 +- yarn.lock | 10 +- 3 files changed, 214 insertions(+), 223 deletions(-) diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index 4b10b177a75..152ffdbc937 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -43,240 +43,240 @@ export type TSlashCommandSection = { items: ISlashCommandItem[]; }; -export const getSlashCommandFilteredSections = - (additionalOptions?: ISlashCommandItem[]) => - ({ query }: { query: string }): TSlashCommandSection[] => { - const slashCommandSections: TSlashCommandSection[] = [ +const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [ + { + key: "general", + items: [ { - key: "general", - items: [ - { - commandKey: "text", - key: "text", - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }: CommandProps) => { - if (range) { - editor.chain().focus().deleteRange(range).clearNodes().run(); - } - editor.chain().focus().clearNodes().run(); - }, - }, - { - commandKey: "h1", - key: "h1", - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }) => toggleHeadingOne(editor, range), - }, - { - commandKey: "h2", - key: "h2", - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }) => toggleHeadingTwo(editor, range), - }, - { - commandKey: "h3", - key: "h3", - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingThree(editor, range), - }, - { - commandKey: "h4", - key: "h4", - title: "Heading 4", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingFour(editor, range), - }, - { - commandKey: "h5", - key: "h5", - title: "Heading 5", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingFive(editor, range), - }, - { - commandKey: "h6", - key: "h6", - title: "Heading 6", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingSix(editor, range), - }, - { - commandKey: "to-do-list", - key: "to-do-list", - title: "To do", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }) => toggleTaskList(editor, range), - }, - { - commandKey: "bulleted-list", - key: "bulleted-list", - title: "Bullet list", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }) => toggleBulletList(editor, range), - }, - { - commandKey: "numbered-list", - key: "numbered-list", - title: "Numbered list", - description: "Create a list with numbering.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }) => toggleOrderedList(editor, range), - }, - { - commandKey: "table", - key: "table", - title: "Table", - description: "Create a table", - searchTerms: ["table", "cell", "db", "data", "tabular"], - icon:
, - command: ({ editor, range }) => insertTableCommand(editor, range), - }, - { - commandKey: "quote", - key: "quote", - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }) => toggleBlockquote(editor, range), - }, - { - commandKey: "code", - key: "code", - title: "Code", - description: "Capture a code snippet.", - searchTerms: ["codeblock"], - icon: , - command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), - }, - { - commandKey: "image", - key: "image", - title: "Image", - icon: , - description: "Insert an image", - searchTerms: ["img", "photo", "picture", "media", "upload"], - command: ({ editor, range }) => - editor.chain().focus().deleteRange(range).setImageUpload({ event: "insert" }).run(), - }, - { - commandKey: "divider", - key: "divider", - title: "Divider", - description: "Visually divide blocks.", - searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , - command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(), - }, - ], + commandKey: "text", + key: "text", + title: "Text", + description: "Just start typing with plain text.", + searchTerms: ["p", "paragraph"], + icon: , + command: ({ editor, range }: CommandProps) => { + if (range) { + editor.chain().focus().deleteRange(range).clearNodes().run(); + } + editor.chain().focus().clearNodes().run(); + }, + }, + { + commandKey: "h1", + key: "h1", + title: "Heading 1", + description: "Big section heading.", + searchTerms: ["title", "big", "large"], + icon: , + command: ({ editor, range }) => toggleHeadingOne(editor, range), + }, + { + commandKey: "h2", + key: "h2", + title: "Heading 2", + description: "Medium section heading.", + searchTerms: ["subtitle", "medium"], + icon: , + command: ({ editor, range }) => toggleHeadingTwo(editor, range), + }, + { + commandKey: "h3", + key: "h3", + title: "Heading 3", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingThree(editor, range), }, { - key: "text-color", - title: "Colors", - items: [ - ...COLORS_LIST.map( - (color) => - ({ - commandKey: "text-color", - key: `text-color-${color.textColor}`, - title: color.label, - description: "Change text color", - searchTerms: ["color", "text", color.label], - icon: ( - - ), - command: ({ editor, range }) => toggleTextColor(color.textColor, editor, range), - }) as ISlashCommandItem - ), - { + commandKey: "h4", + key: "h4", + title: "Heading 4", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingFour(editor, range), + }, + { + commandKey: "h5", + key: "h5", + title: "Heading 5", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingFive(editor, range), + }, + { + commandKey: "h6", + key: "h6", + title: "Heading 6", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }) => toggleHeadingSix(editor, range), + }, + { + commandKey: "to-do-list", + key: "to-do-list", + title: "To do", + description: "Track tasks with a to-do list.", + searchTerms: ["todo", "task", "list", "check", "checkbox"], + icon: , + command: ({ editor, range }) => toggleTaskList(editor, range), + }, + { + commandKey: "bulleted-list", + key: "bulleted-list", + title: "Bullet list", + description: "Create a simple bullet list.", + searchTerms: ["unordered", "point"], + icon: , + command: ({ editor, range }) => toggleBulletList(editor, range), + }, + { + commandKey: "numbered-list", + key: "numbered-list", + title: "Numbered list", + description: "Create a list with numbering.", + searchTerms: ["ordered"], + icon: , + command: ({ editor, range }) => toggleOrderedList(editor, range), + }, + { + commandKey: "table", + key: "table", + title: "Table", + description: "Create a table", + searchTerms: ["table", "cell", "db", "data", "tabular"], + icon:
, + command: ({ editor, range }) => insertTableCommand(editor, range), + }, + { + commandKey: "quote", + key: "quote", + title: "Quote", + description: "Capture a quote.", + searchTerms: ["blockquote"], + icon: , + command: ({ editor, range }) => toggleBlockquote(editor, range), + }, + { + commandKey: "code", + key: "code", + title: "Code", + description: "Capture a code snippet.", + searchTerms: ["codeblock"], + icon: , + command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), + }, + { + commandKey: "image", + key: "image", + title: "Image", + icon: , + description: "Insert an image", + searchTerms: ["img", "photo", "picture", "media", "upload"], + command: ({ editor, range }) => + editor.chain().focus().deleteRange(range).setImageUpload({ event: "insert" }).run(), + }, + { + commandKey: "divider", + key: "divider", + title: "Divider", + description: "Visually divide blocks.", + searchTerms: ["line", "divider", "horizontal", "rule", "separate"], + icon: , + command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(), + }, + ], + }, + { + key: "text-color", + title: "Colors", + items: [ + ...COLORS_LIST.map( + (color) => + ({ commandKey: "text-color", - key: "text-color-default", - title: "Default", + key: `text-color-${color.textColor}`, + title: color.label, description: "Change text color", - searchTerms: ["color", "text", "default"], + searchTerms: ["color", "text", color.label], icon: ( ), - command: ({ editor, range }) => toggleTextColor(undefined, editor, range), - }, - ], - }, + command: ({ editor, range }) => toggleTextColor(color.textColor, editor, range), + }) as ISlashCommandItem + ), { - key: "background-color", - title: "Background colors", - items: [ - ...COLORS_LIST.map( - (color) => - ({ - commandKey: "background-color", - key: `background-color-${color.backgroundColor}`, - title: `${color.label} background`, - description: "Change background color", - searchTerms: ["color", "bg", "background", color.label], - icon: , - iconContainerStyle: { - borderRadius: "4px", - backgroundColor: color.backgroundColor, - }, - command: ({ editor, range }) => toggleBackgroundColor(color.backgroundColor, editor, range), - }) as ISlashCommandItem - ), - { + commandKey: "text-color", + key: "text-color-default", + title: "Default", + description: "Change text color", + searchTerms: ["color", "text", "default"], + icon: ( + + ), + command: ({ editor, range }) => toggleTextColor(undefined, editor, range), + }, + ], + }, + { + key: "background-color", + title: "Background colors", + items: [ + ...COLORS_LIST.map( + (color) => + ({ commandKey: "background-color", - key: "background-color-default", - title: "Default background", + key: `background-color-${color.backgroundColor}`, + title: `${color.label} background`, description: "Change background color", - searchTerms: ["color", "bg", "background", "default"], + searchTerms: ["color", "bg", "background", color.label], icon: , iconContainerStyle: { borderRadius: "4px", - backgroundColor: "rgba(var(--color-background-100))", - border: "1px solid rgba(var(--color-border-300))", + backgroundColor: color.backgroundColor, }, - command: ({ editor, range }) => toggleTextColor(undefined, editor, range), - }, - ], + command: ({ editor, range }) => toggleBackgroundColor(color.backgroundColor, editor, range), + }) as ISlashCommandItem + ), + { + commandKey: "background-color", + key: "background-color-default", + title: "Default background", + description: "Change background color", + searchTerms: ["color", "bg", "background", "default"], + icon: , + iconContainerStyle: { + borderRadius: "4px", + backgroundColor: "rgba(var(--color-background-100))", + border: "1px solid rgba(var(--color-border-300))", + }, + command: ({ editor, range }) => toggleTextColor(undefined, editor, range), }, - ]; + ], + }, +]; +export const getSlashCommandFilteredSections = + (additionalOptions?: ISlashCommandItem[]) => + ({ query }: { query: string }): TSlashCommandSection[] => { if (additionalOptions) { - additionalOptions.map((item) => slashCommandSections?.[0]?.items.push(item)); + additionalOptions.map((item) => SLASH_COMMAND_SECTIONS?.[0]?.items.push(item)); } - const filteredSlashSections = slashCommandSections.map((section) => ({ + const filteredSlashSections = SLASH_COMMAND_SECTIONS.map((section) => ({ ...section, items: section.items.filter((item) => { if (typeof query !== "string") return; diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx index 09f148eade6..ac88f20d3ac 100644 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -81,10 +81,9 @@ const renderItems = () => { onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { component?.updateProps(props); - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); + popup?.[0]?.setProps({ + getReferenceClientRect: props.clientRect, + }); }, onKeyDown: (props: { event: KeyboardEvent }) => { if (props.event.key === "Escape") { diff --git a/yarn.lock b/yarn.lock index 18be853fba2..3bc102278ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4109,7 +4109,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48": +"@types/react@*", "@types/react@18.2.48", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48", "@types/react@^18.3.5": version "18.2.48" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1" integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w== @@ -4118,14 +4118,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^18.3.5": - version "18.3.7" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.7.tgz#6decbfbb01f8d82d56ff5403394121940faa6569" - integrity sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - "@types/reactcss@*": version "1.2.12" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.12.tgz#57f6f046e7aafbe0288689bd96a2d5664378ca7b" From 4c8b091807b56b875b7dbe84455536e0957df51a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 20 Sep 2024 15:08:43 +0530 Subject: [PATCH 5/6] chore: move default text and background options to the top --- packages/editor/src/core/constants/common.ts | 10 ++-- .../slash-commands/command-items-list.tsx | 60 +++++++++---------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/editor/src/core/constants/common.ts b/packages/editor/src/core/constants/common.ts index 1cba97e8d36..4e46fb83769 100644 --- a/packages/editor/src/core/constants/common.ts +++ b/packages/editor/src/core/constants/common.ts @@ -3,11 +3,11 @@ export const COLORS_LIST: { textColor: string; label: string; }[] = [ - { - backgroundColor: "#1c202426", - textColor: "#1c2024", - label: "Black", - }, + // { + // backgroundColor: "#1c202426", + // textColor: "#1c2024", + // label: "Black", + // }, { backgroundColor: "#5c5e6326", textColor: "#5c5e63", diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index 152ffdbc937..b66dcd58def 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -194,6 +194,22 @@ const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [ key: "text-color", title: "Colors", items: [ + { + commandKey: "text-color", + key: "text-color-default", + title: "Default", + description: "Change text color", + searchTerms: ["color", "text", "default"], + icon: ( + + ), + command: ({ editor, range }) => toggleTextColor(undefined, editor, range), + }, ...COLORS_LIST.map( (color) => ({ @@ -213,28 +229,26 @@ const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [ command: ({ editor, range }) => toggleTextColor(color.textColor, editor, range), }) as ISlashCommandItem ), - { - commandKey: "text-color", - key: "text-color-default", - title: "Default", - description: "Change text color", - searchTerms: ["color", "text", "default"], - icon: ( - - ), - command: ({ editor, range }) => toggleTextColor(undefined, editor, range), - }, ], }, { key: "background-color", title: "Background colors", items: [ + { + commandKey: "background-color", + key: "background-color-default", + title: "Default background", + description: "Change background color", + searchTerms: ["color", "bg", "background", "default"], + icon: , + iconContainerStyle: { + borderRadius: "4px", + backgroundColor: "rgba(var(--color-background-100))", + border: "1px solid rgba(var(--color-border-300))", + }, + command: ({ editor, range }) => toggleTextColor(undefined, editor, range), + }, ...COLORS_LIST.map( (color) => ({ @@ -251,20 +265,6 @@ const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [ command: ({ editor, range }) => toggleBackgroundColor(color.backgroundColor, editor, range), }) as ISlashCommandItem ), - { - commandKey: "background-color", - key: "background-color-default", - title: "Default background", - description: "Change background color", - searchTerms: ["color", "bg", "background", "default"], - icon: , - iconContainerStyle: { - borderRadius: "4px", - backgroundColor: "rgba(var(--color-background-100))", - border: "1px solid rgba(var(--color-border-300))", - }, - command: ({ editor, range }) => toggleTextColor(undefined, editor, range), - }, ], }, ]; From 8634852fcd4130a31f914b71d9392dfd79c3d686 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 20 Sep 2024 15:15:03 +0530 Subject: [PATCH 6/6] fix: sections filtering logic --- .../slash-commands/command-items-list.tsx | 2 +- .../slash-commands/command-menu.tsx | 53 +++++++++---------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index b66dcd58def..5b274285ad4 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -290,5 +290,5 @@ export const getSlashCommandFilteredSections = }), })); - return filteredSlashSections; + return filteredSlashSections.filter((s) => s.items.length !== 0); }; diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx index fb4a45f6aa2..977f688286e 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx @@ -96,35 +96,32 @@ export const SlashCommandsMenu = (props: Props) => { ref={commandListContainer} className="z-10 max-h-80 min-w-[12rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg space-y-2" > - {sections.map((section, sectionIndex) => { - if (section.items.length === 0) return; - return ( -
- {section.title &&
{section.title}
} -
- {section.items.map((item, itemIndex) => ( - { - e.stopPropagation(); - selectItem(sectionIndex, itemIndex); - }} - onMouseEnter={() => - setSelectedIndex({ - section: sectionIndex, - item: itemIndex, - }) - } - sectionIndex={sectionIndex} - /> - ))} -
+ {sections.map((section, sectionIndex) => ( +
+ {section.title &&
{section.title}
} +
+ {section.items.map((item, itemIndex) => ( + { + e.stopPropagation(); + selectItem(sectionIndex, itemIndex); + }} + onMouseEnter={() => + setSelectedIndex({ + section: sectionIndex, + item: itemIndex, + }) + } + sectionIndex={sectionIndex} + /> + ))}
- ); - })} +
+ ))}
); };