Tiptap Floating Menu 浮动菜单扩展,当新启一个空行时可以使用浮动菜单悬浮在这行上面,你可以在这个菜单上面放置需要的功能。
npm install @tiptap/extension-floating-menu
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')
},
})
import { Editor } from '@tiptap/core'
import FloatingMenu from '@tiptap/extension-floating-menu'
new Editor({
extensions: [
FloatingMenu.configure({
element: document.querySelector('.menu'),
}),
],
})
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;
}
}