Nuxt3, Vue3, CKEditor and other WYSIWYG

One fine moment I needed to attach a WYSIWYG editor to a project written in Nuxt 3. It quickly became clear that there are a lot of ready-made solutions, but the vast majority are written for Nuxt 2 and Vue 2, there are many solutions that support Vue 3, but attaching them to Nuxt 3 is a whole quest, the passage of which I would like to tell you about.

To begin with, here is a list of what I considered as potential candidates one way or another:

  • CKEditor has official support for Vue 3

  • TipTap in the menu on the left you can see “Nuxt.js”, unfortunately this is about Nuxt 2, but the installation for Vue 3 completely works out of the box for Nuxt 3. True, this is not exactly a ready-made editor, but rather a template for editors, here you will need and layout and select icons for the editor buttons. In general, somehow it didn’t suit me.

  • Element Tiptap This is an editor based on element-ui, but it is for Nuxt 2 (Vue 2), although the 2.0.0.1 alpha version is Tiptap 2 for Vue 3 on element-plus. Here I was happy because… my project uses element-plus and the puzzle seemed to fit, but that was not the case, a couple of hours of dancing with a tambourine, it was not possible to revive the patient normally, it’s a pity.

  • Vue SimpleMDE judging by the github, it’s not particularly lively, I didn’t find a normal demo, it never got to the point of experimentation. Just know that there is one.

  • Tipap Vuetify nice solution, but I didn’t want to drag vuetify into the project, so I left it in reserve. Out of the box it is suitable for Nuxt 2, there is no information about Vue 3 in the doc.

  • mavonEditor markdown editor, responsive, nice, functional, I will definitely use it somewhere, in the current project it is not possible to explain to users what markdown is.

I won’t tell you about all the mistakes I had to make, I want to immediately make something like a guide to CKEditor for Nuxt 3, because… It was not possible to find any general article on this issue.

Let’s add it to the project

npm install --save @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic

To connect to the project, let’s create a plugin – plugins/editor.client.js “client” in the file name means mode: “client” (if we connected the plugin through the config). The root of the plugins folder is scanned and everything is connected from there automatically. More details in dock.

import CKEditor from '@ckeditor/ckeditor5-vue';

export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.vueApp.use(CKEditor)
})

mode:”client” we need so that SSR does not include this plugin, because the plugin uses everything like window. whose existence SSR is not aware of and we will receive errors like "window is not defined"

Next, if you do it in the component import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; then we will again encounter errors generated by ssr, to get around them let’s use a trick, create a component, for example 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>

Now, if we want to draw ckeditor we do it like this:

<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>

All the magic is in the component ClientOnly so SSR does not reach our component where it is called ClassicEditor which causes errors. Now it’s enough to update the config like this:

const editorConfig = ref({
    language: 'ru',
    ckfinder: {
        uploadUrl: '/api/file/upload'
    }
})

and we get a full-fledged editor, but here again “BUT”, ckfinder does not know how to add headers to the request, and I don’t want to use an endpoint without authentication to download files. In such an endpoint it is quite possible to throw the get token as a parameter and check the token on the backend, but if not, then we move on to assembling our own build or the need to install plugins, dock here. To solve the problem we need a plugin SimpleUploadAdapterthe first thing we do (after a quick read of the doc) is add the plugin to the project (we don’t do npm install because it’s in the dependencies of @ckeditor/ckeditor5-build-classic), add it to components/editor.vue

import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload';

and we get an error ckeditor‑duplicated‑modules. The fact is that this plugin is already imported into @ckeditor/ckeditor5-build-classic and when we do not use a ready-made assembly, but build our own, we need to use @ckeditor/ckeditor5-editor-classic i.e. Not buildA editor. For clarity, I left one button so as not to stretch the code, actually the new 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>

after building Nuxt presents a new surprise, in the browser console there will be something like

TypeError: Cannot read properties of null (reading ‘getAttribute’)

if we debug further we’ll come to the line

const viewBox = svg.getAttribute('viewBox')

Google won’t tell you much about this, but there is something similar, for example herehaving spent a little more nerves, we understand that the problem is vite, it turns out CKEditor is aware of this, more details here. And the solution is, first we do

npm install @ckeditor/vite-plugin-ckeditor5 @ckeditor/ckeditor5-theme-lark

then go to nuxt.config.ts and import the vite plugin, a completely empty config will look like this

import ckeditor5 from '@ckeditor/vite-plugin-ckeditor5'
export default defineNuxtConfig({
  devtools: { enabled: true },
  vite: {
    plugins: [ckeditor5({ theme: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) })]
  }
})

That’s all, now you can assemble your builds, CKEditor is quite a powerful tool, and for Nuxt 3 there are practically no full-fledged WYSIWYG editors. I spent a lot of time and would like to summarize it all and leave this mini-instruction. Don’t judge strictly 🙂

PS back for loading images can return

{
    "url": "https://example.com/images/foo.jpg"
}

or

{
    "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"
    }
}

or

{
    "error": {
        "message": "The image upload failed because the image was too big (max 1.5MB)."
    }
}

More details Simple upload adapter

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *