Rich text editor
ProseMirror in ShineUI clothes.
About
Rich text on ProseMirror, in ShineUI clothes, for letters, notes, report commentary and dispute responses. classic shows the full toolbar, compact the memo-field subset and bubble only a selection toolbar. Markdown shortcuts convert as you type, merge fields insert as chips, and html is the form value.
Built from
Usage
Basic
classic shows the full toolbar with the block picker, merge fields and history, and the ProseMirror engine loads once for every editor on the page. Markdown shortcuts convert live: type "## ", "- ", "> " or "```" at a line start; there is no raw markdown mode.
Compact
compact keeps bold, italic, underline, lists and link, for memo fields inside forms.
Bubble
bubble shows no fixed bar. Select text and a sui-mini-toolbar appears above the selection with the four marks.
Invalid
error paints the border in the danger tone, replaces the help line and sets aria-invalid on the document.
Read-only and disabled
readonly locks the document on sunken paper with the lock chip and drops the toolbar; the document still posts. disabled locks it the same way and posts nothing, as a disabled form or fieldset does. invalid paints the danger border alone.
In a form
The element is form associated and the form value is the HTML under name. required holds the submit while the document has no text. Reset returns the document to the HTML it started with.
Events
suiChange fires on every document change with the HTML, the plain text and the word and character counts. suiSelection fires after every transaction with the snapshot, which is how a host mirrors the active marks.
Basicsui-edit-box
Static prose until clicked, then a live ProseMirror mount in the same box; Done serializes back to HTML and emits suiSave, Cancel or Escape discards. The view box is a button named "Edit" plus the label.
Floating toolbarsui-edit-box
toolbar="floating" keeps the exact view-mode box metrics and anchors a sui-mini-toolbar at the caret, so entering edit mode shifts nothing.
Emptysui-edit-box
placeholder shows in view mode while there is no content.
Disabledsui-edit-box
disabled keeps the text readable, opens no editor and posts nothing; a disabled form or fieldset does the same.
Read-only, required and messagessui-edit-box
readonly shows the text as a read-only text box: it keeps its tab stop, opens no editor and still posts. required holds the form while the saved HTML has no text. help and error draw the field lines under the box, and error paints its edge and fails the form.
In a formsui-edit-box
The element is form associated and the form value is the saved HTML under name. Edits count only after Done; Reset returns the box to the HTML it started with.
Eventssui-edit-box
suiSave carries the serialized HTML when Done commits; suiCancel fires with no detail when Cancel or Escape discards. edit(), save() and cancel() drive the same three moves from script.
Basicsui-mini-toolbar
A white capsule positioned at x and y within the nearest relative parent, outside the text flow, shown while open is set. It carries role="toolbar"; the parent owns clamping to its bounds.
Hidden until neededsui-mini-toolbar
The capsule is a contextual surface and starts closed: it renders nothing and is hidden from the accessibility tree until the host sets open, at a selection say; here the button sets it.
Data contract
Documents serialize to ProseMirror JSON. Markdown shortcuts apply as you type. The full page with modes, blocks and rules is the Editor link in the top bar.
How the editor behaves
The rules the chrome follows, so a document written in one place looks like a document written in another.
- Chrome is quiet. 28px ghost toolbar buttons. Active is brand-soft plus navy, never orange. The one orange element is the merge field.
- Merge fields move as one. Dashed accent-soft chips. You cannot type inside one, it selects as a single unit, and it resolves when you send. Never a raw token in prose.
- Markdown is input, not a mode. Type
##,-,>or ``` and the line converts as you type. There is no second raw-markdown pane, so formatting never splits in two. - Sentence case throughout. Toolbar tooltips carry their shortcut, menus are verb first, and the status bar reports words, characters and save state in plain words.
- Locked documents look locked. A read-only document renders on sunken paper with the lock chip, the same treatment a closed period gets.
- Money columns stay aligned. Editor tables inherit the table system: sunken header, money columns right-aligned on fixed-width digits.
The engine, without the chrome
utils/editor-core wraps the ProseMirror packages behind one lazy loader, so the engine downloads on first use and every editor on the page shares one module instance. The schema adds underline and strikethrough marks and the merge_field atom.
To build your own toolbar, call createEditor and drive it. It returns the commands, the serializers and a teardown, and it holds no UI of its own.
Keyboard
| Key | Action |
|---|---|
| Ctrl+B Ctrl+I Ctrl+U sui-rich-text-editor | Bold, italic and underline in the document (Cmd on a Mac). |
| Ctrl+Z Ctrl+Shift+Z sui-rich-text-editor | Undo and redo (Cmd on a Mac; Ctrl+Y also redoes). |
| ArrowLeft ArrowRight sui-rich-text-editor | In the toolbar, move between the buttons in reading order. The toolbar is one tab stop. |
| ArrowDown ArrowUp sui-rich-text-editor | In the block type or merge field menu, move between the options, wrapping. |
| Home End sui-rich-text-editor | In the toolbar, jump to the first or last button; in an open menu, to the first or last option. |
| Enter sui-rich-text-editor | In the link box, applies the address. |
| Escape sui-rich-text-editor | Closes an open menu or link box and returns focus to the document. |
| Enter Space sui-edit-box | On the view box, enters edit mode and focuses the document. |
| Ctrl+B Ctrl+I Ctrl+U sui-edit-box | Bold, italic and underline while editing (Cmd on a Mac). |
| Ctrl+Z Ctrl+Shift+Z sui-edit-box | Undo and redo while editing (Cmd on a Mac; Ctrl+Y also redoes). |
| Escape sui-edit-box | While editing, discards the edit and returns focus to the view box. |
| ArrowRight ArrowLeft sui-mini-toolbar | Move between the buttons and links inside the capsule, which is one tab stop. |
| Home End sui-mini-toolbar | Jump to the first or last button in the capsule. |
API
<sui-rich-text-editor>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
disabled | disabled | boolean | false | Disables the editor: the document locks as with readOnly, and the element posts nothing. |
error | error | stringstring | undefined | Error line under the editor; also paints the border in the danger tone. | |
help | help | stringstring | undefined | Help line under the editor (hidden while error is set). | |
html | html | string | '' | Document as HTML (parsed through the ShineUI schema). Initial and controlled content: the element writes the serialized document back on every change and re-parses when set from outside. The string is read through sanitizeEditorHtml in an inert document before the schema sees it, so a script, a handler attribute or a javascript: link in a stored value never runs; a link the schema will not keep is shown as plain text. A picture is drawn and written back with referrerpolicy="no-referrer". Markdown shortcuts (##, -, >, "```") convert live as you type; there is no raw-markdown mode. |
invalid | invalid | boolean | false | Danger border without a message, for a form that draws its summary elsewhere. |
label | label | stringstring | undefined | Field label above the editor. Without a visible label, a host aria-label or aria-labelledby names the editor the way it names every text field. | |
mergeFields | property only | SuiMergeField[] | [] | Merge-field tokens offered by the "Merge field" menu (property, not attribute). Each inserts as a dashed accent chip that resolves at send time; never type raw {{tokens}}. |
minHeight | min-height | numbernumber | undefined | Minimum height of the writing area in px (default 170; compact 84). | |
name | name | stringstring | undefined | Form control name; the form value is the HTML. Reflected: the form entry is built from the content attribute, and all three framework bindings set properties. | |
placeholder | placeholder | stringstring | undefined | Shown while the document is empty. | |
readOnly | readonly | boolean | false | Locks the document: sunken paper, lock chip, no toolbar. A disabled form does the same. |
required | required | boolean | false | The document must hold some text for the surrounding form to be valid (valueMissing while it is empty; a picture alone counts as empty). A read-only or disabled editor never holds the form. |
variant | variant | "bubble" | "classic" | "compact" | 'classic' | classic = full toolbar (block picker, marks, lists, quote, link, merge fields, history); compact = bold, italic, underline, lists and link for memo fields; bubble = no fixed bar, only a sui-mini-toolbar at the selection. |
Events
| Event | Detail | React | Vue | Description |
|---|---|---|---|---|
suiChange | SuiRichTextChangeDetail | onSuiChange | @sui-change | Fires on every document change with the serialized HTML, plain text and counters. |
suiSelection | SuiEditorSnapshot | onSuiSelection | @sui-selection | Fires after every transaction (selection or document) with the fresh snapshot. A state report, not a value: it fires on every caret move. Does not bubble. |
Methods
| Method | Signature | Description |
|---|---|---|
exec() | exec(cmd: SuiEditorCommandName, arg?: string, arg2?: string) => Promise<void> | Runs an engine command: exec('bold'), exec('block', 'h2'), exec('link', href), exec('mergeField', token, label). |
getSnapshot() | getSnapshot() => Promise<SuiEditorSnapshot | undefined> | Current snapshot (marks, block, counters, history), or undefined before the engine is ready. |
setFocus() | setFocus() => Promise<void> | Moves keyboard focus into the document. |
Slots
| Slot | Description |
|---|---|
status | Right side of the status bar (a save-status line pairs well). |
Types
SuiEditorBlockName typeBlock types the block command and the block picker understand. | |||
|---|---|---|---|
| No members. |
SuiEditorCommandName type | |||
|---|---|---|---|
| No members. |
SuiEditorCommands interfaceThe command map, identical to pm-core.js api.cmd. Every command refocuses the view. | |||
|---|---|---|---|
bold | () => void | ||
italic | () => void | ||
underline | () => void | ||
strike | () => void | ||
code | () => void | ||
block | (name: SuiEditorBlockName | string) => void | ||
bulletList | () => void | ||
orderedList | () => void | ||
outdent | () => void | ||
quote | () => void | ||
unquote | () => void | ||
undo | () => void | ||
redo | () => void | ||
link | (href: string) => void | Wraps the selection in a link. An address that is not http, https, mailto, tel or sms, or a relative path, applies no link at all. | |
unlink | () => void | ||
mergeField | (token: string, label: string) => void | ||
SuiEditorSnapshot interfaceWhich marks / block context the selection sits in, plus document counters. | |||
|---|---|---|---|
marks | { bold: boolean; italic: boolean; underline: boolean; strike: boolean; code: boolean; link: boolean; } | ||
block | string | p, h1..h3, code, or another node type name. | |
list | 'bullet' | 'ordered' | null | ||
quote | boolean | ||
words | number | ||
chars | number | ||
canUndo | boolean | ||
canRedo | boolean | ||
hasSelection | boolean | ||
empty | boolean | True when the document has no visible text. | |
SuiMergeField interfaceA merge-field token the "Merge field" menu offers. Tokens resolve at send time. | |||
|---|---|---|---|
token | string | Machine token, e.g. customer.name (rendered as {{customer.name}} in the menu). | |
label | string | Human label shown inside the chip. | |
SuiRichTextChangeDetail interfaceDetail of suiChange: the serialized document and its counters. | |||
|---|---|---|---|
html | string | ||
text | string | ||
words | number | ||
chars | number | ||
<sui-edit-box>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
disabled | disabled | boolean | false | Disables the box: the text stays readable, nothing opens the editor, and the element posts nothing. A disabled form does the same. |
error | error | stringstring | undefined | Error message under the box; paints its edge in the danger tone, sets aria-invalid and fails the form (customError). | |
help | help | stringstring | undefined | Help text under the box. Replaced by error when set. | |
html | html | string | '' | Current content as HTML (ShineUI schema). Mutable; updated when an edit is saved. View mode and the edit path both read it through sanitizeEditorHtml: tags and attributes the editor cannot produce are dropped, a script or a handler never reaches the page, and a link whose address is not http, https, mailto, tel or sms, or a relative path, is shown as plain text. A picture is shown with referrerpolicy="no-referrer". View mode writes the sanitised markup through the library's Trusted Types policy, shineui. The form value is the string as set, so a value that came from your own store posts back unchanged until an edit is saved. |
invalid | invalid | boolean | false | Danger edge without a message, for a form that draws its summary elsewhere. |
label | label | stringstring | undefined | Label above the box; also names the edit control ("Edit {label}"). | |
minHeight | min-height | number | 56 | Minimum box height in px in both modes. |
name | name | stringstring | undefined | Form control name; the form value is the saved HTML. Reflected: the form entry is built from the content attribute, and all three framework bindings set properties. | |
placeholder | placeholder | stringstring | undefined | Shown in view mode while there is no content. | |
readOnly | readonly | boolean | false | Display only: the text can be read, selected and copied, nothing opens the editor, and the value still posts. The box keeps its tab stop as a read-only text box. Setting it while an edit is open discards the edit. |
required | required | boolean | false | The box must hold some text for the surrounding form to be valid (valueMissing while the saved HTML has none; a picture alone counts as empty). A read-only or disabled box never holds the form. |
toolbar | toolbar | "attached" | "floating" | 'attached' | attached = a small bar above the box; floating = the box keeps its exact view-mode metrics and a sui-mini-toolbar sits at the caret, so entering edit mode shifts nothing. |
Events
| Event | Detail | React | Vue | Description |
|---|---|---|---|---|
suiCancel | void | onSuiCancel | @sui-cancel | Fires when an edit is discarded (Cancel or Escape). |
suiSave | string | onSuiSave | @sui-save | Fires when Done commits an edit; detail is the serialized HTML. |
Methods
| Method | Signature | Description |
|---|---|---|
cancel() | cancel() => Promise<void> | Discards the current edit and returns to view mode. |
edit() | edit() => Promise<void> | Enters edit mode (mounts the engine and focuses it). |
exec() | exec(cmd: SuiEditorCommandName, arg?: string, arg2?: string) => Promise<void> | Runs an engine command while editing: exec('bold'), exec('block', 'h2'). |
save() | save() => Promise<void> | Serializes the live document, leaves edit mode and emits suiSave. |
setFocus() | setFocus() => Promise<void> | Moves keyboard focus to the box without entering edit mode; while editing it focuses the document. sui-form calls this to reach the first invalid control. |
Types
SuiEditorBlockName typeBlock types the block command and the block picker understand. | |||
|---|---|---|---|
| No members. |
SuiEditorCommandName type | |||
|---|---|---|---|
| No members. |
SuiEditorCommands interfaceThe command map, identical to pm-core.js api.cmd. Every command refocuses the view. | |||
|---|---|---|---|
bold | () => void | ||
italic | () => void | ||
underline | () => void | ||
strike | () => void | ||
code | () => void | ||
block | (name: SuiEditorBlockName | string) => void | ||
bulletList | () => void | ||
orderedList | () => void | ||
outdent | () => void | ||
quote | () => void | ||
unquote | () => void | ||
undo | () => void | ||
redo | () => void | ||
link | (href: string) => void | Wraps the selection in a link. An address that is not http, https, mailto, tel or sms, or a relative path, applies no link at all. | |
unlink | () => void | ||
mergeField | (token: string, label: string) => void | ||
<sui-mini-toolbar>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
open | open | boolean | false | Shows the capsule. It is a contextual surface: the host opens it where it is wanted, at a text selection or the caret, and closes it; the toolbar never changes it itself, so it fires no open-state events. Closed, it fades out over the fast duration, then renders nothing visible and is hidden from the accessibility tree. The capsule is a role="toolbar" with one tab stop. Reflected. |
x | x | number | 0 | Left offset in px within the nearest relative parent. |
y | y | number | 0 | Top offset in px within the nearest relative parent. |
Methods
| Method | Signature | Description |
|---|---|---|
setFocus() | setFocus() => Promise<void> | Moves keyboard focus to the capsule's one tab stop while it is open. |
Slots
| Slot | Description |
|---|---|
(default) | Toolbar buttons (sui-rich-text-editor buttons, sui-button, compact actions). |
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.
<sui-rich-text-editor>
Color27
Typography2
Spacing and size5
Radius1
Elevation2
Motion3
<sui-edit-box>
Color23
Typography2
Radius1
Elevation1
Motion2
<sui-mini-toolbar>
Color3
Typography1
Spacing and size1
Radius1
Elevation1
Motion3
Every name above is listed with its value and its role in Foundations.