added comment editor with fixed menu

This commit is contained in:
Palanikannan1437 2023-10-04 01:06:16 +05:30
parent 99618e93a7
commit 95439fbbef
9 changed files with 234 additions and 184 deletions

View File

@ -14,3 +14,7 @@ export { EditorContentWrapper } from "./ui/components/editor-content";
// hooks // hooks
export { useEditor } from "./ui/hooks/useEditor"; export { useEditor } from "./ui/hooks/useEditor";
export { useReadOnlyEditor } from "./ui/hooks/useReadOnlyEditor"; export { useReadOnlyEditor } from "./ui/hooks/useReadOnlyEditor";
// helper items
export * from "./ui/menus/menu-items";
export * from "./lib/editor-commands";

View File

@ -0,0 +1,36 @@
import { Editor } from "@tiptap/react";
import { UploadImage } from "../types/upload-image";
import { startImageUpload } from "../ui/plugins/upload-image";
export const toggleBold = (editor: Editor) => editor?.chain().focus().toggleBold().run();
export const toggleItalic = (editor: Editor) => editor?.chain().focus().toggleItalic().run();
export const toggleUnderline = (editor: Editor) => editor?.chain().focus().toggleUnderline().run();
export const toggleStrike = (editor: Editor) => editor?.chain().focus().toggleStrike().run();
export const toggleCode = (editor: Editor) => editor?.chain().focus().toggleCode().run();
export const toggleBulletList = (editor: Editor) => editor?.chain().focus().toggleBulletList().run();
export const toggleOrderedList = (editor: Editor) => editor?.chain().focus().toggleOrderedList().run();
export const toggleBlockquote = (editor: Editor) => editor?.chain().focus().toggleBlockquote().run();
export const insertTable = (editor: Editor) => editor?.chain().focus().insertTable().run();
export const insertImage = (editor: Editor, uploadFile: UploadImage, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
startImageUpload(file, editor.view, pos, uploadFile, setIsSubmitting);
}
};
input.click();
};

View File

@ -1,13 +0,0 @@
import React from "react";
type Props = {
iconName: string;
className?: string;
};
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
<span className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}>
{iconName}
</span>
);

View File

@ -1,112 +0,0 @@
import { Editor } from "@tiptap/react";
import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react";
import { cn } from "../../../lib/utils";
import { Tooltip } from "../table-menu/tooltip";
import { Icon } from "./icon";
export interface BubbleMenuItem {
name: string;
isActive: () => boolean;
command: () => void;
icon: typeof BoldIcon;
}
type EditorBubbleMenuProps = {
editor: Editor;
accessValue: string;
onAccessChange: (accessKey: string) => void;
commentAccess: {
icon: string;
key: string;
label: "Private" | "Public";
}[] | undefined;
}
export const FixedMenu = (props: EditorBubbleMenuProps) => {
const items: BubbleMenuItem[] = [
{
name: "bold",
isActive: () => props.editor?.isActive("bold"),
command: () => props.editor?.chain().focus().toggleBold().run(),
icon: BoldIcon,
},
{
name: "italic",
isActive: () => props.editor?.isActive("italic"),
command: () => props.editor?.chain().focus().toggleItalic().run(),
icon: ItalicIcon,
},
{
name: "underline",
isActive: () => props.editor?.isActive("underline"),
command: () => props.editor?.chain().focus().toggleUnderline().run(),
icon: UnderlineIcon,
},
{
name: "strike",
isActive: () => props.editor?.isActive("strike"),
command: () => props.editor?.chain().focus().toggleStrike().run(),
icon: StrikethroughIcon,
},
{
name: "code",
isActive: () => props.editor?.isActive("code"),
command: () => props.editor?.chain().focus().toggleCode().run(),
icon: CodeIcon,
},
];
const handleAccessChange = (accessKey: string) => {
props.onAccessChange(accessKey);
};
return (
<div
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
>
<div className="flex">
<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
{props?.commentAccess?.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}>
<button
type="button"
onClick={() => handleAccessChange(access.key)}
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${props.accessValue === access.key ? "bg-custom-background-80" : ""
}`}
>
<Icon
iconName={access.icon}
className={`w-4 h-4 -mt-1 ${props.accessValue === access.key
? "!text-custom-text-100"
: "!text-custom-text-400"
}`}
/>
</button>
</Tooltip>
))}
</div>
{items.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,81 @@
import { BoldIcon, QuoteIcon, ImageIcon, TableIcon, ListIcon, ListOrderedIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react";
import { Editor } from "@tiptap/react";
import { UploadImage } from "../../../types/upload-image";
import { insertImage, insertTable, toggleBlockquote, toggleBold, toggleBulletList, toggleCode, toggleItalic, toggleOrderedList, toggleStrike } from "../../../lib/editor-commands";
export interface EditorMenuItem {
name: string;
isActive: () => boolean;
command: () => void;
icon: typeof BoldIcon;
}
export const BoldItem = (editor: Editor): EditorMenuItem => ({
name: "bold",
isActive: () => editor?.isActive("bold"),
command: () => toggleBold(editor),
icon: BoldIcon,
})
export const ItalicItem = (editor: Editor): EditorMenuItem => ({
name: "italic",
isActive: () => editor?.isActive("italic"),
command: () => toggleItalic(editor),
icon: ItalicIcon,
})
export const UnderLineItem = (editor: Editor): EditorMenuItem => ({
name: "underline",
isActive: () => editor?.isActive("underline"),
command: () => UnderLineItem(editor),
icon: UnderlineIcon,
})
export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
name: "strike",
isActive: () => editor?.isActive("strike"),
command: () => toggleStrike(editor),
icon: StrikethroughIcon,
})
export const CodeItem = (editor: Editor): EditorMenuItem => ({
name: "code",
isActive: () => editor?.isActive("code"),
command: () => toggleCode(editor),
icon: CodeIcon,
})
export const BulletListItem = (editor: Editor): EditorMenuItem => ({
name: "bullet-list",
isActive: () => editor?.isActive("bulletList"),
command: () => toggleBulletList(editor),
icon: ListIcon,
})
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
name: "ordered-list",
isActive: () => editor?.isActive("orderedList"),
command: () => toggleOrderedList(editor),
icon: ListOrderedIcon
})
export const QuoteItem = (editor: Editor): EditorMenuItem => ({
name: "quote",
isActive: () => editor?.isActive("quote"),
command: () => toggleBlockquote(editor),
icon: QuoteIcon
})
export const TableItem = (editor: Editor): EditorMenuItem => ({
name: "quote",
isActive: () => editor?.isActive("table"),
command: () => insertTable(editor),
icon: TableIcon
})
export const ImageItem = (editor: Editor, uploadFile: UploadImage, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorMenuItem => ({
name: "image",
isActive: () => editor?.isActive("image"),
command: () => insertImage(editor, uploadFile, setIsSubmitting),
icon: ImageIcon,
})

View File

@ -83,7 +83,7 @@ const LiteTextEditor = ({
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} /> <EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
{(editable !== false) && {(editable !== false) &&
(<div className="w-full mt-4"> (<div className="w-full mt-4">
<FixedMenu editor={editor} commentAccessSpecifier={commentAccessSpecifier} /> <FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} commentAccessSpecifier={commentAccessSpecifier} />
</div>) </div>)
} }
</div> </div>

View File

@ -1,9 +1,10 @@
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react"; import { BoldIcon } from "lucide-react";
import { cn } from "@plane/editor-core"; import { BoldItem, BulletListItem, cn, CodeItem, ImageItem, ItalicItem, NumberedListItem, QuoteItem, StrikeThroughItem, TableItem, UnderLineItem } from "@plane/editor-core";
import { Icon } from "./icon"; import { Icon } from "./icon";
import { Tooltip } from "../../tooltip"; import { Tooltip } from "../../tooltip";
import { UploadImage } from "../..";
export interface BubbleMenuItem { export interface BubbleMenuItem {
name: string; name: string;
@ -24,40 +25,31 @@ type EditorBubbleMenuProps = {
label: "Private" | "Public"; label: "Private" | "Public";
}[] | undefined; }[] | undefined;
} }
uploadFile: UploadImage;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
} }
export const FixedMenu = (props: EditorBubbleMenuProps) => { export const FixedMenu = (props: EditorBubbleMenuProps) => {
const items: BubbleMenuItem[] = [ const basicMarkItems: BubbleMenuItem[] = [
{ BoldItem(props.editor),
name: "bold", ItalicItem(props.editor),
isActive: () => props.editor?.isActive("bold"), UnderLineItem(props.editor),
command: () => props.editor?.chain().focus().toggleBold().run(), StrikeThroughItem(props.editor),
icon: BoldIcon, ];
},
{ const listItems: BubbleMenuItem[] = [
name: "italic", BulletListItem(props.editor),
isActive: () => props.editor?.isActive("italic"), NumberedListItem(props.editor),
command: () => props.editor?.chain().focus().toggleItalic().run(), ];
icon: ItalicIcon,
}, const userActionItems: BubbleMenuItem[] = [
{ QuoteItem(props.editor),
name: "underline", CodeItem(props.editor),
isActive: () => props.editor?.isActive("underline"), ];
command: () => props.editor?.chain().focus().toggleUnderline().run(),
icon: UnderlineIcon, const complexItems: BubbleMenuItem[] = [
}, TableItem(props.editor),
{ ImageItem(props.editor, props.uploadFile, props.setIsSubmitting),
name: "strike",
isActive: () => props.editor?.isActive("strike"),
command: () => props.editor?.chain().focus().toggleStrike().run(),
icon: StrikethroughIcon,
},
{
name: "code",
isActive: () => props.editor?.isActive("code"),
command: () => props.editor?.chain().focus().toggleCode().run(),
icon: CodeIcon,
},
]; ];
const handleAccessChange = (accessKey: string) => { const handleAccessChange = (accessKey: string) => {
@ -69,14 +61,13 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
<div <div
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl" className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
> >
<div className="flex"> {props.commentAccessSpecifier && (<div className="flex border border-custom-border-300 mt-0 divide-x divide-custom-border-300 rounded overflow-hidden">
{props.commentAccessSpecifier && (<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
{props?.commentAccessSpecifier.commentAccess?.map((access) => ( {props?.commentAccessSpecifier.commentAccess?.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}> <Tooltip key={access.key} tooltipContent={access.label}>
<button <button
type="button" type="button"
onClick={() => handleAccessChange(access.key)} onClick={() => handleAccessChange(access.key)}
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${props.commentAccessSpecifier?.accessValue === access.key ? "bg-custom-background-80" : "" className={`grid place-basicMarkItems-center p-1 hover:bg-custom-background-80 ${props.commentAccessSpecifier?.accessValue === access.key ? "bg-custom-background-80" : ""
}`} }`}
> >
<Icon <Icon
@ -90,7 +81,71 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
</Tooltip> </Tooltip>
))} ))}
</div>)} </div>)}
{items.map((item, index) => ( <div className="flex">
{basicMarkItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
</div>
<div className="flex">
{listItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
</div>
<div className="flex">
{userActionItems.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
>
<item.icon
className={cn("h-4 w-4", {
"text-custom-text-100": item.isActive(),
})}
/>
</button>
))}
</div>
<div className="flex">
{complexItems.map((item, index) => (
<button <button
key={index} key={index}
type="button" type="button"

View File

@ -309,6 +309,7 @@ const renderItems = () => {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => { onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
component = new ReactRenderer(CommandList, { component = new ReactRenderer(CommandList, {
props, props,
// @ts-ignore
editor: props.editor, editor: props.editor,
}); });

View File

@ -85,10 +85,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
}, [issue, reset]); }, [issue, reset]);
const debouncedTitleSave = useDebouncedCallback(async () => { const debouncedTitleSave = useDebouncedCallback(async () => {
setTimeout(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 500); }, 1500);
}, 1000);
return ( return (
<div className="relative"> <div className="relative">