From 3ec184e883dd13aebe6ffa58819790b1f3308200 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Mon, 22 Sep 2025 18:24:29 +0530 Subject: [PATCH] fix: block menu ref --- .../components/editor/lite-text/editor.tsx | 88 ++++--- .../src/core/components/menus/block-menu.tsx | 222 ++++++++++++------ 2 files changed, 204 insertions(+), 106 deletions(-) diff --git a/apps/web/core/components/editor/lite-text/editor.tsx b/apps/web/core/components/editor/lite-text/editor.tsx index 5bdec0b45c..43c264a77c 100644 --- a/apps/web/core/components/editor/lite-text/editor.tsx +++ b/apps/web/core/components/editor/lite-text/editor.tsx @@ -111,36 +111,64 @@ export const LiteTextEditor = React.forwardRef isFullVariant && !showToolbarInitially && setIsFocused(true)} onBlur={() => isFullVariant && !showToolbarInitially && setIsFocused(false)} > - "", - workspaceId, - workspaceSlug, - })} - mentionHandler={{ - searchCallback: async (query) => { - const res = await fetchMentions(query); - if (!res) throw new Error("Failed in fetching mentions"); - return res; - }, - renderComponent: EditorMentionsRoot, - getMentionedEntityDetails: (id) => ({ - display_name: getUserDetails(id)?.display_name ?? "", - }), - }} - placeholder={placeholder} - containerClassName={cn(containerClassName, "relative", { - "p-2": !editable, - })} - extendedEditorProps={{}} - {...rest} - /> - {showToolbar && editable && ( + {/* Wrapper for lite toolbar layout */} +
+ {/* Main Editor - always rendered once */} +
+ "", + workspaceId, + workspaceSlug, + })} + mentionHandler={{ + searchCallback: async (query) => { + const res = await fetchMentions(query); + if (!res) throw new Error("Failed in fetching mentions"); + return res; + }, + renderComponent: EditorMentionsRoot, + getMentionedEntityDetails: (id) => ({ + display_name: getUserDetails(id)?.display_name ?? "", + }), + }} + placeholder={placeholder} + containerClassName={cn(containerClassName, "relative", { + "p-2": !editable, + })} + extendedEditorProps={{ + isSmoothCursorEnabled: is_smooth_cursor_enabled, + }} + editorClassName={editorClassName} + {...rest} + /> +
+ + {/* Lite Toolbar - conditionally rendered */} + {isLiteVariant && editable && ( + { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + editorRef?.executeMenuItemCommand({ + itemKey: item.itemKey, + ...item.extraProps, + }); + }} + onSubmit={(e) => rest.onEnterKeyPress?.(e)} + isSubmitting={isSubmitting} + isEmpty={isEmpty} + /> + )} +
+ + {/* Full Toolbar - conditionally rendered */} + {isFullVariant && editable && (
{ 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 isEmbedFlagged = + props.flaggedExtensions?.includes("external-embed") || props.disabledExtensions?.includes("external-embed"); - 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(); + // Update virtual reference with current drag handle position + virtualReferenceRef.current = { + getBoundingClientRect: () => dragHandle.getBoundingClientRect(), + }; + + // 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; + } + + // If clicking outside and not on a menu item, hide the menu + if (menuRef.current && !menuRef.current.contains(target)) { + setIsOpen(false); + } + }, + [editor, refs] + ); + + const editorState = useEditorState({ + editor, + selector: ({ editor }) => { + const selection = editor.state.selection; + const content = selection.content().content; + const firstChild = content.firstChild; + let linkUrl: string | null = null; + const foundLinkMarks: string[] = []; + + const isEmbedActive = editor.isActive(ADDITIONAL_EXTENSIONS.EXTERNAL_EMBED); + const isRichCard = firstChild?.attrs[EExternalEmbedAttributeNames.IS_RICH_CARD]; + const isNotEmbeddable = firstChild?.attrs[EExternalEmbedAttributeNames.HAS_EMBED_FAILED]; + + if (firstChild) { + for (let i = 0; i < firstChild.childCount; i++) { + const node = firstChild.child(i); + const linkMarks = node.marks?.filter( + (mark) => mark.type.name === CORE_EXTENSIONS.CUSTOM_LINK && mark.attrs?.href + ); + + if (linkMarks && linkMarks.length > 0) { + linkMarks.forEach((mark) => { + foundLinkMarks.push(mark.attrs.href); + }); + } + } + if (firstChild.attrs.src) { + foundLinkMarks.push(firstChild.attrs.src); + } + } + + if (foundLinkMarks.length === 1) { + linkUrl = foundLinkMarks[0]; + } + + return { + isEmbedActive, + isLinkEmbeddable: isEmbedActive || !!linkUrl, + linkUrl, + isRichCard, + isNotEmbeddable, + }; + }, + }); + + // Set up event listeners 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 = () => { @@ -243,7 +302,7 @@ export const BlockMenu = (props: Props) => { label: "Delete", onClick: (e) => { editor.chain().deleteSelection().focus().run(); - popup.current?.hide(); + setIsOpen(false); e.preventDefault(); e.stopPropagation(); @@ -315,37 +374,48 @@ export const BlockMenu = (props: Props) => { }, ]; - if (!isOpen) { - return null; - } - 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" + )} + {...getFloatingProps()} + > + {MENU_ITEMS.map((item) => { + if (item.isDisabled) { + return null; + } + return ( + + ); + })} +
+
); };