Recently in the development project used the rich text editor, in the use of the process of feeling can also, I want to write a share to the future may need a friend, if there is a mistake welcome to point out!
tiptapBased on theProseMirror, support table, picture, video, multi-person collaboration. Support for TS.
Tiptap is initialized without any CSS and needs to be developed by itself. It can be developed in any style you like
Install @tiptap/starter-kit, @tiptap/ vuE-3 dependencies
yarn add @tiptap/starter-kit @tiptap/vue-3
Copy the code
Create a Editor. Vue
1. template
<template>
<div class="editor" v-if="editor" :style="{width}">
<editor-content class="editor-content":editor="editor" />
</div>
</template>
Copy the code
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>
Copy the code
Effect of 3.
Now that the page has only one input box, we can start developing our own tools menu
Create menuitem. vue and 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.25 rem;
background-color: rgba(# 000.1);
margin-left:.5rem;
margin-right:.75rem;
}
</style>
Copy the code
Install the toolbar icon library to use in menuitem. vue
yarn add remixicon
Copy the code
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.75 rem;
height: 1.75 rem;
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>
Copy the code
Go back to editor. vue and introduce the toolbar we wrote
<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>
Copy the code
The effect
Since TIPTap doesn't have any CSS, all CSS needs to be defined by ourselves
Write CSS to make rich text look good
<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; }}/* Basic editor style */
.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 1auto; }}}}</style>
Copy the code
The final result
The official website documentation is very good, there are more API, you can go to learn to use.