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.