mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
fix: bubble menu weird flickering fixed (#6591)
This commit is contained in:
@@ -6,15 +6,15 @@ import { cn } from "@plane/utils";
|
||||
import { TextAlignItem } from "@/components/menus";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
import { EditorStateType } from "./root";
|
||||
|
||||
type Props = {
|
||||
editor: Editor;
|
||||
onClose: () => void;
|
||||
editorState: EditorStateType;
|
||||
};
|
||||
|
||||
export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||
const { editor, onClose } = props;
|
||||
|
||||
const { editor, editorState } = props;
|
||||
const menuItem = TextAlignItem(editor);
|
||||
|
||||
const textAlignmentOptions: {
|
||||
@@ -32,10 +32,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||
menuItem.command({
|
||||
alignment: "left",
|
||||
}),
|
||||
isActive: () =>
|
||||
menuItem.isActive({
|
||||
alignment: "left",
|
||||
}),
|
||||
isActive: () => editorState.left,
|
||||
},
|
||||
{
|
||||
itemKey: "text-align",
|
||||
@@ -45,10 +42,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||
menuItem.command({
|
||||
alignment: "center",
|
||||
}),
|
||||
isActive: () =>
|
||||
menuItem.isActive({
|
||||
alignment: "center",
|
||||
}),
|
||||
isActive: () => editorState.center,
|
||||
},
|
||||
{
|
||||
itemKey: "text-align",
|
||||
@@ -58,10 +52,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||
menuItem.command({
|
||||
alignment: "right",
|
||||
}),
|
||||
isActive: () =>
|
||||
menuItem.isActive({
|
||||
alignment: "right",
|
||||
}),
|
||||
isActive: () => editorState.right,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,7 +65,6 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||
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",
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { ALargeSmall, Ban } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { COLORS_LIST } from "@/constants/common";
|
||||
// helpers
|
||||
import { BackgroundColorItem, TextColorItem } from "../menu-items";
|
||||
import { EditorStateType } from "./root";
|
||||
|
||||
type Props = {
|
||||
editor: Editor;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
editorState: EditorStateType;
|
||||
};
|
||||
|
||||
export const BubbleMenuColorSelector: FC<Props> = (props) => {
|
||||
const { editor, isOpen, setIsOpen } = props;
|
||||
const { editor, isOpen, setIsOpen, editorState } = props;
|
||||
|
||||
const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key }));
|
||||
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key }));
|
||||
const activeTextColor = editorState.color;
|
||||
const activeBackgroundColor = editorState.backgroundColor;
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
|
||||
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection, useEditorState } from "@tiptap/react";
|
||||
import { FC, useEffect, useState, useRef } from "react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import {
|
||||
BackgroundColorItem,
|
||||
BoldItem,
|
||||
BubbleMenuColorSelector,
|
||||
BubbleMenuLinkSelector,
|
||||
@@ -11,8 +12,12 @@ import {
|
||||
CodeItem,
|
||||
ItalicItem,
|
||||
StrikeThroughItem,
|
||||
TextAlignItem,
|
||||
TextColorItem,
|
||||
UnderLineItem,
|
||||
} from "@/components/menus";
|
||||
// constants
|
||||
import { COLORS_LIST } from "@/constants/common";
|
||||
// extensions
|
||||
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
|
||||
// local components
|
||||
@@ -20,16 +25,61 @@ import { TextAlignmentSelector } from "./alignment-selector";
|
||||
|
||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
// states
|
||||
export interface EditorStateType {
|
||||
code: boolean;
|
||||
bold: boolean;
|
||||
italic: boolean;
|
||||
underline: boolean;
|
||||
strike: boolean;
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
center: boolean;
|
||||
color: { key: string; label: string; textColor: string; backgroundColor: string } | undefined;
|
||||
backgroundColor:
|
||||
| {
|
||||
key: string;
|
||||
label: string;
|
||||
textColor: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: { editor: Editor }) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
|
||||
const basicFormattingOptions = props.editor.isActive("code")
|
||||
? [CodeItem(props.editor)]
|
||||
: [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)];
|
||||
const formattingItems = {
|
||||
code: CodeItem(props.editor),
|
||||
bold: BoldItem(props.editor),
|
||||
italic: ItalicItem(props.editor),
|
||||
underline: UnderLineItem(props.editor),
|
||||
strike: StrikeThroughItem(props.editor),
|
||||
textAlign: TextAlignItem(props.editor),
|
||||
};
|
||||
|
||||
const editorState: EditorStateType = useEditorState({
|
||||
editor: props.editor,
|
||||
selector: ({ editor }: { editor: Editor }) => ({
|
||||
code: formattingItems.code.isActive(),
|
||||
bold: formattingItems.bold.isActive(),
|
||||
italic: formattingItems.italic.isActive(),
|
||||
underline: formattingItems.underline.isActive(),
|
||||
strike: formattingItems.strike.isActive(),
|
||||
left: formattingItems.textAlign.isActive({ alignment: "left" }),
|
||||
right: formattingItems.textAlign.isActive({ alignment: "right" }),
|
||||
center: formattingItems.textAlign.isActive({ alignment: "center" }),
|
||||
color: COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key })),
|
||||
backgroundColor: COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key })),
|
||||
}),
|
||||
});
|
||||
|
||||
const basicFormattingOptions = editorState.code
|
||||
? [formattingItems.code]
|
||||
: [formattingItems.bold, formattingItems.italic, formattingItems.underline, formattingItems.strike];
|
||||
|
||||
const bubbleMenuProps: EditorBubbleMenuProps = {
|
||||
...props,
|
||||
@@ -51,6 +101,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
},
|
||||
tippyOptions: {
|
||||
moveTransition: "transform 0.15s ease-out",
|
||||
duration: [300, 0],
|
||||
onHidden: () => {
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
@@ -60,7 +111,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleMouseDown() {
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
if (menuRef.current?.contains(e.target as Node)) return;
|
||||
|
||||
function handleMouseMove() {
|
||||
if (!props.editor.state.selection.empty) {
|
||||
setIsSelecting(true);
|
||||
@@ -70,7 +123,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
|
||||
function handleMouseUp() {
|
||||
setIsSelecting(false);
|
||||
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
@@ -84,27 +136,28 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleMouseDown);
|
||||
};
|
||||
}, []);
|
||||
}, [props.editor]);
|
||||
|
||||
return (
|
||||
<BubbleMenu {...bubbleMenuProps}>
|
||||
{!isSelecting && (
|
||||
<div className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg">
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg"
|
||||
>
|
||||
<div className="px-2">
|
||||
{!props.editor.isActive("table") && (
|
||||
<BubbleMenuNodeSelector
|
||||
editor={props.editor!}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen((prev) => !prev);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<BubbleMenuNodeSelector
|
||||
editor={props.editor!}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen((prev) => !prev);
|
||||
setIsLinkSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2">
|
||||
{!props.editor.isActive("code") && (
|
||||
{!editorState.code && (
|
||||
<div className="px-2">
|
||||
<BubbleMenuLinkSelector
|
||||
editor={props.editor}
|
||||
isOpen={isLinkSelectorOpen}
|
||||
@@ -114,21 +167,22 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-2">
|
||||
{!props.editor.isActive("code") && (
|
||||
</div>
|
||||
)}
|
||||
{!editorState.code && (
|
||||
<div className="px-2">
|
||||
<BubbleMenuColorSelector
|
||||
editor={props.editor}
|
||||
isOpen={isColorSelectorOpen}
|
||||
editorState={editorState}
|
||||
setIsOpen={() => {
|
||||
setIsColorSelectorOpen((prev) => !prev);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-0.5 px-2">
|
||||
{basicFormattingOptions.map((item) => (
|
||||
<button
|
||||
@@ -141,7 +195,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
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(""),
|
||||
"bg-custom-background-80 text-custom-text-100": editorState[item.key],
|
||||
}
|
||||
)}
|
||||
>
|
||||
@@ -149,15 +203,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
</button>
|
||||
))}
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
<TextAlignmentSelector editor={props.editor} editorState={editorState} />
|
||||
</div>
|
||||
)}
|
||||
</BubbleMenu>
|
||||
|
||||
@@ -142,8 +142,8 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
|
||||
icon: UnderlineIcon,
|
||||
});
|
||||
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
|
||||
key: "strikethrough",
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strike"> => ({
|
||||
key: "strike",
|
||||
name: "Strikethrough",
|
||||
isActive: () => editor?.isActive("strike"),
|
||||
command: () => toggleStrike(editor),
|
||||
|
||||
@@ -87,7 +87,7 @@ export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [
|
||||
const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strike">[] = [
|
||||
{
|
||||
itemKey: "bold",
|
||||
renderKey: "bold",
|
||||
@@ -113,7 +113,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
|
||||
editors: ["lite", "document"],
|
||||
},
|
||||
{
|
||||
itemKey: "strikethrough",
|
||||
itemKey: "strike",
|
||||
renderKey: "strikethrough",
|
||||
name: "Strikethrough",
|
||||
icon: Strikethrough,
|
||||
|
||||
@@ -31,7 +31,7 @@ export type TEditorCommands =
|
||||
| "bold"
|
||||
| "italic"
|
||||
| "underline"
|
||||
| "strikethrough"
|
||||
| "strike"
|
||||
| "bulleted-list"
|
||||
| "numbered-list"
|
||||
| "to-do-list"
|
||||
|
||||
Reference in New Issue
Block a user