Tiptap 提及功能实战:如何给编辑器加上 @用户 与 标签

Tiptap 提及功能实战:如何给编辑器加上 @用户 与 #标签

【免费下载链接】tiptap The headless rich text editor framework for web artisans. 【免费下载链接】tiptap 项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap

这篇带你把 Tiptap 提及功能真正跑起来:先敲两行代码看到 @ 弹出候选人,再弄清背后的 Suggestion 机制,最后扩展出 #标签、自定义弹层和配套样式,并提前绕开 3 个高频坑。代码都按"最小能跑"给,复制即用。

先跑一个最小 Demo 🏃

先装依赖:

npm install @tiptap/core @tiptap/extension-mention @tiptap/vue-3

一个 Vue 3 组件就是全部:

<template>
  <editor-content :editor="editor" />
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Mention from '@tiptap/extension-mention'

export default {
  components: { EditorContent },
  data() {
    return { editor: null }
  },
  mounted() {
    this.editor = new Editor({
      extensions: [
        Document,
        Paragraph,
        Text,
        Mention.configure({
          HTMLAttributes: { class: 'mention' }, // 渲染到 span 上,样式全靠它
          suggestion: {
            char: '@', // 触发字符
            items: ({ query }) => {
              const users = [
                { id: '1', label: '张三' },
                { id: '2', label: '李四' },
                { id: '3', label: '王五' },
              ]
              // 按输入过滤,最多给 5 条
              return users
                .filter(u => u.label.includes(query))
                .slice(0, 5)
            },
          },
        }),
      ],
    })
  },
  beforeUnmount() {
    this.editor.destroy()
  },
}
</script>

保存后在编辑器里敲 @,候选人弹层就会浮出来。想对照完整效果,可以看仓库里的 官方 Demo,把 items 换成接口请求就能直接上线。

触发背后发生了什么 🔍

mermaid

机制其实不复杂。Mention 的 addProseMirrorPlugins 会把每份 suggestion 配置各变成一个独立的 ProseMirror 插件;光标附近有输入时,插件去匹配"触发字符 + 后续文本",命中后调用 items 拉候选、交给 render 画弹层;你选中某一项时执行 command,把这段输入整体替换成 mention 节点加一个空格。模块本体在 packages/extension-mention/,节点属性 MentionNodeAttrs(id、label、mentionSuggestionChar)和 renderText/renderHTML 都定义在 mention.ts 里。

扩展到 @ 和 # 多触发 🚦

单人触发用 suggestion 就够;要多触发,就换成 suggestions 数组,每项独立配置 char、pluginKey、items、command:

import { PluginKey } from '@tiptap/pm/state'

Mention.configure({
  HTMLAttributes: { class: 'mention' },
  suggestions: [
    {
      char: '@',
      pluginKey: new PluginKey('user-mention'), // 每个触发器独立 key
      items: ({ query }) => fetchUsers(query), // 换成你的用户接口
      command: ({ editor, range, props }) => {
        editor
          .chain()
          .focus()
          .insertContentAt(range, [
            { type: 'mention', attrs: { ...props, mentionSuggestionChar: '@' } },
            { type: 'text', text: ' ' },
          ])
          .run()
      },
    },
    {
      char: '#',
      pluginKey: new PluginKey('tag-mention'),
      items: ({ query }) => fetchTags(query),
      // 不写 command,默认插入 mention 节点并补一个空格
    },
  ],
})

几个要点。suggestions 一旦写了,suggestion 字段就被忽略,别两个都配。每个条目最好给独立的 pluginKey,两套触发才不会抢事件。节点上会记一个 mentionSuggestionChar 属性,renderText/renderHTML 靠它决定显示 @ 还是 #,所以两套触发渲染时不会串。参考实现见 MultiMention 示例。

把建议框换成自己的 UI 🎨

默认弹层够用,但想做成带头像、带分组的列表,就覆盖 render。它返回四个回调,构成弹层的完整生命周期:

render: () => {
  let component
  return {
    onStart: props => { /* 用 VueRenderer 挂载弹层组件,按光标定位 */ },
    onUpdate: props => { /* 候选变化或光标移动时刷新 */ },
    onKeyDown: props => { /* 拦截 Esc / 方向键 / Enter */ },
    onExit: () => { /* 移除并销毁组件 */ },
  }
}

套路固定:onStart 里用 VueRenderer 把弹层组件挂到 document.body,并用光标位置换算出坐标;onUpdate 负责后续刷新;onExit 负责清理。完整可跑的写法在 官方 Demo 的 suggestion.js,里面还给了 debounce: 300、minQueryLength: 2 和 initialItems(刚敲 @ 还没输入时先垫几条)三个现成选项。

让提及标签好看一点 ✨

提及节点默认渲染成 <span data-type="mention">,把下面这段贴进样式文件即可:

.tiptap .mention {
  background-color: #ece5ff;
  color: #6a3de8;
  border-radius: 6px;
  padding: 2px 6px;
  font-weight: 500;
  box-decoration-break: clone; /* 换行时每段都带圆角背景 */
}

想区分 @ 用户和 # 标签的视觉,可以再用 [data-mention-suggestion-char] 选择器分别命中两种节点。

最容易踩的 3 个坑 ⚠️

坑一:删完 mention 还留一个 @。deleteTriggerWithBackspace 默认是 false,Backspace 删掉节点后会原地留下触发字符。想要连 @ 一起删掉:

Mention.configure({ deleteTriggerWithBackspace: true })

坑二:两个触发器互相抢事件。多触发时如果共用同一个 pluginKey,建议框的显示和退出会互相干扰。按上面写法给每项配不同的 new PluginKey(...) 即可。

坑三:CSS 写了没反应。选择器没对上就白写——它必须和 HTMLAttributes 里注册的 class 一致,或者直接拿默认就有的属性选择:

.tiptap span[data-type="mention"] { /* 这里才是生效的选择器 */ }

最后

Tiptap 用户提及的本质,是把 Suggestion 机制包成了一个可复用的节点。学会这套写法后,搜索插公式、引用代码片段这类需求也能照抄。下一步可以翻翻 packages/suggestion/ 的完整选项,或把 Mention 和 extension-link 组合,让被 @ 的名字直接带跳转,Tiptap 标签功能也就补齐了。

【免费下载链接】tiptap The headless rich text editor framework for web artisans. 【免费下载链接】tiptap 项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值