MarkdownEditor
A Markdown editor with WYSIWYG, source, and preview modes.
Installation
pnpm add @talex-touch/tuffex
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=truefor content written by users or providers. - Use a controlled
modewhen the route or query stores it. - In narrow editors such as comments or release notes, trim the toolbar with
toolbarActionsand turn offslashorbubbleas needed. - Have
uploadImageandaihonoursignal: it aborts when the user deletes the placeholder, stops or discards. - If the host doesn't scan tuffex sources, add
MARKDOWN_EDITOR_ICON_CLASSESfromsrc/toolbar-icons.tsto the UnoCSS safelist.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue / v-model | string | '' | The Markdown source. |
placeholder | string | '' | Placeholder in the editable modes. |
mode | 'wysiwyg' | 'source' | 'preview' | - | Controlled mode. |
defaultMode | 'wysiwyg' | 'source' | 'preview' | 'wysiwyg' | Starting mode when uncontrolled. |
disabled | boolean | false | Disables the toolbar, mode switcher, and editing surface. |
readonly | boolean | false | Blocks editing; reading and mode switching still work. |
sanitize | boolean | true | Sanitizes rendered HTML with DOMPurify; turn off only for trusted content. |
theme | 'auto' | 'light' | 'dark' | 'auto' | Theme; auto follows the page. |
toolbar | boolean | true | Shows the toolbar and mode switcher. |
toolbarActions | MarkdownEditorToolbarActionKey[] | built-in set | Toolbar buttons, in order. |
minHeight | string | number | 220 | Minimum height of the editing area. |
maxHeight | string | number | - | Maximum height of the editing area. |
ariaLabel | string | '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. |
math | boolean | true | Renders $…$ and $$…$$ with KaTeX. |
emoji | boolean | true | Renders :shortcode: and turns on : completion and the emoji picker. |
highlight | boolean | true | ==text== highlights and the highlighter's colours. |
callouts | boolean | true | Shows quotes whose first line is [!NOTE], [!TIP], [!IMPORTANT], [!WARNING] or [!CAUTION] as callouts. |
codeTheme | MarkdownEditorCodeTheme | 'github' | Syntax highlighting preset for code blocks with a language; 'none' leaves them plain. |
slash | boolean | true | In WYSIWYG, / at the start of a block opens the block menu. |
bubble | boolean | true | In WYSIWYG, selecting text floats a formatting bar. |
ai | MarkdownEditorAi | - | AI rewriting on the formatting bar; hidden without it. |
uploadImage | MarkdownEditorUploadImage | - | Uploads pasted, dropped or chosen images and returns their hosted address; without it, the image button asks for an address. |
comments / v-model:comments | MarkdownEditorComment[] | - | Comment threads; comments are hidden without it. |
user | MarkdownEditorUser | { id: 'local', name: labels.you } | The local user: the author of comments, and who collaborators see. |
collaboration | MarkdownEditorCollaboration | - | Transport for real-time collaboration. |
statusBar | boolean | true | Word count, character count and collaborators at the bottom. |
labels | Partial<MarkdownEditorLabels> | English copy | Overrides the interface copy. |
Events
| Event | Params | Description |
|---|---|---|
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
| Method | Description |
|---|---|
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
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.
| Function | Description |
|---|---|
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
modemakes the mode controlled; otherwisedefaultModeseeds 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.
Escdiscards and restores the selection. - An image goes in as a placeholder with progress, then becomes the address
uploadImagereturns; 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+Bbold,Mod+Iitalic,Mod+Einline code,Mod+Shift+Xstrikethrough,Mod+Shift+Hhighlight,Mod+Klink,Mod+Alt+1–6headings (Mod+Alt+0back to a paragraph),Mod+Alt+Mcomment. The mode switcher is arole="group"of toggle buttons;aria-pressedmarks 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