Tiptap Floating Menu 浮动菜单

Tiptap Floating Menu 浮动菜单扩展,当新启一个空行时可以使用浮动菜单悬浮在这行上面,你可以在这个菜单上面放置需要的功能。

Install 安装

npm install @tiptap/extension-floating-menu

Settings 配置

element 放置菜单的元素,默认为null。

FloatingMenu.configure({
  element: null,
})

tippyOptions FloatingMenu依赖tippy.js,您可以直接将配置传递给它。

FloatingMenu.configure({
  tippyOptions: { },
})

pluginKey 当有多个实例时你需要用到这个参数,默认为bubbleMenu。

import { Editor } from '@tiptap/core'
import BubbleMenu from '@tiptap/extension-bubble-menu'

new Editor({
  extensions: [
    FloatingMenu.configure({
      pluginKey: 'bubbleMenuOne',
      element: document.querySelector('.menu-one'),
    }),
    FloatingMenu.configure({
      pluginKey: 'bubbleMenuTwo',
      element: document.querySelector('.menu-two'),
    }),
  ],
})

shouldShow 一个控制菜单是否显示的回调函数,你可以自定义控制菜单是否显示。

FloatingMenu.configure({
  shouldShow: ({ editor, view, state, oldState, from, to }) => {
    //如果是图片或链接才显示菜单
    return editor.isActive('image') || editor.isActive('link')
  },
})

Usage 用例

import { Editor } from '@tiptap/core'
import FloatingMenu from '@tiptap/extension-floating-menu'

new Editor({
  extensions: [
    FloatingMenu.configure({
      element: document.querySelector('.menu'),
    }),
  ],
})

源代码

floating-menu 源代码

在线例子

vue 在线例子

FloatingMenu 例子

  • Vue 例子

  • React 例子

  • React CSS

<template>
  <div>
    <floating-menu :editor="state.editor" :tippy-options="{ duration: 100 }" v-if="state.editor">
      <button @click="state.editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': state.editor.isActive('heading', { level: 1 }) }">
        H1
      </button>
      <button @click="state.editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': state.editor.isActive('heading', { level: 2 }) }">
        H2
      </button>
      <button @click="state.editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': state.editor.isActive('bulletList') }">
        Bullet List
      </button>
    </floating-menu>
  </div>
  <div>
    <editor-content :editor="state.editor" />
  </div>
  <h2>编辑器内容:</h2>
  <div style="min-height: 90px;">
    {{ getHtml }}
  </div>
   </template>
<script setup>
/*
 tiptap 中文文档
 https://www.itxst.com/tiptap/tutorial.html
*/
import { Editor, EditorContent, FloatingMenu } from '@tiptap/vue-3'
import StarterKit from "@tiptap/starter-kit";
import { computed, onMounted, reactive } from 'vue' 

const state = reactive({
  editor: new Editor({
    extensions: [
      EditorContent,
      FloatingMenu,
      StarterKit
    ],
    content: ` 
       <p>拖动下面的图片到文字上方试试看</p>
       <img src="https://www.itxst.com/img/logov31.png" />
      `,
  }) as any,
  result: '', 
});

onMounted(() => {
  
});

const getHtml = computed(() => {
  return state.editor.getHTML();
})
</script>
<style scoped>
.editor,
.html {
  margin: 10px 20px;
  width: 690px;
}
.is-active {
  background-color: #1512e6;
  color: #fff;
}
.editor:deep(.ProseMirror) {
  color: #333;
  border: solid 3px #333;
  padding: 0px 6px;
  height: 260px;
  overflow-y: auto;
  border-radius: 6px;
}
.editor:deep(a) {
  color: #1512e6;
  cursor: pointer;
  text-decoration: underline;
}
.editor:deep(img) {
  background-color: #000;
}
.btn {
  margin-left: 20px;
  margin-top: 10px;
}
button {
  margin-right: 10px;
}
.editor:deep(blockquote) {
  padding-left: 10px;
  border-left: 3px solid #ddd;
}
.html pre {
  white-space: pre-wrap;
}
h2 {
  padding-left: 15px;
  font-size: 15px;
}
.html,
textarea {
  margin: 0px 20px;
  color: #ddd;
  background: #282c34;
  width: 690px;
  min-height: 200px;
  white-space: pre-wrap;
  padding: 6px;
  border-radius: 3px;
}

.editor:deep(.ProseMirror) {
  code {
    font-size: 0.9rem;
    padding: 0.25em;
    border-radius: 0.25em;
    background-color: rgba(#616161, 0.1);
    color: #616161;
    box-decoration-break: clone;
  }
}
</style>
import './styles.scss'
import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React, { useEffect } from 'react'

export default () => {
  const editor = useEditor({
    extensions: [
      StarterKit,
    ],
    content: `
      <p>
        This is an example of a Medium-like editor. Enter a new line and some buttons will appear.
      </p>
      <p></p>
    `,
  })

  const [isEditable, setIsEditable] = React.useState(true)

  useEffect(() => {
    if (editor) {
      editor.setEditable(isEditable)
    }
  }, [isEditable, editor])

  return (
    <>
      <div>
        <input type="checkbox" checked={isEditable} onChange={() => setIsEditable(!isEditable)} />
        Editable
      </div>
      {editor && <FloatingMenu editor={editor} tippyOptions={{ duration: 100 }}>
        <button
          onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
          className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
        >
          h1
        </button>
        <button
          onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
          className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
        >
          h2
        </button>
        <button
          onClick={() => editor.chain().focus().toggleBulletList().run()}
          className={editor.isActive('bulletList') ? 'is-active' : ''}
        >
          bullet list
        </button>
      </FloatingMenu>}
      <EditorContent editor={editor} />
    </>
  )
}
.ProseMirror {
  > * + * {
    margin-top: 0.75em;
  }
  ul,
  ol {
    padding: 0 1rem;
  }
}

下载教程 Demo

本教程Demo的源码点击这里下载,Tiptap Demo,下载完成后运行npm i初始化依赖包。