Rich text editor for Vue using Tiptap and Vuetify
Tags: vue, vuetify, tiptap
Published: 2019-10-06 18:46:22

Recently, I wanted to add a rich text editor in one of my pet project. I dabbled with CKEditor for a while but I found it a bit difficult to adopt in Vue (I'm fairly new to front end development)
Other option was Tiptap, It is a renderless editor based on Prosemirror. It was easy to follow, I was able to create a basic editor without any style quickly.

This is an overview of how to use Vuetify for styling of Tiptap editor

## Setup
Assuming you alreaday have a Vuetify project-
```
$ npm add tiptap tiptap-extensions 
```

## Code Overview
Key concepts before going into the code [(from docs)][1]
### Editor class
This class is a central building block of tiptap. It does most of the heavy lifting of creating a working ProseMirror editor such as creating the EditorView, setting the initial EditorState and so on. The Editor constructor accepts an object of editor options.

### EditorContent
This is like an container component which accepts Editor instance as a property. 

### Extensions
Each editor feature like Headings, Bold, Italics, Images etc. are implemented as extensions. We need to pass instance of each extension in the editor option for each of the feature we want in our editor.

### EditorMenuBar
![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/z3q5lq1aai2vqoskgnlt.PNG)

This component holds all the toolbar buttons. The action is performed through commands eg. commands.bold, commands.image, which can be linked to click event of any button.

With this background we can dive into code - 

Add Editor instance with Heading, Bold, Underline and Image extensions.
```javascript
data() {
    return {
      editor: new Editor({
        content: `Type here...
        `,
        extensions:[
            new Heading({levels: [1,2,3]}),
            new Bold(),
            new Underline(),
            new Image(),
        ]
      })
    }
  },

```
Pass editor as property to editor-content component
```html
 <editor-content class="editor-box" :editor="editor"/>
```

Create Editor's menubar. It uses slots which I don't fully understand at this point, But all we need to understand is that commands are the actions that we wish to perform like making something Bold, inserting image and isActive is used to check if current line or current selection has the Bold/Italic or not.


```html
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
            <div>
                <v-btn text icon
                :class="{ 'is-active': isActive.bold() }"
                @click="commands.bold"
                >
                    <v-icon>mdi-format-bold</v-icon>
                </v-btn>
            </div>
</editor-menu-bar>
```
Here's the full code of the view.
```javascript
<template>
  <v-container>
      <v-row>
        <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
            <div >
                <v-btn text icon
                :class="{ 'is-active': isActive.heading({ level: 1 }) }"
                @click="commands.heading({level: 1})"
                >
                   <b> H1 </b>
                </v-btn>
                <v-btn text icon
                :class="{ 'is-active': isActive.bold() }"
                @click="commands.bold"
                >
                    <v-icon>mdi-format-bold</v-icon>
                </v-btn>

                <v-btn text icon
                :class="{ 'is-active': isActive.underline() }"
                @click="commands.underline"
                >
                    <v-icon>mdi-format-underline</v-icon>
                </v-btn>
               
               <v-btn text icon
               @click="loadImage(commands.image)">
                   <v-icon>mdi-image</v-icon>
               </v-btn>
            </div>            
        </editor-menu-bar>
    </v-row>
    <v-row>
        <v-col cols=12 >
            <editor-content class="editor-box" :editor="editor"/>
        </v-col>
    </v-row>
  </v-container>
</template>


<script>
import { Editor, EditorContent, EditorMenuBar  } from 'tiptap';
import { Heading, 
        Bold, 
        Underline,
        Image } from 'tiptap-extensions';
export default {
components: {
    EditorContent,
    EditorMenuBar,
  },
  data() {
    return {
      editor: new Editor({
        content: `Type here...
        `,
        extensions:[
            new Heading({levels: [1,2,3]}),
            new Bold(),
            new Underline(),
            new Image(),
        ]
      })
    }
  },
  methods:{
      loadImage:function(command){
          command({src: "https://66.media.tumblr.com/dcd3d24b79d78a3ee0f9192246e727f1/tumblr_o00xgqMhPM1qak053o1_400.gif"})
      }
  },
  beforeDestroy() {
    this.editor.destroy()
  },
};
</script>
<style >
.editor-box> * {
    border-color: grey;
    border-style: solid;
    border-width: 1px;
}

.is-active{
    border-color: grey;
    border-style: solid;
    border-width: 1px;
}
 /* *:focus {
    outline: none;
}  */
</style>
```

Here's how it looks ultimately-

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/1ktqe64t9grtyv4nnu0b.PNG)

Hope this helps

[1]: https://tiptap.scrumpy.io/docs