Rich text editing for Nuxt Crouton collections, powered by Nuxt UI's UEditor (TipTap-based).
Status: Beta
The @fyit/crouton-editor package is a Nuxt layer that wraps Nuxt UI's UEditor with Crouton-specific defaults, variable insertion support, and live preview functionality.
Features:
UEditor{{variable_name}}) with live preview interpolationpnpm add @fyit/crouton-editor
Peer dependencies:
@nuxt/ui v4.3.0+ (provides the TipTap-based UEditor)@fyit/crouton-core@nuxt/icon v1.0.0+Add the editor layer to your nuxt.config.ts:
export default defineNuxtConfig({
extends: [
'@fyit/crouton',
'@fyit/crouton-editor'
]
})
<script setup lang="ts">
const content = ref('<p>Hello world!</p>')
</script>
<template>
<CroutonEditorSimple v-model="content" />
</template>
All components auto-register with the CroutonEditor prefix.
The main editor component. Wraps UEditor with a fixed toolbar and a bubble toolbar that appears on text selection.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | null | '' | Content (v-model) |
placeholder | string | - | Placeholder text |
contentType | 'html' | 'markdown' | 'json' | 'html' | Output format |
editable | boolean | true | Enable/disable editing |
autofocus | boolean | 'start' | 'end' | 'all' | number | - | Focus behavior |
showToolbar | boolean | true | Show the fixed toolbar |
showBubbleToolbar | boolean | true | Show bubble toolbar on text selection |
extensions | any[] | - | Additional TipTap extensions |
enableTranslationAI | boolean | false | Show AI translation button (requires @fyit/crouton-ai) |
translationContext | TranslationContext | - | Context for AI translation |
onTranslationAccept | (text: string) => void | - | Callback when translation is accepted |
enableImageUpload | boolean | false | Enable image upload via toolbar, paste, and drag-drop |
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | Content changed |
create | { editor: Editor } | Editor instance created |
update | { editor: Editor } | Content updated |
translationAccept | string | Translation accepted by user |
The fixed toolbar includes: undo/redo, headings (H1-H3), lists (bullet/ordered), blockquote, code block, horizontal rule, bold, italic, underline, strikethrough, inline code, highlight, and link.
The bubble toolbar appears on text selection with a "Turn into" dropdown, text formatting marks, and link insertion.
In a form:
<script setup lang="ts">
const state = ref({
title: '',
content: '<p></p>'
})
</script>
<template>
<UForm :state="state" @submit="handleSubmit">
<UFormField label="Title" name="title">
<UInput v-model="state.title" />
</UFormField>
<UFormField label="Content" name="content">
<CroutonEditorSimple v-model="state.content" />
</UFormField>
<UButton type="submit">Save</UButton>
</UForm>
</template>
With image upload:
<CroutonEditorSimple
v-model="content"
enable-image-upload
/>
When enableImageUpload is true, the toolbar shows an "Insert Image" button. Pasting or dragging images into the editor also triggers an upload to /api/upload-image.
With AI translation:
<CroutonEditorSimple
v-model="content"
enable-translation-ai
:translation-context="{
sourceText: selectedText,
sourceLanguage: 'en',
targetLanguage: 'nl',
fieldType: 'description'
}"
@translation-accept="handleTranslation"
/>
A block-based editor built on UEditor with slash command support, custom NodeView blocks, and a property panel system. Designed for structured page content. Supports real-time collaboration via Yjs.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | TipTapDoc | null | '' | Content (v-model) |
placeholder | string | - | Placeholder text |
contentType | 'html' | 'markdown' | 'json' | 'json' | Output format |
editable | boolean | true | Enable/disable editing |
autofocus | boolean | 'start' | 'end' | 'all' | number | - | Focus behavior |
extensions | any[] | - | Custom TipTap block extensions |
showToolbar | boolean | true | Show the toolbar |
showBubbleToolbar | boolean | true | Show bubble toolbar on selection |
suggestionItems | BlockSuggestionItem[] | [] | Block items for slash command menu |
yxmlFragment | Y.XmlFragment | - | Yjs fragment for real-time collaboration |
collabProvider | { awareness: any } | - | Collaboration provider for cursor awareness |
collabUser | { name: string; color?: string } | - | User info for collaboration cursors |
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | TipTapDoc | Content changed |
create | { editor: Editor } | Editor instance created |
update | { editor: Editor } | Content updated |
block:select | { node, pos } | null | Block selected/deselected |
block:edit | { node, pos } | Block edit requested (property panel) |
The property-panel slot provides a way to render a custom property panel for editing block attributes:
<CroutonEditorBlocks
v-model="content"
:extensions="[MyBlockExtension]"
:suggestion-items="blockItems"
content-type="json"
>
<template #property-panel="{ selectedNode, isOpen, close, updateAttrs, deleteBlock }">
<USlideover :model-value="isOpen" @update:model-value="!$event && close()">
<template #content>
<MyPropertyPanel
v-if="selectedNode"
:node="selectedNode.node"
@update="updateAttrs"
@delete="deleteBlock"
@close="close"
/>
</template>
</USlideover>
</template>
</CroutonEditorBlocks>
| Slot Prop | Type | Description |
|---|---|---|
selectedNode | { node, pos } | null | Currently selected block |
isOpen | boolean | Property panel open state |
close | () => void | Close the panel |
updateAttrs | (attrs: Record<string, unknown>) => void | Update block attributes |
deleteBlock | () => void | Delete selected block |
interface BlockSuggestionItem {
type: string // Block type name (e.g., 'heroBlock')
label: string // Display label in menu
description?: string // Description shown in menu
icon?: string // Icon name (e.g., 'i-lucide-layout-template')
category?: string // Category for grouping
command: string // TipTap command name (e.g., 'insertHeroBlock')
}
A variable insertion menu using Nuxt UI's UEditorMentionMenu. Triggered by typing {{ in the editor. Renders variables with optional category grouping.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | - | TipTap editor instance (from UEditor slot) |
variables | EditorVariable[] | [] | Flat list of variables |
groups | EditorVariableGroup[] | - | Grouped variables (alternative to flat list) |
char | string | '{{' | Trigger character |
Pass the editor instance from the UEditor slot:
<CroutonEditorSimple v-model="content">
<template #default="{ editor }">
<CroutonEditorVariables
:editor="editor"
:variables="emailVariables"
/>
</template>
</CroutonEditorSimple>
<script setup lang="ts">
const emailVariables = [
{ name: 'customer_name', label: 'Customer Name', sample: 'John Doe' },
{ name: 'booking_date', label: 'Booking Date', sample: 'January 15, 2024' },
]
</script>
Variables with a category property are automatically grouped under category headers.
Live preview component with variable interpolation. Displays content with {{variables}} replaced by provided values or sample values from variable definitions.
| Prop | Type | Default | Description |
|---|---|---|---|
content | string | Record<string, any> | - | Raw content with {{variables}}, or TipTap JSON document |
title | string | 'Preview' | Panel title |
values | Record<string, string> | - | Values for interpolation |
variables | EditorVariable[] | - | Variable definitions (for sample values) |
mode | 'inline' | 'panel' | 'panel' | Display mode |
expandable | boolean | true | Allow expanding to modal |
showVariableCount | boolean | true | Show variable count in header |
containerClass | string | - | Custom container CSS class |
contentClass | string | - | Custom content area CSS class |
panel (default): Full preview with header, variable count indicator, and optional expand-to-modal button.inline: Compact 40px-high thumbnail with an eye button to open a modal for full view.<CroutonEditorPreview
:content="emailBody"
:variables="emailVariables"
:values="{ customer_name: 'Jane Smith' }"
title="Email Preview"
/>
Combined editor and preview with tab-based layout. Shows an "Editor" tab and a "Preview" tab, with quick-insert variable chips above the editor.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | null | '' | Content (v-model) |
variables | EditorVariable[] | [] | Variables for insertion |
previewValues | Record<string, string> | - | Override sample values in preview |
previewTitle | string | 'Preview' | Preview panel title |
contentType | 'html' | 'markdown' | 'json' | 'html' | Output format |
placeholder | string | - | Placeholder text |
editable | boolean | true | Enable/disable editing |
showVariableChips | boolean | true | Show quick-insert variable chips above editor |
extensions | any[] | - | Additional TipTap extensions |
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | Content changed |
<script setup lang="ts">
const emailVariables = [
{ name: 'customer_name', label: 'Customer Name', category: 'customer', sample: 'John Doe' },
{ name: 'booking_date', label: 'Booking Date', category: 'booking', sample: 'Monday, January 15, 2024' },
{ name: 'location_name', label: 'Location', category: 'location', sample: 'Main Office' },
]
const sampleData = {
customer_name: 'Jane Smith',
booking_date: 'Tuesday, January 16, 2024',
location_name: 'Downtown Branch'
}
</script>
<template>
<CroutonEditorWithPreview
v-model="emailBody"
:variables="emailVariables"
:preview-values="sampleData"
preview-title="Email Preview"
placeholder="Write your email template..."
/>
</template>
Utilities for working with {{variable}} placeholders in editor content.
const {
interpolate,
extractVariables,
getSampleValues,
findUndefinedVariables
} = useEditorVariables()
| Function | Signature | Description |
|---|---|---|
interpolate | (content: string, values: Record<string, string>) => string | Replace {{vars}} with values |
extractVariables | (content: string) => string[] | Get unique variable names from content |
getSampleValues | (variables: EditorVariable[]) => Record<string, string> | Get sample values from variable definitions |
findUndefinedVariables | (content: string, variables: EditorVariable[]) => string[] | Find variables used in content but not defined |
Examples:
const { interpolate, extractVariables } = useEditorVariables()
// Interpolate content
const rendered = interpolate(
'Hello {{customer_name}}!',
{ customer_name: 'John' }
)
// Result: "Hello John!"
// Extract variables
const vars = extractVariables('Hello {{name}}, your booking is on {{date}}')
// Result: ['name', 'date']
import type { EditorVariable, EditorVariableGroup } from '#crouton-editor/types/editor'
interface EditorVariable {
name: string // Variable name: "customer_name"
label: string // Display label: "Customer Name"
description?: string // Help text
icon?: string // Lucide icon name
category?: string // Grouping: "customer", "booking"
sample?: string // Sample value for preview
}
interface EditorVariableGroup {
label: string
variables: EditorVariable[]
}
The layer auto-registers components with the CroutonEditor prefix at priority 1 (overriding stubs from crouton-core) and auto-imports composables:
// nuxt.config.ts (layer internals)
export default defineNuxtConfig({
components: {
dirs: [{
path: 'app/components',
prefix: 'CroutonEditor',
global: true,
priority: 1
}]
},
imports: {
dirs: ['app/composables']
},
alias: {
'#crouton-editor': 'app'
}
})
Mark fields in your collection schema to use the editor automatically:
{
"collections": {
"posts": {
"fields": {
"content": {
"type": "text",
"meta": {
"label": "Content",
"component": "CroutonEditorSimple"
}
}
}
}
}
}
Generated form components will render CroutonEditorSimple for that field. Generated list components will use CroutonEditorPreview for rich text columns.
The editor outputs HTML by default. Store it in a TEXT column:
export const blogPosts = sqliteTable('blog_posts', {
id: text('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
createdAt: integer('createdAt', { mode: 'timestamp' })
})
Render editor HTML safely on the frontend:
<template>
<div class="prose dark:prose-invert" v-html="post.content" />
</template>
Security: Always sanitize HTML on the backend before saving to prevent XSS attacks.