логотип PurpleSchool
логотип PurpleSchool

Интеграция Tiptap для создания редакторов на Vue

Автор

Олег Марков

Введение

Когда возникает задача добавить богатый текстовый редактор в приложение на Vue, часто рассматривают TinyMCE, CKEditor или Quill. Однако, если вас интересует гибкость, современный подход и возможность тонко настраивать каждый аспект редактора под ваши нужды, стоит обратить внимание на Tiptap.

Tiptap — это headless-редактор, построенный на ProseMirror. Он хорош тем, что позволяет создавать редакторы с нуля, без жесткой привязки к определенному внешнему виду и без перегруженного интерфейса. Вы сами решаете, какие элементы управления нужны вашему пользователю, как они должны выглядеть и работать. Библиотека предоставляет полный контроль над структурой и внешним видом. А в связке с Vue вы сможете создавать интегрированные редакторы, которые легко встраиваются в ваши компоненты и проекты.

В этой статье вы найдете пошаговое руководство по интеграции Tiptap в Vue-проекты, изучите основные принципы настройки, научитесь подключать расширения, работать с содержимым редактора, обрабатывать события, а также узнаете об особенностях кастомизации и создании собственных функциональностей.

Установка Tiptap в проект Vue

О каких версиях пойдет речь

Мы будем рассматривать интеграцию Tiptap 2 с использованием Vue 3. Если у вас Vue 2, интеграция будет похожей, но некоторые детали могут отличаться. Рекомендую использовать свежие версии, чтобы получить все преимущества библиотеки.

Добавление зависимостей

Сначала нужно добавить Tiptap и необходимые зависимости в проект. Основные пакеты — это @tiptap/core и адаптер для фреймворка, то есть @tiptap/vue-3. Для базовых функций понадобятся еще расширения вроде StarterKit.

Выполните в терминале:

npm install @tiptap/vue-3 @tiptap/core @tiptap/starter-kit
  • @tiptap/vue-3 — адаптер для работы с Tiptap во Vue 3.
  • @tiptap/core — ядро редактора.
  • @tiptap/starter-kit — набор наиболее распространенных расширений (жирный, курсив, списки, и т.д.).

Если вы хотите добавить другие функции (например, поддержку изображений), позже можно установить дополнительные расширения.

Быстрый старт — простой редактор

Давайте сразу посмотрим минимальный пример компонента Vue с Tiptap редактором. Это даст базовое понимание структуры.

<script setup>
import { ref, onBeforeUnmount } from 'vue'
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { useEditor, EditorContent } from '@tiptap/vue-3'

// Инициализация редактора
const editor = useEditor({
  extensions: [StarterKit], // Добавляем базовые возможности
  content: '<p>Введите текст...</p>', // Начальный контент
})

// Очищаем редактор при удалении компонента
onBeforeUnmount(() => {
  editor.value?.destroy()
})
</script>

<template>
  <!-- Встроенный компонент для отображения редактора -->
  <EditorContent :editor="editor" />
</template>
  • Мы инициализируем редактор через useEditor.
  • StarterKit подключает базовые возможности, не надо перечислять их по отдельности.
  • <EditorContent> — специальный компонент, который исправно поддерживает связку Tiptap и Vue.

Этот код можно разместить в отдельном компоненте, например, TiptapEditor.vue, и сразу использовать в вашем приложении.

Конфигурация и расширение функциональности редактора

Что такое расширения и как их использовать

Расширения (extensions) — основа архитектуры Tiptap. Каждая функция (жирный, курсива, изображение, списки, упоминания) реализована как отдельное расширение, которые вы включаете или отключаете в зависимости от нужд.

StarterKit — набор самых стандартных возможностей, называемых "бедроком" (bold, italic, lists и др). Если StarterKit недостаточно, вы можете добавлять расширения как из сторонних пакетов, так и создавать свои.

Добавление и настройка расширений

Хотите добавить возможность вставлять изображения? Просто установите нужное расширение:

npm install @tiptap/extension-image

Теперь добавьте его к редактору:

import Image from '@tiptap/extension-image'

// В useEditor:
const editor = useEditor({
  extensions: [
    StarterKit,
    Image,
  ],
  content: '<p>Текст с возможностью вставлять изображения</p>',
})

Если требуется кастомизация, расширения принимают опции. Например — для Image можно задать разрешенные размеры или добавить пользовательское меню:

const editor = useEditor({
  extensions: [
    StarterKit,
    Image.configure({
      inline: false, // Картинки будут как блочные элементы
      allowBase64: true, // Разрешить base64 изображения
    }),
  ],
  // остальное как прежде
})

Создание собственной панели инструментов (toolbar)

Tiptap по умолчанию headless — это значит, что у него нет стандартной панели инструментов. Все элементы управления вы добавляете сами, используя методы редактора.

Рассмотрим пример создания простого тулбара со стилями Bold и Italic:

<template>
  <div>
    <div class="toolbar">
      <button
        @click="editor.chain().focus().toggleBold().run()"
        :class="{ 'active': editor.isActive('bold') }"
      >
        B
      </button>
      <button
        @click="editor.chain().focus().toggleItalic().run()"
        :class="{ 'active': editor.isActive('italic') }"
      >
        I
      </button>
    </div>
    <EditorContent :editor="editor" />
  </div>
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Привет, <strong>мир</strong>!</p>',
})
</script>

<style>
.toolbar {
  display: flex;
  gap: 10px;
  margin-bottom: 8px;
}
.active {
  background: #e3e3e3;
  font-weight: bold;
}
</style>
  • Используем @click для вызова команды, связанной с нужным стилем.
  • Функция chain().focus().toggleBold().run() активирует необходимую команду.
  • Через editor.isActive('bold') определяется, находится ли курсор в выделенном (жирном) тексте.

Обратите внимание, что команды Tiptap строятся цепочками, позволяя легко комбинировать действия.

Смена и сброс содержимого, работа с HTML

Иногда важно cчитать, изменять или сбрасывать содержимое редактора. Tiptap предоставляет несколько удобных методов для этого.

// Получить HTML
const html = editor.getHTML()

// Сменить содержимое на новое
editor.commands.setContent('<p>Новый текст</p>')

// Получить JSON представление (удобно для хранения и передачи)
const json = editor.getJSON()

Обычно хранить только HTML недостаточно — формат JSON позволяет реализовывать автосохранения, версионирование и прочие продвинутые функции.

Реакция на изменения в редакторе

Если нужно обновлять внешний источник данных при каждом изменении в редакторе (например, синхронизировать с Vuex или отправлять на сервер), используйте событие onUpdate:

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Ваш текст…</p>',
  onUpdate({ editor }) {
    // Это событие вызывается при каждом изменении текста
    const html = editor.getHTML()
    // Можно, например, передать html вашему родительскому компоненту или отправить на сервер
    console.log('HTML:', html)
  },
})

Пример интеграции с внешними моделями данных

Если вы хотите подключить Tiptap к реактивным данным (например, из props или v-model), настройте перенос данных самостоятельно.

Вот пример двусторонней связи с prop modelValue (аналогично v-model):

<script setup>
import { computed, watch } from 'vue'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const props = defineProps({
  modelValue: {
    type: String,
    required: true,
  },
})

const emit = defineEmits(['update:modelValue'])

const editor = useEditor({
  extensions: [StarterKit],
  content: props.modelValue,
  onUpdate({ editor }) {
    emit('update:modelValue', editor.getHTML())
  },
})

// Следим за внешним изменением props
watch(() => props.modelValue, newValue => {
  if (editor && editor.getHTML() !== newValue) {
    editor.commands.setContent(newValue)
  }
})
</script>

<template>
  <EditorContent :editor="editor" />
</template>
  • Событие onUpdate обновляет внешнюю модель.
  • Один watch следит за изменением данных извне и обновляет состояние редактора, чтобы не возникал рассинхрон.

Работа с расширениями Tiptap

Использование готовых расширений

Вы можете найти список расширений на сайте Tiptap (https://tiptap.dev/extensions/). Многие расширения реализованы как отдельные npm-пакеты. Некоторые популярные из них:

  • Table — поддержка таблиц
  • Mention — система упоминаний (@user)
  • Link — автоматическое распознавание и форматирование ссылок
  • Placeholder — отображение подсказок при пустом поле
  • Highlight — подсветка фрагментов текста

Установка происходит обычно так:

npm install @tiptap/extension-table

Далее расширение добавляется в useEditor:

import Table from '@tiptap/extension-table'

const editor = useEditor({
  extensions: [
    StarterKit,
    Table.configure({
      resizable: true, // позволяет менять размер колонок
    }),
  ],
  content: '<p>Таблицы теперь поддерживаются</p>',
})

Создание собственных расширений

Если вам нужно специальное поведение, которого нет в готовых расширениях, создайте свое. Пример — расширение для простого акцента:

import { Mark } from '@tiptap/core'

// Простейшее расширение Mark для выделения текста
const AccentMark = Mark.create({
  name: 'accent',
  parseHTML() {
    return [{ tag: 'span[data-accent]' }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['span', { ...HTMLAttributes, 'data-accent': 'true', style: 'background: yellow;' }, 0]
  },
})

// Подключаем его в редактор
const editor = useEditor({
  extensions: [StarterKit, AccentMark],
})

Теперь достаточно добавить кнопку, которая будет применять новый стиль:

<button @click="editor.chain().focus().toggleMark('accent').run()">Accent</button>

Этот прием позволит создавать любые свои форматы и миксы поведения — важное отличие Tiptap от большинства "коробочных" редакторов.

Стилизация и кастомизация интерфейса

Работа с темой редактора

Tiptap headless — это значит, что вся разметка и стилизация полностью на вашей стороне. Вы можете оформить редактор в стиле Bootstrap, Material или вообще сделать свой уникальный необработанный вид.

Оформить стандартные элементы (например, Title, панель инструментов) можно как обычные Vue компоненты. Сам EditorContent генерирует базовую структуру: теги p, strong, em, и так далее. Стилизуйте их как любые элементы. Например, добавьте CSS:

.ProseMirror {
  min-height: 200px;
  padding: 1rem;
  border: 1px solid #e2e2e2;
  border-radius: 6px;
  background: #fff;
  font-size: 1rem;
}
.ProseMirror:focus {
  outline: none;
  border-color: #1976d2;
}
  • Класс ProseMirror выставляет EditorContent сам.
  • Стилизация активных кнопок toolbar уже обсуждалась выше.

Если хочется больше анимаций и интерактива, подключайте любые css-фреймворки или библиотеки UI компонентов.

Ограничение формата и контроль структуры

Многие задачи (например, разрешить только определенные типы блоков) также решаются через настройки расширений:

StarterKit.configure({
  heading: {
    levels: [2, 3], // разрешаем только h2 и h3
  },
  bulletList: false, // отключаем списки
})

Подробности о параметрах можно найти в документации каждого расширения — многие из них поддерживают обширную конфигурацию.

Работа с изображениями, ссылками, инлайновыми объектами

Для изображений, ссылок, embeds (например, Youtube) существуют отдельные расширения, которые позволяют реализовать богатые сценарии вставки контента:

import Link from '@tiptap/extension-link'

const editor = useEditor({
  extensions: [
    StarterKit,
    Link.configure({
      openOnClick: true, // Открывать ссылку в новой вкладке при клике
      autolink: true,    // Преобразовывать обычные url в ссылки
    }),
  ],
})

Вся дополнительная логика (например, всплывающие меню или кастомные диалоги для вставки картинки) реализуется в Vue-компонентах как обычно.

Обработка и валидация данных редактора

Сценарии использования: сохранение, фильтрация XSS, валидация

  • editor.getHTML() — сохраняя HTML, используйте библиотеку sanitize-html или DOMPurify, если выводите его где-то еще, чтобы предотвратить XSS.
  • Для строгого контроля редактирования рекомендуется использовать только JSON-представление: это удобнее для автосохранений, рабочих процессов одобрения и контроля версий.
  • Если редактор доступен пользователям, расширения Tiptap обычно не позволяют вставить произвольный JS или iframes без вашего ведома, но все равно выполняйте финальную валидацию перед сохранением данных.

Лучшие практики интеграции Tiptap в Vue приложения

  • Используйте отдельные компоненты для тулбаров, EditorContent и расширений. Это повышает переиспользуемость.
  • Следите за освобождением ресурсов: не забывайте вызывать editor.destroy() при размонтировании компонента.
  • Встраивайте редактор в формы через v-model или emit, чтобы связать его с состоянием приложения.
  • Планируйте расширения заранее — подключайте только те, что нужны проекту, чтобы не раздувать bundle.
  • Интернационализация реализуется вручную, так как Tiptap headless — вы сами управляете всем UI, включая тексты.

Заключение

Tiptap в связке с Vue — это гибкий инструмент для создания современных богатых редакторов с любым поведением и дизайном. Библиотека строится вокруг модульной архитектуры расширений, которые легко комбинировать и настраивать. Благодаря headless-подходу вы не привязаны к "коробочным" решениям и можете реализовать абсолютно любой UX, включая интеграцию с внешними данными, кастомными тулбарами, динамическими расширениями и сложной структурой контента.

Вы изучили, как быстро добавить Tiptap в проект на Vue, расширять его функциональность, интегрировать с моделями приложения, а также создавать свои расширения и интерфейсы управления. Этот подход универсален: его можно адаптировать под ввод простых заметок, контент-менеджмент, корпоративные приложения и даже сложные онлайн-редакторы.


Частозадаваемые технические вопросы по теме статьи и ответы на них

Как реализовать drag-n-drop изображений в редакторе Tiptap?

Tiptap поддерживает drag-n-drop картинок через расширение Image и стандартные события браузера. Нужно дополнительно реализовать обработку события drop и вставку base64 или blob-URLs в редактор. Установите расширение Image и добавьте свой обработчик:

// В компоненте редактора
onMounted(() => {
  const dom = editor.value.view.dom
  dom.addEventListener('drop', async event => {
    const files = event.dataTransfer.files
    if (files.length) {
      // Прочитать изображение и вставить в редактор
      const reader = new FileReader()
      reader.onload = e => {
        editor.value.chain().focus().setImage({ src: e.target.result }).run()
      }
      reader.readAsDataURL(files[0])
    }
  })
})

Как интегрировать Tiptap в Nuxt?

Для Nuxt 3 интеграция аналогична обычному Vue 3. Вам нужно обернуть инициализацию редактора в onMounted или использовать client-only, если SSR не используется. Не забывайте про destroy() при демонтировании компонента. Вот как это может выглядеть:

<template>
  <ClientOnly>
    <EditorContent :editor="editor" />
  </ClientOnly>
</template>

Можно ли использовать v-model c EditorContent напрямую?

Нет, EditorContent не реализует v-model. Вам нужно связать содержимое через onUpdate и emit (пример с modelValue был в статье). Используйте кастомную синхронизацию модели Vue с содержимым редактора.

Как ограничить максимальную длину или количество блоков в редакторе?

Tiptap сам по себе не ограничивает ввод. Можно добавить кастомное расширение или слушатель событий input, который будет проверять длину содержимого (editor.getText()). Если превышен лимит, отменять введенный текст через transaction.setMeta.

Как реализовать автофокус редактора при появлении?

Используйте метод editor.commands.focus() в onMounted или после появления редактора в DOM. Например:

onMounted(() => {
  editor.value.commands.focus()
})

Это обеспечит курсор внутри редактора после его отображения.

Стрелочка влевоРуководство по валидации форм во Vue.jsРабота с таблицами во Vue через TanStackСтрелочка вправо

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Управление переменными и реактивными свойствами во VueИспользование v for и slot в VueПрименение v-bind для динамической привязки атрибутов в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueИспользование transition во VueТестирование компонентов и приложений на VueРабота с teleport для управления DOM во VueПять шагов по настройке SSR в VuejsИспользование router-link для навигации в Vue RouterРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовКак использовать require в Vue для динамического импорта модулейИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиРуководство по nextTick для работы с DOMСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний