Components/MarkdownEditor

MarkdownEditor

A Markdown editor with WYSIWYG, source, and preview modes.

VerifiedSince 0.3.9

Installation

pnpm add @talex-touch/tuffex
EXAMPLE.TYPESCRIPT
import { TxMarkdownEditor } from '@talex-touch/tuffex/markdown-editor'
import '@talex-touch/tuffex/markdown-editor/style.css'
// It renders these components, whose sheets are separate
import '@talex-touch/tuffex/base-anchor/style.css'
import '@talex-touch/tuffex/base-surface/style.css'
import '@talex-touch/tuffex/card/style.css'
import '@talex-touch/tuffex/flow-light/style.css'
import '@talex-touch/tuffex/glass-surface/style.css'
import '@talex-touch/tuffex/icon/style.css'
import '@talex-touch/tuffex/spinner/style.css'
import '@talex-touch/tuffex/stream-text/style.css'
import 'katex/dist/katex.min.css' // optional: KaTeX typesets formulas, MathML otherwise
import '@talex-touch/tuffex/base.css' // tokens + resets, once per app

Usage

Basic

v-model binds the Markdown; pass uploadImage, ai and v-model:comments to turn on image uploads, AI rewriting and comments.

Loading demo...

Collaboration

collaboration connects real-time editing: the editor sends and receives change sets and carets, and your adapter merges concurrent edits.

Loading demo...

Best Practices

  • Keep sanitize=true for content written by users or providers.
  • Use a controlled mode when the route or query stores it.
  • In narrow editors such as comments or release notes, trim the toolbar with toolbarActions and turn off slash or bubble as needed.
  • Have uploadImage and ai honour signal: it aborts when the user deletes the placeholder, stops or discards.
  • If the host doesn't scan tuffex sources, add MARKDOWN_EDITOR_ICON_CLASSES from src/toolbar-icons.ts to the UnoCSS safelist.

API Reference

Props

PropTypeDefaultDescription
modelValue / v-modelstring''The Markdown source.
placeholderstring''Placeholder in the editable modes.
mode'wysiwyg' | 'source' | 'preview'-Controlled mode.
defaultMode'wysiwyg' | 'source' | 'preview''wysiwyg'Starting mode when uncontrolled.
disabledbooleanfalseDisables the toolbar, mode switcher, and editing surface.
readonlybooleanfalseBlocks editing; reading and mode switching still work.
sanitizebooleantrueSanitizes rendered HTML with DOMPurify; turn off only for trusted content.
theme'auto' | 'light' | 'dark''auto'Theme; auto follows the page.
toolbarbooleantrueShows the toolbar and mode switcher.
toolbarActionsMarkdownEditorToolbarActionKey[]built-in setToolbar buttons, in order.
minHeightstring | number220Minimum height of the editing area.
maxHeightstring | number-Maximum height of the editing area.
ariaLabelstring'Markdown editor'Accessible name shared by both editing surfaces.
linkPrompt(selectedText: string) => string | Promise<string>-Replaces the built-in link field; return the URL, or '' to cancel.
mathbooleantrueRenders $…$ and $$…$$ with KaTeX.
emojibooleantrueRenders :shortcode: and turns on : completion and the emoji picker.
highlightbooleantrue==text== highlights and the highlighter's colours.
calloutsbooleantrueShows quotes whose first line is [!NOTE], [!TIP], [!IMPORTANT], [!WARNING] or [!CAUTION] as callouts.
codeThemeMarkdownEditorCodeTheme'github'Syntax highlighting preset for code blocks with a language; 'none' leaves them plain.
slashbooleantrueIn WYSIWYG, / at the start of a block opens the block menu.
bubblebooleantrueIn WYSIWYG, selecting text floats a formatting bar.
aiMarkdownEditorAi-AI rewriting on the formatting bar; hidden without it.
uploadImageMarkdownEditorUploadImage-Uploads pasted, dropped or chosen images and returns their hosted address; without it, the image button asks for an address.
comments / v-model:commentsMarkdownEditorComment[]-Comment threads; comments are hidden without it.
userMarkdownEditorUser{ id: 'local', name: labels.you }The local user: the author of comments, and who collaborators see.
collaborationMarkdownEditorCollaboration-Transport for real-time collaboration.
statusBarbooleantrueWord count, character count and collaborators at the bottom.
labelsPartial<MarkdownEditorLabels>English copyOverrides the interface copy.

Events

EventParamsDescription
update:modelValue(value: string)Fires when the content changes.
change(value: string)Fires together with update:modelValue.
update:mode(mode: MarkdownEditorMode)Fires on a mode switch, for v-model:mode.
mode-change(mode: MarkdownEditorMode)Fires together with update:mode.
focus()Fires when the editing surface gains focus.
blur()Fires when the editing surface loses focus.
update:comments(comments: MarkdownEditorComment[])Fires when threads change or their ranges move with edits, for v-model:comments.
comment-create(comment: MarkdownEditorComment)Fires when a thread is created.
comment-reply(comment: MarkdownEditorComment, reply: MarkdownEditorCommentReply)Fires when a thread gets a reply.
comment-resolve(comment: MarkdownEditorComment)Fires when a thread is resolved or reopened; resolved tells which.
comment-delete(comment: MarkdownEditorComment)Fires when a thread is deleted.
upload-error(error: unknown, file: File)Fires when an image upload fails.
ai-error(error: unknown)Fires when an AI rewrite fails.

Exposed Methods

MethodDescription
focus()Focuses the current mode's editing surface.
blur()Blurs the current editing surface.
setMode(mode)Switches the mode.
getMode()Returns the current mode.
getValue()Returns the current Markdown.
setValue(value)Replaces the whole text as one edit: emits updates and sends it to collaborators.
getSelection()Returns the selection as offsets into the Markdown source.
setSelection(selection)Focuses and sets the selection.
insert(markdown)Inserts Markdown at the selection, as a user edit.

Types

EXAMPLE.TS
type MarkdownEditorMode = 'wysiwyg' | 'source' | 'preview'

type MarkdownEditorToolbarActionKey =
  | 'heading' | 'bold' | 'italic' | 'strike' | 'code' | 'highlight' | 'quote'
  | 'bulletList' | 'orderedList' | 'taskList' | 'link' | 'image' | 'table'
  | 'codeBlock' | 'math' | 'divider' | 'emoji' | 'undo' | 'redo'

type MarkdownEditorCodeTheme = 'github' | 'one' | 'vitesse' | 'catppuccin' | 'solarized' | 'none'

interface MarkdownEditorAiRequest {
  action: 'improve' | 'shorten' | 'expand' | 'fix' | 'translate' | 'custom'
  instruction: string // what the user typed, for custom
  text: string // the selected Markdown, which the answer replaces
  before: string // up to 2,000 characters of context on each side
  after: string
  signal: AbortSignal // aborts on stop, retry or discard
}
type MarkdownEditorAi = (request: MarkdownEditorAiRequest) => string | Promise<string> | AsyncIterable<string>

type MarkdownEditorUploadImage = (
  file: File,
  context: { onProgress: (percent: number) => void, signal: AbortSignal },
) => Promise<string | { url: string, alt?: string, title?: string }>

interface MarkdownEditorUser { id: string, name: string, color?: string, avatar?: string }

interface MarkdownEditorComment {
  id: string
  from: number // a source range that moves with edits
  to: number
  quote: string // relocates the range when it no longer matches
  author: MarkdownEditorUser
  body: string
  createdAt: number
  resolved?: boolean
  replies?: MarkdownEditorCommentReply[]
}

// One replacement; every offset in a change set is against the document before it
interface MarkdownTextChange { from: number, to: number, insert: string }

interface MarkdownEditorCollaboration {
  send: (changes: MarkdownTextChange[]) => void
  sendPresence?: (presence: { selection: MarkdownEditorSelection | null, mode: MarkdownEditorMode }) => void
  subscribe: (handlers: {
    onChanges: (changes: MarkdownTextChange[]) => void // against the editor's current document
    onPeers?: (peers: MarkdownEditorPeer[]) => void
  }) => () => void
}

Change Sets

Collaboration adapters merge concurrent edits with these functions, all exported from @talex-touch/tuffex/markdown-editor.

FunctionDescription
applyMarkdownChanges(doc, changes)Applies a change set to a text.
composeMarkdownChanges(a, b, docLength)Combines two consecutive change sets into one.
mapMarkdownPosition(pos, changes, assoc?)Where a position lands after a change set; assoc puts a position at an insertion before (-1) or after (1) the inserted text.
transformMarkdownChanges(a, b, docLength)Rebases two concurrent change sets over each other and returns [a', b']; at the same insertion point, a goes first.

Overview

  • Passing mode makes the mode controlled; otherwise defaultMode seeds it. All three modes share one Markdown document; preview takes no typing and disables the formatting buttons, but its task boxes still toggle.
  • WYSIWYG renders block by block; when the caret enters bold, a link, inline math or another format, its Markdown marks appear and can be edited in place. / at the start of a block (、 with a Chinese input method) opens the block menu.
  • Selecting text floats a bar with formats, highlight colours, link, comment and AI. The AI's answer streams in and can show its changes word by word; replace, insert it below, try again or discard. Esc discards and restores the selection.
  • An image goes in as a placeholder with progress, then becomes the address uploadImage returns; deleting the placeholder aborts the upload.
  • Every edit commits as a change set, which moves the undo history, comment ranges and collaborators' carets. The local caret is sent at most every 50 ms; a collaborator's name shows for 2.5 s after their caret moves.
  • Both editing modes share shortcuts: Mod+B bold, Mod+I italic, Mod+E inline code, Mod+Shift+X strikethrough, Mod+Shift+H highlight, Mod+K link, Mod+Alt+1–6 headings (Mod+Alt+0 back to a paragraph), Mod+Alt+M comment. The mode switcher is a role="group" of toggle buttons; aria-pressed marks the active mode.

Technologies

  • Source mode is CodeMirror 6; WYSIWYG is a contenteditable block view rendered from marked's tokens and mapped to the source character by character. Code blocks are coloured with the CSS Custom Highlight API; shiki, KaTeX and the AI panel load when first used.
  • The built-in emoji data comes from emojilib 2.4.0 (MIT).
  • Source: packages/tuffex/packages/components/src/markdown-editor/.
查看源码
packages/tuffex/packages/components/src/markdown-editor/index.ts