返回 导航

Vue.js

hangge.com

Vue.js - 富文本编辑器Tiptap使用详解9(气泡菜单)

作者:hangge | 2026-02-24 12:35
    TiptapBubbleMenu(气泡菜单)是在选中文本时出现的上下文工具栏,体验接近 Word/Notion 的文本选中工具。下面我将通过样例演示如何实现气泡菜单。

九、气泡菜单实现

1,准备工作

(1)首先我们项目总需要安装 Tiptap 基础依赖,具体安装方法见之前的文章: 

(2)为了让气泡菜单更加美观,我们还需要安装 FontAwesomeElementUITailwind 依赖,并在 main.js 中引入。
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import '@fortawesome/fontawesome-free/css/all.css'
Vue.use(ElementUI)

2,实现步骤

(1)下面是气泡菜单的设置与使用代码,说明如下:
  • Editor:由 @tiptap/core 创建,注册需要的扩展(示例使用 StarterKit + Link 等)。
  • BubbleMenu:从 @tiptap/vue-2/menus 导入的 Vue 组件(它会在内部注册气泡菜单插件并使用 tippy.js 做定位)。BubbleMenu 接收两个重要 prop
    • editorTiptapEditor 实例(必需)。
    • tippy-options:传给 tippy.js 的配置(动画、placementoffsetdelay 等)。
  • BubbleMenu 的插槽内容就是气泡里的 HTML(一组按钮),我们在里面直接用 editor API(例如 editor.chain().focus().toggleBold().run())。
  • BubbleMenu 默认显示条件:当选区非空(有文字被选中)时会自动显示;当选区为空或光标在某些节点时默认不显示(可自定义)。
  • editor.isActive('bold') 等方法用于判断按钮是否处于激活态,从而给出视觉状态反馈。
<template>
  <div class="editor-wrapper p-4">
    <!-- 气泡菜单:只在 editor 已初始化时渲染 -->
    <BubbleMenu v-if="editor" :editor="editor" :tippy-options="tippyOptions" class="bubble-menu">
      <button
        type="button"
        class="bm-btn"
        :class="{ active: editor.isActive('bold') }"
        @click="toggleBold"
        title="加粗 (Ctrl/Cmd+B)"
      >
        <i class="fas fa-bold"></i>
      </button>

      <button
        type="button"
        class="bm-btn"
        :class="{ active: editor.isActive('italic') }"
        @click="toggleItalic"
        title="斜体 (Ctrl/Cmd+I)"
      >
        <i class="fas fa-italic"></i>
      </button>

      <button
        type="button"
        class="bm-btn"
        @click="setLink"
        title="插入 / 编辑 链接"
      >
        <i class="fas fa-link"></i>
      </button>

      <div class="bm-sep"></div>

      <button type="button" class="bm-btn" @click="toggleHeading(1)" 
      :class="{ active: editor.isActive('heading', { level: 1 }) }" title="H1">H1</button>
      <button type="button" class="bm-btn" @click="toggleHeading(2)" 
      :class="{ active: editor.isActive('heading', { level: 2 }) }" title="H2">H2</button>
      <button type="button" class="bm-btn" @click="clearFormatting" title="清除格式">
        <i class="fas fa-eraser"></i>
      </button>
    </BubbleMenu>

    <!-- 编辑器主体 -->
    <editor-content :editor="editor" class="editor-content" />
  </div>
</template>

<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
import { BubbleMenu }   from '@tiptap/vue-2/menus'
import Link from '@tiptap/extension-link'
import Underline from '@tiptap/extension-underline'
import Strike from '@tiptap/extension-strike'

export default {
  name: 'EditorWithBubbleMenu',
  components: { EditorContent, BubbleMenu },
  data() {
    return {
      editor: null,
      // tippy options 可以传给 tippy.js 做定位/动画/延迟等配置
      tippyOptions: {
        duration: 120,
        placement: 'top',
        // offset: [0, 8],
        // delay: [80, 0]
      },
    }
  },
  mounted() {
    this.editor = new Editor({
      extensions: [
        StarterKit,
        Link,
        Underline,
        Strike,
      ],
      content: `<p>选中任意文字,气泡菜单会出现(示例:试试加粗 / 斜体 / 插入链接)。</p>`,
    })
  },
  beforeDestroy() {
    if (this.editor) this.editor.destroy()
  },
  methods: {
    toggleBold() {
      this.editor.chain().focus().toggleBold().run()
    },
    toggleItalic() {
      this.editor.chain().focus().toggleItalic().run()
    },
    toggleHeading(level) {
      this.editor.chain().focus().toggleHeading({ level }).run()
    },
    setLink() {
      // 先检测是否已有链接,方便编辑
      const previousUrl = this.editor.getAttributes('link').href || ''
      const url = window.prompt('请输入链接 URL(以 http(s):// 开头)', previousUrl)
      if (url === null) return // 取消
      if (url === '') {
        // 清除链接
        this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
        return
      }
      this.editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
    },
    clearFormatting() {
      this.editor.chain().focus().clearNodes().unsetAllMarks().run()
    },
  },
}
</script>

<style scoped>
.editor-content {
  min-height: 220px;
  border: 1px solid #e6e6e6;
  border-radius: 6px;
  padding: 12px;
  background: #fff;
}

/* 气泡菜单样式(可自定义)*/
.bubble-menu {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px;
  background: #111827;
  color: #fff;
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0,0,0,0.18);
}

/* 按钮基础样式 */
.bm-btn {
  background: transparent;
  border: none;
  color: inherit;
  padding: 6px;
  border-radius: 4px;
  cursor: pointer;
}
.bm-btn:hover { background: rgba(255,255,255,0.06); }

/* 激活态 */
.bm-btn.active { background: rgba(255,255,255,0.14); }

/* 分隔符 */
.bm-sep {
  width: 1px;
  height: 20px;
  background: rgba(255,255,255,0.08);
  margin: 0 6px;
}
</style>

(2)运行程序,当我们选中文本时,其会附近自动出现气泡菜单。点击气泡菜单上的按钮即可执行相应的命令。
评论

全部评论(0)

回到顶部