[PULSE-42] feat: text alignment for all editors (#5847)

* feat: text alignment for editors

* fix: text alignment types

* fix: build errors

* fix: build error

* fix: toolbar movement post alignment selection

* fix: callout type

* fix: image node types

* chore: add ts error warning
This commit is contained in:
Aaryan Khandelwal 2024-11-05 17:46:34 +05:30 committed by GitHub
parent bb311b750f
commit b4de055463
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 641 additions and 324 deletions

View file

@ -48,6 +48,7 @@
"@tiptap/extension-placeholder": "^2.3.0", "@tiptap/extension-placeholder": "^2.3.0",
"@tiptap/extension-task-item": "^2.1.13", "@tiptap/extension-task-item": "^2.1.13",
"@tiptap/extension-task-list": "^2.1.13", "@tiptap/extension-task-list": "^2.1.13",
"@tiptap/extension-text-align": "^2.8.0",
"@tiptap/extension-text-style": "^2.7.1", "@tiptap/extension-text-style": "^2.7.1",
"@tiptap/extension-underline": "^2.1.13", "@tiptap/extension-underline": "^2.1.13",
"@tiptap/pm": "^2.1.13", "@tiptap/pm": "^2.1.13",

View file

@ -0,0 +1,91 @@
import { Editor } from "@tiptap/core";
import { AlignCenter, AlignLeft, AlignRight, LucideIcon } from "lucide-react";
// components
import { TextAlignItem } from "@/components/menus";
// helpers
import { cn } from "@/helpers/common";
// types
import { TEditorCommands } from "@/types";
type Props = {
editor: Editor;
onClose: () => void;
};
export const TextAlignmentSelector: React.FC<Props> = (props) => {
const { editor, onClose } = props;
const menuItem = TextAlignItem(editor);
const textAlignmentOptions: {
itemKey: TEditorCommands;
renderKey: string;
icon: LucideIcon;
command: () => void;
isActive: () => boolean;
}[] = [
{
itemKey: "text-align",
renderKey: "text-align-left",
icon: AlignLeft,
command: () =>
menuItem.command({
alignment: "left",
}),
isActive: () =>
menuItem.isActive({
alignment: "left",
}),
},
{
itemKey: "text-align",
renderKey: "text-align-center",
icon: AlignCenter,
command: () =>
menuItem.command({
alignment: "center",
}),
isActive: () =>
menuItem.isActive({
alignment: "center",
}),
},
{
itemKey: "text-align",
renderKey: "text-align-right",
icon: AlignRight,
command: () =>
menuItem.command({
alignment: "right",
}),
isActive: () =>
menuItem.isActive({
alignment: "right",
}),
},
];
return (
<div className="flex gap-0.5 px-2">
{textAlignmentOptions.map((item) => (
<button
key={item.renderKey}
type="button"
onClick={(e) => {
e.stopPropagation();
item.command();
onClose();
}}
className={cn(
"size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors",
{
"bg-custom-background-80 text-custom-text-100": item.isActive(),
}
)}
>
<item.icon className="size-4" />
</button>
))}
</div>
);
};

View file

@ -16,8 +16,8 @@ type Props = {
export const BubbleMenuColorSelector: FC<Props> = (props) => { export const BubbleMenuColorSelector: FC<Props> = (props) => {
const { editor, isOpen, setIsOpen } = props; const { editor, isOpen, setIsOpen } = props;
const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive(c.key)); const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key }));
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive(c.key)); const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key }));
return ( return (
<div className="relative h-full"> <div className="relative h-full">
@ -64,7 +64,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
style={{ style={{
backgroundColor: color.textColor, backgroundColor: color.textColor,
}} }}
onClick={() => TextColorItem(editor).command(color.key)} onClick={() => TextColorItem(editor).command({ color: color.key })}
/> />
))} ))}
<button <button
@ -87,7 +87,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
style={{ style={{
backgroundColor: color.backgroundColor, backgroundColor: color.backgroundColor,
}} }}
onClick={() => BackgroundColorItem(editor).command(color.key)} onClick={() => BackgroundColorItem(editor).command({ color: color.key })}
/> />
))} ))}
<button <button

View file

@ -19,6 +19,8 @@ import {
} from "@/components/menus"; } from "@/components/menus";
// helpers // helpers
import { cn } from "@/helpers/common"; import { cn } from "@/helpers/common";
// types
import { TEditorCommands } from "@/types";
type Props = { type Props = {
editor: Editor; editor: Editor;
@ -29,7 +31,7 @@ type Props = {
export const BubbleMenuNodeSelector: FC<Props> = (props) => { export const BubbleMenuNodeSelector: FC<Props> = (props) => {
const { editor, isOpen, setIsOpen } = props; const { editor, isOpen, setIsOpen } = props;
const items: EditorMenuItem[] = [ const items: EditorMenuItem<TEditorCommands>[] = [
TextItem(editor), TextItem(editor),
HeadingOneItem(editor), HeadingOneItem(editor),
HeadingTwoItem(editor), HeadingTwoItem(editor),
@ -44,7 +46,7 @@ export const BubbleMenuNodeSelector: FC<Props> = (props) => {
CodeItem(editor), CodeItem(editor),
]; ];
const activeItem = items.filter((item) => item.isActive("")).pop() ?? { const activeItem = items.filter((item) => item.isActive()).pop() ?? {
name: "Multiple", name: "Multiple",
}; };

View file

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { BubbleMenu, BubbleMenuProps, isNodeSelection } from "@tiptap/react"; import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
// components // components
import { import {
BoldItem, BoldItem,
@ -7,7 +7,6 @@ import {
BubbleMenuLinkSelector, BubbleMenuLinkSelector,
BubbleMenuNodeSelector, BubbleMenuNodeSelector,
CodeItem, CodeItem,
EditorMenuItem,
ItalicItem, ItalicItem,
StrikeThroughItem, StrikeThroughItem,
UnderLineItem, UnderLineItem,
@ -16,6 +15,8 @@ import {
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
// helpers // helpers
import { cn } from "@/helpers/common"; import { cn } from "@/helpers/common";
// local components
import { TextAlignmentSelector } from "./alignment-selector";
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">; type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
@ -26,7 +27,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
const [isSelecting, setIsSelecting] = useState(false); const [isSelecting, setIsSelecting] = useState(false);
const items: EditorMenuItem[] = props.editor.isActive("code") const basicFormattingOptions = props.editor.isActive("code")
? [CodeItem(props.editor)] ? [CodeItem(props.editor)]
: [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)]; : [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)];
@ -132,7 +133,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
)} )}
</div> </div>
<div className="flex gap-0.5 px-2"> <div className="flex gap-0.5 px-2">
{items.map((item) => ( {basicFormattingOptions.map((item) => (
<button <button
key={item.key} key={item.key}
type="button" type="button"
@ -151,6 +152,15 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
</button> </button>
))} ))}
</div> </div>
<TextAlignmentSelector
editor={props.editor}
onClose={() => {
const editor = props.editor as Editor;
if (!editor) return;
const pos = editor.state.selection.to;
editor.commands.setTextSelection(pos ?? 0);
}}
/>
</> </>
)} )}
</BubbleMenu> </BubbleMenu>

View file

@ -1,4 +1,3 @@
import { Selection } from "@tiptap/pm/state";
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { import {
BoldIcon, BoldIcon,
@ -22,6 +21,7 @@ import {
LucideIcon, LucideIcon,
MinusSquare, MinusSquare,
Palette, Palette,
AlignCenter,
} from "lucide-react"; } from "lucide-react";
// helpers // helpers
import { import {
@ -29,6 +29,7 @@ import {
insertImage, insertImage,
insertTableCommand, insertTableCommand,
setText, setText,
setTextAlign,
toggleBackgroundColor, toggleBackgroundColor,
toggleBlockquote, toggleBlockquote,
toggleBold, toggleBold,
@ -48,24 +49,20 @@ import {
toggleUnderline, toggleUnderline,
} from "@/helpers/editor-commands"; } from "@/helpers/editor-commands";
// types // types
import { TColorEditorCommands, TNonColorEditorCommands } from "@/types"; import { TCommandWithProps, TEditorCommands } from "@/types";
export type EditorMenuItem = { type isActiveFunction<T extends TEditorCommands> = (params?: TCommandWithProps<T>) => boolean;
type commandFunction<T extends TEditorCommands> = (params?: TCommandWithProps<T>) => void;
export type EditorMenuItem<T extends TEditorCommands> = {
key: T;
name: string; name: string;
command: (...args: any) => void; command: commandFunction<T>;
icon: LucideIcon; icon: LucideIcon;
} & ( isActive: isActiveFunction<T>;
| { };
key: TNonColorEditorCommands;
isActive: () => boolean;
}
| {
key: TColorEditorCommands;
isActive: (color: string | undefined) => boolean;
}
);
export const TextItem = (editor: Editor): EditorMenuItem => ({ export const TextItem = (editor: Editor): EditorMenuItem<"text"> => ({
key: "text", key: "text",
name: "Text", name: "Text",
isActive: () => editor.isActive("paragraph"), isActive: () => editor.isActive("paragraph"),
@ -73,7 +70,7 @@ export const TextItem = (editor: Editor): EditorMenuItem => ({
icon: CaseSensitive, icon: CaseSensitive,
}); });
export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({ export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => ({
key: "h1", key: "h1",
name: "Heading 1", name: "Heading 1",
isActive: () => editor.isActive("heading", { level: 1 }), isActive: () => editor.isActive("heading", { level: 1 }),
@ -81,7 +78,7 @@ export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({
icon: Heading1, icon: Heading1,
}); });
export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({ export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => ({
key: "h2", key: "h2",
name: "Heading 2", name: "Heading 2",
isActive: () => editor.isActive("heading", { level: 2 }), isActive: () => editor.isActive("heading", { level: 2 }),
@ -89,7 +86,7 @@ export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({
icon: Heading2, icon: Heading2,
}); });
export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({ export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => ({
key: "h3", key: "h3",
name: "Heading 3", name: "Heading 3",
isActive: () => editor.isActive("heading", { level: 3 }), isActive: () => editor.isActive("heading", { level: 3 }),
@ -97,7 +94,7 @@ export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({
icon: Heading3, icon: Heading3,
}); });
export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({ export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => ({
key: "h4", key: "h4",
name: "Heading 4", name: "Heading 4",
isActive: () => editor.isActive("heading", { level: 4 }), isActive: () => editor.isActive("heading", { level: 4 }),
@ -105,7 +102,7 @@ export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({
icon: Heading4, icon: Heading4,
}); });
export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({ export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => ({
key: "h5", key: "h5",
name: "Heading 5", name: "Heading 5",
isActive: () => editor.isActive("heading", { level: 5 }), isActive: () => editor.isActive("heading", { level: 5 }),
@ -113,7 +110,7 @@ export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({
icon: Heading5, icon: Heading5,
}); });
export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({ export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => ({
key: "h6", key: "h6",
name: "Heading 6", name: "Heading 6",
isActive: () => editor.isActive("heading", { level: 6 }), isActive: () => editor.isActive("heading", { level: 6 }),
@ -121,7 +118,7 @@ export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({
icon: Heading6, icon: Heading6,
}); });
export const BoldItem = (editor: Editor): EditorMenuItem => ({ export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({
key: "bold", key: "bold",
name: "Bold", name: "Bold",
isActive: () => editor?.isActive("bold"), isActive: () => editor?.isActive("bold"),
@ -129,7 +126,7 @@ export const BoldItem = (editor: Editor): EditorMenuItem => ({
icon: BoldIcon, icon: BoldIcon,
}); });
export const ItalicItem = (editor: Editor): EditorMenuItem => ({ export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({
key: "italic", key: "italic",
name: "Italic", name: "Italic",
isActive: () => editor?.isActive("italic"), isActive: () => editor?.isActive("italic"),
@ -137,7 +134,7 @@ export const ItalicItem = (editor: Editor): EditorMenuItem => ({
icon: ItalicIcon, icon: ItalicIcon,
}); });
export const UnderLineItem = (editor: Editor): EditorMenuItem => ({ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
key: "underline", key: "underline",
name: "Underline", name: "Underline",
isActive: () => editor?.isActive("underline"), isActive: () => editor?.isActive("underline"),
@ -145,7 +142,7 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem => ({
icon: UnderlineIcon, icon: UnderlineIcon,
}); });
export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({ export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
key: "strikethrough", key: "strikethrough",
name: "Strikethrough", name: "Strikethrough",
isActive: () => editor?.isActive("strike"), isActive: () => editor?.isActive("strike"),
@ -153,7 +150,7 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
icon: StrikethroughIcon, icon: StrikethroughIcon,
}); });
export const BulletListItem = (editor: Editor): EditorMenuItem => ({ export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> => ({
key: "bulleted-list", key: "bulleted-list",
name: "Bulleted list", name: "Bulleted list",
isActive: () => editor?.isActive("bulletList"), isActive: () => editor?.isActive("bulletList"),
@ -161,7 +158,7 @@ export const BulletListItem = (editor: Editor): EditorMenuItem => ({
icon: ListIcon, icon: ListIcon,
}); });
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({ export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list"> => ({
key: "numbered-list", key: "numbered-list",
name: "Numbered list", name: "Numbered list",
isActive: () => editor?.isActive("orderedList"), isActive: () => editor?.isActive("orderedList"),
@ -169,7 +166,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
icon: ListOrderedIcon, icon: ListOrderedIcon,
}); });
export const TodoListItem = (editor: Editor): EditorMenuItem => ({ export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({
key: "to-do-list", key: "to-do-list",
name: "To-do list", name: "To-do list",
isActive: () => editor.isActive("taskItem"), isActive: () => editor.isActive("taskItem"),
@ -177,7 +174,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({
icon: CheckSquare, icon: CheckSquare,
}); });
export const QuoteItem = (editor: Editor): EditorMenuItem => ({ export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({
key: "quote", key: "quote",
name: "Quote", name: "Quote",
isActive: () => editor?.isActive("blockquote"), isActive: () => editor?.isActive("blockquote"),
@ -185,7 +182,7 @@ export const QuoteItem = (editor: Editor): EditorMenuItem => ({
icon: TextQuote, icon: TextQuote,
}); });
export const CodeItem = (editor: Editor): EditorMenuItem => ({ export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({
key: "code", key: "code",
name: "Code", name: "Code",
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
@ -193,7 +190,7 @@ export const CodeItem = (editor: Editor): EditorMenuItem => ({
icon: CodeIcon, icon: CodeIcon,
}); });
export const TableItem = (editor: Editor): EditorMenuItem => ({ export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({
key: "table", key: "table",
name: "Table", name: "Table",
isActive: () => editor?.isActive("table"), isActive: () => editor?.isActive("table"),
@ -201,14 +198,14 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({
icon: TableIcon, icon: TableIcon,
}); });
export const ImageItem = (editor: Editor) => export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
({ key: "image",
key: "image", name: "Image",
name: "Image", isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"), command: ({ savedSelection }) =>
command: (savedSelection: Selection | null) => insertImage({ editor, event: "insert", pos: savedSelection?.from }), insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }),
icon: ImageIcon, icon: ImageIcon,
}) as const; });
export const HorizontalRuleItem = (editor: Editor) => export const HorizontalRuleItem = (editor: Editor) =>
({ ({
@ -219,23 +216,31 @@ export const HorizontalRuleItem = (editor: Editor) =>
icon: MinusSquare, icon: MinusSquare,
}) as const; }) as const;
export const TextColorItem = (editor: Editor): EditorMenuItem => ({ export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({
key: "text-color", key: "text-color",
name: "Color", name: "Color",
isActive: (color) => editor.isActive("customColor", { color }), isActive: ({ color }) => editor.isActive("customColor", { color }),
command: (color: string) => toggleTextColor(color, editor), command: ({ color }) => toggleTextColor(color, editor),
icon: Palette, icon: Palette,
}); });
export const BackgroundColorItem = (editor: Editor): EditorMenuItem => ({ export const BackgroundColorItem = (editor: Editor): EditorMenuItem<"background-color"> => ({
key: "background-color", key: "background-color",
name: "Background color", name: "Background color",
isActive: (color) => editor.isActive("customColor", { backgroundColor: color }), isActive: ({ color }) => editor.isActive("customColor", { backgroundColor: color }),
command: (color: string) => toggleBackgroundColor(color, editor), command: ({ color }) => toggleBackgroundColor(color, editor),
icon: Palette, icon: Palette,
}); });
export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => { export const TextAlignItem = (editor: Editor): EditorMenuItem<"text-align"> => ({
key: "text-align",
name: "Text align",
isActive: ({ alignment }) => editor.isActive({ textAlign: alignment }),
command: ({ alignment }) => setTextAlign(alignment, editor),
icon: AlignCenter,
});
export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem<TEditorCommands>[] => {
if (!editor) return []; if (!editor) return [];
return [ return [
@ -260,5 +265,6 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => {
HorizontalRuleItem(editor), HorizontalRuleItem(editor),
TextColorItem(editor), TextColorItem(editor),
BackgroundColorItem(editor), BackgroundColorItem(editor),
TextAlignItem(editor),
]; ];
}; };

View file

@ -16,6 +16,7 @@ import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props
import { CustomMentionWithoutProps } from "./mentions/mentions-without-props"; import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
import { CustomQuoteExtension } from "./quote"; import { CustomQuoteExtension } from "./quote";
import { TableHeader, TableCell, TableRow, Table } from "./table"; import { TableHeader, TableCell, TableRow, Table } from "./table";
import { CustomTextAlignExtension } from "./text-align";
import { CustomCalloutExtensionConfig } from "./callout/extension-config"; import { CustomCalloutExtensionConfig } from "./callout/extension-config";
import { CustomColorExtension } from "./custom-color"; import { CustomColorExtension } from "./custom-color";
@ -85,6 +86,7 @@ export const CoreEditorExtensionsWithoutProps = [
TableCell, TableCell,
TableRow, TableRow,
CustomMentionWithoutProps(), CustomMentionWithoutProps(),
CustomTextAlignExtension,
CustomCalloutExtensionConfig, CustomCalloutExtensionConfig,
CustomColorExtension, CustomColorExtension,
]; ];

View file

@ -19,6 +19,7 @@ import {
CustomLinkExtension, CustomLinkExtension,
CustomMention, CustomMention,
CustomQuoteExtension, CustomQuoteExtension,
CustomTextAlignExtension,
CustomTypographyExtension, CustomTypographyExtension,
DropHandlerExtension, DropHandlerExtension,
ImageExtension, ImageExtension,
@ -158,6 +159,7 @@ export const CoreEditorExtensions = (args: TArguments) => {
includeChildren: true, includeChildren: true,
}), }),
CharacterCount, CharacterCount,
CustomTextAlignExtension,
CustomCalloutExtension, CustomCalloutExtension,
CustomColorExtension, CustomColorExtension,
]; ];

View file

@ -16,10 +16,10 @@ export * from "./custom-color";
export * from "./drop"; export * from "./drop";
export * from "./enter-key-extension"; export * from "./enter-key-extension";
export * from "./extensions"; export * from "./extensions";
export * from "./headers";
export * from "./horizontal-rule"; export * from "./horizontal-rule";
export * from "./keymap"; export * from "./keymap";
export * from "./quote"; export * from "./quote";
export * from "./read-only-extensions"; export * from "./read-only-extensions";
export * from "./side-menu"; export * from "./side-menu";
export * from "./slash-commands"; export * from "./text-align";
export * from "./headers";

View file

@ -21,6 +21,7 @@ import {
CustomMention, CustomMention,
HeadingListExtension, HeadingListExtension,
CustomReadOnlyImageExtension, CustomReadOnlyImageExtension,
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension, CustomCalloutReadOnlyExtension,
CustomColorExtension, CustomColorExtension,
} from "@/extensions"; } from "@/extensions";
@ -125,6 +126,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
CharacterCount, CharacterCount,
CustomColorExtension, CustomColorExtension,
HeadingListExtension, HeadingListExtension,
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension, CustomCalloutReadOnlyExtension,
]; ];
}; };

View file

@ -3,12 +3,12 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
import { TSlashCommandSection } from "./command-items-list"; import { TSlashCommandSection } from "./command-items-list";
import { CommandMenuItem } from "./command-menu-item"; import { CommandMenuItem } from "./command-menu-item";
type Props = { export type SlashCommandsMenuProps = {
items: TSlashCommandSection[]; items: TSlashCommandSection[];
command: any; command: any;
}; };
export const SlashCommandsMenu = (props: Props) => { export const SlashCommandsMenu = (props: SlashCommandsMenuProps) => {
const { items: sections, command } = props; const { items: sections, command } = props;
// states // states
const [selectedIndex, setSelectedIndex] = useState({ const [selectedIndex, setSelectedIndex] = useState({

View file

@ -6,7 +6,7 @@ import tippy from "tippy.js";
import { ISlashCommandItem } from "@/types"; import { ISlashCommandItem } from "@/types";
// components // components
import { getSlashCommandFilteredSections } from "./command-items-list"; import { getSlashCommandFilteredSections } from "./command-items-list";
import { SlashCommandsMenu } from "./command-menu"; import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu";
export type SlashCommandOptions = { export type SlashCommandOptions = {
suggestion: Omit<SuggestionOptions, "editor">; suggestion: Omit<SuggestionOptions, "editor">;
@ -55,7 +55,7 @@ interface CommandListInstance {
} }
const renderItems = () => { const renderItems = () => {
let component: ReactRenderer<CommandListInstance, typeof SlashCommandsMenu> | null = null; let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
let popup: any | null = null; let popup: any | null = null;
return { return {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {

View file

@ -0,0 +1,8 @@
import TextAlign from "@tiptap/extension-text-align";
export type TTextAlign = "left" | "center" | "right";
export const CustomTextAlignExtension = TextAlign.configure({
alignments: ["left", "center", "right"],
types: ["heading", "paragraph"],
});

View file

@ -181,10 +181,14 @@ export const toggleBackgroundColor = (color: string | undefined, editor: Editor,
} }
}; };
export const setTextAlign = (alignment: string, editor: Editor) => {
editor.chain().focus().setTextAlign(alignment).run();
};
export const insertHorizontalRule = (editor: Editor, range?: Range) => { export const insertHorizontalRule = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run(); if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run();
else editor.chain().focus().setHorizontalRule().run(); else editor.chain().focus().setHorizontalRule().run();
} };
export const insertCallout = (editor: Editor, range?: Range) => { export const insertCallout = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).insertCallout().run(); if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
else editor.chain().focus().insertCallout().run(); else editor.chain().focus().insertCallout().run();

View file

@ -6,7 +6,7 @@ import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useTiptapEditor, Editor } from "@tiptap/react"; import { useEditor as useTiptapEditor, Editor } from "@tiptap/react";
import * as Y from "yjs"; import * as Y from "yjs";
// components // components
import { getEditorMenuItems } from "@/components/menus"; import { EditorMenuItem, getEditorMenuItems } from "@/components/menus";
// extensions // extensions
import { CoreEditorExtensions } from "@/extensions"; import { CoreEditorExtensions } from "@/extensions";
// helpers // helpers
@ -155,11 +155,11 @@ export const useEditor = (props: CustomEditorProps) => {
const item = getEditorMenuItem(itemKey); const item = getEditorMenuItem(itemKey);
if (item) { if (item) {
if (item.key === "image") { if (item.key === "image") {
item.command(savedSelectionRef.current); (item as EditorMenuItem<"image">).command({
} else if (itemKey === "text-color" || itemKey === "background-color") { savedSelection: savedSelectionRef.current,
item.command(props.color); });
} else { } else {
item.command(); item.command(props);
} }
} else { } else {
console.warn(`No command found for item: ${itemKey}`); console.warn(`No command found for item: ${itemKey}`);
@ -173,11 +173,7 @@ export const useEditor = (props: CustomEditorProps) => {
const item = getEditorMenuItem(itemKey); const item = getEditorMenuItem(itemKey);
if (!item) return false; if (!item) return false;
if (itemKey === "text-color" || itemKey === "background-color") { return item.isActive(props);
return item.isActive(props.color);
} else {
return item.isActive("");
}
}, },
onHeadingChange: (callback: (headings: IMarking[]) => void) => { onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension // Subscribe to update event emitted from headers extension

View file

@ -1,4 +1,5 @@
import { JSONContent } from "@tiptap/core"; import { JSONContent } from "@tiptap/core";
import { Selection } from "@tiptap/pm/state";
// helpers // helpers
import { IMarking } from "@/helpers/scroll-to-node"; import { IMarking } from "@/helpers/scroll-to-node";
// types // types
@ -6,14 +7,64 @@ import {
IMentionHighlight, IMentionHighlight,
IMentionSuggestion, IMentionSuggestion,
TAIHandler, TAIHandler,
TColorEditorCommands,
TDisplayConfig, TDisplayConfig,
TEmbedConfig, TEmbedConfig,
TExtensions, TExtensions,
TFileHandler, TFileHandler,
TNonColorEditorCommands,
TServerHandler, TServerHandler,
} from "@/types"; } from "@/types";
import { TTextAlign } from "@/extensions";
export type TEditorCommands =
| "text"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "bold"
| "italic"
| "underline"
| "strikethrough"
| "bulleted-list"
| "numbered-list"
| "to-do-list"
| "quote"
| "code"
| "table"
| "image"
| "divider"
| "issue-embed"
| "text-color"
| "background-color"
| "text-align"
| "callout";
export type TCommandExtraProps = {
image: {
savedSelection: Selection | null;
};
"text-color": {
color: string | undefined;
};
"background-color": {
color: string | undefined;
};
"text-align": {
alignment: TTextAlign;
};
};
// Create a utility type that maps a command to its extra props or an empty object if none are defined
export type TCommandWithProps<T extends TEditorCommands> = T extends keyof TCommandExtraProps
? TCommandExtraProps[T] // If the command has extra props, include them
: object; // Otherwise, just return the command type with no extra props
type TCommandWithPropsWithItemKey<T extends TEditorCommands> = T extends keyof TCommandExtraProps
? { itemKey: T } & TCommandExtraProps[T]
: { itemKey: T };
// editor refs // editor refs
export type EditorReadOnlyRefApi = { export type EditorReadOnlyRefApi = {
getMarkDown: () => string; getMarkDown: () => string;
@ -39,26 +90,8 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void; scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
getCurrentCursorPosition: () => number | undefined; getCurrentCursorPosition: () => number | undefined;
setEditorValueAtCursorPosition: (content: string) => void; setEditorValueAtCursorPosition: (content: string) => void;
executeMenuItemCommand: ( executeMenuItemCommand: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => void;
props: isMenuItemActive: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => boolean;
| {
itemKey: TNonColorEditorCommands;
}
| {
itemKey: TColorEditorCommands;
color: string | undefined;
}
) => void;
isMenuItemActive: (
props:
| {
itemKey: TNonColorEditorCommands;
}
| {
itemKey: TColorEditorCommands;
color: string | undefined;
}
) => boolean;
onStateChange: (callback: () => void) => () => void; onStateChange: (callback: () => void) => () => void;
setFocusAtPosition: (position: number) => void; setFocusAtPosition: (position: number) => void;
isEditorReadyToDiscard: () => boolean; isEditorReadyToDiscard: () => boolean;

View file

@ -1,33 +1,7 @@
import { CSSProperties } from "react"; import { CSSProperties } from "react";
import { Editor, Range } from "@tiptap/core"; import { Editor, Range } from "@tiptap/core";
// types
export type TEditorCommands = import { TEditorCommands } from "@/types";
| "text"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "bold"
| "italic"
| "underline"
| "strikethrough"
| "bulleted-list"
| "numbered-list"
| "to-do-list"
| "quote"
| "code"
| "table"
| "image"
| "divider"
| "issue-embed"
| "text-color"
| "background-color"
| "callout";
export type TColorEditorCommands = Extract<TEditorCommands, "text-color" | "background-color">;
export type TNonColorEditorCommands = Exclude<TEditorCommands, "text-color" | "background-color">;
export type CommandProps = { export type CommandProps = {
editor: Editor; editor: Editor;

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
// editor // editor
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
// components // components
import { IssueCommentToolbar } from "@/components/editor"; import { IssueCommentToolbar } from "@/components/editor";
// helpers // helpers
@ -30,11 +30,12 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
} = props; } = props;
// use-mention // use-mention
const { mentionHighlights } = useMention(); const { mentionHighlights } = useMention();
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> { function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
return !!ref && typeof ref === "object" && "current" in ref; return !!ref && typeof ref === "object" && "current" in ref;
} }
// derived values
const isEmpty = isCommentEmpty(props.initialValue); const isEmpty = isCommentEmpty(props.initialValue);
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
return ( return (
<div className="border border-custom-border-200 rounded p-3 space-y-3"> <div className="border border-custom-border-200 rounded p-3 space-y-3">
@ -54,18 +55,19 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
containerClassName={cn(containerClassName, "relative")} containerClassName={cn(containerClassName, "relative")}
/> />
<IssueCommentToolbar <IssueCommentToolbar
executeCommand={(key) => { executeCommand={(item) => {
if (isMutableRefObject<EditorRefApi>(ref)) { // TODO: update this while toolbar homogenization
ref.current?.executeMenuItemCommand({ // @ts-expect-error type mismatch here
itemKey: key as TNonColorEditorCommands, editorRef?.executeMenuItemCommand({
}); itemKey: item.itemKey,
} ...item.extraProps,
});
}} }}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
showSubmitButton={showSubmitButton} showSubmitButton={showSubmitButton}
handleSubmit={(e) => rest.onEnterKeyPress?.(e)} handleSubmit={(e) => rest.onEnterKeyPress?.(e)}
isCommentEmpty={isEmpty} isCommentEmpty={isEmpty}
editorRef={isMutableRefObject<EditorRefApi>(ref) ? ref : null} editorRef={editorRef}
/> />
</div> </div>
); );

View file

@ -2,21 +2,21 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
// editor // editor
import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; import { EditorRefApi } from "@plane/editor";
// ui // ui
import { Button, Tooltip } from "@plane/ui"; import { Button, Tooltip } from "@plane/ui";
// constants // constants
import { TOOLBAR_ITEMS } from "@/constants/editor"; import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
type Props = { type Props = {
executeCommand: (commandKey: TEditorCommands) => void; executeCommand: (item: ToolbarMenuItem) => void;
handleSubmit: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; handleSubmit: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
isCommentEmpty: boolean; isCommentEmpty: boolean;
isSubmitting: boolean; isSubmitting: boolean;
showSubmitButton: boolean; showSubmitButton: boolean;
editorRef: React.MutableRefObject<EditorRefApi | null> | null; editorRef: EditorRefApi | null;
}; };
const toolbarItems = TOOLBAR_ITEMS.lite; const toolbarItems = TOOLBAR_ITEMS.lite;
@ -28,24 +28,25 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
// Function to update active states // Function to update active states
const updateActiveStates = useCallback(() => { const updateActiveStates = useCallback(() => {
if (editorRef?.current) { if (!editorRef) return;
const newActiveStates: Record<string, boolean> = {}; const newActiveStates: Record<string, boolean> = {};
Object.values(toolbarItems) Object.values(toolbarItems)
.flat() .flat()
.forEach((item) => { .forEach((item) => {
// Assert that editorRef.current is not null // TODO: update this while toolbar homogenization
newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ // @ts-expect-error type mismatch here
itemKey: item.key as TNonColorEditorCommands, newActiveStates[item.renderKey] = editorRef.isMenuItemActive({
}); itemKey: item.itemKey,
...item.extraProps,
}); });
setActiveStates(newActiveStates); });
} setActiveStates(newActiveStates);
}, [editorRef]); }, [editorRef]);
// useEffect to call updateActiveStates when isActive prop changes // useEffect to call updateActiveStates when isActive prop changes
useEffect(() => { useEffect(() => {
if (!editorRef?.current) return; if (!editorRef) return;
const unsubscribe = editorRef.current.onStateChange(updateActiveStates); const unsubscribe = editorRef.onStateChange(updateActiveStates);
updateActiveStates(); updateActiveStates();
return () => unsubscribe(); return () => unsubscribe();
}, [editorRef, updateActiveStates]); }, [editorRef, updateActiveStates]);
@ -61,35 +62,39 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
"pl-0": index === 0, "pl-0": index === 0,
})} })}
> >
{toolbarItems[key].map((item) => ( {toolbarItems[key].map((item) => {
<Tooltip const isItemActive = activeStates[item.renderKey];
key={item.key}
tooltipContent={ return (
<p className="flex flex-col gap-1 text-center text-xs"> <Tooltip
<span className="font-medium">{item.name}</span> key={item.renderKey}
{item.shortcut && <kbd className="text-custom-text-400">{item.shortcut.join(" + ")}</kbd>} tooltipContent={
</p> <p className="flex flex-col gap-1 text-center text-xs">
} <span className="font-medium">{item.name}</span>
> {item.shortcut && <kbd className="text-custom-text-400">{item.shortcut.join(" + ")}</kbd>}
<button </p>
type="button" }
onClick={() => executeCommand(item.key)}
className={cn(
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-400 hover:bg-custom-background-80",
{
"bg-custom-background-80 text-custom-text-100": activeStates[item.key],
}
)}
> >
<item.icon <button
className={cn("h-3.5 w-3.5", { type="button"
"text-custom-text-100": activeStates[item.key], onClick={() => executeCommand(item)}
})} className={cn(
strokeWidth={2.5} "grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-400 hover:bg-custom-background-80",
/> {
</button> "bg-custom-background-80 text-custom-text-100": isItemActive,
</Tooltip> }
))} )}
>
<item.icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": isItemActive,
})}
strokeWidth={2.5}
/>
</button>
</Tooltip>
);
})}
</div> </div>
))} ))}
</div> </div>

View file

@ -91,7 +91,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
} }
onChange={(comment_json, comment_html) => onChange(comment_html)} onChange={(comment_json, comment_html) => onChange(comment_html)}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
placeholder="Add Comment..." placeholder="Add comment..."
uploadFile={async (file) => { uploadFile={async (file) => {
const { asset_id } = await uploadCommentAsset(file, anchor); const { asset_id } = await uploadCommentAsset(file, anchor);
setUploadAssetIds((prev) => [...prev, asset_id]); setUploadAssetIds((prev) => [...prev, asset_id]);

View file

@ -1,5 +1,9 @@
import { import {
AlignCenter,
AlignLeft,
AlignRight,
Bold, Bold,
CaseSensitive,
Code2, Code2,
Heading1, Heading1,
Heading2, Heading2,
@ -19,30 +23,99 @@ import {
Underline, Underline,
} from "lucide-react"; } from "lucide-react";
// editor // editor
import { TEditorCommands } from "@plane/editor"; import { TCommandExtraProps, TEditorCommands } from "@plane/editor";
type TEditorTypes = "lite" | "document"; type TEditorTypes = "lite" | "document";
export type ToolbarMenuItem = { // Utility type to enforce the necessary extra props or make extraProps optional
key: TEditorCommands; type ExtraPropsForCommand<T extends TEditorCommands> = T extends keyof TCommandExtraProps
? TCommandExtraProps[T]
: object; // Default to empty object for commands without extra props
export type ToolbarMenuItem<T extends TEditorCommands = TEditorCommands> = {
itemKey: T;
renderKey: string;
name: string; name: string;
icon: LucideIcon; icon: LucideIcon;
shortcut?: string[]; shortcut?: string[];
editors: TEditorTypes[]; editors: TEditorTypes[];
extraProps?: ExtraPropsForCommand<T>;
}; };
export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ export const TYPOGRAPHY_ITEMS: ToolbarMenuItem<"text" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6">[] = [
{ key: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] }, { itemKey: "text", renderKey: "text", name: "Text", icon: CaseSensitive, editors: ["document"] },
{ key: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] }, { itemKey: "h1", renderKey: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] },
{ key: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] }, { itemKey: "h2", renderKey: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] },
{ key: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] }, { itemKey: "h3", renderKey: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] },
{ key: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] }, { itemKey: "h4", renderKey: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] },
{ key: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] }, { itemKey: "h5", renderKey: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] },
{ key: "bold", name: "Bold", icon: Bold, shortcut: ["Cmd", "B"], editors: ["lite", "document"] }, { itemKey: "h6", renderKey: "h6", name: "Heading 6", icon: Heading6, editors: ["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"] },
export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [
{ {
key: "strikethrough", itemKey: "text-align",
renderKey: "text-align-left",
name: "Left align",
icon: AlignLeft,
shortcut: ["Cmd", "Shift", "L"],
editors: ["lite", "document"],
extraProps: {
alignment: "left",
},
},
{
itemKey: "text-align",
renderKey: "text-align-center",
name: "Center align",
icon: AlignCenter,
shortcut: ["Cmd", "Shift", "E"],
editors: ["lite", "document"],
extraProps: {
alignment: "center",
},
},
{
itemKey: "text-align",
renderKey: "text-align-right",
name: "Right align",
icon: AlignRight,
shortcut: ["Cmd", "Shift", "R"],
editors: ["lite", "document"],
extraProps: {
alignment: "right",
},
},
];
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [
{
itemKey: "bold",
renderKey: "bold",
name: "Bold",
icon: Bold,
shortcut: ["Cmd", "B"],
editors: ["lite", "document"],
},
{
itemKey: "italic",
renderKey: "italic",
name: "Italic",
icon: Italic,
shortcut: ["Cmd", "I"],
editors: ["lite", "document"],
},
{
itemKey: "underline",
renderKey: "underline",
name: "Underline",
icon: Underline,
shortcut: ["Cmd", "U"],
editors: ["lite", "document"],
},
{
itemKey: "strikethrough",
renderKey: "strikethrough",
name: "Strikethrough", name: "Strikethrough",
icon: Strikethrough, icon: Strikethrough,
shortcut: ["Cmd", "Shift", "S"], shortcut: ["Cmd", "Shift", "S"],
@ -50,23 +123,26 @@ export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [
}, },
]; ];
export const LIST_ITEMS: ToolbarMenuItem[] = [ const LIST_ITEMS: ToolbarMenuItem<"bulleted-list" | "numbered-list" | "to-do-list">[] = [
{ {
key: "bulleted-list", itemKey: "bulleted-list",
renderKey: "bulleted-list",
name: "Bulleted list", name: "Bulleted list",
icon: List, icon: List,
shortcut: ["Cmd", "Shift", "7"], shortcut: ["Cmd", "Shift", "7"],
editors: ["lite", "document"], editors: ["lite", "document"],
}, },
{ {
key: "numbered-list", itemKey: "numbered-list",
renderKey: "numbered-list",
name: "Numbered list", name: "Numbered list",
icon: ListOrdered, icon: ListOrdered,
shortcut: ["Cmd", "Shift", "8"], shortcut: ["Cmd", "Shift", "8"],
editors: ["lite", "document"], editors: ["lite", "document"],
}, },
{ {
key: "to-do-list", itemKey: "to-do-list",
renderKey: "to-do-list",
name: "To-do list", name: "To-do list",
icon: ListTodo, icon: ListTodo,
shortcut: ["Cmd", "Shift", "9"], shortcut: ["Cmd", "Shift", "9"],
@ -74,14 +150,14 @@ export const LIST_ITEMS: ToolbarMenuItem[] = [
}, },
]; ];
export const USER_ACTION_ITEMS: ToolbarMenuItem[] = [ export const USER_ACTION_ITEMS: ToolbarMenuItem<"quote" | "code">[] = [
{ key: "quote", name: "Quote", icon: TextQuote, editors: ["lite", "document"] }, { itemKey: "quote", renderKey: "quote", name: "Quote", icon: TextQuote, editors: ["lite", "document"] },
{ key: "code", name: "Code", icon: Code2, editors: ["lite", "document"] }, { itemKey: "code", renderKey: "code", name: "Code", icon: Code2, editors: ["lite", "document"] },
]; ];
export const COMPLEX_ITEMS: ToolbarMenuItem[] = [ export const COMPLEX_ITEMS: ToolbarMenuItem<"table" | "image">[] = [
{ key: "table", name: "Table", icon: Table, editors: ["document"] }, { itemKey: "table", renderKey: "table", name: "Table", icon: Table, editors: ["document"] },
{ key: "image", name: "Image", icon: Image, editors: ["lite", "document"] }, { itemKey: "image", renderKey: "image", name: "Image", icon: Image, editors: ["lite", "document"] },
]; ];
export const TOOLBAR_ITEMS: { export const TOOLBAR_ITEMS: {
@ -91,12 +167,14 @@ export const TOOLBAR_ITEMS: {
} = { } = {
lite: { lite: {
basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("lite")), basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("lite")),
alignment: TEXT_ALIGNMENT_ITEMS.filter((item) => item.editors.includes("lite")),
list: LIST_ITEMS.filter((item) => item.editors.includes("lite")), list: LIST_ITEMS.filter((item) => item.editors.includes("lite")),
userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("lite")), userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("lite")),
complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("lite")), complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("lite")),
}, },
document: { document: {
basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("document")), basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("document")),
alignment: TEXT_ALIGNMENT_ITEMS.filter((item) => item.editors.includes("document")),
list: LIST_ITEMS.filter((item) => item.editors.includes("document")), list: LIST_ITEMS.filter((item) => item.editors.includes("document")),
userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("document")), userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("document")),
complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("document")), complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("document")),

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
// editor // editor
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
// types // types
import { IUserLite } from "@plane/types"; import { IUserLite } from "@plane/types";
// components // components
@ -61,12 +61,12 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
}); });
// file size // file size
const { maxFileSize } = useFileSize(); const { maxFileSize } = useFileSize();
const isEmpty = isCommentEmpty(props.initialValue);
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> { function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
return !!ref && typeof ref === "object" && "current" in ref; return !!ref && typeof ref === "object" && "current" in ref;
} }
// derived values
const isEmpty = isCommentEmpty(props.initialValue);
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
return ( return (
<div className="border border-custom-border-200 rounded p-3 space-y-3"> <div className="border border-custom-border-200 rounded p-3 space-y-3">
@ -89,19 +89,20 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
/> />
<IssueCommentToolbar <IssueCommentToolbar
accessSpecifier={accessSpecifier} accessSpecifier={accessSpecifier}
executeCommand={(key) => { executeCommand={(item) => {
if (isMutableRefObject<EditorRefApi>(ref)) { // TODO: update this while toolbar homogenization
ref.current?.executeMenuItemCommand({ // @ts-expect-error type mismatch here
itemKey: key as TNonColorEditorCommands, editorRef?.executeMenuItemCommand({
}); itemKey: item.itemKey,
} ...item.extraProps,
});
}} }}
handleAccessChange={handleAccessChange} handleAccessChange={handleAccessChange}
handleSubmit={(e) => rest.onEnterKeyPress?.(e)} handleSubmit={(e) => rest.onEnterKeyPress?.(e)}
isCommentEmpty={isEmpty} isCommentEmpty={isEmpty}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
showAccessSpecifier={showAccessSpecifier} showAccessSpecifier={showAccessSpecifier}
editorRef={isMutableRefObject<EditorRefApi>(ref) ? ref : null} editorRef={editorRef}
showSubmitButton={showSubmitButton} showSubmitButton={showSubmitButton}
/> />
</div> </div>

View file

@ -3,25 +3,25 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { Globe2, Lock, LucideIcon } from "lucide-react"; import { Globe2, Lock, LucideIcon } from "lucide-react";
// editor // editor
import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; import { EditorRefApi } from "@plane/editor";
// ui // ui
import { Button, Tooltip } from "@plane/ui"; import { Button, Tooltip } from "@plane/ui";
// constants // constants
import { TOOLBAR_ITEMS } from "@/constants/editor"; import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor";
import { EIssueCommentAccessSpecifier } from "@/constants/issue"; import { EIssueCommentAccessSpecifier } from "@/constants/issue";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
type Props = { type Props = {
accessSpecifier?: EIssueCommentAccessSpecifier; accessSpecifier?: EIssueCommentAccessSpecifier;
executeCommand: (commandKey: TEditorCommands) => void; executeCommand: (item: ToolbarMenuItem) => void;
handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void; handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void;
handleSubmit: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; handleSubmit: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
isCommentEmpty: boolean; isCommentEmpty: boolean;
isSubmitting: boolean; isSubmitting: boolean;
showAccessSpecifier: boolean; showAccessSpecifier: boolean;
showSubmitButton: boolean; showSubmitButton: boolean;
editorRef: React.MutableRefObject<EditorRefApi | null> | null; editorRef: EditorRefApi | null;
}; };
type TCommentAccessType = { type TCommentAccessType = {
@ -63,24 +63,25 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
// Function to update active states // Function to update active states
const updateActiveStates = useCallback(() => { const updateActiveStates = useCallback(() => {
if (editorRef?.current) { if (!editorRef) return;
const newActiveStates: Record<string, boolean> = {}; const newActiveStates: Record<string, boolean> = {};
Object.values(toolbarItems) Object.values(toolbarItems)
.flat() .flat()
.forEach((item) => { .forEach((item) => {
// Assert that editorRef.current is not null // TODO: update this while toolbar homogenization
newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ // @ts-expect-error type mismatch here
itemKey: item.key as TNonColorEditorCommands, newActiveStates[item.renderKey] = editorRef.isMenuItemActive({
}); itemKey: item.itemKey,
...item.extraProps,
}); });
setActiveStates(newActiveStates); });
} setActiveStates(newActiveStates);
}, [editorRef]); }, [editorRef]);
// useEffect to call updateActiveStates when isActive prop changes // useEffect to call updateActiveStates when isActive prop changes
useEffect(() => { useEffect(() => {
if (!editorRef?.current) return; if (!editorRef) return;
const unsubscribe = editorRef.current.onStateChange(updateActiveStates); const unsubscribe = editorRef.onStateChange(updateActiveStates);
updateActiveStates(); updateActiveStates();
return () => unsubscribe(); return () => unsubscribe();
}, [editorRef, updateActiveStates]); }, [editorRef, updateActiveStates]);
@ -122,35 +123,39 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
"pl-0": index === 0, "pl-0": index === 0,
})} })}
> >
{toolbarItems[key].map((item) => ( {toolbarItems[key].map((item) => {
<Tooltip const isItemActive = activeStates[item.renderKey];
key={item.key}
tooltipContent={ return (
<p className="flex flex-col gap-1 text-center text-xs"> <Tooltip
<span className="font-medium">{item.name}</span> key={item.renderKey}
{item.shortcut && <kbd className="text-custom-text-400">{item.shortcut.join(" + ")}</kbd>} tooltipContent={
</p> <p className="flex flex-col gap-1 text-center text-xs">
} <span className="font-medium">{item.name}</span>
> {item.shortcut && <kbd className="text-custom-text-400">{item.shortcut.join(" + ")}</kbd>}
<button </p>
type="button" }
onClick={() => executeCommand(item.key)}
className={cn(
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-400 hover:bg-custom-background-80",
{
"bg-custom-background-80 text-custom-text-100": activeStates[item.key],
}
)}
> >
<item.icon <button
className={cn("h-3.5 w-3.5", { type="button"
"text-custom-text-100": activeStates[item.key], onClick={() => executeCommand(item)}
})} className={cn(
strokeWidth={2.5} "grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-400 hover:bg-custom-background-80",
/> {
</button> "bg-custom-background-80 text-custom-text-100": isItemActive,
</Tooltip> }
))} )}
>
<item.icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": isItemActive,
})}
strokeWidth={2.5}
/>
</button>
</Tooltip>
);
})}
</div> </div>
))} ))}
</div> </div>

View file

@ -4,13 +4,19 @@ import { memo } from "react";
import { ALargeSmall, Ban } from "lucide-react"; import { ALargeSmall, Ban } from "lucide-react";
import { Popover } from "@headlessui/react"; import { Popover } from "@headlessui/react";
// plane editor // plane editor
import { COLORS_LIST, TColorEditorCommands } from "@plane/editor"; import { COLORS_LIST, TEditorCommands } from "@plane/editor";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
type Props = { type Props = {
handleColorSelect: (key: TColorEditorCommands, color: string | undefined) => void; handleColorSelect: (
isColorActive: (key: TColorEditorCommands, color: string | undefined) => boolean; key: Extract<TEditorCommands, "text-color" | "background-color">,
color: string | undefined
) => void;
isColorActive: (
key: Extract<TEditorCommands, "text-color" | "background-color">,
color: string | undefined
) => boolean;
}; };
export const ColorDropdown: React.FC<Props> = memo((props) => { export const ColorDropdown: React.FC<Props> = memo((props) => {

View file

@ -3,7 +3,7 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { Check, ChevronDown } from "lucide-react"; import { Check, ChevronDown } from "lucide-react";
// editor // editor
import { EditorRefApi, TNonColorEditorCommands } from "@plane/editor"; import { EditorRefApi } from "@plane/editor";
// ui // ui
import { CustomMenu, Tooltip } from "@plane/ui"; import { CustomMenu, Tooltip } from "@plane/ui";
// components // components
@ -36,11 +36,13 @@ const ToolbarButton: React.FC<ToolbarButtonProps> = React.memo((props) => {
} }
> >
<button <button
key={item.key}
type="button" type="button"
onClick={() => onClick={() =>
// TODO: update this while toolbar homogenization
// @ts-expect-error type mismatch here
executeCommand({ executeCommand({
itemKey: item.key as TNonColorEditorCommands, itemKey: item.itemKey,
...item.extraProps,
}) })
} }
className={cn("grid size-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", {
@ -66,15 +68,20 @@ export const PageToolbar: React.FC<Props> = ({ editorRef }) => {
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({}); const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
const updateActiveStates = useCallback(() => { const updateActiveStates = useCallback(() => {
// console.log("Updating status");
const newActiveStates: Record<string, boolean> = {}; const newActiveStates: Record<string, boolean> = {};
Object.values(toolbarItems) Object.values(toolbarItems)
.flat() .flat()
.forEach((item) => { .forEach((item) => {
newActiveStates[item.key] = editorRef.isMenuItemActive({ // TODO: update this while toolbar homogenization
itemKey: item.key as TNonColorEditorCommands, // @ts-expect-error type mismatch here
newActiveStates[item.renderKey] = editorRef.isMenuItemActive({
itemKey: item.itemKey,
...item.extraProps,
}); });
}); });
setActiveStates(newActiveStates); setActiveStates(newActiveStates);
// console.log("newActiveStates", newActiveStates);
}, [editorRef]); }, [editorRef]);
useEffect(() => { useEffect(() => {
@ -85,7 +92,8 @@ export const PageToolbar: React.FC<Props> = ({ editorRef }) => {
const activeTypography = TYPOGRAPHY_ITEMS.find((item) => const activeTypography = TYPOGRAPHY_ITEMS.find((item) =>
editorRef.isMenuItemActive({ editorRef.isMenuItemActive({
itemKey: item.key as TNonColorEditorCommands, itemKey: item.itemKey,
...item.extraProps,
}) })
); );
@ -105,11 +113,12 @@ export const PageToolbar: React.FC<Props> = ({ editorRef }) => {
> >
{TYPOGRAPHY_ITEMS.map((item) => ( {TYPOGRAPHY_ITEMS.map((item) => (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={item.key} key={item.renderKey}
className="flex items-center justify-between gap-2" className="flex items-center justify-between gap-2"
onClick={() => onClick={() =>
editorRef.executeMenuItemCommand({ editorRef.executeMenuItemCommand({
itemKey: item.key as TNonColorEditorCommands, itemKey: item.itemKey,
...item.extraProps,
}) })
} }
> >
@ -117,7 +126,9 @@ export const PageToolbar: React.FC<Props> = ({ editorRef }) => {
<item.icon className="size-3" /> <item.icon className="size-3" />
{item.name} {item.name}
</span> </span>
{activeTypography?.key === item.key && <Check className="size-3 text-custom-text-300 flex-shrink-0" />} {activeTypography?.itemKey === item.itemKey && (
<Check className="size-3 text-custom-text-300 flex-shrink-0" />
)}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
))} ))}
</CustomMenu> </CustomMenu>
@ -139,9 +150,9 @@ export const PageToolbar: React.FC<Props> = ({ editorRef }) => {
<div key={key} className="flex items-center gap-0.5 px-2 first:pl-0 last:pr-0"> <div key={key} className="flex items-center gap-0.5 px-2 first:pl-0 last:pr-0">
{toolbarItems[key].map((item) => ( {toolbarItems[key].map((item) => (
<ToolbarButton <ToolbarButton
key={item.key} key={item.renderKey}
item={item} item={item}
isActive={activeStates[item.key]} isActive={activeStates[item.renderKey]}
executeCommand={editorRef.executeMenuItemCommand} executeCommand={editorRef.executeMenuItemCommand}
/> />
))} ))}

View file

@ -1,5 +1,8 @@
import { Styles, StyleSheet } from "@react-pdf/renderer"; import { Styles, StyleSheet } from "@react-pdf/renderer";
import { import {
AlignCenter,
AlignLeft,
AlignRight,
Bold, Bold,
CaseSensitive, CaseSensitive,
Code2, Code2,
@ -21,7 +24,7 @@ import {
Underline, Underline,
} from "lucide-react"; } from "lucide-react";
// editor // editor
import { TEditorCommands, TEditorFontStyle } from "@plane/editor"; import { TCommandExtraProps, TEditorCommands, TEditorFontStyle } from "@plane/editor";
// ui // ui
import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui"; import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
// helpers // helpers
@ -29,30 +32,95 @@ import { convertRemToPixel } from "@/helpers/common.helper";
type TEditorTypes = "lite" | "document"; type TEditorTypes = "lite" | "document";
export type ToolbarMenuItem = { // Utility type to enforce the necessary extra props or make extraProps optional
key: TEditorCommands; type ExtraPropsForCommand<T extends TEditorCommands> = T extends keyof TCommandExtraProps
? TCommandExtraProps[T]
: object; // Default to empty object for commands without extra props
export type ToolbarMenuItem<T extends TEditorCommands = TEditorCommands> = {
itemKey: T;
renderKey: string;
name: string; name: string;
icon: LucideIcon; icon: LucideIcon;
shortcut?: string[]; shortcut?: string[];
editors: TEditorTypes[]; editors: TEditorTypes[];
extraProps?: ExtraPropsForCommand<T>;
}; };
export const TYPOGRAPHY_ITEMS: ToolbarMenuItem[] = [ export const TYPOGRAPHY_ITEMS: ToolbarMenuItem<"text" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6">[] = [
{ key: "text", name: "Text", icon: CaseSensitive, editors: ["document"] }, { itemKey: "text", renderKey: "text", name: "Text", icon: CaseSensitive, editors: ["document"] },
{ key: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] }, { itemKey: "h1", renderKey: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] },
{ key: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] }, { itemKey: "h2", renderKey: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] },
{ key: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] }, { itemKey: "h3", renderKey: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] },
{ key: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] }, { itemKey: "h4", renderKey: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] },
{ key: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] }, { itemKey: "h5", renderKey: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] },
{ key: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] }, { itemKey: "h6", renderKey: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] },
]; ];
const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [
{ 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: "strikethrough", itemKey: "text-align",
renderKey: "text-align-left",
name: "Left align",
icon: AlignLeft,
shortcut: ["Cmd", "Shift", "L"],
editors: ["lite", "document"],
extraProps: {
alignment: "left",
},
},
{
itemKey: "text-align",
renderKey: "text-align-center",
name: "Center align",
icon: AlignCenter,
shortcut: ["Cmd", "Shift", "E"],
editors: ["lite", "document"],
extraProps: {
alignment: "center",
},
},
{
itemKey: "text-align",
renderKey: "text-align-right",
name: "Right align",
icon: AlignRight,
shortcut: ["Cmd", "Shift", "R"],
editors: ["lite", "document"],
extraProps: {
alignment: "right",
},
},
];
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [
{
itemKey: "bold",
renderKey: "bold",
name: "Bold",
icon: Bold,
shortcut: ["Cmd", "B"],
editors: ["lite", "document"],
},
{
itemKey: "italic",
renderKey: "italic",
name: "Italic",
icon: Italic,
shortcut: ["Cmd", "I"],
editors: ["lite", "document"],
},
{
itemKey: "underline",
renderKey: "underline",
name: "Underline",
icon: Underline,
shortcut: ["Cmd", "U"],
editors: ["lite", "document"],
},
{
itemKey: "strikethrough",
renderKey: "strikethrough",
name: "Strikethrough", name: "Strikethrough",
icon: Strikethrough, icon: Strikethrough,
shortcut: ["Cmd", "Shift", "S"], shortcut: ["Cmd", "Shift", "S"],
@ -60,23 +128,26 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [
}, },
]; ];
const LIST_ITEMS: ToolbarMenuItem[] = [ const LIST_ITEMS: ToolbarMenuItem<"bulleted-list" | "numbered-list" | "to-do-list">[] = [
{ {
key: "bulleted-list", itemKey: "bulleted-list",
renderKey: "bulleted-list",
name: "Bulleted list", name: "Bulleted list",
icon: List, icon: List,
shortcut: ["Cmd", "Shift", "7"], shortcut: ["Cmd", "Shift", "7"],
editors: ["lite", "document"], editors: ["lite", "document"],
}, },
{ {
key: "numbered-list", itemKey: "numbered-list",
renderKey: "numbered-list",
name: "Numbered list", name: "Numbered list",
icon: ListOrdered, icon: ListOrdered,
shortcut: ["Cmd", "Shift", "8"], shortcut: ["Cmd", "Shift", "8"],
editors: ["lite", "document"], editors: ["lite", "document"],
}, },
{ {
key: "to-do-list", itemKey: "to-do-list",
renderKey: "to-do-list",
name: "To-do list", name: "To-do list",
icon: ListTodo, icon: ListTodo,
shortcut: ["Cmd", "Shift", "9"], shortcut: ["Cmd", "Shift", "9"],
@ -84,29 +155,31 @@ const LIST_ITEMS: ToolbarMenuItem[] = [
}, },
]; ];
const USER_ACTION_ITEMS: ToolbarMenuItem[] = [ const USER_ACTION_ITEMS: ToolbarMenuItem<"quote" | "code">[] = [
{ key: "quote", name: "Quote", icon: TextQuote, editors: ["lite", "document"] }, { itemKey: "quote", renderKey: "quote", name: "Quote", icon: TextQuote, editors: ["lite", "document"] },
{ key: "code", name: "Code", icon: Code2, editors: ["lite", "document"] }, { itemKey: "code", renderKey: "code", name: "Code", icon: Code2, editors: ["lite", "document"] },
]; ];
const COMPLEX_ITEMS: ToolbarMenuItem[] = [ const COMPLEX_ITEMS: ToolbarMenuItem<"table" | "image">[] = [
{ key: "table", name: "Table", icon: Table, editors: ["document"] }, { itemKey: "table", renderKey: "table", name: "Table", icon: Table, editors: ["document"] },
{ key: "image", name: "Image", icon: Image, editors: ["lite", "document"] }, { itemKey: "image", renderKey: "image", name: "Image", icon: Image, editors: ["lite", "document"], extraProps: {} },
]; ];
export const TOOLBAR_ITEMS: { export const TOOLBAR_ITEMS: {
[editorType in TEditorTypes]: { [editorType in TEditorTypes]: {
[key: string]: ToolbarMenuItem[]; [key: string]: ToolbarMenuItem<TEditorCommands>[];
}; };
} = { } = {
lite: { lite: {
basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("lite")), basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("lite")),
alignment: TEXT_ALIGNMENT_ITEMS.filter((item) => item.editors.includes("lite")),
list: LIST_ITEMS.filter((item) => item.editors.includes("lite")), list: LIST_ITEMS.filter((item) => item.editors.includes("lite")),
userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("lite")), userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("lite")),
complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("lite")), complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("lite")),
}, },
document: { document: {
basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("document")), basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("document")),
alignment: TEXT_ALIGNMENT_ITEMS.filter((item) => item.editors.includes("document")),
list: LIST_ITEMS.filter((item) => item.editors.includes("document")), list: LIST_ITEMS.filter((item) => item.editors.includes("document")),
userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("document")), userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("document")),
complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("document")), complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("document")),

View file

@ -3597,6 +3597,11 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.9.1.tgz#e0ca3ec1379dbc39a98070c650d3759df85db794" resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.9.1.tgz#e0ca3ec1379dbc39a98070c650d3759df85db794"
integrity sha512-vmUkclPi02iVf+uu74iyUp5xGNib0Gxs73DJ1z+a7CzjuLRqqCa/KEde95CR0Y//DaK/Csz4DOSUyTfLCMvpWg== integrity sha512-vmUkclPi02iVf+uu74iyUp5xGNib0Gxs73DJ1z+a7CzjuLRqqCa/KEde95CR0Y//DaK/Csz4DOSUyTfLCMvpWg==
"@tiptap/extension-text-align@^2.8.0":
version "2.9.1"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-align/-/extension-text-align-2.9.1.tgz#5f7920a16c95b283c961cf1e22357bdc355c1626"
integrity sha512-oUp0XnwJpAImcOVV68vsY2CpkHpRZ3gzWfIRTuy+aYitQim3xDKis/qfWQUWZsANp9/TZ0VyjtkZxNMwOfcu1g==
"@tiptap/extension-text-style@^2.7.1", "@tiptap/extension-text-style@^2.9.1": "@tiptap/extension-text-style@^2.7.1", "@tiptap/extension-text-style@^2.9.1":
version "2.9.1" version "2.9.1"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.9.1.tgz#b9fc9cd8e90747357fbd4cac541a33aaa8b76875" resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.9.1.tgz#b9fc9cd8e90747357fbd4cac541a33aaa8b76875"
@ -3878,9 +3883,9 @@
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": "@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
version "5.0.0" version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz#91f06cda1049e8f17eeab364798ed79c97488a1c" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz#3c9997ae9d00bc236e45c6374e84f2596458d9db"
integrity sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw== integrity sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/qs" "*" "@types/qs" "*"
@ -4031,11 +4036,11 @@
"@types/node" "*" "@types/node" "*"
"@types/node@*", "@types/node@^22.0.0", "@types/node@^22.5.4": "@types/node@*", "@types/node@^22.0.0", "@types/node@^22.5.4":
version "22.7.9" version "22.8.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.9.tgz#2bf2797b5e84702d8262ea2cf843c3c3c880d0e9" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.0.tgz#193c6f82f9356ce0e6bba86b59f2ffe06e7e320b"
integrity sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg== integrity sha512-84rafSBHC/z1i1E3p0cJwKA+CfYDNSXX9WSZBRopjIzLET8oNt6ht2tei4C7izwDeEiLLfdeSVBv1egOH916hg==
dependencies: dependencies:
undici-types "~6.19.2" undici-types "~6.19.8"
"@types/node@18.14.1": "@types/node@18.14.1":
version "18.14.1" version "18.14.1"
@ -4053,9 +4058,9 @@
integrity sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA== integrity sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==
"@types/node@^20.14.9", "@types/node@^20.5.2": "@types/node@^20.14.9", "@types/node@^20.5.2":
version "20.17.0" version "20.17.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.0.tgz#d0620ba0fe4cf2a0f12351c7bdd805fc4e1f036b" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.1.tgz#2b968e060dfb04b7f9550fe3db5f552721c14566"
integrity sha512-a7zRo0f0eLo9K5X9Wp5cAqTUNGzuFLDG2R7C4HY2BhcMAsxgSPuRvAC1ZB6QkuUQXf0YZAgfOX2ZyrBa2n4nHQ== integrity sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==
dependencies: dependencies:
undici-types "~6.19.2" undici-types "~6.19.2"
@ -9876,9 +9881,9 @@ postgres-range@^1.1.1:
integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==
posthog-js@^1.131.3: posthog-js@^1.131.3:
version "1.174.4" version "1.176.0"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.174.4.tgz#67abe7ba9c3b99db8fb472be0017b7d45217184f" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.176.0.tgz#39841ab213aa9c5500982659dc6d537a407f7205"
integrity sha512-wfnSp1nDYHvV4+qy+UnDTED3afe8tVOiLa4Y83RLI2HZdMKovnLq11GJX6cYJ99+hs88HyGD1XmNTxShIQoOhQ== integrity sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==
dependencies: dependencies:
core-js "^3.38.1" core-js "^3.38.1"
fflate "^0.4.8" fflate "^0.4.8"
@ -10109,9 +10114,9 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, pr
prosemirror-view "^1.27.0" prosemirror-view "^1.27.0"
prosemirror-tables@^1.4.0: prosemirror-tables@^1.4.0:
version "1.5.1" version "1.6.0"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.5.1.tgz#75e6ace7427834f2150f9f08bf8fa400429f5238" resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.6.0.tgz#b05fbb1172d55dd22ad2662af8e243c969bbbfdd"
integrity sha512-zL0vI0rGdhLLKXaZU1Jw1I8RuXwa5bv4aEY6G9TdynNRIU2FodtfI/YdhqVlimilhOIBGMbhvTvnQy5fvbHt2A== integrity sha512-eirSS2fwVYzKhvM2qeXSn9ix/SBn7QOLDftPQ4ImEQIevFDiSKAB6Lbrmm/WEgrbTDbCm+xhSq4gOD9w7wT59Q==
dependencies: dependencies:
prosemirror-keymap "^1.1.2" prosemirror-keymap "^1.1.2"
prosemirror-model "^1.8.1" prosemirror-model "^1.8.1"
@ -11735,17 +11740,17 @@ tiptap-markdown@^0.8.9:
markdown-it-task-lists "^2.1.1" markdown-it-task-lists "^2.1.1"
prosemirror-markdown "^1.11.1" prosemirror-markdown "^1.11.1"
tldts-core@^6.1.54: tldts-core@^6.1.55:
version "6.1.54" version "6.1.55"
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.54.tgz#a3c3b5f45a64a1f9ea4bb32a94642218c7b7baa5" resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.55.tgz#cab0d412672fca9c77d3c51312c69bb5b5ee95c2"
integrity sha512-5cc42+0G0EjYRDfIJHKraaT3I5kPm7j6or3Zh1T9sF+Ftj1T+isT4thicUyQQ1bwN7/xjHQIuY2fXCoXP8Haqg== integrity sha512-BL+BuKHHaOpntE5BGI6naXjULU6aRlgaYdfDHR3T/hdbNTWkWUZ9yuc11wGnwgpvRwlyUiIK+QohYK3olaVU6Q==
tldts@^6.1.32: tldts@^6.1.32:
version "6.1.54" version "6.1.55"
resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.54.tgz#782594001a7b95e577b4cc391c0f0ed7c8307d37" resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.55.tgz#9a27d1708652bbae93d4b842dc2f8554fdabffc6"
integrity sha512-rDaL1t59gb/Lg0HPMUGdV1vAKLQcXwU74D26aMaYV4QW7mnMvShd1Vmkg3HYAPWx2JCTUmsrXt/Yl9eJ5UFBQw== integrity sha512-HxQR/9roQ07Pwc8RyyrJMAxRz5/ssoF3qIPPUiIo3zUt6yMdmYZjM2OZIFMiZ3jHyz9jrGHEHuQZrUhoc1LkDw==
dependencies: dependencies:
tldts-core "^6.1.54" tldts-core "^6.1.55"
to-regex-range@^5.0.1: to-regex-range@^5.0.1:
version "5.0.1" version "5.0.1"
@ -12063,7 +12068,7 @@ undefsafe@^2.0.5:
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
undici-types@~6.19.2: undici-types@~6.19.2, undici-types@~6.19.8:
version "6.19.8" version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==