fix: bubble menu weird flickering fixed (#6591)
This commit is contained in:
parent
d3af913ec7
commit
126575d22a
7 changed files with 105 additions and 67 deletions
|
|
@ -6,15 +6,15 @@ import { cn } from "@plane/utils";
|
||||||
import { TextAlignItem } from "@/components/menus";
|
import { TextAlignItem } from "@/components/menus";
|
||||||
// types
|
// types
|
||||||
import { TEditorCommands } from "@/types";
|
import { TEditorCommands } from "@/types";
|
||||||
|
import { EditorStateType } from "./root";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
onClose: () => void;
|
editorState: EditorStateType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||||
const { editor, onClose } = props;
|
const { editor, editorState } = props;
|
||||||
|
|
||||||
const menuItem = TextAlignItem(editor);
|
const menuItem = TextAlignItem(editor);
|
||||||
|
|
||||||
const textAlignmentOptions: {
|
const textAlignmentOptions: {
|
||||||
|
|
@ -32,10 +32,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||||
menuItem.command({
|
menuItem.command({
|
||||||
alignment: "left",
|
alignment: "left",
|
||||||
}),
|
}),
|
||||||
isActive: () =>
|
isActive: () => editorState.left,
|
||||||
menuItem.isActive({
|
|
||||||
alignment: "left",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
itemKey: "text-align",
|
itemKey: "text-align",
|
||||||
|
|
@ -45,10 +42,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||||
menuItem.command({
|
menuItem.command({
|
||||||
alignment: "center",
|
alignment: "center",
|
||||||
}),
|
}),
|
||||||
isActive: () =>
|
isActive: () => editorState.center,
|
||||||
menuItem.isActive({
|
|
||||||
alignment: "center",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
itemKey: "text-align",
|
itemKey: "text-align",
|
||||||
|
|
@ -58,10 +52,7 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||||
menuItem.command({
|
menuItem.command({
|
||||||
alignment: "right",
|
alignment: "right",
|
||||||
}),
|
}),
|
||||||
isActive: () =>
|
isActive: () => editorState.right,
|
||||||
menuItem.isActive({
|
|
||||||
alignment: "right",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -74,7 +65,6 @@ export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
item.command();
|
item.command();
|
||||||
onClose();
|
|
||||||
}}
|
}}
|
||||||
className={cn(
|
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",
|
"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 { Editor } from "@tiptap/react";
|
||||||
import { ALargeSmall, Ban } from "lucide-react";
|
import { ALargeSmall, Ban } from "lucide-react";
|
||||||
|
import { Dispatch, FC, SetStateAction } from "react";
|
||||||
// plane utils
|
// plane utils
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// constants
|
// constants
|
||||||
import { COLORS_LIST } from "@/constants/common";
|
import { COLORS_LIST } from "@/constants/common";
|
||||||
// helpers
|
// helpers
|
||||||
import { BackgroundColorItem, TextColorItem } from "../menu-items";
|
import { BackgroundColorItem, TextColorItem } from "../menu-items";
|
||||||
|
import { EditorStateType } from "./root";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
editorState: EditorStateType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BubbleMenuColorSelector: FC<Props> = (props) => {
|
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 activeTextColor = editorState.color;
|
||||||
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key }));
|
const activeBackgroundColor = editorState.backgroundColor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { FC, useEffect, useState } from "react";
|
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection, useEditorState } from "@tiptap/react";
|
||||||
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
|
import { FC, useEffect, useState, useRef } from "react";
|
||||||
// plane utils
|
// plane utils
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
|
BackgroundColorItem,
|
||||||
BoldItem,
|
BoldItem,
|
||||||
BubbleMenuColorSelector,
|
BubbleMenuColorSelector,
|
||||||
BubbleMenuLinkSelector,
|
BubbleMenuLinkSelector,
|
||||||
|
|
@ -11,8 +12,12 @@ import {
|
||||||
CodeItem,
|
CodeItem,
|
||||||
ItalicItem,
|
ItalicItem,
|
||||||
StrikeThroughItem,
|
StrikeThroughItem,
|
||||||
|
TextAlignItem,
|
||||||
|
TextColorItem,
|
||||||
UnderLineItem,
|
UnderLineItem,
|
||||||
} from "@/components/menus";
|
} from "@/components/menus";
|
||||||
|
// constants
|
||||||
|
import { COLORS_LIST } from "@/constants/common";
|
||||||
// extensions
|
// extensions
|
||||||
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
|
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
|
||||||
// local components
|
// local components
|
||||||
|
|
@ -20,16 +25,61 @@ import { TextAlignmentSelector } from "./alignment-selector";
|
||||||
|
|
||||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
||||||
|
|
||||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
export interface EditorStateType {
|
||||||
// states
|
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 [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
|
|
||||||
const basicFormattingOptions = props.editor.isActive("code")
|
const formattingItems = {
|
||||||
? [CodeItem(props.editor)]
|
code: CodeItem(props.editor),
|
||||||
: [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(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 = {
|
const bubbleMenuProps: EditorBubbleMenuProps = {
|
||||||
...props,
|
...props,
|
||||||
|
|
@ -51,6 +101,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
},
|
},
|
||||||
tippyOptions: {
|
tippyOptions: {
|
||||||
moveTransition: "transform 0.15s ease-out",
|
moveTransition: "transform 0.15s ease-out",
|
||||||
|
duration: [300, 0],
|
||||||
onHidden: () => {
|
onHidden: () => {
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
setIsLinkSelectorOpen(false);
|
setIsLinkSelectorOpen(false);
|
||||||
|
|
@ -60,7 +111,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleMouseDown() {
|
function handleMouseDown(e: MouseEvent) {
|
||||||
|
if (menuRef.current?.contains(e.target as Node)) return;
|
||||||
|
|
||||||
function handleMouseMove() {
|
function handleMouseMove() {
|
||||||
if (!props.editor.state.selection.empty) {
|
if (!props.editor.state.selection.empty) {
|
||||||
setIsSelecting(true);
|
setIsSelecting(true);
|
||||||
|
|
@ -70,7 +123,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
|
|
||||||
function handleMouseUp() {
|
function handleMouseUp() {
|
||||||
setIsSelecting(false);
|
setIsSelecting(false);
|
||||||
|
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
}
|
}
|
||||||
|
|
@ -84,14 +136,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleMouseDown);
|
document.removeEventListener("mousedown", handleMouseDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [props.editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu {...bubbleMenuProps}>
|
<BubbleMenu {...bubbleMenuProps}>
|
||||||
{!isSelecting && (
|
{!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">
|
<div className="px-2">
|
||||||
{!props.editor.isActive("table") && (
|
|
||||||
<BubbleMenuNodeSelector
|
<BubbleMenuNodeSelector
|
||||||
editor={props.editor!}
|
editor={props.editor!}
|
||||||
isOpen={isNodeSelectorOpen}
|
isOpen={isNodeSelectorOpen}
|
||||||
|
|
@ -101,10 +155,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{!editorState.code && (
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
{!props.editor.isActive("code") && (
|
|
||||||
<BubbleMenuLinkSelector
|
<BubbleMenuLinkSelector
|
||||||
editor={props.editor}
|
editor={props.editor}
|
||||||
isOpen={isLinkSelectorOpen}
|
isOpen={isLinkSelectorOpen}
|
||||||
|
|
@ -114,21 +167,22 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
setIsColorSelectorOpen(false);
|
setIsColorSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{!editorState.code && (
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
{!props.editor.isActive("code") && (
|
|
||||||
<BubbleMenuColorSelector
|
<BubbleMenuColorSelector
|
||||||
editor={props.editor}
|
editor={props.editor}
|
||||||
isOpen={isColorSelectorOpen}
|
isOpen={isColorSelectorOpen}
|
||||||
|
editorState={editorState}
|
||||||
setIsOpen={() => {
|
setIsOpen={() => {
|
||||||
setIsColorSelectorOpen((prev) => !prev);
|
setIsColorSelectorOpen((prev) => !prev);
|
||||||
setIsNodeSelectorOpen(false);
|
setIsNodeSelectorOpen(false);
|
||||||
setIsLinkSelectorOpen(false);
|
setIsLinkSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex gap-0.5 px-2">
|
<div className="flex gap-0.5 px-2">
|
||||||
{basicFormattingOptions.map((item) => (
|
{basicFormattingOptions.map((item) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -141,7 +195,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||||
className={cn(
|
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",
|
"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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<TextAlignmentSelector
|
<TextAlignmentSelector editor={props.editor} editorState={editorState} />
|
||||||
editor={props.editor}
|
|
||||||
onClose={() => {
|
|
||||||
const editor = props.editor as Editor;
|
|
||||||
if (!editor) return;
|
|
||||||
const pos = editor.state.selection.to;
|
|
||||||
editor.commands.setTextSelection(pos ?? 0);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,8 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
|
||||||
icon: UnderlineIcon,
|
icon: UnderlineIcon,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
|
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strike"> => ({
|
||||||
key: "strikethrough",
|
key: "strike",
|
||||||
name: "Strikethrough",
|
name: "Strikethrough",
|
||||||
isActive: () => editor?.isActive("strike"),
|
isActive: () => editor?.isActive("strike"),
|
||||||
command: () => toggleStrike(editor),
|
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",
|
itemKey: "bold",
|
||||||
renderKey: "bold",
|
renderKey: "bold",
|
||||||
|
|
@ -113,7 +113,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
|
||||||
editors: ["lite", "document"],
|
editors: ["lite", "document"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
itemKey: "strikethrough",
|
itemKey: "strike",
|
||||||
renderKey: "strikethrough",
|
renderKey: "strikethrough",
|
||||||
name: "Strikethrough",
|
name: "Strikethrough",
|
||||||
icon: Strikethrough,
|
icon: Strikethrough,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export type TEditorCommands =
|
||||||
| "bold"
|
| "bold"
|
||||||
| "italic"
|
| "italic"
|
||||||
| "underline"
|
| "underline"
|
||||||
| "strikethrough"
|
| "strike"
|
||||||
| "bulleted-list"
|
| "bulleted-list"
|
||||||
| "numbered-list"
|
| "numbered-list"
|
||||||
| "to-do-list"
|
| "to-do-list"
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,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",
|
itemKey: "bold",
|
||||||
renderKey: "bold",
|
renderKey: "bold",
|
||||||
|
|
@ -119,7 +119,7 @@ const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strik
|
||||||
editors: ["lite", "document"],
|
editors: ["lite", "document"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
itemKey: "strikethrough",
|
itemKey: "strike",
|
||||||
renderKey: "strikethrough",
|
renderKey: "strikethrough",
|
||||||
name: "Strikethrough",
|
name: "Strikethrough",
|
||||||
icon: Strikethrough,
|
icon: Strikethrough,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue