You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Tiptap编辑器锚点元素下渲染Notion风格Tooltip?

实现Tiptap链接的Notion风格Tooltip(结合Radix UI)

核心思路

通过自定义Tiptap的Link Mark添加标识,优化hover事件捕获目标元素,再结合Radix UI的Tooltip组件实现受控式悬浮层,精准定位并展示链接操作选项。

先给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

关键注意点

  1. 精准捕获元素:用closest('.tiptap-link')避免误触发非链接元素的hover事件
  2. 定位优化:通过元素的getBoundingClientRect()计算Tooltip位置,确保悬浮层在链接下方居中
  3. 避免裁剪:使用Radix的Tooltip.Portal将悬浮层渲染到body,防止被编辑器的overflow样式裁剪
  4. 交互体验:添加onMouseLeave事件自动关闭Tooltip,提升用户体验

内容的提问来源于stack exchange,提问作者Emiliano

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:23:12