В один прекрасный момент мне понадобилось прикрутить WYSIWYG редактор в проект написанный на Nuxt 3. Очень быстро выяснилось что готовых решений полно, но, подавляющее большинство написано для Nuxt 2 и Vue 2, есть немало решений поддерживающих Vue 3, правда прикрутить их в Nuxt 3 это целый квест, о прохождении которого я хотел бы и рассказать.
Для начала вот список того что так или иначе рассматривал как потенциальных кандидатов:
CKEditor имеет официальную поддержку Vue 3
TipTap в меню слева можно увидеть "Nuxt.js", к сожалению это про Nuxt 2, но установка для Vue 3 полностью работает из коробки и для Nuxt 3. Правда это не то чтобы прям готовый редактор, а скорее заготовка для редакторов, тут надо будет и поверстать, и подобрать иконки для кнопок редактора. В общем как-то мне не подошло.
Element Tiptap это редактор основанный на element-ui, но он для Nuxt 2 (Vue 2), правда 2.0.0.1 alpha версия - это Tiptap 2 для Vue 3 на element-plus. Тут я обрадовался т.к. мой проект использует element-plus и вроде бы пазл сошелся, но не тут то было, пару часов танцев с бубном, нормально оживить пациента так и не удалось, жаль.
Vue SimpleMDE судя по гитхабу не особо-то и живой, не нашел нормальной демки, до экспериментов так и не дошло. Просто знайте, есть и такой.
TipTap Vuetify симпатичное решение, но не хотелось тащить еще и vuetify в проект, оставил про запас. Из коробки подходит для Nuxt 2, про Vue 3 информации в доке нет.
mavonEditor markdown редактор, отзывчивый, симпатичный, функциональный, обязательно его где-то задействую, в текущем проекте объяснять юзерам что такое markdown не представляется возможным.
Не буду рассказывать про все грабли на которые пришлось наступить, хочу сразу сделать что-то вроде руководства по CKEditor'у для Nuxt 3, т.к. найти какую-то общую статью по этому вопросу так и не удалось.
Добавим в проект
npm install --save @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic
Для подключения к проекту создадим плагин - plugins/editor.client.js
"client" в имени файла означает mode:"client" (еслиб мы подключали плагин через конфиг). Корень папки plugins сканируется и оттуда все подключается автоматически. Подробнее в доке.
import CKEditor from '@ckeditor/ckeditor5-vue';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(CKEditor)
})
mode:"client" нам нужно для того, чтобы SSR не включал в себя этот плагин, потому что плагин использует всякое вроде window.
о чьем существовании SSR не в курсе и мы будем получать ошибки "window is not defined"
Далее, если сделать в компоненте import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
то мы опять столкнемся с ошибками порождаемыми ssr, чтобы их обойти пойдем на хитрость, создаем компонент, например components/editor.vue
<template>
<div>
<ckeditor :editor="ClassicEditor" :config="editorConfig" v-model="editorHtml"></ckeditor>
<div> Content is: <div v-html="editorHtml"></div> </div>
</div>
</template>
<script setup>
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import '@ckeditor/ckeditor5-build-classic/build/translations/ru';
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const editorConfig = ref({
language: 'ru'
})
const editorHtml = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
</script>
Теперь, если мы хотим отрисовать ckeditor делаем это так:
<template>
<div>
<ClientOnly>
<Editor v-model="content" />
</ClientOnly>
<div>content is:{{ content }}</div>
</div>
</template>
<script setup>
import Editor from "~/components/editor"
const content = ref()
</script>
Тут вся магия в компоненте ClientOnly
, таким образом SSR не добирается до нашего компонента где вызывается ClassicEditor
который и вызывает ошибки. Теперь достаточно обновить конфиг примерно так:
const editorConfig = ref({
language: 'ru',
ckfinder: {
uploadUrl: '/api/file/upload'
}
})
и мы получаем полноценный редактор, но тут опять "НО", ckfinder не умеет добавить к запросу headers, а мне не хочется костылить эндпоинт без аутентификации для загрузки файлов. В такой эндпоинт вполне возможно кидать токен get параметром и проверять токен на бэке, но если нет, то переходим к сборке собственного билда или к необходимости ставить плагины, дока тут. Для решения проблемы нам понадобится плагин SimpleUploadAdapter, первое что делаем (бегло почитав доку) добавляем плагин в проект (npm install не делаем т.к. он в зависимостях у @ckeditor/ckeditor5-build-classic) , добавим в components/editor.vue
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload';
и получим ошибку ckeditor‑duplicated‑modules.
Дело в том, что этот плагин уже импортится в@ckeditor/ckeditor5-build-classic и когда мы используем не готовую сборку, а собираем свою, нам нужно использовать @ckeditor/ckeditor5-editor-classic т.е. не build, а editor. Для наглядности оставил одну кнопку чтобы не растягивать код, собственно новый components/editor.vue
<template>
<div>
<ckeditor :editor="ClassicEditor" :config="editorConfig" v-model="editorHtml"></ckeditor>
<div> Content is: <div v-html="editorHtml"></div> </div>
</div>
</template>
<script setup>
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload'
import ImagePlugin from '@ckeditor/ckeditor5-image/src/image'
import ImageCaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption'
import ImageStylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle'
import ImageToolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar'
import ImageUploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload'
import '@ckeditor/ckeditor5-build-classic/build/translations/ru';
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const editorConfig = ref({
language: 'ru',
plugins: [
SimpleUploadAdapter,
ImagePlugin,
ImageCaptionPlugin,
ImageToolbarPlugin,
ImageStylePlugin,
ImageUploadPlugin
],
toolbar: {
items: [
'imageUpload'
]
},
simpleUpload: {
uploadUrl: '/api/upload',
withCredentials: true,
headers: {
Authorization: 'Bearer <token>',
}
},
})
const editorHtml = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
</script>
после сборки Nuxt преподносит новый сюрприз,в консоле браузера будет что-то вроде
TypeError: Cannot read properties of null (reading 'getAttribute')
если дебажить дальше то придем к строчке
const viewBox = svg.getAttribute('viewBox')
google мало что про это подскажет, но подобное есть, например тут, потратив еще немного нервов понимаем что дело в vite, оказывается CKEditor об этом в курсе, подробнее тут. А решение такое, сначала делаем
npm install @ckeditor/vite-plugin-ckeditor5 @ckeditor/ckeditor5-theme-lark
потом идем в nuxt.config.ts импортим плагин vite , совсем пустой конфиг будет выглядеть так
import ckeditor5 from '@ckeditor/vite-plugin-ckeditor5'
export default defineNuxtConfig({
devtools: { enabled: true },
vite: {
plugins: [ckeditor5({ theme: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) })]
}
})
Вот собственно и все, теперь можно собирать свои build'ы, CKEditor довольно мощный инструмент, а для Nuxt 3 практически нет полноценных WYSIWYG редакторов. Потратил много времени и хотелось бы все это обобщить, оставить эту мини-инструкцию. Строго не судите :-)
P.S. бэк для загрузки изображений может возвращать
{
"url": "https://example.com/images/foo.jpg"
}
или
{
"urls": {
"default": "https://example.com/images/foo.jpg",
"800": "https://example.com/images/foo-800.jpg",
"1024": "https://example.com/images/foo-1024.jpg",
"1920": "https://example.com/images/foo-1920.jpg"
}
}
или
{
"error": {
"message": "The image upload failed because the image was too big (max 1.5MB)."
}
}
Подробнее Simple upload adapter
ArutaGerman
Ставил я, значится, для ckeditor и билд и не билд... результат один: ругается на уже подключенный модуль. А я всего лишь хотел иметь возможность подключать только нужные плагины, но пришлось отложить до "будет время - поковыряю еще"
levantez Автор
Если ругается на уже подключенный то это точно значит, что где-то подключен билд, скорее всего в плагине, т.к. в сети везде рекомендуют (зачем-то) билд так же в плагине подключать