如何在Tiptap编辑器锚点元素下渲染Notion风格Tooltip?
实现Tiptap链接的Notion风格Tooltip(结合Radix UI)
核心思路
通过自定义Tiptap的Link Mark添加标识,优化hover事件捕获目标元素,再结合Radix UI的Tooltip组件实现受控式悬浮层,精准定位并展示链接操作选项。
步骤1:自定义Link Mark(添加识别标识)
先给Tiptap的链接标签添加自定义class,方便后续精准捕获:
import { Mark } from '@tiptap/core' export const CustomLink = Mark.create({ name: 'link', // 保留原有schema配置(比如attrs、inclusive等) renderHTML({ HTMLAttributes }) { return [ 'a', { ...HTMLAttributes, class: 'tiptap-link', // 自定义class用于识别链接元素 href: HTMLAttributes.href, target: '_blank', rel: 'noopener noreferrer', }, 0, ] }, })
步骤2:结合Radix UI实现Tooltip
在编辑器组件中集成Radix Tooltip,通过状态控制显示/隐藏及位置:
import React, { useState, useCallback } from 'react' import { Editor } from '@tiptap/react' import { EditorView } from '@tiptap/pm/view' import * as Tooltip from '@radix-ui/react-tooltip' const RichTextEditor = () => { const [editor, setEditor] = useState<Editor | null>(null) const [currentLinkTooltip, setCurrentLinkTooltip] = useState<{ href: string | null top: number left: number isVisible: boolean }>({ href: null, top: 0, left: 0, isVisible: false, }) // 处理链接hover事件 const handleLinkHover = useCallback( (event: React.MouseEvent<HTMLDivElement>) => { if (!editor) return // 精准定位到tiptap链接元素 const linkElement = event.target.closest('.tiptap-link') as HTMLAnchorElement | null if (!linkElement) { setCurrentLinkTooltip(prev => ({ ...prev, isVisible: false })) return } const view = editor.view as EditorView const pos = view.posAtDOM(linkElement, 0) if (pos === null || pos < 0) { setCurrentLinkTooltip(prev => ({ ...prev, isVisible: false })) return } const node = view.state.doc.nodeAt(pos) const linkMark = node?.marks.find(mark => mark.type.name === 'link') if (linkMark) { const rect = linkElement.getBoundingClientRect() // 计算Tooltip位置:链接下方居中 const tooltipLeft = rect.left + rect.width / 2 - 100 // 假设Tooltip宽200px const tooltipTop = rect.top + rect.height + 8 // 与链接间距8px setCurrentLinkTooltip({ href: linkMark.attrs.href, top: tooltipTop, left: tooltipLeft, isVisible: true, }) } else { setCurrentLinkTooltip(prev => ({ ...prev, isVisible: false })) } }, [editor] ) // 编辑链接逻辑 const handleEditLink = useCallback(() => { if (!editor || !currentLinkTooltip.href) return // 这里可扩展为打开弹窗修改链接,示例用console演示 console.log('准备编辑链接:', currentLinkTooltip.href) // 示例:用Tiptap命令更新链接 // editor.commands.updateLink({ href: '新的链接地址' }) }, [editor, currentLinkTooltip.href]) // 关闭Tooltip const handleTooltipClose = useCallback(() => { setCurrentLinkTooltip(prev => ({ ...prev, isVisible: false })) }, []) return ( <div> <div onMouseMove={handleLinkHover} onMouseLeave={handleTooltipClose} style={{ position: 'relative' }} > {/* Tiptap编辑器 */} <Editor editor={editor} onCreated={setEditor} extensions={[/* 加载你的CustomLink扩展 */]} content="<p>这是一个<a href='https://example.com'>测试链接</a></p>" /> </div> {/* Radix UI Tooltip悬浮层 */} {currentLinkTooltip.isVisible && currentLinkTooltip.href && ( <Tooltip.Portal> <Tooltip.Content style={{ position: 'fixed', top: currentLinkTooltip.top, left: currentLinkTooltip.left, width: '200px', padding: '8px 12px', backgroundColor: '#fff', border: '1px solid #e5e7eb', borderRadius: '6px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', zIndex: 1000, }} > <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <span style={{ fontSize: '14px', color: '#374151' }}> {currentLinkTooltip.href} </span> <button onClick={handleEditLink} style={{ padding: '4px 8px', fontSize: '12px', backgroundColor: '#f3f4f6', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > 编辑链接 </button> </div> </Tooltip.Content> </Tooltip.Portal> )} </div> ) } export default RichTextEditor
关键注意点
- 精准捕获元素:用
closest('.tiptap-link')避免误触发非链接元素的hover事件 - 定位优化:通过元素的
getBoundingClientRect()计算Tooltip位置,确保悬浮层在链接下方居中 - 避免裁剪:使用Radix的
Tooltip.Portal将悬浮层渲染到body,防止被编辑器的overflow样式裁剪 - 交互体验:添加
onMouseLeave事件自动关闭Tooltip,提升用户体验
内容的提问来源于stack exchange,提问作者Emiliano
相关产品推荐
相关产品推荐

