Skip to main content

RichTextEditor

import { RichTextEditor } from '@react-x11/components/rich-text-editor';

<RichTextEditor
defaultValue={note.body}
onChange={(ev) => save(ev.value)}
placeholder="Write something…"
toolbar
style={{ flexGrow: 1 }}
/>;

A WYSIWYG editor — notes, comments, a chat composer, a document pane — whose value is markdown unless format says otherwise.

It is ProseMirror's model with a view of this package's own. The schema, the document, transactions, commands and the plugin system are ProseMirror's modules, unmodified. The view — in a browser, prosemirror-view's EditorView over contenteditable — is RichEditorView, which draws with react-x11 and is shaped like EditorView, so a plugin, a command or an input rule is handed what it was written to expect. The PRD has the survey behind that and the full ledger of what a plugin can count on.

It registers two host elements at its module scope: <richeditor> (RICH_EDITOR_ELEMENT), the root — focus, keys, composition, and the one text a screen reader reads — and <richeditortext> (RICH_EDITOR_TEXT_ELEMENT), one per textblock: <richtext> plus a caret and a selection band the view sets directly, so a blink or a drag re-renders nothing.

Imported from its subpath only. Every other component is also exported from @react-x11/components; this one is not, because ProseMirror's declarations name DOM globals, and an app that imports anything from the barrel loads every re-exported module's declarations. With skipLibCheck (most templates' default) or "dom" in lib there is nothing to do; otherwise declare the four names in src/rich-text-editor/dom-globals.d.ts, which ships in the package.

Props

The props are one ladder on one element: each group below is an opt-in on top of the one before, and none of them replaces another.

The document

PropTypeNotes
defaultValuestring | NodeUncontrolled initial document: a string in format, or a ProseMirror node.
valuestringControlled. A value different from the one the editor last reported replaces the document — a reset: not undoable, the caret kept near where it was. Handing back what onChange reported changes nothing, and costs a comparison.
onChange(ev: RichTextEditorChangeEvent) => voidEvery change to the document; the selection moving is onSelectionChange. ev.value is serialized the first time it is read.
format'markdown' | 'html' | 'text'What value, defaultValue and ev.value are written in. Default 'markdown'.
namestringEchoed on every event.

Behaviour

PropTypeNotes
placeholderstringDrawn in an empty document, after the caret. Also the accessible name when there is no aria-label.
readOnlybooleanFocusable, selectable and copyable; nothing edits.
disabledbooleanInert, out of the tab order, dimmed.
autoFocusboolean
onSubmit(ev: RichTextEditorEvent) => voidMod-Enter — or plain Enter, with submitOnEnter.
submitOnEnterbooleanEnter submits and Shift+Enter breaks the line: a chat composer. Inside a list or a code block Enter still edits.
onSelectionChange(ev: RichTextEditorEvent) => void
onLink(href: string, ev: MouseEvent) => voidA link was activated — Mod-click while editing, a plain click when read-only. The editor never opens anything by itself.
suggestionsSuggester[]Lists that open at a trigger character as its word is typed — @ for people, # for issues, / for a block menu. See Suggestions.

Chrome and looks

PropTypeNotes
toolbarboolean | ToolbarEntry[] | (editor: RichTextEditorHandle) => ReactNodetrue for DEFAULT_TOOLBAR, less whatever the schema cannot do; an array to pick and order built-in names, '|' separators and ToolbarItems of your own; a function to render your own bar, handed the editor.
fontSizenumberBase text size. Default: the theme's.
fontFamilystringDefault 'sans-serif'.
monoFamilystringInline code and fences. Default 'monospace'.
markStylesRecord<string, MarkStyle>How a mark looks, by mark name: a run style merged over the built-in look, or a function of the mark and the style under it.
decorationClassesRecord<string, RunStyle>How a decoration's class looks — see Decorations.
highlightbooleanSyntax colouring in fenced code. Default true.
resolveLanguage(tag: string) => Language | nullA tokenizer for a fence tag the built-ins do not cover — the same seam as <Markdown>'s.
renderImage(image: ImageInfo) => ReactNodeAn image alone in its paragraph, drawn by you. Without it an image is its alt text: the editor fetches nothing.
styleStyle | Style[]The frame: width, height, maxHeight, flexGrow, border, background. Given no height the editor is as tall as its content up to maxHeight, and scrolls past it — a composer grows as it is typed in.
styles{ toolbar?: Style; content?: Style }The parts inside the frame.
virtualboolean | 'auto'Draw only the top-level blocks near the viewport — see Long documents. Default 'auto': past 200 of them.

ProseMirror's seams

PropTypeNotes
schemaSchemaThe document model. Default: schema from this module — GFM, in ProseMirror's reference names.
markdownMarkdownCodecA markdown codec for a schema the built-in one cannot read or write.
pluginsPlugin[]Ahead of the editor's own, so a keymap here wins over the defaults. Give the array a stable identity: a new one is a reconfigure — cheap, and every plugin instance still in it keeps its state, the undo history included.
historybooleanThe undo history. Default true.
inputRulesbooleanThe markdown shortcuts. Default true.
keymapbooleanThe formatting shortcuts. Default true.
typographybooleanSmart quotes, and as they are typed. Default false.
editorPropsEditorPropsProseMirror's view props — handleKeyDown, handlePaste, decorations, transformPasted… — without a plugin to hold them. See What a plugin can count on.
nodeViewsRecord<string, ComponentType<NodeViewProps>>A React component per node type.
stateEditorStateThe top rung: the app owns the state, instead of value/defaultValue. The editor's default plugins are then the app's to include — defaultPlugins(schema).
dispatchTransaction(tr: Transaction) => voidWith state: every transaction, for the app to apply and pass back — ProseMirror's own contract.

Focus and the rest

PropTypeNotes
refRef<RichTextEditorHandle>
onKeyDown(ev: KeyboardEvent) => voidRuns before the editor does anything with the key; preventDefault() keeps it from the editor entirely.
onMouseDown, onFocus, onBlur
aria-labelstringDefault: the placeholder.
data-testnamestringFor react-x11/test's queries.

RichTextEditorHandle

The ref. Everything the toolbar does, it does through this.

MemberNotes
viewThe EditorView-shaped view: dispatch, state, someProp, posAtCoords… — what a ProseMirror command is handed.
state, schemaCurrent.
getValue(format?)The document, in format or the editor's own.
setValue(value, { format?, addToHistory? })Replace the document with a string or a node. Not undoable unless addToHistory says so, the way a form reset is not.
insertContent(value, format?)Insert markdown, HTML, text or a node at the selection; a single paragraph joins the one the caret is in.
run(command), can(command)Run a ProseMirror command, or ask whether it could run now — what greys a toolbar button.
isActive(name, attrs?)A mark on the selection, or a node (with attributes) around it: isActive('strong'), isActive('heading', { level: 2 }).
toggleMark(name, attrs?)
setBlock(name, attrs?)A textblock type — back to a paragraph when it already is one.
toggleList(name), toggleWrap(name)toggleList('bullet_list'), toggleWrap('blockquote').
undo(), redo()
editLink()Open the link editor on the selection — what Mod-K does.
focus(), blur()

Events

onChange hears a RichTextEditorChangeEvent: type: 'change', value (the document in format, serialized when first read — a listener that only needs to know that something changed never pays for it), doc, state, the transactions that made the change, name and target (the handle).

onSubmit and onSelectionChange hear a RichTextEditorEvent: type: 'submit' | 'selectionchange', value, state, name, target.

Keys

Mod is Ctrl on the X11 backend and Cmd on the macOS one — the backend's convention, whatever host the process runs on.

KeysDoes
Mod-B, Mod-I, Mod-`, Shift-Mod-XBold, italic, inline code, strikethrough
Mod-KLink the selection: a field at the selection asks for the target, Enter applies, Escape cancels, an empty target unlinks
Mod-Z; Shift-Mod-Z or Mod-YUndo; redo
Mod-Alt-0; Mod-Alt-1 … 6; Mod-Alt-CParagraph; heading 1–6; code block
Shift-Mod-8, Shift-Mod-7, Shift-Mod-9Bulleted, numbered, task list
Ctrl->Quote
Mod-_Divider
Shift-Enter, Mod-EnterLine break (Mod-Enter submits instead when there is an onSubmit)
EnterNew paragraph; in a list a new item, and on an empty item the end of the list
Tab, Shift-TabIn a list, nest and un-nest the item; in a code block, indent and dedent; in a table, the next and previous cell (Tab in the last cell adds a row). Anywhere else Tab is not the editor's and moves focus on
Escape, then TabLeave the editor, from anywhere. An Escape that closes a suggestion list closes only the list
Arrows, Home/End, PageUp/PageDownMove, by grapheme and by visual line; Home and End go to the ends of the line as it wraps. Shift extends
Ctrl-arrows, Ctrl-Home/End (X11)By word; to the ends of the document
Alt-arrows, Cmd-arrows (macOS)By word; to the ends of the line, or of the document
Backspace, DeleteBy grapheme; with Ctrl (X11) or Alt (macOS) by word, with Cmd (macOS) to the line's start
Mod-A, Mod-C, Mod-X, Mod-V, Shift-Mod-VSelect all, copy, cut, paste, paste as plain text
Shift-Delete, Ctrl-Insert, Shift-Insert (X11)Cut, copy, paste

With the pointer: a press places the caret, Shift extends, a double press selects a word and a triple one the block, a drag selects (and scrolls at the edges), a drag that starts on the selection moves it (Drag and drop), a press on a divider — any block that is a leaf — selects it whole, a press on a task's box toggles it, and the right button opens core's edit menu (Copy and Select All only, when read-only). On X11 a selection is offered as PRIMARY and the middle button pastes PRIMARY at the pointer.

Markdown shortcuts

Typed at the start of a paragraph: # to ###### (headings), > (quote), - , * or + (bulleted list), 1. (numbered list, starting where the number says), [ ] or [x] (a task — in a list item too), ``` or ```lang and a space (a code block), and --- (a divider). Anywhere: **bold**, __bold__, *italic*, _italic_, `code` and ~~strike~~ take their mark as the closing delimiter is typed. Backspace straight after any of them gives the characters back. inputRules={false} turns them all off.

Suggestions

suggestions opens a list at a trigger character as its word is typed — the @ of a mention, the # of an issue, the / of a block menu:

<RichTextEditor
suggestions={[
{ char: '@', items: people }, // an array: filtered as the name is typed
{ char: '#', items: ({ query }) => searchIssues(query) }, // a function: asked
{ char: '/', startOfLine: true, items: blockMenu }, // rows that are commands
]}
/>

A Suggester is a trigger, its rows, and three options:

FieldNotes
charThe trigger. It opens a list at the start of a word — after a space, an opening bracket or quote, or at the start of a block — so ada@example.com is an address. Never in code.
itemsAn array of SuggestionItems, which the editor filters as the word is typed: labels that start with the query first, then labels with a word that does, then any that contain it. Or a function, handed { query, char, state }, whose rows are shown as it returns them; it may return a promise, and an answer that arrives after the query has moved on is dropped.
startOfLineOnly at the start of a textblock — a block menu.
allowSpacesThe query may hold spaces — a full name. Two spaces in a row end it either way.
renderItem(item, { selected, query }) => ReactNode: a row of your own — an avatar, a presence dot, a shortcut drawn as keys. The editor still draws the highlight behind it (selected says which row sits on the accent) and takes a press on it, and an array is still filtered on label. Suggester<Item> takes your own row type, so renderItem is handed it typed.

A SuggestionItem is a row, and what taking it does:

FieldNotes
labelThe row — and what an array is filtered on.
detailMuted text after the label: a handle, a description, a shortcut.
insertWhat replaces the trigger and its word: text, or a node of the schema — text carrying a link mark, a mention node of your own schema. Default: the trigger and the label. A space follows it, unless one already does.
commandRun instead of inserting anything, where the trigger and its word were: a block menu's "Heading 1". It is one undo step with the deletion.

The list hangs below the trigger and follows its text as the document scrolls. It takes plain keys, and only while it has rows: Up and Down move the highlight, PageUp and PageDown by a page, Enter and Tab take the row — so a submitOnEnter composer takes the mention rather than sending the message — and Escape closes the list. A press on a row takes it, and the caret stays where it was. One undo straight after a choice gives back what was typed.

A list opens as its trigger's word is typed, never when the caret merely moves into one. The value is markdown, so a mention stays text — @ada — and a document soon holds many words that start with a trigger: clicking into one opens nothing, typing in it does. A list closed with Escape, or by a choice, stays closed for that trigger until its character is deleted. While the word is typed it carries the decoration class suggestion, drawn in the accent colour; decorationClasses={{ suggestion: … }} restyles it.

A mention is text, or text with a mark. The default schema is GFM, and markdown has no mention — GitHub keeps @ada as text too. For a mention that links, insert: schema.text('@ada', [schema.marks.link.create({ href })]); for a mention node, hand the editor a schema that has one and insert that.

The list is plugin state. The prop installs suggestions(suggesters) — one plugin per editor, holding every trigger — and the popup is drawn from nothing but suggestionState(state): { char, query, from, to, items, selected }. An app that owns the EditorState puts the same plugin in its plugins, ahead of the defaults so a row takes Enter before the keymap does, and gets the same list:

EditorState.create({
schema,
plugins: [
suggestions([{ char: '@', items: people }]),
...defaultPlugins(schema),
],
});

The list's commands are exported for a toolbar, a test, or a list of your own: acceptSuggestion(index?), selectSuggestion(index) and dismissSuggestion; filterSuggestions(items, query) is the editor's own filter, and suggesterFor(state) is the suggester whose list is open.

Tables

A table is typed in like any text — Tab and Shift-Tab go to the next and the previous cell, and Tab in the last cell adds a row — and its rows and columns go in and out from the toolbar. Table puts a table where the caret is, a header row and two more of three cells, with the caret in its first cell; while the caret is in a table the bar also shows Row above, Row below, Column before, Column after, Delete row, Delete column and Delete table, and hides them again when it leaves. alignLeft, alignCenter and alignRight are there by name, for a bar of your own: they set a column's alignment, and pressed again take it off. An item of your own can come and go the same way, with visible(state).

The commands are prosemirror-tables' own, exported for a toolbar of your own and for handle.run: insertTable(rows?, cols?), addRowBefore, addRowAfter, addColumnBefore, addColumnAfter, deleteRow, deleteColumn, deleteTable, setColumnAlign(align), and — to light an alignment button — columnAlign(state).

A markdown table keeps markdown's shape. Markdown's table has one header row, and it is the first; its alignment belongs to a column, not to a cell. So a row added above the header becomes the header, deleting the header hands it to the row below, and a new cell takes its column's alignment: what the editor shows is what the markdown reads back as. A table of another shape — a header column pasted from HTML, say — is left the way prosemirror-tables leaves it. tableRepair(), one of the default plugins, keeps every table rectangular, which a paste can break.

Columns are as wide as their content, capped so that one long cell cannot starve the rest, and a table narrower than the document stays narrow — the way <Markdown> sizes the same table. Wider than the document, every column gives up the same share and its text wraps. A cell is measured when it changes and not otherwise, so typing in a large table costs what typing in a paragraph does.

Collaboration

The editor runs y-prosemirror — the binding a Yjs-backed ProseMirror editor uses — as it is: its sync plugin binds the document to a Y.XmlFragment, its undo plugin takes the place of the editor's own history with one that takes back only this user's edits, and its cursor plugin shows where everyone else is. The one piece that is this editor's is remoteCaret, the cursor builder:

import {
ySyncPlugin,
yCursorPlugin,
yUndoPlugin,
undo,
redo,
} from 'y-prosemirror';
import { keymap } from 'prosemirror-keymap';
import {
RichTextEditor,
remoteCaret,
} from '@react-x11/components/rich-text-editor';

const plugins = [
ySyncPlugin(ydoc.getXmlFragment('prosemirror')),
yCursorPlugin(provider.awareness, { cursorBuilder: remoteCaret }),
yUndoPlugin(),
keymap({ 'Mod-z': undo, 'Mod-y': redo, 'Shift-Mod-z': redo }),
];

<RichTextEditor plugins={plugins} history={false} />;

Give the plugins a stable identity, and turn history off: two undo histories over one document fight. The document is the fragment's — the sync plugin replaces whatever the editor started with — so value and defaultValue have nothing to say here, and onChange still hears every change, a collaborator's included.

A collaborator's caret is a bar and a small flag in their awareness colour (user.color, a #rrggbb), drawn beside the editor's own caret and set the same way, re-rendering nothing; their selection is lit by the cursor plugin's own decoration. y-prosemirror's default cursor builder makes a DOM element, which has nothing to draw it here — remoteCaret is the builder this editor reads. The name is not drawn beside the caret yet.

Drag and drop

A press on the selection and a drag takes it along; let go, and it goes in where the drop caret was drawn — moved, or copied with Ctrl (Option on the Mac backend) held. A press on the selection that does not move is a click, and puts the caret there. The drag carries what a copy does — its HTML and its text, for another application — and, to another editor in the app, the content itself, so a list stays a list.

A drop is read the way a paste is, at the point it lands: HTML by the schema's own rules, text a paragraph to a line. Files dropped from a file manager go in as what markdown can say of them: an image as an image, drawn by renderImage — the editor reads nothing from the file — and anything else as its name, linked to it. editorProps.handleDrop — or a plugin's — sees a drop first, with a DOM-shaped event whose dataTransfer answers what was read and whose files are { name, path, uri }: a path to read, not a browser's File. A read-only editor can be dragged from, to copy, and takes no drop.

Long documents

A document of more than 200 top-level blocks — a book, a log, a long README — draws only the blocks near the viewport. The ones above and below are space the scrollbar measures: each block the height it had when it was last drawn, or a guess the drawn ones teach. Scrolling draws what comes into view. The caret, a click and the keys work as they do anywhere, and a key that needs a block outside the window — the document's end, a Down pressed after scrolling away from the caret, a letter typed there — has it drawn and scrolled into view first.

virtual turns the window on for any document (true) or off for every one (false). It matters only in an editor given a height to scroll in: one that grows with its content has nothing out of view. The window is of top-level blocks, so one enormous list or table is drawn whole.

The document model

schema is GFM in the names of ProseMirror's reference schemas, so commands written against prosemirror-schema-basic and -list work unchanged:

  • Nodes: doc, paragraph, heading (level), blockquote, code_block (params, the fence's info string), horizontal_rule, bullet_list and ordered_list (order; both tight), list_item (checked: null for an ordinary item, a boolean for a task), table, table_row, table_header and table_cell (prosemirror-tables' cell attributes, plus align), text, image (src, alt, title), and hard_break.
  • Marks: link (href, title; not inclusive, so typing at a link's end does not extend it), em, strong, strike, code.

nodes and marks are exported as specs, to build a schema of your own from. The markdown codec and the renderer also know TipTap's names for the same things (bulletList, listItem, codeBlock, bold, italic…), so a schema built from TipTap's extension specs reads and writes markdown too; anything else is drawn from what its own toDOM says it is, and markdown takes a codec for what the built-in one cannot say.

Markdown out is written to be read back as the same document — the model test round-trips a generated corpus and fails on any document that loses text or changes on a second trip. What markdown cannot express at all is dropped rather than mangled.

Decorations

A plugin's decorations are drawn, because a decoration is state rather than DOM — with one limit, which is a widget:

  • Inline decorations style the text they cover: their class through decorationClasses, their style attribute's CSS (colour, background, weight, style, decoration), and spec.run, a RunStyle, when a plugin written for this editor wants to say it directly.
  • Node decorations colour the block's background the same three ways.
  • Widgets are drawn when their spec carries text (styled by spec.run, placed by side); a widget described only by a toDOM function has nothing to draw it with and is skipped. The placeholder and a composition's preedit are widgets of the view's own.
new Plugin({
props: {
decorations: (state) =>
DecorationSet.create(state.doc, [
Decoration.inline(from, to, { class: 'todo' }),
Decoration.widget(pos, () => document.createElement('span'), {
text: '@',
run: { color: '#888' },
}),
]),
},
});

What a plugin can count on

The view a plugin is handed is RichEditorView. It has state, dispatch, props, someProp (ProseMirror's order: direct props, then direct plugins, then the state's), setProps, update, updateState, editable, composing, hasFocus, focus, destroy, posAtCoords, coordsAtPos, endOfTextblock, pasteText and pasteHTML. dom is the root element — a react-x11 node, not an HTMLElement — and there is no domAtPos, nodeDOM or posAtDOM, because there is no DOM to answer with. It does answer addEventListener for the focus events — focus, blur, focusin and focusout, what y-prosemirror's cursor plugin listens for — and docView is truthy while the editor is mounted. Plugin views are made once the editor is mounted, so a plugin view finds view.dom from its first call.

View props honoured: handleKeyDown, handleKeyPress, handleTextInput, handleClickOn/handleClick and their double and triple forms, handlePaste, handleScrollToSelection, decorations, editable, dispatchTransaction, and the whole clipboard family — transformCopied, clipboardSerializer, clipboardTextSerializer, transformPastedHTML, transformPastedText, clipboardParser, clipboardTextParser, domParser and transformPasted — and handleDrop. Not honoured: handleDOMEvents, DOM nodeViews (use the component's nodeViews), markViews, attributes and createSelectionBetween.

An event a prop receives is DOM-shaped: handleKeyDown gets a DomKeyEventkey, code, keyCode and the modifiers, named from the keysym, so Mod-b matches Ctrl+B whatever layout typed it — and a click handler gets clientX/clientY in the same logical window coordinates as posAtCoords. TypeScript still calls them KeyboardEvent and MouseEvent; cast to DomKeyEvent to read one.

Node views

A node view here is a React component, handed NodeViewProps: node, getPos() (a function — positions move with every edit before the node, and the view is not re-rendered for that), selected, children (the node's content, rendered — editable text for a textblock, blocks for a container; place it where it goes), updateAttributes(attrs), editable and view.

function Fence({ node, children, updateAttributes }: NodeViewProps) {
return (
<box style={{ flexDirection: 'column' }}>
<text onMouseDown={() => updateAttributes({ params: 'py' })}>
{node.attrs.params || 'plain'}
</text>
{children}
</box>
);
}

<RichTextEditor nodeViews={{ code_block: Fence }} />;

Decisions

  • The value is markdown, because the apps this is for — notes, comments, chat — store markdown, and because this package already reads it: the parser is <Markdown>'s own, so what an editor wrote is what a <Markdown> beside it shows. format="html" is there for mail and CMS fields; state for anything that stores ProseMirror's JSON.
  • A reset is not an edit. A new value, or setValue, replaces the document outside the undo history, so Undo never resurrects the draft the app just cleared.
  • The toolbar never takes focus. Its buttons are not focusable and stop the press before the editor sees it, so the caret stays put and the command runs on the selection that is visible.
  • Links and images reach nothing by themselves. A link is onLink's to open and an image is renderImage's to draw — the line <Markdown> holds, for the same reason: a document is not a licence to make requests.
  • Tab is the editor's only where it means something — lists, code, tables — and Escape then Tab leaves from anywhere, the rule <CodeEditor> keeps too.
  • A copy keeps its structure on every backend. A copy offers HTML and text, and keeps the slice itself in the process; a paste whose text is exactly that copy takes the slice back. That matters on macOS, where react-x11's clipboard carries text only for now: without it, copy and paste inside one editor would flatten every list.
  • A long document draws a window of its top-level blocks. Only the blocks an edit touched re-render at any length; past 200 of them only the ones near the viewport are mounted at all — <Tree>'s and <Table>'s window, over block keys. A block inside a list or a quote is drawn with its top-level block, so one enormous list is drawn whole.
  • A suggestion list opens as its word is typed, never as the caret moves into one — the rule GitHub's comment box keeps, and <CodeEditor>'s completion. A markdown document keeps its mentions as text, so it is soon full of words that start with a trigger, and a click into one of them should place a caret, not open a list.
  • A markdown table keeps markdown's shape through every table command — one header row, first, and alignment by column — so the value never reads back as a different table from the one on screen.
  • Table actions live in the toolbar, not the right-click menu. The edit menu is core's, with a fixed set of verbs; the bar shows a table's own buttons only while the caret is in one.
  • Collaboration is y-prosemirror's, run as it is. The editor adds a cursor builder, remoteCaret, because the default one builds a DOM element, and everything else — sync, undo, awareness — is the binding every Yjs-backed ProseMirror editor runs.
  • A drag starts only from the selection, as it does in a browser's editor: a press anywhere else is the caret's, and a drag from there selects.
  • Dropped files become what markdown can say — an image, or a link. The editor reads nothing from them; a drop that uploads, or inlines, is handleDrop's.

Backends

Both. Mod follows the backend (Ctrl on X11, Cmd on macOS), and so do word motion and the other chords above. PRIMARY and the middle-button paste are X11's. On macOS react-x11's clipboard is text-only today, so HTML copied in another application arrives as its text (react-x11's docs/clipboard.md, "Limits").

Also exported

RichEditorView; schema, nodes, marks; defaultPlugins, editingKeymap, markdownInputRules; the commands the toolbar is built on (toggleBlockType, toggleList, toggleTaskList, toggleWrap, setLink, setTaskChecked, splitItem, insertHorizontalRule, indentCode, dedentCode, goToCell, isMarkActive, isBlockActive, markAttrs); the codecs (docFromMarkdown, markdownFromDoc, markdownCodec, docFromHTML, htmlFromContent, docFromText, textFromDoc); DEFAULT_TOOLBAR and toolbarItems; the suggestion plugin and its commands (suggestions, suggestionState, suggesterFor, acceptSuggestion, selectSuggestion, dismissSuggestion, filterSuggestions); the table commands (insertTable, addRowBefore, addRowAfter, addColumnBefore, addColumnAfter, deleteRow, deleteColumn, deleteTable, setColumnAlign, columnAlign, isInTable) and tableRepair; remoteCaret, y-prosemirror's cursor builder for this editor; and the types DomKeyEvent, NodeViewProps, ImageInfo, MarkStyle, RunStyle, ToolbarEntry, ToolbarItem, MarkdownCodec, Suggester, SuggestionItem, SuggestionQuery, SuggestionRow, SuggestionState, ColumnAlign, RemoteCaret and DomFocusEvent.

Example

npm run examples:rich-text-editor shows a notes pane — toolbar, markdown source beside it, a stock ProseMirror decoration plugin, and a / block menu with a Table row — next to a chat composer that grows as it is typed in, sends on Enter, mentions people with @ — each row drawn by the app, a badge of initials beside the name — and names channels with #.