npm package: @mpdev_ab/document-creator
Description: A rich text editor Vue 3 component built on Tiptap. Drop-in replacement for TinyMCE with font selection, tables, image upload (base64), templates, and full formatting — outputs HTML.
Latest version: 0.2.4

# @mpdev_ab/document-creator

A rich text editor Vue 3 component built on Tiptap. Drop-in replacement for TinyMCE with font selection, tables, image upload (base64), templates, and full formatting — outputs HTML.

## Install

```bash
npm install @mpdev_ab/document-creator
```

### Peer dependencies

Your project needs Vue 3 and the Tiptap packages:

```bash
npm install vue@^3.3.0 @tiptap/core @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-text-style @tiptap/extension-font-family @tiptap/extension-image @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-header @tiptap/extension-underline @tiptap/extension-subscript @tiptap/extension-superscript @tiptap/extension-text-align @tiptap/extension-link @tiptap/extension-color @tiptap/extension-placeholder
```

## Basic usage

```vue
<template>
  <DocumentEditor
    v-model="content"
    @save="onSave"
  />
</template>

<script setup>
import { ref } from 'vue'
import { DocumentEditor } from '@mpdev_ab/document-creator'

const content = ref('')

function onSave(html) {
  // html is a string — send it to your API however you like
  console.log(html)
}
</script>
```

## Nuxt 3

Create a plugin at `plugins/document-creator.client.ts`:

```ts
import { DocumentEditor } from '@mpdev_ab/document-creator'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('DocumentEditor', DocumentEditor)
})
```

Then use it in any page/component:

```vue
<template>
  <ClientOnly>
    <DocumentEditor v-model="content" @save="onSave" />
  </ClientOnly>
</template>
```

Wrap in `<ClientOnly>` since the editor requires the DOM.

## Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `v-model` | `string` | Yes | — | HTML content (two-way binding) |
| `templates` | `Template[]` | No | `[]` | List of templates to show in the load modal |
| `templatesLoading` | `boolean` | No | `false` | Shows loading state in the load templates modal |
| `image` | `ImageConfig` | No | `{ maxSize: 5MB, allowedTypes: ['image/jpeg', 'image/png', 'image/gif'] }` | Image upload constraints |
| `theme` | `Partial<ThemeConfig>` | No | See defaults below | Tailwind class overrides |
| `placeholder` | `string` | No | `''` | Placeholder text when editor is empty |
| `readonly` | `boolean` | No | `false` | Hides toolbar and disables editing |

## Events

| Event | Payload | Description |
|-------|---------|-------------|
| `update:modelValue` | `string` | Emitted on every content change (used by v-model) |
| `save` | `string` | Emitted with the HTML string when the user clicks Save |
| `saveTemplate` | `{ name: string, type: 'document' \| 'snippet', content: string }` | Emitted when the user saves a template |
| `loadTemplates` | — | Emitted when the load templates modal opens — fetch your templates and pass them via the `templates` prop |
| `deleteTemplate` | `Template` | Emitted when the user clicks delete on a template |
| `error` | `{ message: string, detail: unknown }` | Emitted on image upload errors |

## How saving works

The editor does **not** make any API calls. When the user clicks Save, it emits the `save` event with the HTML string. Your app handles persistence:

```vue
<template>
  <DocumentEditor v-model="content" @save="onSave" />
</template>

<script setup>
import { ref } from 'vue'
import { DocumentEditor } from '@mpdev_ab/document-creator'
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'

const content = ref('')

const SAVE_DOCUMENT = gql`
  mutation SaveDocument($content: String!) {
    saveDocument(content: $content) {
      id
    }
  }
`

const { mutate } = useMutation(SAVE_DOCUMENT)

async function onSave(html) {
  await mutate({ content: html })
}
</script>
```

## Templates

Templates are managed by your app. The editor emits events — you handle fetching, saving, and deleting.

```vue
<template>
  <DocumentEditor
    v-model="content"
    :templates="templates"
    :templates-loading="loading"
    @save="onSave"
    @load-templates="fetchTemplates"
    @save-template="onSaveTemplate"
    @delete-template="onDeleteTemplate"
  />
</template>

<script setup>
import { ref } from 'vue'
import { DocumentEditor } from '@mpdev_ab/document-creator'

const content = ref('')
const templates = ref([])
const loading = ref(false)

async function fetchTemplates() {
  loading.value = true
  // Fetch from your GraphQL API, REST, etc.
  const result = await yourApi.getTemplates()
  templates.value = result // must be { id, name, type, content }[]
  loading.value = false
}

async function onSave(html) {
  await yourApi.saveDocument(html)
}

async function onSaveTemplate(data) {
  // data = { name: string, type: 'document' | 'snippet', content: string }
  await yourApi.createTemplate(data)
  await fetchTemplates() // refresh the list
}

async function onDeleteTemplate(template) {
  // template = { id, name, type, content }
  await yourApi.deleteTemplate(template.id)
  await fetchTemplates() // refresh the list
}
</script>
```

### Template object shape

```ts
interface Template {
  id: string | number
  name: string
  type: 'document' | 'snippet'
  content: string
}
```

Template types:
- `document` — replaces the entire editor content when loaded
- `snippet` — inserts at the cursor position when loaded

## ImageConfig

```ts
const imageConfig = {
  maxSize: 10 * 1024 * 1024,  // 10MB (default: 5MB)
  allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
}
```

Images are converted to base64 and stored inline in the HTML. After inserting, users can resize images using preset buttons (25%, 50%, 75%, 100%) or enter a custom width.

## ThemeConfig

Override any part of the default Tailwind classes:

```vue
<DocumentEditor
  v-model="content"
  :theme="{
    toolbar: 'bg-gray-900 border-b border-gray-700 p-2 flex flex-wrap items-center gap-1',
    toolbarButton: 'hover:bg-gray-700 rounded p-1.5 text-gray-300 cursor-pointer',
    toolbarButtonActive: 'bg-blue-900 text-blue-300',
    editor: 'bg-gray-800 text-white min-h-[400px] p-4 prose prose-invert max-w-none',
    dropdown: 'bg-gray-800 border border-gray-700 rounded shadow-lg py-1 z-50',
    modal: 'bg-gray-800 text-white rounded-lg shadow-xl p-6',
    saveButton: 'bg-blue-500 text-white rounded px-4 py-2 hover:bg-blue-600',
  }"
/>
```

**Defaults:**
| Key | Default classes |
|-----|----------------|
| `toolbar` | `bg-white border-b border-gray-200 p-2 flex flex-wrap items-center gap-1` |
| `toolbarButton` | `hover:bg-gray-100 rounded p-1.5 text-gray-600 cursor-pointer` |
| `toolbarButtonActive` | `bg-blue-100 text-blue-600` |
| `editor` | `bg-white min-h-[400px] p-4 prose max-w-none focus:outline-none` |
| `dropdown` | `bg-white border border-gray-200 rounded shadow-lg py-1 z-50` |
| `modal` | `bg-white rounded-lg shadow-xl p-6` |
| `saveButton` | `bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700` |

## useDocumentEditor composable

Access the editor instance from child components:

```vue
<script setup>
import { useDocumentEditor } from '@mpdev_ab/document-creator'

const { editor, getHTML, getText, setContent, focus, isEmpty } = useDocumentEditor()

// Get current HTML
const html = getHTML()

// Set content programmatically
setContent('<p>New content</p>')

// Check if empty
if (isEmpty()) {
  // ...
}
</script>
```

This must be called from a component that is a child of `<DocumentEditor>`.

## Full example

```vue
<template>
  <DocumentEditor
    v-model="content"
    placeholder="Write your document..."
    :readonly="false"
    :templates="templates"
    :templates-loading="templatesLoading"
    :image="{
      maxSize: 10 * 1024 * 1024,
      allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
    }"
    @save="onSave"
    @save-template="onSaveTemplate"
    @load-templates="onLoadTemplates"
    @delete-template="onDeleteTemplate"
    @error="onError"
  />
</template>

<script setup>
import { ref } from 'vue'
import { DocumentEditor } from '@mpdev_ab/document-creator'

const content = ref('<p>Hello world</p>')
const templates = ref([])
const templatesLoading = ref(false)

function onSave(html) {
  // Send html to your backend
}

function onSaveTemplate(data) {
  // data = { name, type, content }
}

async function onLoadTemplates() {
  templatesLoading.value = true
  templates.value = await fetchTemplatesFromApi()
  templatesLoading.value = false
}

function onDeleteTemplate(template) {
  // template = { id, name, type, content }
}

function onError(error) {
  console.error(error.message, error.detail)
}
</script>
```

## Features

- Bold, italic, underline, strikethrough, subscript, superscript
- Font family (web-safe fonts), font size, font color
- Text alignment (left, center, right, justify)
- Line height
- Headings (H1-H6) and paragraph
- Bullet lists, numbered lists, blockquotes, horizontal rules
- Links with URL input
- Image upload (base64) with in-editor resize (25/50/75/100% presets + custom width)
- Tables with Google Docs-style grid picker, row/column operations, merge/split cells, header toggle, cell background color, column resize
- Video embed (YouTube/Vimeo URL)
- Save/load templates (full documents and snippets)
- Readonly mode
- Tailwind CSS theming via class string props