Editor

TipTapGitHub
A rich text editor component based on TipTap with support for markdown, HTML, and JSON content types.

Usage

Examples

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

modelValue null | string | JSONContent | JSONContent[]
contentType'json' "json" | "html" | "markdown"

The content type the content is provided as.

starterKit{ headings: { levels: [1, 2, 3, 4] }, link: { openOnClick: false }, dropcursor: { color: 'var(--ui-primary)', width: 2 } } Partial<StarterKitOptions>

The starter kit options to configure the editor.

placeholder string | PlaceholderOptions

The placeholder text to show in empty paragraphs. { showOnlyWhenEditable: false, showOnlyCurrent: true } Can be a string or PlaceholderOptions from @tiptap/extension-placeholder.

handlers Partial<EditorHandlers>

Custom item handlers to override or extend the default handlers. These handlers are provided to all child components (toolbar, suggestion menu, etc.).

extensions Extensions

The extensions to use

injectCSSboolean

Whether to inject base CSS styles

injectNonce string

A nonce to use for CSP while injecting styles

autofocus null | number | false | true | "start" | "end" | "all"

The editor's initial focus position

editableboolean

Whether the editor is editable

textDirection "ltr" | "rtl" | "auto"

The default text direction for all content in the editor. When set to 'ltr' or 'rtl', all nodes will have the corresponding dir attribute. When set to 'auto', the dir attribute will be set based on content detection. When undefined, no dir attribute will be added.

editorProps EditorProps<any>

The editor's props

parseOptions ParseOptions

The editor's content parser options

coreExtensionOptions { clipboardTextSerializer?: { blockSeparator?: string | undefined; } | undefined; delete?: { async?: boolean | undefined; filterTransaction?: ((transaction: Transaction) => boolean) | undefined; } | undefined; }

The editor's core extension options

enableInputRules false | true | (string | AnyExtension)[]

Whether to enable input rules behavior

enablePasteRules false | true | (string | AnyExtension)[]

Whether to enable paste rules behavior

enableCoreExtensionsboolean | Partial<Record<"editable" | "textDirection" | "delete" | "clipboardTextSerializer" | "commands" | "focusEvents" | "keymap" | "tabindex" | "drop" | "paste", false>>

Determines whether core extensions are enabled.

If set to false, all core extensions will be disabled. To disable specific core extensions, provide an object where the keys are the extension names and the values are false. Extensions not listed in the object will remain enabled.

enableContentCheckboolean

If true, the editor will check the content for errors on initialization. Emitting the contentError event if the content is invalid. Which can be used to show a warning or error message to the user.

emitContentErrorboolean

If true, the editor will emit the contentError event if invalid content is encountered but enableContentCheck is false. This lets you preserve the invalid editor content while still showing a warning or error message to the user.

onBeforeCreate (props: { editor: Editor; }): void

Called before the editor is constructed.

onCreate (props: { editor: Editor; }): void

Called after the editor is constructed.

onMount (props: { editor: Editor; }): void

Called when the editor is mounted.

onUnmount (props: { editor: Editor; }): void

Called when the editor is unmounted.

onContentError (props: { editor: Editor; error: Error; disableCollaboration: () => void; }): void

Called when the editor encounters an error while parsing the content. Only enabled if enableContentCheck is true.

onUpdate (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): void

Called when the editor's content is updated.

onSelectionUpdate (props: { editor: Editor; transaction: Transaction; }): void

Called when the editor's selection is updated.

onTransaction (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): void

Called after a transaction is applied to the editor.

onFocus (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): void

Called on focus events.

onBlur (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): void

Called on blur events.

onDestroy (props: void): void

Called when the editor is destroyed.

onPaste (e: ClipboardEvent, slice: Slice): void

Called when content is pasted into the editor.

onDrop (e: DragEvent, slice: Slice, moved: boolean): void

Called when content is dropped into the editor.

onDelete (props: { editor: Editor; deletedRange: Range; newRange: Range; transaction: Transaction; combinedTransform: Transform; partial: boolean; from: number; to: number; } & ({ ...; } | { ...; })): void

Called when content is deleted from the editor.

ui { root?: ClassNameValue; content?: ClassNameValue; base?: ClassNameValue; }

Slots

Slot Type
default{ editor: Editor; handlers: EditorHandlers; }

Emits

Event Type
update:modelValue[value: Content]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    editor: {
      slots: {
        root: '',
        content: 'relative size-full flex-1',
        base: [
          'w-full outline-none *:my-5 *:first:mt-0 *:last:mb-0 sm:px-8 selection:bg-primary/10',
          '[&_:is(p,h1,h2,h3,h4).is-empty]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4).is-empty]:before:text-dimmed [&_:is(p,h1,h2,h3,h4).is-empty]:before:float-left [&_:is(p,h1,h2,h3,h4).is-empty]:before:h-0 [&_:is(p,h1,h2,h3,h4).is-empty]:before:pointer-events-none',
          '[&_li_.is-empty]:before:content-none',
          '[&_p]:leading-7',
          '[&_a]:text-primary [&_a]:border-b [&_a]:border-transparent [&_a]:hover:border-primary [&_a]:font-medium',
          '[&_a]:transition-colors',
          '[&_.mention]:text-primary [&_.mention]:font-medium',
          '[&_:is(h1,h2,h3,h4)]:text-highlighted [&_:is(h1,h2,h3,h4)]:font-bold',
          '[&_h1]:text-3xl',
          '[&_h2]:text-2xl',
          '[&_h3]:text-xl',
          '[&_h4]:text-lg',
          '[&_blockquote]:border-s-4 [&_blockquote]:border-accented [&_blockquote]:ps-4 [&_blockquote]:italic',
          '[&_[data-type=horizontalRule]]:my-8 [&_[data-type=horizontalRule]]:py-2',
          '[&_hr]:border-t [&_hr]:border-default',
          '[&_pre]:text-sm/6 [&_pre]:border [&_pre]:border-muted [&_pre]:bg-muted [&_pre]:rounded-md [&_pre]:px-4 [&_pre]:py-3 [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:overflow-x-auto',
          '[&_pre_code]:p-0 [&_pre_code]:text-inherit [&_pre_code]:font-inherit [&_pre_code]:rounded-none [&_pre_code]:inline [&_pre_code]:border-0 [&_pre_code]:bg-transparent',
          '[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono [&_code]:font-medium [&_code]:rounded-md [&_code]:inline-block [&_code]:border [&_code]:border-muted [&_code]:text-highlighted [&_code]:bg-muted',
          '[&_:is(ul,ol)]:ps-6',
          '[&_ul]:list-disc [&_ul]:marker:text-(--ui-border-accented)',
          '[&_ol]:list-decimal [&_ol]:marker:text-muted',
          '[&_li]:my-1.5 [&_li]:ps-1.5',
          '[&_img]:rounded-md [&_img]:block [&_img]:max-w-full [&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline-primary',
          '[&_.ProseMirror-selectednode:not(img):not(pre):not([data-node-view-wrapper])]:bg-primary/10'
        ]
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        editor: {
          slots: {
            root: '',
            content: 'relative size-full flex-1',
            base: [
              'w-full outline-none *:my-5 *:first:mt-0 *:last:mb-0 sm:px-8 selection:bg-primary/10',
              '[&_:is(p,h1,h2,h3,h4).is-empty]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4).is-empty]:before:text-dimmed [&_:is(p,h1,h2,h3,h4).is-empty]:before:float-left [&_:is(p,h1,h2,h3,h4).is-empty]:before:h-0 [&_:is(p,h1,h2,h3,h4).is-empty]:before:pointer-events-none',
              '[&_li_.is-empty]:before:content-none',
              '[&_p]:leading-7',
              '[&_a]:text-primary [&_a]:border-b [&_a]:border-transparent [&_a]:hover:border-primary [&_a]:font-medium',
              '[&_a]:transition-colors',
              '[&_.mention]:text-primary [&_.mention]:font-medium',
              '[&_:is(h1,h2,h3,h4)]:text-highlighted [&_:is(h1,h2,h3,h4)]:font-bold',
              '[&_h1]:text-3xl',
              '[&_h2]:text-2xl',
              '[&_h3]:text-xl',
              '[&_h4]:text-lg',
              '[&_blockquote]:border-s-4 [&_blockquote]:border-accented [&_blockquote]:ps-4 [&_blockquote]:italic',
              '[&_[data-type=horizontalRule]]:my-8 [&_[data-type=horizontalRule]]:py-2',
              '[&_hr]:border-t [&_hr]:border-default',
              '[&_pre]:text-sm/6 [&_pre]:border [&_pre]:border-muted [&_pre]:bg-muted [&_pre]:rounded-md [&_pre]:px-4 [&_pre]:py-3 [&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:overflow-x-auto',
              '[&_pre_code]:p-0 [&_pre_code]:text-inherit [&_pre_code]:font-inherit [&_pre_code]:rounded-none [&_pre_code]:inline [&_pre_code]:border-0 [&_pre_code]:bg-transparent',
              '[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono [&_code]:font-medium [&_code]:rounded-md [&_code]:inline-block [&_code]:border [&_code]:border-muted [&_code]:text-highlighted [&_code]:bg-muted',
              '[&_:is(ul,ol)]:ps-6',
              '[&_ul]:list-disc [&_ul]:marker:text-(--ui-border-accented)',
              '[&_ol]:list-decimal [&_ol]:marker:text-muted',
              '[&_li]:my-1.5 [&_li]:ps-1.5',
              '[&_img]:rounded-md [&_img]:block [&_img]:max-w-full [&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline-primary',
              '[&_.ProseMirror-selectednode:not(img):not(pre):not([data-node-view-wrapper])]:bg-primary/10'
            ]
          }
        }
      }
    })
  ]
})

Changelog

No recent changes