From 490fda6502bb15dd1ed725a7663c40c31ad51b0d Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Wed, 17 Sep 2025 14:29:46 +0530 Subject: [PATCH 1/3] fix : block menu for rich editor --- .../components/editors/rich-text/editor.tsx | 9 +- .../src/core/components/menus/block-menu.tsx | 230 ++++++++++++------ 2 files changed, 162 insertions(+), 77 deletions(-) diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index f110c789b2b..dcdb8c295f9 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -1,7 +1,7 @@ import { forwardRef, useCallback } from "react"; // components import { EditorWrapper } from "@/components/editors"; -import { EditorBubbleMenu } from "@/components/menus"; +import { BlockMenu, EditorBubbleMenu } from "@/components/menus"; // extensions import { SideMenuExtension } from "@/extensions"; // plane editor imports @@ -40,7 +40,12 @@ const RichTextEditor: React.FC = (props) => { return ( - {(editor) => <>{editor && bubbleMenuEnabled && }} + {(editor) => ( + <> + {editor && bubbleMenuEnabled && }{" "} + + + )} ); }; diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index 43c0193c715..612d681549f 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -1,8 +1,18 @@ +import { + useFloating, + offset, + flip, + shift, + autoUpdate, + useDismiss, + useInteractions, + FloatingPortal, +} from "@floating-ui/react"; import type { Editor } from "@tiptap/react"; import { Copy, LucideIcon, Trash2 } from "lucide-react"; -import { useCallback, useEffect, useRef } from "react"; -import tippy, { Instance } from "tippy.js"; +import { useCallback, useEffect, useRef, useState } from "react"; // constants +import { cn } from "@plane/utils"; import { CORE_EXTENSIONS } from "@/constants/extension"; import { IEditorProps } from "@/types"; @@ -14,62 +24,94 @@ type Props = { export const BlockMenu = (props: Props) => { const { editor } = props; - const menuRef = useRef(null); - const popup = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [isAnimatedIn, setIsAnimatedIn] = useState(false); + const menuRef = useRef(null); + const virtualReferenceRef = useRef<{ getBoundingClientRect: () => DOMRect }>({ + getBoundingClientRect: () => new DOMRect(), + }); + // const { t } = useTranslation(); - const handleClickDragHandle = useCallback((event: MouseEvent) => { - const target = event.target as HTMLElement; - if (target.matches("#drag-handle")) { - event.preventDefault(); + // Set up Floating UI with virtual reference element + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + middleware: [offset({ crossAxis: -10 }), flip(), shift()], + whileElementsMounted: autoUpdate, + placement: "left-start", + }); - popup.current?.setProps({ - getReferenceClientRect: () => target.getBoundingClientRect(), - }); + const dismiss = useDismiss(context); + const { getFloatingProps } = useInteractions([dismiss]); - popup.current?.show(); - return; - } + // Handle click on drag handle + const handleClickDragHandle = useCallback( + (event: MouseEvent) => { + const target = event.target as HTMLElement; + const dragHandle = target.closest("#drag-handle"); - popup.current?.hide(); - return; - }, []); + if (dragHandle) { + event.preventDefault(); - useEffect(() => { - if (menuRef.current) { - menuRef.current.remove(); - menuRef.current.style.visibility = "visible"; - - // @ts-expect-error - Tippy types are incorrect - popup.current = tippy(document.body, { - getReferenceClientRect: null, - content: menuRef.current, - appendTo: () => document.querySelector(".frame-renderer"), - trigger: "manual", - interactive: true, - arrow: false, - placement: "left-start", - animation: "shift-away", - maxWidth: 500, - hideOnClick: true, - onShown: () => { - menuRef.current?.focus(); - }, - }); - } + // Update virtual reference with current drag handle position + virtualReferenceRef.current = { + getBoundingClientRect: () => dragHandle.getBoundingClientRect(), + }; - return () => { - popup.current?.destroy(); - popup.current = null; - }; - }, []); + // Set the virtual reference as the reference element + refs.setReference(virtualReferenceRef.current); + + // Show the menu + setIsOpen(true); + return; + } + + // If clicking outside and not on a menu item, hide the menu + if (menuRef.current && !menuRef.current.contains(target)) { + setIsOpen(false); + } + }, + [refs] + ); + + // useEffect(() => { + // if (menuRef.current) { + // menuRef.current.remove(); + // menuRef.current.style.visibility = "visible"; + + // // @ts-expect-error - Tippy types are incorrect + // popup.current = tippy(document.body, { + // getReferenceClientRect: null, + // content: menuRef.current, + // appendTo: () => document.querySelector(".frame-renderer"), + // trigger: "manual", + // interactive: true, + // arrow: false, + // placement: "left-start", + // animation: "shift-away", + // maxWidth: 500, + // hideOnClick: true, + // onShown: () => { + // menuRef.current?.focus(); + // }, + // }); + // } + + // return () => { + // popup.current?.destroy(); + // popup.current = null; + // }; + // }, []); useEffect(() => { - const handleKeyDown = () => { - popup.current?.hide(); + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsOpen(false); + } }; const handleScroll = () => { - popup.current?.hide(); + setIsOpen(false); }; document.addEventListener("click", handleClickDragHandle); document.addEventListener("contextmenu", handleClickDragHandle); @@ -84,6 +126,23 @@ export const BlockMenu = (props: Props) => { }; }, [handleClickDragHandle]); + // Animation effect + useEffect(() => { + if (isOpen) { + setIsAnimatedIn(false); + // Add a small delay before starting the animation + const timeout = setTimeout(() => { + requestAnimationFrame(() => { + setIsAnimatedIn(true); + }); + }, 50); + + return () => clearTimeout(timeout); + } else { + setIsAnimatedIn(false); + } + }, [isOpen]); + const MENU_ITEMS: { icon: LucideIcon; key: string; @@ -96,10 +155,13 @@ export const BlockMenu = (props: Props) => { key: "delete", label: "Delete", onClick: (e) => { - editor.chain().deleteSelection().focus().run(); - popup.current?.hide(); e.preventDefault(); e.stopPropagation(); + + // Execute the delete action + editor.chain().deleteSelection().focus().run(); + + setIsOpen(false); }, }, { @@ -146,36 +208,54 @@ export const BlockMenu = (props: Props) => { console.error(error.message); } } - - popup.current?.hide(); + setIsOpen(false); }, }, ]; + if (!isOpen) { + return null; + } return ( -
- {MENU_ITEMS.map((item) => { - // Skip rendering the button if it should be disabled - if (item.isDisabled && item.key === "duplicate") { - return null; - } - - return ( - - ); - })} -
+ +
{ + refs.setFloating(node); + menuRef.current = node; + }} + style={{ + ...floatingStyles, + zIndex: 99, + animationFillMode: "forwards", + transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)", // Expo ease out + }} + className={cn( + "z-20 max-h-60 min-w-[7rem] overflow-y-scroll rounded-lg border border-custom-border-200 bg-custom-background-100 p-1.5 shadow-custom-shadow-rg", + "transition-all duration-300 transform origin-top-right", + isAnimatedIn ? "opacity-100 scale-100" : "opacity-0 scale-75" + )} + data-prevent-outside-click + {...getFloatingProps()} + > + {MENU_ITEMS.map((item) => { + if (item.isDisabled) { + return null; + } + return ( + + ); + })} +
+
); }; From cbf167acb21e59407f51342c8dc3d66ef6bb17d3 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Wed, 17 Sep 2025 14:33:30 +0530 Subject: [PATCH 2/3] chore: remove comments --- .../src/core/components/menus/block-menu.tsx | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index 612d681549f..daf132e870c 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -30,7 +30,6 @@ export const BlockMenu = (props: Props) => { const virtualReferenceRef = useRef<{ getBoundingClientRect: () => DOMRect }>({ getBoundingClientRect: () => new DOMRect(), }); - // const { t } = useTranslation(); // Set up Floating UI with virtual reference element const { refs, floatingStyles, context } = useFloating({ @@ -74,35 +73,6 @@ export const BlockMenu = (props: Props) => { [refs] ); - // useEffect(() => { - // if (menuRef.current) { - // menuRef.current.remove(); - // menuRef.current.style.visibility = "visible"; - - // // @ts-expect-error - Tippy types are incorrect - // popup.current = tippy(document.body, { - // getReferenceClientRect: null, - // content: menuRef.current, - // appendTo: () => document.querySelector(".frame-renderer"), - // trigger: "manual", - // interactive: true, - // arrow: false, - // placement: "left-start", - // animation: "shift-away", - // maxWidth: 500, - // hideOnClick: true, - // onShown: () => { - // menuRef.current?.focus(); - // }, - // }); - // } - - // return () => { - // popup.current?.destroy(); - // popup.current = null; - // }; - // }, []); - useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { @@ -251,7 +221,6 @@ export const BlockMenu = (props: Props) => { > {item.label} - {/* {t(item.label)} */} ); })} From d76bfdda0efd94b81ff58f1f8bab0e53c7c6a66d Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Mon, 22 Sep 2025 17:47:52 +0530 Subject: [PATCH 3/3] chore : update selection logic --- .../src/core/components/editors/rich-text/editor.tsx | 2 +- packages/editor/src/core/components/menus/block-menu.tsx | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index dcdb8c295f9..40a2b0c0be4 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -42,7 +42,7 @@ const RichTextEditor: React.FC = (props) => { {(editor) => ( <> - {editor && bubbleMenuEnabled && }{" "} + {editor && bubbleMenuEnabled && } )} diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index daf132e870c..e8a644c1fa7 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -60,6 +60,15 @@ export const BlockMenu = (props: Props) => { // Set the virtual reference as the reference element refs.setReference(virtualReferenceRef.current); + // Ensure the targeted block is selected + const rect = dragHandle.getBoundingClientRect(); + const coords = { left: rect.left + rect.width / 2, top: rect.top + rect.height / 2 }; + const posAtCoords = editor.view.posAtCoords(coords); + if (posAtCoords) { + const $pos = editor.state.doc.resolve(posAtCoords.pos); + const nodePos = $pos.before($pos.depth); + editor.chain().setNodeSelection(nodePos).run(); + } // Show the menu setIsOpen(true); return;