最近在开发项目中用到了富文本编辑器,在使用的过程中感觉还可以,就想写篇分享给以后可能需要朋友,如果有错欢迎大家指出!
tiptap基于ProseMirror,支持表格、图片、视频、多人协同。支持TS。
tiptap初始化以后是没有任何css的,都需要自己开发,可以完全按照自己喜欢的样式来开发
安装@tiptap/starter-kit,@tiptap/vue-3依赖
yarn add @tiptap/starter-kit @tiptap/vue-3
创建Editor.vue
1. template
<template>
<div class="editor" v-if="editor" :style="{width}">
<editor-content class="editor-content":editor="editor" />
</div>
</template>
2.script
<script>
import { defineComponent } from 'vue'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
export default defineComponent({
components: {
EditorContent
},
props: {
width: {
type: String,
default: '800px'
}
},
setup() {
const editor = useEditor({
content: '<p>I’m running Tiptap with vue-next. 🎉</p>',
extensions: [
StarterKit
]
})
return {
editor
}
}
})
</script>
3.效果
现在的页面只有一个输入框,我们可以开始开发自己的工具菜单
1.创建MenuItem.vue和MenuBar.vue
MenuItem.vue
<template>
<div>
<template v-for="(item, index) in items">
<div class="divider" v-if="item.type === 'divider'" :key="`divider${index}`" />
<menu-item v-else :key="index" v-bind="item" />
</template>
</div>
</template>
<script>
import MenuItem from './MenuItem.vue'
export default {
components: {
MenuItem
},
props: {
editor: {
type: Object,
required: true
}
},
setup(props){
const items = reactive([
{
icon: 'bold',
title: 'Bold',
action: () => props.editor.chain().focus().toggleBold().run(),
isActive: () => props.editor.isActive('bold')
},
{
icon: 'italic',
title: 'Italic',
action: () => props.editor.chain().focus().toggleItalic().run(),
isActive: () => props.editor.isActive('italic')
},
{
icon: 'strikethrough',
title: 'Strike',
action: () => props.editor.chain().focus().toggleStrike().run(),
isActive: () => props.editor.isActive('strike')
},
{
icon: 'code-view',
title: 'Code',
action: () => props.editor.chain().focus().toggleCode().run(),
isActive: () => props.editor.isActive('code')
},
{
icon: 'mark-pen-line',
title: 'Highlight',
action: () => props.editor.chain().focus().toggleHighlight().run(),
isActive: () => props.editor.isActive('highlight')
},
{
type: 'divider'
},
{
icon: 'h-1',
title: 'Heading 1',
action: () => props.editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => props.editor.isActive('heading', { level: 1 })
},
{
icon: 'h-2',
title: 'Heading 2',
action: () => props.editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => props.editor.isActive('heading', { level: 2 })
},
{
icon: 'paragraph',
title: 'Paragraph',
action: () => props.editor.chain().focus().setParagraph().run(),
isActive: () => props.editor.isActive('paragraph')
},
{
icon: 'list-unordered',
title: 'Bullet List',
action: () => props.editor.chain().focus().toggleBulletList().run(),
isActive: () => props.editor.isActive('bulletList')
},
{
icon: 'list-ordered',
title: 'Ordered List',
action: () => props.editor.chain().focus().toggleOrderedList().run(),
isActive: () => props.editor.isActive('orderedList')
},
{
icon: 'list-check-2',
title: 'Task List',
action: () => props.editor.chain().focus().toggleTaskList().run(),
isActive: () => props.editor.isActive('taskList')
},
{
icon: 'code-box-line',
title: 'Code Block',
action: () => props.editor.chain().focus().toggleCodeBlock().run(),
isActive: () => props.editor.isActive('codeBlock')
},
{
type: 'divider'
},
{
icon: 'double-quotes-l',
title: 'Blockquote',
action: () => props.editor.chain().focus().toggleBlockquote().run(),
isActive: () => props.editor.isActive('blockquote')
},
{
icon: 'separator',
title: 'Horizontal Rule',
action: () => props.editor.chain().focus().setHorizontalRule().run()
},
{
type: 'divider'
},
{
icon: 'text-wrap',
title: 'Hard Break',
action: () => props.editor.chain().focus().setHardBreak().run()
},
{
icon: 'format-clear',
title: 'Clear Format',
action: () => props.editor.chain()
.focus()
.clearNodes()
.unsetAllMarks()
.run()
},
{
type: 'divider'
},
{
icon: 'arrow-go-back-line',
title: 'Undo',
action: () => props.editor.chain().focus().undo().run()
},
{
icon: 'arrow-go-forward-line',
title: 'Redo',
action: () => props.editor.chain().focus().redo().run()
}
])
return {
items
}
},
}
</script>
<style lang="scss">
.divider {
width: 2px;
height: 1.25rem;
background-color: rgba(#000, .1);
margin-left: .5rem;
margin-right: .75rem;
}
</style>
安装工具栏图标库,在MenuItem.vue中使用
yarn add remixicon
MenuItem.vue
<template>
<button
class="menu-item"
:class="{ 'is-active': isActive ? isActive(): null }"
@click="action"
:title="title"
>
<svg class="remix">
<use :xlink:href="`${iconUrl}#ri-${icon}`" />
</svg>
</button>
</template>
<script>
import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg'
export default {
props: {
icon: {
type: String,
required: true
},
title: {
type: String,
required: true
},
action: {
type: Function,
required: true
},
isActive: {
type: Function,
default: null
}
},
setup(){
const iconUrl = ref(remixiconUrl)
return {
iconUrl
}
}
}
</script>
<style lang="scss">
.menu-item {
width: 1.75rem;
height: 1.75rem;
color: #0d0d0d;
border: none;
background-color: transparent;
border-radius: .4rem;
padding: .25rem;
margin-right: .25rem;
svg {
width: 100%;
height: 100%;
fill: currentColor;
}
&.is-active,
&:hover {
color: #fff;
background-color: #0d0d0d;
}
}
</style>
回到刚刚Editor.vue,引入我们写好的工具栏
<template>
<div class="editor" v-if="editor" :style="{width}">
<MenuBar class="editor-header" :editor="editor" />
<editor-content class="editor-content" :editor="editor" />
</div>
</template>
<script>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { defineComponent } from 'vue'
import MenuBar from './MenuBar.vue'
export default defineComponent({
components: {
EditorContent,
MenuBar
},
props: {
width: {
type: String,
default: '800px'
}
},
setup() {
const editor = useEditor({
content: '<p>I’m running Tiptap with vue-next. 🎉</p>',
extensions: [
StarterKit
]
})
return {
editor
}
}
})
</script>
效果
因为tiptap没有任何css所以所有css都需要我们自己定义
写一套css让富文本好看些
<style lang="scss">
.editor {
display: flex;
flex-direction: column;
max-height: 26rem;
color: #0d0d0d;
background-color: #fff;
border: 3px solid #0d0d0d;
border-radius: .75rem;
&-header {
display: flex;
align-items: center;
flex: 0 0 auto;
flex-wrap: wrap;
padding: .25rem;
border-bottom: 3px solid #0d0d0d;
}
&-content {
padding: .7rem .5rem;
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}
/* 基本编辑器样式 */
.ProseMirror {
height: 100%;
&:focus {
outline: none;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, .1);
color: #616161;
}
pre {
background: #0d0d0d;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding: .75rem 1rem;
border-radius: .5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: .8rem;
}
}
mark {
background-color: #faf594;
}
img {
max-width: 100%;
height: auto;
}
hr {
margin: 1rem 0;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0d0d0d, .1);
}
hr {
border: none;
border-top: 2px solid rgba(#0d0d0d, .1);
margin: 2rem 0;
}
ul[data-type="taskList"] {
list-style: none;
padding: 0;
li {
display: flex;
align-items: center;
> label {
flex: 0 0 auto;
margin-right: .5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
}
}
</style>
最终效果
官网文档写的很好,还有更多的api,大家可以自己去学习使用。