style: lite text editor editor toolbar (#2601)

* style: comment editor toolbar

* style: updated icon styling
This commit is contained in:
Aaryan Khandelwal 2023-11-02 16:26:57 +05:30 committed by GitHub
parent 5b808571e5
commit c394a4f64e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 182 additions and 142 deletions

View File

@ -7,7 +7,11 @@ interface EditorContainerProps {
children: ReactNode; children: ReactNode;
} }
export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => ( export const EditorContainer = ({
editor,
editorClassNames,
children,
}: EditorContainerProps) => (
<div <div
id="editor-container" id="editor-container"
onClick={() => { onClick={() => {

View File

@ -1,14 +1,14 @@
"use client" "use client";
import * as React from 'react'; import * as React from "react";
import { Extension } from "@tiptap/react"; import { Extension } from "@tiptap/react";
import { UploadImage } from '../types/upload-image'; import { UploadImage } from "../types/upload-image";
import { DeleteImage } from '../types/delete-image'; import { DeleteImage } from "../types/delete-image";
import { getEditorClassNames } from '../lib/utils'; import { getEditorClassNames } from "../lib/utils";
import { EditorProps } from '@tiptap/pm/view'; import { EditorProps } from "@tiptap/pm/view";
import { useEditor } from './hooks/useEditor'; import { useEditor } from "./hooks/useEditor";
import { EditorContainer } from '../ui/components/editor-container'; import { EditorContainer } from "../ui/components/editor-container";
import { EditorContentWrapper } from '../ui/components/editor-content'; import { EditorContentWrapper } from "../ui/components/editor-content";
import { IMentionSuggestion } from '../types/mention-suggestion'; import { IMentionSuggestion } from "../types/mention-suggestion";
interface ICoreEditor { interface ICoreEditor {
value: string; value: string;
@ -19,7 +19,9 @@ interface ICoreEditor {
customClassName?: string; customClassName?: string;
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
editable?: boolean; editable?: boolean;
forwardedRef?: any; forwardedRef?: any;
@ -72,22 +74,29 @@ const CoreEditor = ({
forwardedRef, forwardedRef,
}); });
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); const editorClassNames = getEditorClassNames({
noBorder,
borderOnFocus,
customClassName,
});
if (!editor) return null; if (!editor) return null;
return ( return (
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col"> <div className="flex flex-col">
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} /> <EditorContentWrapper
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</div> </div>
</EditorContainer > </EditorContainer>
); );
}; };
const CoreEditorWithRef = React.forwardRef<EditorHandle, ICoreEditor>((props, ref) => ( const CoreEditorWithRef = React.forwardRef<EditorHandle, ICoreEditor>(
<CoreEditor {...props} forwardedRef={ref} /> (props, ref) => <CoreEditor {...props} forwardedRef={ref} />,
)); );
CoreEditorWithRef.displayName = "CoreEditorWithRef"; CoreEditorWithRef.displayName = "CoreEditorWithRef";

View File

@ -18,9 +18,9 @@ export type IMentionSuggestion = {
title: string; title: string;
subtitle: string; subtitle: string;
redirect_uri: string; redirect_uri: string;
} };
export type IMentionHighlight = string export type IMentionHighlight = string;
interface ILiteTextEditor { interface ILiteTextEditor {
value: string; value: string;
@ -32,7 +32,7 @@ interface ILiteTextEditor {
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void;
setIsSubmitting?: ( setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved" isSubmitting: "submitting" | "submitted" | "saved",
) => void; ) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any; forwardedRef?: any;
@ -50,6 +50,7 @@ interface ILiteTextEditor {
onEnterKeyPress?: (e?: any) => void; onEnterKeyPress?: (e?: any) => void;
mentionHighlights?: string[]; mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[]; mentionSuggestions?: IMentionSuggestion[];
submitButton?: React.ReactNode;
} }
interface LiteTextEditorProps extends ILiteTextEditor { interface LiteTextEditorProps extends ILiteTextEditor {
@ -61,24 +62,27 @@ interface EditorHandle {
setEditorValue: (content: string) => void; setEditorValue: (content: string) => void;
} }
const LiteTextEditor = ({ const LiteTextEditor = (props: LiteTextEditorProps) => {
onChange, const {
debouncedUpdatesEnabled, onChange,
setIsSubmitting, debouncedUpdatesEnabled,
setShouldShowAlert, setIsSubmitting,
editorContentCustomClassNames, setShouldShowAlert,
value, editorContentCustomClassNames,
uploadFile, value,
deleteFile, uploadFile,
noBorder, deleteFile,
borderOnFocus, noBorder,
customClassName, borderOnFocus,
forwardedRef, customClassName,
commentAccessSpecifier, forwardedRef,
onEnterKeyPress, commentAccessSpecifier,
mentionHighlights, onEnterKeyPress,
mentionSuggestions mentionHighlights,
}: LiteTextEditorProps) => { mentionSuggestions,
submitButton,
} = props;
const editor = useEditor({ const editor = useEditor({
onChange, onChange,
debouncedUpdatesEnabled, debouncedUpdatesEnabled,
@ -90,7 +94,7 @@ const LiteTextEditor = ({
forwardedRef, forwardedRef,
extensions: LiteTextEditorExtensions(onEnterKeyPress), extensions: LiteTextEditorExtensions(onEnterKeyPress),
mentionHighlights, mentionHighlights,
mentionSuggestions mentionSuggestions,
}); });
const editorClassNames = getEditorClassNames({ const editorClassNames = getEditorClassNames({
@ -114,6 +118,7 @@ const LiteTextEditor = ({
uploadFile={uploadFile} uploadFile={uploadFile}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
commentAccessSpecifier={commentAccessSpecifier} commentAccessSpecifier={commentAccessSpecifier}
submitButton={submitButton}
/> />
</div> </div>
</div> </div>
@ -122,7 +127,7 @@ const LiteTextEditor = ({
}; };
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>( const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>(
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} /> (props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />,
); );
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef"; LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";

View File

@ -1,5 +1,5 @@
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { BoldIcon, LucideIcon } from "lucide-react"; import { BoldIcon } from "lucide-react";
import { import {
BoldItem, BoldItem,
@ -14,7 +14,6 @@ import {
TableItem, TableItem,
UnderLineItem, UnderLineItem,
} from "@plane/editor-core"; } from "@plane/editor-core";
import { Icon } from "./icon";
import { Tooltip } from "../../tooltip"; import { Tooltip } from "../../tooltip";
import { UploadImage } from "../.."; import { UploadImage } from "../..";
@ -41,8 +40,9 @@ type EditorBubbleMenuProps = {
}; };
uploadFile: UploadImage; uploadFile: UploadImage;
setIsSubmitting?: ( setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved" isSubmitting: "submitting" | "submitted" | "saved",
) => void; ) => void;
submitButton: React.ReactNode;
}; };
export const FixedMenu = (props: EditorBubbleMenuProps) => { export const FixedMenu = (props: EditorBubbleMenuProps) => {
@ -72,116 +72,132 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
props.commentAccessSpecifier?.onAccessChange(accessKey); props.commentAccessSpecifier?.onAccessChange(accessKey);
}; };
console.log(complexItems);
return ( 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 items-stretch gap-1.5 w-full h-9">
{props.commentAccessSpecifier && ( {props.commentAccessSpecifier && (
<div className="flex border border-custom-border-300 mt-0 divide-x divide-custom-border-300 rounded overflow-hidden"> <div className="flex-shrink-0 flex items-stretch gap-0.5 border border-custom-border-200 rounded p-1">
{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-basicMarkItems-center p-1 hover:bg-custom-background-80 ${ className={`aspect-square grid place-items-center p-1 rounded-sm hover:bg-custom-background-90 ${
props.commentAccessSpecifier?.accessValue === access.key props.commentAccessSpecifier?.accessValue === access.key
? "bg-custom-background-80" ? "bg-custom-background-90"
: "" : ""
}`} }`}
> >
<access.icon <access.icon
className={`w-4 h-4 ${ className={`w-3.5 h-3.5 ${
props.commentAccessSpecifier?.accessValue === access.key props.commentAccessSpecifier?.accessValue === access.key
? "!text-custom-text-100" ? "text-custom-text-100"
: "!text-custom-text-400" : "text-custom-text-400"
}`} }`}
strokeWidth={2}
/> />
</button> </button>
</Tooltip> </Tooltip>
))} ))}
</div> </div>
)} )}
<div className="flex"> <div className="flex items-stretch justify-between gap-2 w-full border border-custom-border-200 bg-custom-background-90 rounded p-1">
{basicMarkItems.map((item, index) => ( <div className="flex items-stretch">
<button <div className="flex items-stretch gap-0.5 pr-2.5 border-r border-custom-border-200">
key={index} {basicMarkItems.map((item, index) => (
type="button" <button
onClick={item.command} key={index}
className={cn( type="button"
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", onClick={item.command}
{ className={cn(
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
} {
)} "text-custom-text-100 bg-custom-background-80":
> item.isActive(),
<item.icon },
className={cn("h-4 w-4", { )}
"text-custom-text-100": item.isActive(), >
})} <item.icon
/> className={cn("h-3.5 w-3.5", {
</button> "text-custom-text-100": item.isActive(),
))} })}
</div> strokeWidth={2.5}
<div className="flex"> />
{listItems.map((item, index) => ( </button>
<button ))}
key={index} </div>
type="button" <div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
onClick={item.command} {listItems.map((item, index) => (
className={cn( <button
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", key={index}
{ type="button"
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), onClick={item.command}
} className={cn(
)} "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
> {
<item.icon "text-custom-text-100 bg-custom-background-80":
className={cn("h-4 w-4", { item.isActive(),
"text-custom-text-100": item.isActive(), },
})} )}
/> >
</button> <item.icon
))} className={cn("h-3.5 w-3.5", {
</div> "text-custom-text-100": item.isActive(),
<div className="flex"> })}
{userActionItems.map((item, index) => ( strokeWidth={2.5}
<button />
key={index} </button>
type="button" ))}
onClick={item.command} </div>
className={cn( <div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", {userActionItems.map((item, index) => (
{ <button
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), key={index}
} type="button"
)} onClick={item.command}
> className={cn(
<item.icon "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
className={cn("h-4 w-4", { {
"text-custom-text-100": item.isActive(), "text-custom-text-100 bg-custom-background-80":
})} item.isActive(),
/> },
</button> )}
))} >
</div> <item.icon
<div className="flex"> className={cn("h-3.5 w-3.5", {
{complexItems.map((item, index) => ( "text-custom-text-100": item.isActive(),
<button })}
key={index} strokeWidth={2.5}
type="button" />
onClick={item.command} </button>
className={cn( ))}
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", </div>
{ <div className="flex items-stretch gap-0.5 pl-2.5">
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), {complexItems.map((item, index) => (
} <button
)} key={index}
> type="button"
<item.icon onClick={item.command}
className={cn("h-4 w-4", { className={cn(
"text-custom-text-100": item.isActive(), "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
})} {
/> "text-custom-text-100 bg-custom-background-80":
</button> item.isActive(),
))} },
)}
>
<item.icon
className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(),
})}
strokeWidth={2.5}
/>
</button>
))}
</div>
</div>
{props.submitButton}
</div> </div>
</div> </div>
); );

View File

@ -49,7 +49,7 @@ export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const editorSuggestions = useEditorSuggestions(workspaceSlug, projectId) const editorSuggestions = useEditorSuggestions(workspaceSlug, projectId);
const { const {
formState: { isSubmitting }, formState: { isSubmitting },

View File

@ -54,7 +54,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined) const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined);
const { const {
control, control,
@ -75,7 +75,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
return ( return (
<form onSubmit={handleSubmit(handleAddComment)}> <form onSubmit={handleSubmit(handleAddComment)}>
<div className="space-y-2"> <div className="space-y-2">
<div className="relative"> <div className="relative h-full">
{showAccessSpecifier && ( {showAccessSpecifier && (
<div className="absolute bottom-2 left-3 z-[1]"> <div className="absolute bottom-2 left-3 z-[1]">
<Controller <Controller
@ -119,22 +119,28 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
deleteFile={fileService.deleteImage} deleteFile={fileService.deleteImage}
ref={editorRef} ref={editorRef}
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue} value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
customClassName="p-3 min-h-[100px] shadow-sm" customClassName="p-2 h-full"
debouncedUpdatesEnabled={false} debouncedUpdatesEnabled={false}
mentionSuggestions={editorSuggestions.mentionSuggestions} mentionSuggestions={editorSuggestions.mentionSuggestions}
mentionHighlights={editorSuggestions.mentionHighlights} mentionHighlights={editorSuggestions.mentionHighlights}
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)} onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
commentAccessSpecifier={{ accessValue, onAccessChange, showAccessSpecifier, commentAccess }} commentAccessSpecifier={{ accessValue, onAccessChange, showAccessSpecifier, commentAccess }}
submitButton={
<Button
variant="primary"
type="submit"
className="!px-2.5 !py-1.5 !text-xs"
disabled={isSubmitting || disabled}
>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
}
/> />
)} )}
/> />
)} )}
/> />
</div> </div>
<Button variant="neutral-primary" type="submit" disabled={isSubmitting || disabled}>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
</div> </div>
</form> </form>
); );