Skip to content

Editor

Overview

The v-bsb-editor component is a flexible and customizable rich text editor toolbar that utilizes the @tiptap/vue-3 library. It includes various formatting options like bold, italic, underline, strike-through, bullet lists, ordered lists, and headings. The component provides a clean interface for managing editor content and emits changes through a Vue event.

This component is designed to be integrated with the Tiptap editor and supports dynamic toolbar options. It allows developers to control which buttons are available on the toolbar and responds to user interactions with formatting options.

Usage Examples

Basic Example

vue
<template>
  <EditorToolbar :toolbar="['bold', 'italic', 'underline']" @updated="onUpdate" />
</template>

<script setup>
import EditorToolbar from './EditorToolbar.vue'

const onUpdate = (content) => {
  console.log('Updated content:', content)
}
</script>

Full Example with All Options

vue
<template>
  <EditorToolbar
    :toolbar="[
      'bold',
      'italic',
      'underline',
      'strike',
      'bulletList',
      'orderedList',
      'heading1',
      'heading2',
      'heading3',
    ]"
    @updated="onUpdate"
  />
</template>

<script setup>
import EditorToolbar from './EditorToolbar.vue'

const onUpdate = (content) => {
  console.log('Updated content:', content)
}
</script>

Styling the Toolbar

vue
<template>
  <EditorToolbar
    :toolbar="['bold', 'italic', 'underline']"
    toolbarClass="custom-toolbar-class"
    @updated="onUpdate"
  />
</template>

<style scoped>
.custom-toolbar-class {
  background-color: #f5f5f5;
  padding: 10px;
  border-radius: 5px;
}
</style>

API

Props

NameTypeDefaultDescription
toolbarArray<String>[]A list of toolbar buttons to display. Supported values are bold, italic, underline, strike, bulletList, orderedList, heading1, heading2, heading3.
toolbarClassString''A class name to apply custom styling to the toolbar container.

Emits

Event NameParametersDescription
updatedcontent: StringEmitted when the editor's content is updated, passing the updated HTML content.

Source

  1. Install Dependencies
ps
npm install @tiptap/vue-3
npm install @tiptap/pm
npm install @tiptap/starter-kit
npm install @tiptap/extension-underline
  1. Create component and unit test.
Component @/components/VBsbEditor.vue
vue
<template>
  <div class="editor-toolbar" :class="props.toolbarClass">
    <v-btn
      v-if="props.toolbar.includes('bold')"
      :variant="editor?.isActive('bold') ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleBold().run()"
      icon="$mdiFormatBold"
    />
    <v-btn
      v-if="props.toolbar.includes('italic')"
      :variant="editor?.isActive('italic') ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleItalic().run()"
      icon="$mdiFormatItalic"
    />
    <v-btn
      v-if="props.toolbar.includes('underline')"
      :variant="editor?.isActive('underline') ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleUnderline().run()"
      icon="$mdiFormatUnderline"
    />
    <v-btn
      v-if="props.toolbar.includes('strike')"
      :variant="editor?.isActive('strike') ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleStrike().run()"
      icon="$mdiFormatStrikethrough"
    />
    <v-btn
      v-if="props.toolbar.includes('bulletList')"
      :variant="editor?.isActive('bulletList') ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleBulletList().run()"
      icon="$mdiFormatListBulleted"
    />
    <v-btn
      v-if="props.toolbar.includes('orderedList')"
      :variant="editor?.isActive('orderedList') ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleOrderedList().run()"
      icon="$mdiFormatListNumbered"
    />
    <v-btn
      v-if="props.toolbar.includes('heading1')"
      :variant="editor?.isActive('heading', { level: 1 }) ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleHeading({ level: 1 }).run()"
      icon="$mdiFormatHeader1"
    />
    <v-btn
      v-if="props.toolbar.includes('heading2')"
      :variant="editor?.isActive('heading', { level: 2 }) ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleHeading({ level: 2 }).run()"
      icon="$mdiFormatHeader2"
    />
    <v-btn
      v-if="props.toolbar.includes('heading3')"
      :variant="editor?.isActive('heading', { level: 3 }) ? 'outlined' : 'text'"
      density="comfortable"
      @click="editor?.chain().focus().toggleHeading({ level: 3 }).run()"
      icon="$mdiFormatHeader3"
    />
  </div>
  <editor-content :editor="editor" />
</template>

<script setup lang="ts">
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'

const model = defineModel({ type: String, default: '' })

const props = defineProps({
  toolbar: {
    type: Array<string>,
    default: () => [],
  },
  toolbarClass: {
    type: String,
    default: '',
  },
})

const emits = defineEmits(['updated'])

const editor = useEditor({
  content: model.value,
  extensions: [StarterKit, Underline],
  onUpdate: ({ editor }) => {
    model.value = editor.getHTML()
    emits('updated', model.value)
  },
  onSelectionUpdate: ({ editor }) => {
    if (model.value !== editor.getHTML()) {
      model.value = editor.getHTML()
      emits('updated', model.value)
    }
  },
})

onBeforeUnmount(() => {
  if (editor) {
    editor.value?.destroy()
  }
})

defineExpose({
  editor,
})
</script>

<style>
.tiptap ol,
.tiptap ul {
  padding-left: 2em;
}
</style>
Test @/components/__tests__/VBsbEditor.test.ts
ts
import { describe, it, expect, beforeEach } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import VBsbEditor from '../../components/VBsbEditor.vue'

describe('VBsbEditor.vue', () => {
  let wrapper: VueWrapper

  beforeEach(() => {
    wrapper = mount(VBsbEditor, {
      props: {
        toolbar: ['bold', 'italic', 'underline'],
        toolbarClass: 'test-toolbar-class',
      },
    })
  })
  it('renders the toolbar buttons based on the "toolbar" prop', () => {
    const buttons = wrapper.findAllComponents({ name: 'v-btn' })
    expect(buttons.length).toBe(3) // bold, italic, underline
  })

  it('applies the toolbarClass to the toolbar container', () => {
    const toolbarDiv = wrapper.find('.editor-toolbar')
    expect(toolbarDiv.classes()).toContain('test-toolbar-class')
  })
})