Rich Text Editor Vue
December 2, 2024 · AI Conversations
—
### 1. **Quill**
Quill is a lightweight and user-friendly rich text editor.
#### Installation
yarn add vue3-quill
#### Usage
<template>
<div>
<quill-editor v-model="content" :options="editorOptions" />
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import { QuillEditor } from 'vue3-quill';
export default defineComponent({
components: { QuillEditor },
setup() {
const content = ref('');
const editorOptions = {
theme: 'snow',
};
return { content, editorOptions };
},
});
</script>
<style>
/* Add Quill CSS */
@import 'quill/dist/quill.snow.css';
</style>
—
### 2. **Tiptap**
Tiptap is a modern, extensible, and feature-rich text editor.
#### Installation
yarn add @tiptap/vue-3 @tiptap/starter-kit
#### Usage
<template>
<editor-content :editor="editor" />
</template>
<script>
import { defineComponent, onMounted, onBeforeUnmount } from 'vue';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import { EditorContent } from '@tiptap/vue-3';
export default defineComponent({
components: { EditorContent },
setup() {
const editor = new Editor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
});
onBeforeUnmount(() => editor.destroy());
return { editor };
},
});
</script>
—
### 3. **TinyMCE**
TinyMCE is a widely-used, full-featured WYSIWYG editor.
#### Installation
yarn add @tinymce/tinymce-vue
#### Usage
<template>
<editor v-model="content" :init="editorConfig" />
</template>
<script>
import { defineComponent, ref } from 'vue';
import { Editor } from '@tinymce/tinymce-vue';
export default defineComponent({
components: { Editor },
setup() {
const content = ref('<p>This is the initial content of the editor</p>');
const editorConfig = {
height: 500,
menubar: false,
plugins: ['lists link image paste help wordcount'],
toolbar:
'undo redo | formatselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
};
return { content, editorConfig };
},
});
</script>
—
### 4. **CKEditor**
CKEditor is another robust and popular choice for rich text editing.
#### Installation
yarn add @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic
#### Usage
<template>
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" />
</template>
<script>
import { defineComponent, ref } from 'vue';
import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
export default defineComponent({
components: { ckeditor: CKEditor.component },
setup() {
const editor = ClassicEditor;
const editorData = ref('<p>Hello, CKEditor!</p>');
const editorConfig = {
toolbar: ['bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote'],
};
return { editor, editorData, editorConfig };
},
});
</script>
—
### Recommendations
– Use **Quill** or **Tiptap** for simpler and more customizable needs.
– Opt for **TinyMCE** or **CKEditor** if you require enterprise-grade features or plugins.
Let me know which editor you prefer, and I can help with advanced configurations!
—
### **1. Using `vue-summernote` Package**
#### Installation
yarn add vue-summernote
#### Usage
<template>
<div>
<summernote v-model="content" :options="editorOptions"></summernote>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import Summernote from 'vue-summernote';
export default defineComponent({
components: { Summernote },
setup() {
const content = ref('<p>Initial Content</p>');
const editorOptions = {
placeholder: 'Write something...',
tabsize: 2,
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']],
],
};
return { content, editorOptions };
},
});
</script>
<style>
@import 'summernote/dist/summernote-lite.css';
</style>
—
### **2. Using Summernote with jQuery**
If you don't want to use a Vue wrapper, you can integrate Summernote directly by initializing it in a Vue lifecycle hook.
#### Installation
yarn add summernote jquery popper.js
#### Usage
<template>
<div>
<textarea id="summernote"></textarea>
</div>
</template>
<script>
import { defineComponent, onMounted, ref } from 'vue';
import 'summernote/dist/summernote-lite.css';
import 'summernote/dist/summernote-lite.js';
export default defineComponent({
setup() {
const content = ref('<p>Initial Content</p>');
onMounted(() => {
$('#summernote').summernote({
placeholder: 'Write something...',
tabsize: 2,
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']],
],
callbacks: {
onChange: function (contents) {
content.value = contents;
},
},
});
});
return { content };
},
});
</script>
—
### Key Notes
1. **Dependencies**:
– Summernote relies on **jQuery** and **Popper.js**.
– Make sure these are included in your project if you're not using the `vue-summernote` package.
2. **Styling**:
– Use the Summernote `lite` or `bs5` styles based on your project requirements.
3. **Advanced Features**:
– Summernote supports image uploads, but you may need to configure a backend API for handling the file storage.
Let me know if you need help configuring the image upload or other advanced features!
<script>
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link,useForm, usePage } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const form = useForm({
title: '',
content: '',
meta_keywords: '',
meta_description: '',
});
</script>
{{ $t('create_post','Add New Post') }}
—
### Full Updated Code
<script setup lang="ts">
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
// Import Summernote
import 'summernote/dist/summernote-lite.css';
import 'summernote/dist/summernote-lite.js';
import { ref, onMounted } from 'vue';
const form = useForm({
title: '',
content: '',
meta_keywords: '',
meta_description: '',
});
const editorOptions = {
placeholder: 'Write your content here...',
tabsize: 2,
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']],
],
};
// Initialize Summernote on mount
const initializeSummernote = () => {
const contentElement = document.getElementById('summernote-editor');
if (contentElement) {
$(contentElement).summernote({
...editorOptions,
callbacks: {
onChange: (contents: string) => {
form.content = contents; // Sync with form data
},
},
});
}
};
onMounted(() => {
initializeSummernote();
});
</script>
<template>
<Head :title="$t('Create Post','Create Post')" />
<AuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ $t('create_post', 'Add New Post') }}
</h2>
</template>
<div class="py-6">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="bg-white shadow-sm sm:rounded-lg">
<form
@submit.prevent="form.post(route('userarea.posts.store'))"
class="mt-6 space-y-6"
>
<div>
<InputLabel for="name" value="Title" />
<TextInput
id="name"
type="text"
class="mt-1 block w-full"
v-model="form.title"
required
autofocus
autocomplete="name"
/>
<InputError class="mt-2" :message="form.errors.title" />
</div>
<!-- Summernote -->
<div>
<label for="summernote-editor" class="block text-sm font-medium text-gray-700">
Content
</label>
<textarea id="summernote-editor"></textarea>
<InputError class="mt-2" :message="form.errors.content" />
</div>
<PrimaryButton :disabled="form.processing">
Submit
</PrimaryButton>
</form>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
—
### Key Points
1. **Summernote Initialization:**
– The `initializeSummernote` function is invoked within the `onMounted` lifecycle hook to ensure the editor is attached after the DOM element is rendered.
2. **Two-Way Binding:**
– The `onChange` callback updates `form.content` whenever the user modifies the content in Summernote.
3. **Error Handling:**
– An `InputError` component is included to display validation errors for the `content` field.
4. **Dependencies:**
– Ensure you have `jQuery` installed, as Summernote depends on it:
yarn add jquery
5. **Styling:**
– The `summernote-lite.css` file is imported to style the editor.
This setup integrates Summernote while maintaining compatibility with Inertia.js and your Vue components. Let me know if you encounter any issues!
—
### **1. Create the `Summernote.vue` Component**
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import 'summernote/dist/summernote-lite.css';
import 'summernote/dist/summernote-lite.js';
// Props for customization
defineProps({
modelValue: {
type: String,
default: '',
},
options: {
type: Object,
default: () => ({
placeholder: 'Write something...',
tabsize: 2,
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']],
],
}),
},
});
// Emit events
const emit = defineEmits(['update:modelValue']);
const editor = ref<HTMLDivElement | null>(null);
const initializeSummernote = () => {
if (editor.value) {
$(editor.value).summernote({
...options,
callbacks: {
onChange: (contents: string) => {
emit('update:modelValue', contents);
},
},
});
// Set initial value
$(editor.value).summernote('code', modelValue);
}
};
// Clean up on unmount
onBeforeUnmount(() => {
if (editor.value) {
$(editor.value).summernote('destroy');
}
});
// Watch for external changes to modelValue
watch(
() => modelValue,
(newValue) => {
if (editor.value) {
$(editor.value).summernote('code', newValue);
}
}
);
onMounted(() => {
initializeSummernote();
});
</script>
<template>
<div>
<div ref="editor"></div>
</div>
</template>
—
### **2. Use the Component**
You can now use the `Summernote` component in your forms or anywhere in your application.
#### Example Usage
<script setup lang="ts">
import Summernote from '@/Components/Summernote.vue';
import InputError from '@/Components/InputError.vue';
import { useForm } from '@inertiajs/vue3';
const form = useForm({
title: '',
content: '',
});
</script>
<template>
<form @submit.prevent="form.post(route('userarea.posts.store'))" class="space-y-6">
<div>
<label for="title" class="block text-sm font-medium text-gray-700">
Title
</label>
<input
id="title"
type="text"
v-model="form.title"
class="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
<InputError class="mt-2" :message="form.errors.title" />
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-700">
Content
</label>
<Summernote v-model="form.content" />
<InputError class="mt-2" :message="form.errors.content" />
</div>
<button
type="submit"
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Submit
</button>
</form>
</template>
—
### **Key Features of the Component**
1. **Two-Way Binding:**
– Uses `v-model` for seamless two-way data binding via the `modelValue` prop.
2. **Customizable Options:**
– The `options` prop allows you to pass specific Summernote configurations.
3. **Lifecycle Management:**
– Initializes Summernote on `onMounted`.
– Destroys the instance on `onBeforeUnmount`.
4. **Reactive Updates:**
– Watches for changes to `modelValue` and updates the editor accordingly.
—
### **Advantages**
– Easy to reuse in multiple forms or pages.
– Fully decoupled logic, making it simple to maintain and extend.
– Compatible with reactive data and state management systems like Inertia.js or Vuex.
Let me know if you need further customization or enhancements!
Here’s how to resolve it:
—
### Updated `Summernote.vue`
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import 'summernote/dist/summernote-lite.css';
import 'summernote/dist/summernote-lite.js';
// Define props
const props = defineProps({
modelValue: {
type: String,
default: '', // Default value for the editor content
},
options: {
type: Object,
default: () => ({
placeholder: 'Write something...',
tabsize: 2,
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']],
],
}),
},
});
// Emit events for v-model
const emit = defineEmits(['update:modelValue']);
// Ref for the editor DOM element
const editor = ref<HTMLDivElement | null>(null);
// Initialize Summernote
const initializeSummernote = () => {
if (editor.value) {
$(editor.value).summernote({
...props.options,
callbacks: {
onChange: (contents: string) => {
emit('update:modelValue', contents); // Emit content changes to the parent
},
},
});
// Set initial value
$(editor.value).summernote('code', props.modelValue);
}
};
// Clean up Summernote instance
onBeforeUnmount(() => {
if (editor.value) {
$(editor.value).summernote('destroy');
}
});
// Watch for changes in the modelValue prop
watch(
() => props.modelValue,
(newValue) => {
if (editor.value) {
$(editor.value).summernote('code', newValue);
}
}
);
onMounted(() => {
initializeSummernote();
});
</script>
<template>
<div>
<div ref="editor"></div>
</div>
</template>
—
### Correct Usage in Parent Component
Here’s an example of how to use the `Summernote` component in a parent component with `v-model`.
#### Example Parent Component
<script setup lang="ts">
import Summernote from '@/Components/Summernote.vue';
import { useForm } from '@inertiajs/vue3';
const form = useForm({
title: '',
content: '', // Binds to the Summernote editor
});
</script>
<template>
<form @submit.prevent="form.post(route('userarea.posts.store'))" class="space-y-6">
<div>
<label for="title" class="block text-sm font-medium text-gray-700">
Title
</label>
<input
id="title"
type="text"
v-model="form.title"
class="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-700">
Content
</label>
<Summernote v-model="form.content" />
</div>
<button
type="submit"
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Submit
</button>
</form>
</template>
—
### Explanation of Fixes
1. **`modelValue` Prop**:
– Ensure `modelValue` is explicitly defined in the `props` with a default value.
– This is necessary for two-way binding with `v-model`.
2. **`update:modelValue` Event**:
– Emit this event from the `Summernote.vue` component to synchronize the editor content with the parent component.
3. **Ref Usage**:
– The `ref` is used to initialize Summernote on the specific DOM element.
4. **Watch for Changes**:
– The `watch` ensures that any changes to `modelValue` from the parent are reflected in the editor.
—
This updated implementation ensures the `modelValue` prop works seamlessly with `v-model`. Let me know if you need further clarification!
### Fixing `$ is not defined`
#### Steps to Resolve:
1. **Install jQuery**:
Ensure jQuery is installed in your project. You can install it using npm or yarn:
yarn add jquery
# or
npm install jquery
2. **Expose jQuery Globally**:
Add jQuery to your global scope, so Summernote can access it.
Update your `vite.config.js` (if you're using Vite):
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
define: {
'window.jQuery': 'jquery',
'window.$': 'jquery',
},
});
3. **Import jQuery in Your `Summernote.vue` Component**:
Modify your `Summernote.vue` script to explicitly import and define jQuery globally.
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import 'summernote/dist/summernote-lite.css';
import 'summernote/dist/summernote-lite.js';
// Import jQuery and expose it globally
import $ from 'jquery';
window.$ = $;
window.jQuery = $;
// Define props
const props = defineProps({
modelValue: {
type: String,
default: '',
},
options: {
type: Object,
default: () => ({
placeholder: 'Write something...',
tabsize: 2,
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview', 'help']],
],
}),
},
});
const emit = defineEmits(['update:modelValue']);
const editor = ref<HTMLDivElement | null>(null);
const initializeSummernote = () => {
if (editor.value) {
$(editor.value).summernote({
...props.options,
callbacks: {
onChange: (contents: string) => {
emit('update:modelValue', contents);
},
},
});
$(editor.value).summernote('code', props.modelValue);
}
};
onBeforeUnmount(() => {
if (editor.value) {
$(editor.value).summernote('destroy');
}
});
watch(
() => props.modelValue,
(newValue) => {
if (editor.value) {
$(editor.value).summernote('code', newValue);
}
}
);
onMounted(() => {
initializeSummernote();
});
</script>
4. **Rebuild Your Project**:
After updating your project, rebuild it to ensure all dependencies are properly bundled:
yarn dev
# or
npm run dev
—
### Verifying the Fix
1. Check if the error "`$ is not defined`" is resolved.
2. Verify that Summernote initializes correctly.
3. Ensure the two-way binding with `v-model` works as expected.
—
### Common Issues
– **Order of Imports**: jQuery must be imported *before* Summernote, as it’s a dependency.
– **Vite Compatibility**: Ensure your Vite configuration properly defines jQuery globally.
Let me know if you encounter any further issues!