Tiptap 的 @tiptap/suggestion 如何用 mount 托管定位挂载浮层并自动跟随滚动与缩放?

发布时间:2026/9/12 11:43:01
Tiptap 的 @tiptap/suggestion 如何用 mount 托管定位挂载浮层并自动跟随滚动与缩放?
Tiptap 的 tiptap/suggestion 如何用 mount 托管定位挂载浮层并自动跟随滚动与缩放【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap在做 Mention提及下拉框时常见的麻烦是浮层自己定位要自己监听滚动、自己算视口边界、缩放窗口后还会跟丢。tiptap/suggestion的SuggestionProps上提供了一个mount函数把浮层元素交给插件后挂载、锚定到光标的 rect、以及在滚动、缩放resize和布局变化时重新定位全部由插件通过 Floating UI 的autoUpdate完成不需要你手动挂任何监听器。本文基于仓库中 类型定义、挂载实现 和 SuggestionPositioning 示例 说明这条路径怎么用、怎么配、怎么验证。mount 托管后插件接管了哪些事mount的类型签名是(element: HTMLElement, options?: SuggestionMountOptions) () void即传入浮层元素返回一个unmount函数。根据 types.ts 中的注释与 floating-ui.ts 的实现托管后插件会把元素追加到配置的container默认document.body。如果你已经把元素放进了 DOM插件不再移动它卸载时也由你自己负责用computePosition持续计算位置并写入元素的style.position、style.left、style.top通过 Floating UI 的autoUpdate观察滚动、缩放和布局变化后自动重算reference 是一个每次重新读取光标 rect 的虚拟元素滚动祖先会从编辑器上下文元素自动向上发现不需要手动指定滚动容器首次测量完成前把元素设为visibility: hidden并设width: max-content避免在初始坐标上闪一下首次定位成功后恢复可见返回的unmount会清理autoUpdate监听、移除外部点击监听并在插件负责挂载时把元素从 DOM 移除。文档明确要求在onExit中调用它。SuggestionMountOptions提供两个可选参数onPosition(data)覆写坐标的应用方式。传入后插件不再自己写style.left/style.top而是把{ x, y, placement, strategy }交给你适合需要自定义 transform、动画或写回框架 ref 的场景autoUpdate原样转发给 Floating UI 的autoUpdate。文档注释给出的用途锚点位于被 transform 或动画驱动的容器内时用它开启animationFrame轮询或关闭某些 observer。准备依赖与引入tiptap/suggestion的 package.json 声明了peerDependenciespeerDependencies: { floating-ui/dom: ^1.0.0, tiptap/core: workspace:*, tiptap/pm: workspace:* }即使用mount时需要自行安装floating-ui/dom^1.0.0定位计算与autoUpdate都来自这个包。按仓库示例的用法suggestion 配置对象通过Mention.configure({ suggestion })交给 Mention 扩展mount则出现在 suggestion 的render各回调收到的SuggestionProps上。React 侧渲染组件用tiptap/react的ReactRendererVue 侧用tiptap/vue-3的VueRenderer。编写 suggestion 配置下面是 React 示例 的配置原文items 为该示例自带的演示数据import { flip, shift } from floating-ui/dom import { ReactRenderer } from tiptap/react import DropdownList from ./DropdownList.jsx const items [ { id: alice, label: Alice Johnson }, { id: bob, label: Bob Smith }, { id: carol, label: Carol Williams }, { id: dave, label: Dave Brown }, { id: eve, label: Eve Davis }, { id: frank, label: Frank Miller }, { id: grace, label: Grace Wilson }, { id: hank, label: Hank Moore }, { id: iris, label: Iris Taylor }, { id: jack, label: Jack Anderson }, ] export default { items: ({ query }) items.filter(item item.label.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5), placement: top-start, offset: { mainAxis: 8 }, flip: false, floatingUi: { strategy: fixed, middleware: [flip({ padding: 8 }), shift({ padding: 8 })], }, // Dismiss the popup when clicking outside it (and outside the editor). dismissOnOutsideClick: true, render: () { let component let unmount null return { onStart: props { component new ReactRenderer(DropdownList, { props, editor: props.editor, }) // Managed positioning: the plugin mounts the element into the container, // keeps it anchored to the cursor, and repositions it on scroll/resize. unmount props.mount(component.element) }, onUpdate(props) { component.updateProps(props) }, onKeyDown(props) { if (props.event.key Escape) { component.destroy() return true } return component.ref?.onKeyDown(props) }, onExit() { unmount?.() component.destroy() }, } }, }几个与定位直接相关的配置项默认值取自 types.ts 的注释placement浮层相对光标的位置可选top | top-start | top-end | bottom | bottom-start | bottom-end默认bottom-start。示例用top-startoffset浮层像素偏移默认{ mainAxis: 4, crossAxis: 0 }示例覆盖为{ mainAxis: 8 }flip空间不足时是否自动翻转默认true。示例设为false关闭内置翻转同时在floatingUi.middleware里追加了自己带padding: 8的flip与shift中间件floatingUi透传给定位库的附加选项可设strategyabsolute | fixed不传时实现里默认absolute见 floating-ui.ts和追加的middleware。插件始终保留对锚点和 placement 的所有权你只是追加中间件。示例用strategy: fixed让浮层相对视口定位配合示例文案所述的行为钉在 top-start 一侧空间不足时在视口内平移shiftdismissOnOutsideClick点击浮层和编辑器之外的区域时关闭 suggestion默认true。该选项只在走mount托管挂载时生效因为插件需要知道浮层元素container配置项CSS 选择器或元素把浮层 portal 进弹窗/对话框内部渲染。解析失败时实现会回退到document.body。在 render 生命周期里接入 mount托管挂载的主路径只有三处React 版本如上所示Vue 版本 唯一差异是用VueRenderer构造组件并在onStart开头对已存在的component先destroy()再重建其余onUpdate/onKeyDown/onExit完全一致。接入要点onStart中用框架 Renderer 创建下拉组件后立即unmount props.mount(component.element)把元素交给插件onUpdate只负责component.updateProps(props)同步查询结果不碰位置onExit中调用unmount?.()再component.destroy()否则autoUpdate监听和外部点击监听不会清理。编辑器侧按 示例入口 组装useEditor({ extensions: [ Document, Paragraph, Text, Mention.configure({ HTMLAttributes: { class: mention, }, suggestion, }), ], content: pTry mentioning a colleague by typing code/code./p, })可选分支自己处理坐标或自己挂载两条逃生通道都来自 types.ts 的注释保留托管、改坐标应用方式给props.mount(element, { onPosition })传入回调插件停止写style.left/style.top改为把计算好的{ x, y, placement, strategy }交给你同时实现会跳过首帧隐藏逻辑由你自己控制可见性。完全自己挂载SuggestionProps上还暴露了floatingUi解析好的placement/strategy/middleware可直接喂给computePosition()和clientRect读取当前光标 rect 的函数。注释明确说这是自己挂载元素、手动跑定位循环时才用的逃生口走这条路就不需要mount。验证行为仓库自带 Playwright 用例 index.spec.ts 覆盖了这个示例其断言可以直接作为行为核对清单在编辑器中输入后.dropdown-menu浮层可见说明mount挂载成功且首帧定位完成输入a后下拉里只有 1 个按钮且文本为Alice对应items的按前缀过滤逻辑点击第一项后编辑器内出现.tiptap span.mention插入成功;按ArrowDownEnter插入data-idbob的 mention键盘选中路径。跟随滚动与缩放这一部分由autoUpdate驱动用例中没有直接的滚动断言可参照实现核对createMount 中cleanupAutoUpdate autoUpdate(reference, element, update, options.autoUpdate)一行reference的getBoundingClientRect每次都会重新读取光标 rect滚动/缩放触发的每次回调都会走computePosition更新left/top。边界与注意dismissOnOutsideClick只在托管挂载下生效自己挂载时插件不知道浮层元素外部点击关闭需要自行实现container传选择器时查不到或选择器非法都会静默回退到document.body不会报错portal 到弹窗里时要确认选择器确实能命中默认strategy是absolutefloatingUi.strategy不传时改成fixed属于示例中的可选配置不是必须本文代码路径以仓库内示例为准React 版 与 Vue 版 两个文件完整可查组件DropdownList分别在同目录的DropdownList.jsx/DropdownList.vue。【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考