mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
added comment editor with fixed menu
This commit is contained in:
parent
99618e93a7
commit
95439fbbef
@ -14,3 +14,7 @@ export { EditorContentWrapper } from "./ui/components/editor-content";
|
||||
// hooks
|
||||
export { useEditor } from "./ui/hooks/useEditor";
|
||||
export { useReadOnlyEditor } from "./ui/hooks/useReadOnlyEditor";
|
||||
|
||||
// helper items
|
||||
export * from "./ui/menus/menu-items";
|
||||
export * from "./lib/editor-commands";
|
||||
|
36
packages/editor/core/src/lib/editor-commands.ts
Normal file
36
packages/editor/core/src/lib/editor-commands.ts
Normal 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();
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
81
packages/editor/core/src/ui/menus/menu-items/index.tsx
Normal file
81
packages/editor/core/src/ui/menus/menu-items/index.tsx
Normal 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,
|
||||
})
|
@ -83,7 +83,7 @@ const LiteTextEditor = ({
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
{(editable !== false) &&
|
||||
(<div className="w-full mt-4">
|
||||
<FixedMenu editor={editor} commentAccessSpecifier={commentAccessSpecifier} />
|
||||
<FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} commentAccessSpecifier={commentAccessSpecifier} />
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
|
@ -1,9 +1,10 @@
|
||||
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 { Tooltip } from "../../tooltip";
|
||||
import { UploadImage } from "../..";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
@ -24,40 +25,31 @@ type EditorBubbleMenuProps = {
|
||||
label: "Private" | "Public";
|
||||
}[] | undefined;
|
||||
}
|
||||
uploadFile: UploadImage;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
}
|
||||
|
||||
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 basicMarkItems: BubbleMenuItem[] = [
|
||||
BoldItem(props.editor),
|
||||
ItalicItem(props.editor),
|
||||
UnderLineItem(props.editor),
|
||||
StrikeThroughItem(props.editor),
|
||||
];
|
||||
|
||||
const listItems: BubbleMenuItem[] = [
|
||||
BulletListItem(props.editor),
|
||||
NumberedListItem(props.editor),
|
||||
];
|
||||
|
||||
const userActionItems: BubbleMenuItem[] = [
|
||||
QuoteItem(props.editor),
|
||||
CodeItem(props.editor),
|
||||
];
|
||||
|
||||
const complexItems: BubbleMenuItem[] = [
|
||||
TableItem(props.editor),
|
||||
ImageItem(props.editor, props.uploadFile, props.setIsSubmitting),
|
||||
];
|
||||
|
||||
const handleAccessChange = (accessKey: string) => {
|
||||
@ -69,14 +61,13 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
<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">
|
||||
{props.commentAccessSpecifier && (<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
|
||||
{props.commentAccessSpecifier && (<div className="flex border border-custom-border-300 mt-0 divide-x divide-custom-border-300 rounded overflow-hidden">
|
||||
{props?.commentAccessSpecifier.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.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
|
||||
@ -90,7 +81,71 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</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
|
||||
key={index}
|
||||
type="button"
|
||||
|
@ -309,6 +309,7 @@ const renderItems = () => {
|
||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||
component = new ReactRenderer(CommandList, {
|
||||
props,
|
||||
// @ts-ignore
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
|
@ -85,10 +85,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
}, [issue, reset]);
|
||||
|
||||
const debouncedTitleSave = useDebouncedCallback(async () => {
|
||||
setTimeout(async () => {
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||
}, 500);
|
||||
}, 1000);
|
||||
}, 1500);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
|
Loading…
Reference in New Issue
Block a user