Home

Awesome

Vuetify Pro Tiptap

A WYSIWYG rich-text editor using tiptap and vuetify for Vue.js

download version gzip Test LICENSE semantic-release: vue

English | 中文

Demo

👉https://yikoyu.github.io/vuetify-pro-tiptap/

👾Stackblitz

👾Nuxt3 Stackblitz

Features

Vuetify 2.x

For Vuetify 2.x please use the latest version of vuetify-pro-tiptap@1.X.X

Installation

NPM

pnpm add vuetify-pro-tiptap
# or
yarn add vuetify-pro-tiptap
# or
npm i vuetify-pro-tiptap -S

Install plugin

<details> <summary>tiptap.ts</summary>
import { markRaw } from 'vue'
import { VuetifyTiptap, VuetifyViewer, createVuetifyProTipTap } from 'vuetify-pro-tiptap'
import { BaseKit, Bold, Italic, Underline, Strike, Color, Highlight, Heading, TextAlign, FontFamily, FontSize, SubAndSuperScript, BulletList, OrderedList, TaskList, Indent, Link, Image, Video, Table, Blockquote, HorizontalRule, Code, CodeBlock, Clear, Fullscreen, History } from 'vuetify-pro-tiptap'
import 'vuetify-pro-tiptap/style.css'
import SelectImage from './components/SelectImage.vue'

export const vuetifyProTipTap = createVuetifyProTipTap({
  lang: 'zhHans',
  components: {
    VuetifyTiptap,
    VuetifyViewer
  },
  extensions: [
    BaseKit.configure({
      placeholder: {
        placeholder: 'Enter some text...'
      }
    }),
    Bold,
    Italic,
    Underline,
    Strike,
    Code.configure({ divider: true }),
    Heading,
    TextAlign,
    FontFamily,
    FontSize,
    Color,
    Highlight.configure({ divider: true }),
    SubAndSuperScript.configure({ divider: true }),
    Clear.configure({ divider: true }),
    BulletList,
    OrderedList,
    TaskList,
    Indent.configure({ divider: true }),
    Link,
    Image.configure({
      imageTabs: [{ name: 'SELECT', component: markRaw(SelectImage) }],
      // hiddenTabs: ['upload'],
      upload(file: File) {
        const url = URL.createObjectURL(file)
        console.log('mock upload api :>> ', url)
        return Promise.resolve(url)
      }
    }),
    Video,
    Table.configure({ divider: true }),
    Blockquote,
    HorizontalRule,
    CodeBlock.configure({ divider: true }),
    History.configure({ divider: true }),
    Fullscreen
  ]
})
</details> <details> <summary>main.ts</summary>
import { createApp } from 'vue'
import { createVuetify } from 'vuetify'
import 'vuetify/styles'
import App from './App.vue'

import { vuetifyProTipTap } from './tiptap'

const vuetify = createVuetify()

const app = createApp(App)
app.use(vuetify)
app.use(vuetifyProTipTap)

// fix warning injected property "decorationClasses" is a ref and will be auto-unwrapped
// https://github.com/ueberdosis/tiptap/issues/1719
app.config.unwrapInjectedRef = true

app.mount('#app')
</details>

Global Settings

import { markRaw } from 'vue'
import { VuetifyTiptap, VuetifyViewer, createVuetifyProTipTap, defaultBubbleList } from 'vuetify-pro-tiptap'
import { BaseKit, Image, Fullscreen } from 'vuetify-pro-tiptap'
import 'vuetify-pro-tiptap/style.css'
import SelectImage from './components/SelectImage.vue'

export const vuetifyProTipTap = createVuetifyProTipTap({
  // Set default lang
  lang: 'zhHans',
  // Set markdown theme
  markdownTheme: 'github',
  // Global registration app.component
  components: {
    VuetifyTiptap,
    VuetifyViewer
  },
  // Global registration extensions
  extensions: [
    BaseKit.configure({
      placeholder: {
        placeholder: 'Enter some text...'
      },
      bubble: {
        // default config
        list: {
          image: [ 'float-left', 'float-none', 'float-right', 'divider', 'size-small', 'size-medium', 'size-large', 'divider', 'textAlign', 'divider', 'image', 'image-aspect-ratio', 'remove'],
          text: ['bold', 'italic', 'underline', 'strike', 'divider', 'color', 'highlight', 'textAlign', 'divider', 'link'],
          video: ['video', 'remove']
        },
        defaultBubbleList: editor => {
          // You can customize the bubble menu here
          return defaultBubbleList(editor) // default customize bubble list
        }
      }
    }),
    Image.configure({
      // Generate a VDivider after the button
      divider: true,
      // Custom image tabs
      imageTabs: [{ name: 'SELECT', component: markRaw(SelectImage) }],
      // hidden default tab
      hiddenTabs: ['upload'],
      // custom upload function
      upload(file) {
        const url = URL.createObjectURL(file)
        console.log('mock upload api :>> ', url)
        return Promise.resolve(url)
      }
    }),
    Fullscreen.configure({
      // Generate a VSpacer after the button
      spacer: true
    })
  ]
})

<span id="extensions">Extensions</span>

You can use the necessary extensions. The corresponding command-buttons will be added by declaring the order of the extension.

All available extensions:

Custom theme

Create github.scss

$value: 'github';

.vuetify-pro-tiptap-editor__content.markdown-theme-#{$value} {
  // your custom styles
  &.__dark {
    // your dark mode custom styles
  }
}

Import github.scss in ts

// import 'vuetify-pro-tiptap/style.css' // import all(editor and markdown) styles
import 'vuetify-pro-tiptap/styles/editor.css' // only use editor style, not using markdown style
import './styles/markdown/github.scss'

In the component using

<template>
  <VuetifyTiptap v-model="content" markdown-theme="github" />
  <VuetifyViewer :value="content" markdown-theme="github" />
</template>

Custom extensions

<details> <summary>PreviewActionButton.vue</summary>
<script setup lang="ts">
import { ref } from 'vue'
import { mdiClose, mdiFileCodeOutline } from '@mdi/js'
import type { Editor } from '@tiptap/vue-3'
import { ActionButton } from 'vuetify-pro-tiptap'

interface Props {
  editor: Editor
  tooltip?: string
  disabled?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  tooltip: undefined,
  disabled: false
})

const dialog = ref(false)
const maxWidth = ref<number>(900)
</script>

<template>
  <ActionButton tooltip="全屏" :disabled="disabled">
    <VIcon>{{ `svg:${mdiFileCodeOutline}` }}</VIcon>
    <VDialog v-model="dialog" fullscreen hide-overlay activator="parent">
      <VCard>
        <VToolbar dark color="primary">
          <VBtn icon dark @click="dialog = false">
            <VIcon>{{ `svg:${mdiClose}` }}</VIcon>
          </VBtn>
        </VToolbar>

        <VContainer>
          <VSheet class="mx-auto" :max-width="maxWidth">
            <VuetifyViewer :value="editor.getHTML()" />
          </VSheet>
        </VContainer>
      </VCard>
    </VDialog>
  </ActionButton>
</template>
</details> <details> <summary>preview.ts</summary>
import { Extension } from '@tiptap/core'

import type { ButtonView, GeneralOptions } from 'vuetify-pro-tiptap'
import PreviewActionButton from '../components/PreviewActionButton.vue'

export interface PreviewOptions extends GeneralOptions {
  button: ButtonView
}

export default Extension.create<PreviewOptions>({
  name: 'preview',
  addOptions() {
    return {
      divider: false,
      spacer: false,
      button: () => ({
        component: PreviewActionButton,
        componentProps: {}
      })
    }
  }
})
</details>

I18n

Setting language

You can declare when you install the plugin.

import { createVuetifyProTipTap } from 'vuetify-pro-tiptap'

const VuetifyProTipTap = createVuetifyProTipTap({
  lang: 'zhHans'
})

Or use setLang dynamic change

import { locale } from 'vuetify-pro-tiptap'

locale.setLang('en')

Available languages:

Use unavailable language

Loading unavailable language, use setMessage for Settings

import { locale } from 'vuetify-pro-tiptap'

locale.setMessage('zhHant', {
  // i18n text
})
locale.setLang('zhHant')

Usage

<script setup lang="ts">
import { ref } from 'vue'
import { BaseKit, Bold, Color, Fullscreen, Heading, Highlight, History, Image, Italic, Link, Strike, Table, Underline, Video, VuetifyTiptap, VuetifyViewer } from 'vuetify-pro-tiptap'
import 'vuetify-pro-tiptap/style.css'

const extensions = [
  BaseKit.configure({
    placeholder: {
      placeholder: 'Enter some text...'
    }
  }),
  Bold,
  Italic,
  Underline,
  Strike,
  Color,
  Highlight,
  Heading,
  Link,
  Image,
  Video,
  Table,
  Fullscreen,
  History
]

const content = ref('')
</script>

<template>
  <VApp id="app">
    <VContainer>
      <VuetifyTiptap v-model="content" label="Title" rounded :min-height="200" :max-height="465" :max-width="900" :extensions="extensions" />
      <VuetifyViewer :value="content" />
    </VContainer>
  </VApp>
</template>

Props

VuetifyTiptap

Props

NameTypeDefaultDescription
modelValuestring | JSONContent''The input’s value
markdownThemestring | false'default'Markdown theme
output'html' | 'json' | 'text''html'Output format
darkbooleanfalseApplies the dark theme variant to the component.
densebooleanfalseReduces the input height
outlinedbooleantrueApplies the outlined style to the input
flatbooleantrueRemoves the card’s elevation
disabledbooleanfalseDisable the input
labelstringundefinedSets input label
hideToolbarbooleanfalseHidden the toolbar
disableToolbarbooleanfalseDisable the toolbar
hideBubblebooleanfalseHidden the bubble menu
removeDefaultWrapperbooleanfalseDefault wrapper when the delete editor is empty
maxWidthstring | numberundefinedSets the maximum width for the component.
minHeightstring | numberundefinedSets the minimum height for the component.
maxHeightstring | numberundefinedSets the maximum height for the component.
extensionsAnyExtension[][]Tiptap the extensions
editorClassstring | string[] | Record<string, any>undefinedEditor class

Slots

NameDescription
editorSlot to customize editor
bottomSlot to customize editor bottom

Event

NameTypeDescription
update:modelValuestring | JSONContentEmitted when editor onUpdate
update:markdownThemestringEmitted when change theme
change{ editor: Editor, output: string | JSONContent }Emitted when editor onUpdate
enterKeyboard enter return

VuetifyViewer

Props

NameTypeDefaultDescription
valuestring | JSONContent''The preview’s value
darkbooleanfalseApplies the dark theme variant to the component.
densebooleanfalseReduces the input height
markdownThemestring | false'default'Markdown theme
xssbooleantrueEnable xss filter
xssOptionsxss.IWhiteListDefault ruleXss filter rule config
maxWidthstring | numberundefinedSets the maximum width for the component.
extensionsAnyExtension[][]Tiptap the extensions

Slots

NameDescription
beforeAdd content at the before
afterAdd content at the after

🏗 Contributing

  1. 🍴Fork it
  2. 🔀Create your branch: git checkout -b your-branch
  3. 🎨Make your changes
  4. 📝Commit your changes with Semantic Commit Messages (recommended)
  5. 🚀Push to the branch: git push origin your-branch
  6. 🎉Submit a PR to develop branch

📄 License

MIT

Thanks