English
Tiptap Editor
About 3026 wordsAbout 10 min
Audience: Mini-program business developers.
Status: Current public component integration guide.
Reading order: Start with registration, properties, methods, and events in this file. Read the extension lifecycle only when adding custom nodes, and open the image, style, or local-debug guide only for those scenarios.
Introduction
avaui-sub/tiptap-editor is the only public mini-program component for the shared Tiptap editor. It is a thin adapter that forwards properties, commands, and events to the shared runtime.
It does not contain business toolbars, Slash UI, business data, business APIs, or custom Tiptap NodeView code.
Documentation scope
This README is the public mini-program component guide: registration, properties, methods, events, and integration examples. It is not the implementation specification for tiptap-core or the SDK internal runtime. Shared runtime behavior is maintained in the fe-dev-libs repository under docs/tiptap/, as needed:
Shared runtime repository: fe-dev-libs.
core-capabilities.md: implemented core behavior, prerequisites, and exclusions;contracts.md: cross-platform fields, commands, events, and errors;extension-lifecycle.md: where static nodes, private plugins, and shared plugins belong;guides/image-extension.md: image defaults and extension paths;style-governance.md: typography, tokens, and style ownership.
_example is the sole source for the generated pages/avaui-sub/tiptap-editor demo; do not edit the generated page or documentation-site copies directly. Test integration uses a versioned CDN URL from the release notes. SDK-local build diagnostics belong to fe-dev-libs/docs/tiptap/development/local-cdn.md.
Usage
{
"usingComponents": {
"tiptap-editor": "/avaui-sub/tiptap-editor/index"
}
}<tiptap-editor
id="editor"
src="{{sdkUrl}}"
extensions="{{extensions}}"
pluginUrls="{{pluginUrls}}"
extensionConfigs="{{extensionConfigs}}"
value="{{editorValue}}"
readonly="{{readonly}}"
placeholder="{{placeholder}}"
theme="{{theme}}"
styleVars="{{styleVars}}"
viewportStyle="{{viewportStyle}}"
autoGrow="{{autoGrow}}"
bind:ready="onEditorReady"
bind:update="onEditorUpdate"
bind:focus="onEditorFocus"
bind:blur="onEditorBlur"
bind:selection-change="onEditorSelectionChange"
bind:trigger="onEditorTrigger"
bind:height-change="onEditorHeightChange"
bind:error="onEditorError"
/>The _example directory is the complete demo source. Running copy-docs copies its files directly to pages/avaui-sub/tiptap-editor; do not maintain a second page copy manually.
Complete demo
The _example contains index.js, index.json, index.wxml, and index.wxss. It demonstrates the public component, Composer/Slash workflow, configuration-only customBlocks, the built-in image node, and the optional Markdown plugin. This is a complete page demo, not the minimum structure that business pages must copy.
Properties
| Property | Type | Description | Default |
|---|---|---|---|
src | String | Required at runtime; host-provided Tiptap Hera IIFE URL | '' (property placeholder only) |
extensions | Array | Extension names; every item must be a String | ['starter-kit'] |
extensionConfigs | Object | Serializable extension configuration | {} |
pluginUrls | Array | L2 plugin IIFE URLs; each item must be a non-empty absolute http(s) URL | [] |
pluginPolicy | Object | Optional plugin policy: requireHttps and allowedOrigins | {} |
pluginIntegrity | Object | SRI integrity values keyed by absolute plugin URL | {} |
customBlocks | Array | Configuration-only static custom nodes | [] |
triggers | Array | Trigger definitions | null |
value | JSONContent, JSONContent[], String, or null | Initial editor content | null |
contentType | json, html, or markdown | Input content format; Markdown requires the optional adapter | json |
emitMarkdown | Boolean | Include markdown in update details when the Markdown adapter is loaded | false |
readonly | Boolean | Converted to editable: !readonly | false |
placeholder | String | Initial placeholder | '' |
theme | String | Editor theme name | default |
styleVars | Object | CSS variable overrides | {} |
viewportStyle | Object | Optional business-owned H5 viewport height and overflow policy | {} (no height or overflow policy) |
autoGrow | Object or null | Optional { minHeight?, maxHeight? } content-height synchronization | null (host-controlled) |
readyTimeoutMs | Number | Wait for Hera ready before emitting SDK_LOAD_FAILED | 15000 |
src is required at runtime. Its empty-string property default is only a declaration placeholder; the component does not include a built-in SDK and cannot create an editor without a valid URL. An empty src emits SDK_LOAD_FAILED immediately (fatal: true).
For production, configure pluginPolicy: { requireHttps: true, allowedOrigins: [...] } and a real SRI digest in pluginIntegrity for every plugin URL. HTTP LAN URLs remain available for local debugging, but the default empty policy does not add HTTPS or integrity protection. The component only forwards these serializable fields; Hera performs validation and sets the script attributes.
Production CDN constraint: Hera and plugin URLs in the Ava mobile production environment must use the
//a9.fspage.com/FSR/...prefix; other origins can fail the WebView cross-origin policy.www.ceshi112.comis for test integration only and must not ship in a production ava_ui release. Use the exactFSRpath returned by the deployment platform, then verify HTTP 200, the JavaScript Content-Type, and therelease/manifest.jsondigest before switching. Do not infer the production path from the test URL.
The component treats the Hera ready message as the SDK load success signal. If ready does not arrive within readyTimeoutMs, it emits SDK_LOAD_FAILED once (fatal: true) and does not retry. The host decides whether to degrade UI or remount the component. Hera INIT_FAILED / PLUGIN_LOAD_FAILED events are still forwarded as-is.
Where src comes from
Business apps should not build tiptap-hera themselves and should not pick files from a local fe-dev-libs/dist tree. src is the full CDN URL of a released Hera IIFE. Copy it from this guide or the release notes.
| Scenario | Where to get it | Shape |
|---|---|---|
| Production | “Recommended CDN” below, release notes | https://{CDN}/tiptap-hera/{semver}/tiptap-hera.iife.js |
| Test integration | _example, current release notes | https://www.ceshi112.com/fsh5/fe-dev-libs/tiptap-hera/{semver}/tiptap-hera.iife.js |
| Upgrade | New release notes | Replace only {semver}, e.g. 0.1.0 → 0.1.1 |
Path convention:
/tiptap-hera/{semver}/tiptap-hera.iife.js
/tiptap-markdown/{semver}/tiptap-markdown.iife.js- Versioned directories are immutable after publish.
- The component does not embed a default CDN URL and does not use remote config; the host page always passes
srcexplicitly. - Building, uploading, and publishing URLs is the SDK maintainer’s job.
Recommended CDN
Test integration uses the versioned
www.ceshi112.comURLs. Ava mobile production must use the//a9.fspage.com/FSR/...URL returned by the release platform.
Main SDK:
https://www.ceshi112.com/fsh5/fe-dev-libs/tiptap-hera/0.1.0/tiptap-hera.iife.js
Optional Markdown plugin:
https://www.ceshi112.com/fsh5/fe-dev-libs/tiptap-markdown/0.1.0/tiptap-markdown.iife.jsCopy the complete URL from the current release notes and verify HTTP 200, the JavaScript Content-Type, and the published digest before switching. Do not infer the production path from the test URL.
Minimal business integration
Page({
data: {
// Copy from “Recommended CDN” or release notes
sdkUrl: 'https://www.ceshi112.com/fsh5/fe-dev-libs/tiptap-hera/0.1.0/tiptap-hera.iife.js',
// extensions may be omitted; default is ['starter-kit']
},
})<tiptap-editor src="{{sdkUrl}}" bind:ready="onReady" bind:update="onUpdate" bind:error="onError" />For Markdown, pass a separate plugin URL (do not fold it into src):
Page({
data: {
sdkUrl: 'https://www.ceshi112.com/fsh5/fe-dev-libs/tiptap-hera/0.1.0/tiptap-hera.iife.js',
extensions: ['starter-kit', 'markdown'],
pluginUrls: [
'https://www.ceshi112.com/fsh5/fe-dev-libs/tiptap-markdown/0.1.0/tiptap-markdown.iife.js',
],
contentType: 'markdown', // only when initial value is a Markdown string
emitMarkdown: true, // only when update/getContent should include markdown
},
})Initialization properties are read when the component is attached. Use setContent for runtime content replacement. They cross the WebView boundary and must be serializable. Invalid declared field types emit INIT_FAILED. Load complex extensions through pluginUrls, then enable them by string name in extensions.
Built-in extensions and default capabilities
When extensions is omitted or empty, the editor uses:
extensions: ['starter-kit']The current starter-kit is based on @tiptap/starter-kit@3.30.0 and includes:
| Category | Default capabilities |
|---|---|
| Document structure | doc, paragraph, text |
| Headings | heading, supporting h1 through h6 |
| Block nodes | blockquote, codeBlock, horizontalRule |
| Lists | bulletList, orderedList, listItem |
| Text marks | bold, italic, strike, code, underline |
| Links | link |
| Editing behavior | hardBreak, dropcursor, gapcursor, trailingNode |
| History and keymaps | undoRedo, listKeymap |
The default configuration therefore supports regular text, headings, bold, italic, strike, inline code, underline, lists, blockquotes, code blocks, horizontal rules, links, undo, and redo.
The core also exposes these extension names:
| Name | Enabled by default | Description |
|---|---|---|
link | No (already included in starter-kit) | Standalone use; core defaults to openOnClick: false, autolink: true, and markdownLinks: true |
underline | No (already included in starter-kit) | Standalone use; options come from the official extension |
image | No | Image node; block-level by default, Base64 disabled, resizing disabled |
placeholder | No | Empty-document placeholder; automatically added when the top-level placeholder prop is provided |
Do not repeat link and underline when using the default StarterKit:
// Do not duplicate these extensions
extensions: ['starter-kit', 'link', 'underline']To configure the Link extension included inside StarterKit, use nested config:
extensions: ['starter-kit'],
extensionConfigs: {
'starter-kit': {
link: {
openOnClick: false,
autolink: true,
markdownLinks: true,
},
},
}Images must be enabled explicitly:
extensions: ['starter-kit', 'image'],
extensionConfigs: {
image: {
inline: false,
allowBase64: false,
},
}The core provides the image node, attributes, and serialization only. Image selection, upload, compression, authentication, preview, retry, and business errors remain the responsibility of the business. table, mention, file preview, and complex NodeViews are not built-in capabilities; load them as L2 plugins through pluginUrls.
Custom block classes
The component enables addGlobalClass: true. A simple static node can declare instance fields such as class and icon in customBlocks.schema, then interpolate them into a safe template class without changing the SDK or creating a plugin package:
customBlocks: [{
type: 'businessIconInline',
inline: true,
template: '<span class="{{class}}"><span class="{{icon}}"></span><span>{{label}}</span></span>',
schema: { class: 'string', icon: 'string', label: 'string' },
className: 'business-icon-inline',
}]
editor.execCommand('insertContent', {
type: 'businessIconInline',
attrs: {
class: 'business-icon-inline--sortable',
icon: 'fxui_all tuodongpaixu',
label: 'Sortable inline',
},
})className is the fixed definition-level root class. The template {{class}} is instance data; the core merges both class lists and removes duplicates. The core only transports and serializes classes and does not bundle FXUI icon styles. For a block node, set inline: false and use one <div> root. See the Class block and Class inline actions in _example for complete examples.
Height and scrolling
The default is host-controlled: when autoGrow is omitted, the component does not measure content or change the height declared by the business. To start at a business minimum and grow with content, pass:
Page({
data: {
editorAutoGrow: {
minHeight: 140,
maxHeight: 480,
},
},
})<tiptap-editor
id="editor"
style="display:block;width:100%;"
src="{{sdkUrl}}"
autoGrow="{{editorAutoGrow}}"
bind:height-change="onEditorHeightChange"
/>| Configuration | Behavior |
|---|---|
Omit autoGrow | The host owns height; existing fixed-height behavior is unchanged |
autoGrow: {} | Start from natural content height and grow without a maximum |
{ minHeight: 140 } | Never shrink below 140px; otherwise grow and shrink with content |
{ minHeight: 140, maxHeight: 480 } | Grow between 140px and 480px; scroll inside Lego H5 after the maximum |
minHeight and maxHeight are numeric H5 CSS pixel values. The component automatically applies the height reported by Hera. height-change is optional and exists for layout coordination, telemetry, or diagnostics; business pages do not need to set the height again.
After maxHeight is reached, Hera constrains the editor root and enables vertical scrolling so content cannot paint over subsequent host UI. When the content becomes shorter than the maximum, the component resumes natural shrinking.
For a fixed height with internal scrolling, omit autoGrow, set the component height, and pass viewportStyle explicitly:
<view class="editor-wrap">
<tiptap-editor
id="editor"
style="display:block;width:100%;height:140px;"
src="{{sdkUrl}}"
viewportStyle="{{editorViewportStyle}}"
/>
</view>.editor-wrap {
width: 100%;
height: 140px;
overflow: hidden;
}Page({
data: {
editorViewportStyle: {
height: '100%',
minHeight: '0',
overflowX: 'hidden',
overflowY: 'auto',
webkitOverflowScrolling: 'touch',
},
},
})autoGrow and a non-empty viewportStyle are mutually exclusive; combining them emits fatal INIT_FAILED instead of silently choosing precedence. Do not set a fixed component or wrapper height while auto grow is enabled. Web runs in the same DOM and does not need this bridge option: omit fixed height for natural growth, or use business CSS for a fixed viewport.
Content format semantics
contentType describes how input content is parsed. It does not control the output fields. emitMarkdown controls whether update and getContent() include the markdown field. They are independent switches: setting contentType to markdown does not implicitly enable emitMarkdown.
A complete mini-program Markdown initialization must load and enable the optional adapter:
Page({
data: {
sdkUrl: 'https://cdn.example.com/tiptap-hera/0.1.0/tiptap-hera.iife.js',
extensions: ['starter-kit', 'markdown'],
pluginUrls: [
'https://cdn.example.com/tiptap-markdown/0.1.0/tiptap-markdown.iife.js',
],
editorValue: '# Markdown content',
contentType: 'markdown',
emitMarkdown: true,
},
})<tiptap-editor
id="editor"
src="{{sdkUrl}}"
extensions="{{extensions}}"
pluginUrls="{{pluginUrls}}"
value="{{editorValue}}"
contentType="{{contentType}}"
emitMarkdown="{{emitMarkdown}}"
/>The initialization contentType applies only to the initial value/content. An explicit contentType in a runtime setContent or insertContent command applies only to that call and takes precedence for that call:
editor.execCommand('setContent', {
content: '<p>HTML content</p>',
contentType: 'html',
})When a runtime command omits contentType, the SDK does not implicitly inherit the initialization value. Structured objects may omit contentType; string input must declare it explicitly. The SDK never guesses string formats.
Business themes should override declared --tt-* tokens through styleVars instead of targeting internal .ProseMirror selectors from page WXSS.
styleVars: {
'--tt-font-size-h4': '16px',
'--tt-font-size-h5': '15px',
'--tt-font-size-h6': '14px',
'--tt-line-height': '1.5',
'--tt-line-height-h1': '1.2',
'--tt-line-height-h2': '1.3',
'--tt-line-height-h3': '1.4',
}Heading-level line heights from h1 through h6 fall back to --tt-line-height when omitted, so all six levels default to 1.6. Override only the matching --tt-line-height-h1 through --tt-line-height-h6 tokens that need different values.
The platform defaults for h4, h5, and h6 are 16px, 15px, and 14px. Businesses can override them through the matching --tt-font-size-h4/h5/h6 tokens.
Content API
The component does not expose separate getText(), getJSON(), getHTML(), or getMarkdown() methods. Use getContent() and read the field required by the business:
const content = this.editor.getContent()
const plainText = content && content.text
const json = content && content.json
const html = content && content.html
const markdown = content && content.markdownThe payload shape is:
{
text: string,
json: JSONContent,
html: string,
markdown?: string,
}text, json, and html are always available. markdown is included only when the Markdown adapter has loaded successfully and emitMarkdown is true. The update event and getContent() use the same payload shape.
On mini-program, getContent() returns the most recent payload received through update. It may return null before the first update; wait for ready and the initial update, or read the payload directly from the update event.
onEditorUpdate(event) {
const { text, json, html, markdown } = event.detail
}Methods
| Method | Description |
|---|---|
execCommand(action, data) | Send a command to the SDK |
setContent(content, contentType?) | Replace content; structured objects may omit contentType, strings must declare it |
getContent() | Return the latest content received through update |
focus() | Focus the editor |
blur() | Blur the editor |
Structured objects may omit contentType. String input must declare it:
this.editor.setContent({ type: 'doc', content: [{ type: 'paragraph' }] })
this.editor.setContent(JSON.stringify(doc), 'json')
this.editor.setContent('<p>HTML content</p>', 'html')
this.editor.setContent('# Markdown content', 'markdown')The second argument applies only to that call and does not change the initialization contentType. The command form remains available:
this.editor.execCommand('setContent', {
content: '# Markdown content',
contentType: 'markdown',
})Raw Tiptap JSON documents and nodes are also accepted. An object with a string type is always treated as raw content, so its own content array and root attributes are preserved:
this.editor.execCommand('setContent', {
type: 'doc',
attrs: { source: 'business' },
content: [{ type: 'paragraph' }],
})Use the { content, contentType } envelope when the input format must be explicit. The same distinction applies to insertContent.
Commands
The public execCommand(action, data) commands are:
| action | data | Description |
|---|---|---|
setContent | { content, contentType? } or content | Replace the document |
focus / blur | None | Focus or blur the editor |
undo / redo | None | Undo or redo |
toggleBold / bold | None | Toggle bold |
toggleItalic / italic | None | Toggle italic |
toggleStrike / strike | None | Toggle strike-through |
toggleUnderline / underline | None | Toggle underline |
toggleHeading | { level?: 1..6 } | Toggle a heading |
toggleBulletList / bulletList | None | Toggle a bullet list |
toggleOrderedList / orderedList | None | Toggle an ordered list |
toggleBlockquote | None | Toggle a blockquote |
toggleCode | None | Toggle inline code |
insertContent | { content, contentType? } or content | Insert JSON, HTML, or Markdown |
insertCustomBlock | { type, data } | Insert a custom block |
setIsReadonly | { isReadonly: boolean } | Change read-only state |
applyTrigger | ApplyTriggerPayload | Apply a trigger range and insert content |
applySlash | ApplyTriggerPayload | Command-level alias for applyTrigger |
New business integrations should use the primary command names. See the shared public contract for complete payload fields, trigger semantics, and error codes.
Once an L2 plugin is enabled, its commands use the same public component entry:
this.selectComponent('#editor').execCommand('toggleBadgeSetActive', {
id: 'account-badge',
active: true,
})Stable Core actions enter the Core branch first and never reach the plugin fallback. Otherwise, the runtime resolves the action only from commands registered on the current editor instance. The plugin owns its command names, payload types, and runtime validation, and must not declare names used by stable Core commands. The component does not maintain a second business-command registry. A missing or throwing plugin command emits non-fatal COMMAND_FAILED. A false result is a handled no-op and does not emit an error.
Events
| Event | Detail |
|---|---|
ready | SDK/core version and protocol metadata |
update | { text, json, html }, optionally markdown |
focus | { source: 'editor' } |
blur | { source: 'editor' } |
activeMarks | Current active mark state |
selection-change | { hasSelection, marks, rect }; rect is a CSS-pixel rectangle relative to the editor root for business-owned floating menus |
trigger | Headless trigger data such as name, phase, query, range, and session ID |
height-change | { height, contentHeight, constrained }; emitted after the component applies an auto-grow height |
error | Host SDK load failure, runtime init failure, or plugin loading error |
trigger is the only input-trigger event. Its name can be slash, mention, or a business-defined name; pages do not need separate mini-program events for different trigger types.
Business-owned floating menu
The component does not provide a floating menu. A business page can listen to selection-change, render its own WXML menu, and call execCommand on the component instance:
<view class="editor-wrap">
<tiptap-editor
id="editor"
src="{{sdkUrl}}"
bind:selection-change="onEditorSelectionChange"
/>
<view
wx:if="{{bubbleMenu.visible}}"
class="business-bubble-menu"
style="left: {{bubbleMenu.left}}px; top: {{bubbleMenu.top}}px;"
>
<view data-action="toggleBold" bindtap="onBubbleAction">B</view>
</view>
</view>e.detail.rect is relative to the editor root. Positioning, styling, visibility, and button ownership remain with the business page. When there is no selection, hasSelection is false and rect is null.
Custom extensions
Custom Tiptap Nodes/Extensions are L2 plugins. Keep their schema, NodeView, interaction, and serialization in an independent plugin package. The page explicitly enables a plugin through extensions and pluginUrls; this wrapper only forwards those values.
The Tiptap MIT notice is maintained in ava_ui/THIRD_PARTY_NOTICES.md.
