Editor API
The Editor class is the central API for Domternal. It wraps ProseMirror’s EditorView and EditorState with a clean, type-safe interface for creating editors, executing commands, listening to events, and reading content.
Creating an Editor
Section titled “Creating an Editor”import { Editor, Document, Paragraph, Text } from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text], content: '<p>Hello world</p>',});The constructor requires either extensions or a ProseMirror schema. If you pass extensions, the schema is built automatically. If neither is provided, the constructor throws.
Configuration
Section titled “Configuration”All options are passed to the Editor constructor:
| Option | Type | Default | Description |
|---|---|---|---|
element | HTMLElement | null | null | DOM element to mount the editor. Creates a detached <div> if omitted (useful for testing or headless mode). |
extensions | AnyExtension[] | [] | Extensions to load. The schema, commands, plugins, and toolbar items are derived from these. |
schema | Schema | - | A ProseMirror schema. Optional if extensions are provided. |
content | Content | null | Initial content as an HTML string, JSON object, or null for an empty document. |
editable | boolean | true | Whether the editor is editable. |
autofocus | FocusPosition | false | Focus the editor on mount. See FocusPosition for accepted values. |
clipboardHTMLTransform | (html: string) => string | - | Transform function applied to clipboard HTML on copy/cut. Useful with inlineStyles to auto-apply CSS on copy. |
onBeforeCreate | (props) => void | - | Called before the editor is created. |
onCreate | (props) => void | - | Called when the editor is ready. |
onMount | (props) => void | - | Called when the editor view is mounted to the DOM. |
onUpdate | (props) => void | - | Called when the document content changes. |
onSelectionUpdate | (props) => void | - | Called when the selection changes (without content change). |
onTransaction | (props) => void | - | Called on every transaction (content or selection). |
onFocus | (props) => void | - | Called when the editor receives focus. |
onBlur | (props) => void | - | Called when the editor loses focus. |
onDestroy | () => void | - | Called before the editor is destroyed. |
onContentError | (props) => void | - | Called when content does not match the schema. |
onError | (props) => void | - | Called when an extension throws an error. |
Content type
Section titled “Content type”The Content type accepts multiple formats:
type Content = string | JSONContent | JSONContent[] | null;string- HTML string, parsed into ProseMirror nodesJSONContent- A JSON document object (the format returned byeditor.getJSON())JSONContent[]- An array of node objectsnull- Empty document
FocusPosition
Section titled “FocusPosition”Controls where the cursor is placed when focusing:
| Value | Behavior |
|---|---|
true or 'end' | Focus at the end of the document |
'start' | Focus at the beginning |
'all' | Select all content |
number | Focus at a specific document position |
false or null | Don’t autofocus |
Clipboard transform example
Section titled “Clipboard transform example”Apply inline styles automatically when users copy content:
import { Editor, inlineStyles } from '@domternal/core';
const editor = new Editor({ extensions: [/* ... */], clipboardHTMLTransform: inlineStyles,});This ensures copied content retains its formatting when pasted into email clients, Google Docs, and other external tools.
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
view | EditorView | The ProseMirror EditorView instance. Direct access to the underlying view for advanced use cases. |
state | EditorState | The current ProseMirror EditorState (shorthand for editor.view.state). |
schema | Schema | The ProseMirror schema built from extensions. |
isEditable | boolean | Whether the editor is currently editable. |
isEmpty | boolean | Whether the document is empty. |
isFocused | boolean | Whether the editor has DOM focus. |
isDestroyed | boolean | Whether the editor has been destroyed. |
storage | Record<string, unknown> | Extension storage, keyed by extension name. |
toolbarItems | ToolbarItem[] | Toolbar items registered by all extensions. |
Accessing extension storage
Section titled “Accessing extension storage”Extensions can store mutable state via addStorage(). Access it from the editor:
// Character count extension stores the counteditor.storage.characterCount.characters();editor.storage.characterCount.words();
// Code block lowlight stores language listeditor.storage.codeBlockLowlight.listLanguages();Commands
Section titled “Commands”Domternal has three ways to execute commands: single, chained, and dry-run.
Single commands
Section titled “Single commands”Execute a command immediately and return whether it succeeded:
editor.commands.toggleBold(); // true or falseeditor.commands.setHeading({ level: 2 });editor.commands.insertText('Hello');Chained commands
Section titled “Chained commands”Batch multiple commands into a single ProseMirror transaction. All changes are applied atomically when you call run():
editor.chain() .focus() .insertText('Hello ') .toggleBold() .insertText('world') .toggleBold() .run();If any command in the chain fails, the entire chain is not dispatched and run() returns false. You can inspect the failure:
const chain = editor.chain().toggleBold().setHeading({ level: 7 });if (!chain.run()) { const failure = chain.getFailure(); // { command: 'setHeading', args: [{ level: 7 }], index: 1 }}Custom commands in chains
Section titled “Custom commands in chains”Insert inline logic with the command() method:
editor.chain() .focus() .command(({ tr }) => { tr.insertText('custom logic'); return true; }) .run();Dry-run checks
Section titled “Dry-run checks”Check if commands can execute without modifying the document:
if (editor.can().toggleBold()) { // Bold can be applied at the current selection}
// Chain checksif (editor.can().chain().toggleBold().toggleItalic().run()) { // Both commands can execute}Dry-run mode passes dispatch: undefined to the command. Commands check feasibility and return a boolean without side effects.
Built-in commands
Section titled “Built-in commands”These commands are available on every editor, regardless of which extensions are loaded:
Selection commands
Section titled “Selection commands”| Command | Arguments | Description |
|---|---|---|
focus | position?: FocusPosition | Focuses the editor. Optionally places the cursor at the given position. |
blur | - | Removes focus from the editor. |
selectAll | - | Selects the entire document. |
selectNodeBackward | - | Selects the node before the cursor (when at start of a textblock). |
deleteSelection | - | Deletes the current selection. Returns false if the selection is empty. |
Content commands
Section titled “Content commands”| Command | Arguments | Description |
|---|---|---|
setContent | content: Content, options?: SetContentOptions | Replaces the entire document. Options: emitUpdate (default true), parseOptions. |
clearContent | options?: ClearContentOptions | Clears the document. Options: emitUpdate (default true). |
insertText | text: string | Inserts text at the current selection. Returns false if the parent does not allow inline content. |
insertContent | content: Content | Inserts content (HTML string, JSON, or array of JSON nodes) at the current selection. |
Mark commands
Section titled “Mark commands”| Command | Arguments | Description |
|---|---|---|
toggleMark | markName: string, attributes?: Attrs | Toggles a mark. In cursor mode, toggles stored marks. In range mode, applies or removes across the selection. |
setMark | markName: string, attributes?: Attrs | Adds a mark. Merges attributes with existing marks to preserve sibling attributes (e.g., setting fontSize preserves fontFamily on textStyle). |
unsetMark | markName: string | Removes a mark from the selection. |
unsetAllMarks | - | Removes all formatting marks. Marks with isFormatting: false (like Link) are preserved. Returns false for empty selections. |
Node commands
Section titled “Node commands”| Command | Arguments | Description |
|---|---|---|
setBlockType | nodeName: string, attributes?: Attrs | Changes the block type of selected textblocks. Preserves global attributes (textAlign, lineHeight) by merging. |
toggleBlockType | nodeName: string, defaultNodeName: string, attributes?: Attrs | Toggles between a block type and a default type (usually 'paragraph'). |
wrapIn | nodeName: string, attributes?: Attrs | Wraps the selection in a node type (e.g., blockquote). |
toggleWrap | nodeName: string, attributes?: Attrs | Toggles wrapping. If already wrapped, lifts out. Supports CellSelection. |
lift | - | Lifts the current block out of its parent wrapper. |
List commands
Section titled “List commands”| Command | Arguments | Description |
|---|---|---|
toggleList | listNodeName: string, listItemNodeName: string, attributes?: Attrs | Toggles a list. If not in a list, wraps. If in the same type, lifts out. If in a different type, converts in-place. Handles mixed selections and CellSelection. |
Attribute commands
Section titled “Attribute commands”| Command | Arguments | Description |
|---|---|---|
updateAttributes | typeOrName: string, attributes: Record<string, unknown> | Updates attributes on all matching nodes or marks in the selection. Merges with existing attributes. |
resetAttributes | typeOrName: string, attributeName: string | Resets an attribute to its schema default value on all matching nodes or marks. |
Extension commands
Section titled “Extension commands”Extensions add their own commands. Each extension’s documentation lists its commands. For example:
// From Bold extensioneditor.commands.toggleBold();editor.commands.setBold();editor.commands.unsetBold();
// From Heading extensioneditor.commands.setHeading({ level: 2 });editor.commands.toggleHeading({ level: 3 });
// From Table extensioneditor.commands.insertTable({ rows: 3, cols: 3 });editor.commands.addRowAfter();editor.commands.deleteTable();All extension commands are type-safe. TypeScript provides autocomplete for editor.commands.*, editor.chain().*, and editor.can().*.
How commands work internally
Section titled “How commands work internally”Commands use a two-layer curried pattern:
// Layer 1: Capture argumentsconst setHeading = (attributes?: { level?: number }) => // Layer 2: Receive execution context ({ state, tr, dispatch }) => { const nodeType = state.schema.nodes['heading']; if (!nodeType) return false;
if (!dispatch) return true; // Dry-run: just check feasibility
const { from, to } = tr.selection; tr.setBlockType(from, to, nodeType, attributes); dispatch(tr); return true; };The CommandProps passed to every command:
| Property | Type | Description |
|---|---|---|
editor | Editor | The editor instance. |
state | EditorState | Current ProseMirror state. |
tr | Transaction | The current transaction. Shared across commands in a chain. |
dispatch | ((tr) => void) | undefined | Dispatch function. undefined in dry-run mode. |
chain | () => ChainedCommands | Start a new command chain from within a command. |
can | () => CanCommands | Check command feasibility from within a command. |
commands | SingleCommands | Access other commands from within a command. |
In chains, all commands share the same tr. Each command sees changes made by prior commands in the chain. The accumulating dispatch copies steps and metadata (critical for undo/redo) from inner transactions to the shared transaction.
Events
Section titled “Events”The editor emits typed events. You can listen using either constructor callbacks or the .on() API.
Event listener API
Section titled “Event listener API”// Register a listenereditor.on('update', ({ editor, transaction }) => { console.log('Content changed');});
// Register a one-time listenereditor.once('create', ({ editor }) => { console.log('Editor ready');});
// Remove a specific listenereditor.off('update', myHandler);
// Remove all listeners for an eventeditor.off('update');
// Remove all listenerseditor.removeAllListeners();
// Check listener counteditor.listenerCount('update'); // number
// Get event names with active listenerseditor.eventNames(); // ('update' | 'create' | ...)[]Constructor callbacks
Section titled “Constructor callbacks”const editor = new Editor({ extensions: [/* ... */], onUpdate({ editor, transaction }) { console.log('Content changed'); }, onFocus({ editor, event }) { console.log('Focused'); },});Both approaches work simultaneously. Constructor callbacks fire alongside .on() listeners.
Event reference
Section titled “Event reference”| Event | Payload | When it fires |
|---|---|---|
beforeCreate | { editor } | Before extensions are initialized. The listener receives a partially initialized editor. |
create | { editor } | After the editor is fully initialized and ready. |
mount | { editor, view } | After the ProseMirror view is attached to the DOM element. |
unmount | { editor, view } | When the editor view is unmounted from the DOM (during destroy). |
update | { editor, transaction } | When the document content changes. Does not fire if the transaction sets skipUpdate metadata. |
selectionUpdate | { editor, transaction } | When the selection changes without a content change. |
transaction | { editor, transaction } | On every transaction, regardless of whether content or selection changed. |
focus | { editor, event } | When the editor receives DOM focus. event is the native FocusEvent. |
blur | { editor, event } | When the editor loses DOM focus. event is the native FocusEvent. |
paste | { editor, event, slice } | When content is pasted. event is the native ClipboardEvent, slice is the ProseMirror Slice. |
drop | { editor, event, slice, moved } | When content is dropped. event is the native DragEvent, moved is true if the content was moved (not copied). |
delete | { editor, from, to } | When content is deleted. from and to are the document positions of the deleted range. |
destroy | - | Before the editor is destroyed. No payload. |
contentError | { editor, error, content } | When initial content does not match the schema. The editor falls back to an empty document. |
error | { editor, error, context } | When an extension throws an error. context describes where (e.g., 'Bold.onUpdate'). |
linkEdit | { anchorElement? } | When the link editing UI should open (from toolbar link button or Mod-K). |
Common patterns
Section titled “Common patterns”Saving on change
Section titled “Saving on change”editor.on('update', ({ editor }) => { const json = editor.getJSON(); saveToDatabase(json);});Character count on update
Section titled “Character count on update”editor.on('update', ({ editor }) => { const count = editor.storage.characterCount.characters(); document.getElementById('count')!.textContent = `${count} characters`;});React to every transaction
Section titled “React to every transaction”editor.on('transaction', ({ editor, transaction }) => { // Runs on every change (content, selection, or both) updateToolbarState(editor);});Active state
Section titled “Active state”isActive
Section titled “isActive”Checks if a node or mark is active at the current selection:
editor.isActive('bold'); // Mark active?editor.isActive('heading', { level: 2 }); // In h2?editor.isActive('textStyle', { color: '#e03131' }); // Specific text color?
// Object form (used internally by toolbar items)editor.isActive({ name: 'textAlign', attributes: { textAlign: 'center' } });How it works for marks:
- Empty selection: checks stored marks or marks at cursor position
- Range selection: returns
trueonly if ALL applicable text in the selection has the mark. Text inside blocks that don’t allow the mark (like code blocks) or text carrying an excluding mark (like inline code) is skipped.
How it works for nodes:
- NodeSelection: checks the selected node directly
- Range selection: the node must be an ancestor of both ends of the selection
- List nodes: only the innermost list ancestor is considered active, preventing both
bulletListandorderedListfrom showing active when nested
getAttributes
Section titled “getAttributes”Gets the attributes of the active node or mark:
editor.getAttributes('heading'); // { level: 2 }editor.getAttributes('link'); // { href: '...', target: '_blank' }editor.getAttributes('textStyle'); // { color: '#e03131', fontSize: '18px' }Returns an empty object if the node/mark is not found. For range selections, returns attributes from the start of the selection.
Content methods
Section titled “Content methods”getJSON
Section titled “getJSON”Returns the document as a JSON object:
const json = editor.getJSON();// {// type: 'doc',// content: [// { type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }// ]// }getHTML
Section titled “getHTML”Returns the document as an HTML string:
const html = editor.getHTML();// '<p>Hello</p>'With inline styles for external rendering (email, CMS):
// Apply default light-theme stylesconst styled = editor.getHTML({ styled: true });
// With custom overridesconst custom = editor.getHTML({ styled: { linkColor: '#ff6600', tableBorder: '2px solid #333', },});The styled option applies structural CSS (borders, padding, fonts, colors) so the HTML renders correctly outside the editor. See inlineStyles for all override keys.
getText
Section titled “getText”Returns the document as plain text:
const text = editor.getText();// 'Hello\n\nWorld'
const singleLine = editor.getText({ blockSeparator: ' ' });// 'Hello World'setContent
Section titled “setContent”Replaces the entire document:
// From HTMLeditor.setContent('<p>New content</p>');
// From JSONeditor.setContent({ type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'New' }] }],});
// Empty documenteditor.setContent(null);
// Without emitting update eventeditor.setContent('<p>Silent update</p>', false);Returns false if the content is invalid.
clearContent
Section titled “clearContent”Clears the document to an empty state:
editor.clearContent();
// Without emitting update eventeditor.clearContent(false);Lifecycle methods
Section titled “Lifecycle methods”setEditable
Section titled “setEditable”Toggles whether the editor is editable:
editor.setEditable(false); // Read-onlyeditor.setEditable(true); // EditableDispatches an empty transaction to trigger ProseMirror’s editability re-evaluation.
Focuses the editor:
editor.focus(); // Focus without changing selectioneditor.focus('start'); // Focus at the beginningeditor.focus('end'); // Focus at the endeditor.focus('all'); // Select alleditor.focus(42); // Focus at position 42Uses TextSelection.near() to find the nearest valid text position. Chainable.
Removes focus:
editor.blur();destroy
Section titled “destroy”Cleans up the editor and releases all resources:
editor.destroy();Destroy sequence:
- Clear pending autofocus timer
- Emit
destroyevent - Call
onDestroycallback - Destroy ProseMirror view
- Destroy extension manager
- Remove all event listeners
- Mark as destroyed
After destroy(), the editor instance should not be used. The isDestroyed property returns true.
Dynamic plugin management
Section titled “Dynamic plugin management”Add or remove ProseMirror plugins at runtime:
import { Plugin, PluginKey } from '@domternal/pm/state';
const myPluginKey = new PluginKey('myPlugin');const myPlugin = new Plugin({ key: myPluginKey, // ...});
// Register (prevents duplicates automatically)editor.registerPlugin(myPlugin);
// Unregistereditor.unregisterPlugin(myPluginKey);This is used internally by framework wrappers (e.g., the Angular BubbleMenu component) to add plugins after the editor is created.
Server-side utilities
Section titled “Server-side utilities”Three functions convert content between formats without creating an editor instance. They work in both browser and Node.js (Node.js requires linkedom as a peer dependency).
generateHTML
Section titled “generateHTML”Converts JSON content to an HTML string:
import { generateHTML, Document, Paragraph, Text } from '@domternal/core';
const html = generateHTML( { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }] }, [Document, Paragraph, Text]);// '<p>Hello</p>'| Parameter | Type | Description |
|---|---|---|
content | JSONContent | The JSON content to convert. |
extensions | AnyExtension[] | Extensions that define the schema (same ones you use in the editor). |
options.document | Document | Custom DOM implementation. Uses native document in browser, linkedom in Node.js. |
generateJSON
Section titled “generateJSON”Converts an HTML string to JSON content:
import { generateJSON, Document, Paragraph, Text } from '@domternal/core';
const json = generateJSON('<p>Hello</p>', [Document, Paragraph, Text]);// { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }] }| Parameter | Type | Description |
|---|---|---|
html | string | The HTML string to parse. |
extensions | AnyExtension[] | Extensions that define the schema. |
options.document | Document | Custom DOM implementation. |
generateText
Section titled “generateText”Extracts plain text from JSON content:
import { generateText, Document, Paragraph, Text } from '@domternal/core';
const text = generateText(jsonContent, [Document, Paragraph, Text]);// 'Hello\n\nWorld'
const singleLine = generateText(jsonContent, extensions, { blockSeparator: ' ' });// 'Hello World'| Parameter | Type | Description |
|---|---|---|
content | JSONContent | The JSON content to extract text from. |
extensions | AnyExtension[] | Extensions that define the schema. |
options.blockSeparator | string | String between block elements (default: '\n\n'). |
inlineStyles
Section titled “inlineStyles”Applies inline CSS styles to HTML for external rendering. This is critical for email clients, CMS editors, and Google Docs, which strip <style> tags and class-based styling.
import { inlineStyles } from '@domternal/core';
// Apply default light-theme stylesconst styled = inlineStyles(html);
// With custom overridesconst custom = inlineStyles(html, { blockquoteBorder: '5px solid red', linkColor: '#ff6600', tableHeaderBg: '#e0e0e0',});An applyInlineStyles(container, overrides?) function is also exported for applying styles directly to a DOM element (used internally by the clipboard serializer).
Override keys
Section titled “Override keys”| Key | Default | Description |
|---|---|---|
blockquoteBorder | '3px solid #6a6a6a' | Blockquote left border. |
blockquoteColor | '#6a6a6a' | Blockquote text color. |
tableBorder | '1px solid #e5e7eb' | Table and cell border. |
tableHeaderBg | '#f8f9fa' | Table header cell background. |
codeBg | '#f0f0f0' | Inline code background. |
codeFont | monospace stack | Inline code font. |
codeBorder | '1px solid #e5e7eb' | Inline code border. |
codeBlockBg | '#f0f0f0' | Code block background. |
codeBlockFont | monospace stack | Code block font. |
hrBorder | '2px solid #e5e7eb' | Horizontal rule border. |
linkColor | '#2563eb' | Link color. |
detailsBorder | '1px solid #e5e7eb' | Details/accordion border. |
detailsBg | '#f8f9fa' | Details/accordion background. |
tableColumnWidths | 'percent' | How table column widths are exported: 'percent', 'pixel', or 'none'. |
codeHighlighter | - | Callback for syntax highlighting code blocks. See Code Block Lowlight. |
Styled elements
Section titled “Styled elements”inlineStyles applies CSS to:
- Blockquotes - left border, color, padding, margin
- Tables - border-collapse, cell borders and padding, header backgrounds, column widths
- Code - inline (background, font, border, padding) and blocks (background, font, padding)
- Links - color and text decoration
- Headings - font-size, font-weight, margins
- Lists - margin, padding, task list flex layout with checkboxes
- Details/Accordion - border, background, cursor on summary
- Horizontal rules - border style
- Syntax highlighting - hljs-* classes mapped to GitHub light theme colors
ToolbarController
Section titled “ToolbarController”A headless, framework-agnostic state machine for toolbar UI. It manages active states, disabled states, dropdown toggling, and keyboard navigation.
import { ToolbarController } from '@domternal/core';
const controller = new ToolbarController(editor, () => { // Called whenever toolbar state changes - re-render your UI here renderToolbar(controller);});
controller.subscribe(); // Start listening to editor transactions
// Optional: pass a custom layout to reorder/group itemsconst custom = new ToolbarController(editor, onChange, [ 'bold', 'italic', 'underline', '|', 'heading',]);Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
groups | ToolbarGroup[] | Toolbar items grouped by their group property. |
activeMap | ReadonlyMap<string, boolean> | Active state per button name. |
disabledMap | ReadonlyMap<string, boolean> | Disabled state per button name. |
expandedMap | ReadonlyMap<string, boolean> | Expanded state for emitEvent buttons (true when their panel is open). |
openDropdown | string | null | Name of the currently open dropdown. |
focusedIndex | number | Index of the focused button for keyboard navigation. |
flatButtonCount | number | Total number of top-level buttons. |
Methods
Section titled “Methods”| Method | Returns | Description |
|---|---|---|
isActive(item) | boolean | Check if a toolbar button is active. |
isDisabled(item) | boolean | Check if a toolbar button’s command can execute. |
executeCommand(item) | void | Run the button’s command. |
toggleDropdown(name) | void | Open or close a dropdown. |
closeDropdown() | void | Close any open dropdown. |
navigateNext() | number | Move focus to the next button. |
navigatePrev() | number | Move focus to the previous button. |
navigateFirst() | number | Move focus to the first button. |
navigateLast() | number | Move focus to the last button. |
setFocusedIndex(idx) | void | Set focus to a specific index. |
getFlatIndex(name) | number | Get the flat index of an item by name. |
subscribe() | void | Start listening to editor transactions. |
destroy() | void | Clean up and stop listening. |
Static helpers
Section titled “Static helpers”| Method | Returns | Description |
|---|---|---|
ToolbarController.resolveActive(editor, item) | boolean | Check if a single item is active. |
ToolbarController.executeItem(editor, item) | void | Execute a single item’s command. |
Initialization lifecycle
Section titled “Initialization lifecycle”The editor follows a precise initialization sequence:
- beforeCreate event fires
- ExtensionManager is created (flattens, deduplicates, sorts, validates extensions)
- Extension
onBeforeCreatehooks fire - Schema is validated (must have
docandtextnodes) - Document is created from
content(falls back to empty doc on error, emittingcontentError) - Plugins are collected from extensions
- EditorState is created
- EditorView is created and mounted to the DOM element
- mount event fires
- CommandManager is created
- Autofocus is scheduled (via
setTimeoutfor DOM readiness) - Error handler is wired up
- create event fires
- Extension
onCreatehooks fire
Transaction flow
Section titled “Transaction flow”On every ProseMirror transaction:
- Transaction is applied to create a new state
- View is updated with the new state
- transaction event fires,
onTransactioncallback runs, extensiononTransactionhooks fire - If selection changed (no doc change): selectionUpdate event fires, extension
onSelectionUpdatehooks fire - If document changed (no
skipUpdatemeta): update event fires, extensiononUpdatehooks fire
Error handling
Section titled “Error handling”Content errors
Section titled “Content errors”If initial content does not match the schema, the editor:
- Emits the
contentErrorevent with the error and original content - Calls
onContentErrorif provided - Falls back to an empty document
const editor = new Editor({ extensions: [Document, Paragraph, Text], content: '<table><tr><td>No table extension</td></tr></table>', onContentError({ error, content }) { console.warn('Invalid content:', error.message); // The editor continues with an empty document },});Extension error isolation
Section titled “Extension error isolation”Extension lifecycle hooks are wrapped in error boundaries. If an extension throws, the editor continues working:
const editor = new Editor({ extensions: [/* ... */], onError({ error, context }) { // context: 'MyExtension.onUpdate', 'Bold.addProseMirrorPlugins', etc. console.error(`Extension error in ${context}:`, error); },});JSON content format
Section titled “JSON content format”The JSON format used by getJSON() and setContent():
interface JSONContent { type: string; // Node type name attrs?: Record<string, JSONAttribute>; // Node attributes content?: JSONContent[]; // Child nodes marks?: JSONMark[]; // Marks applied to text text?: string; // Text content (for text nodes)}
interface JSONMark { type: string; // Mark type name attrs?: Record<string, JSONAttribute>; // Mark attributes}Example document:
{ "type": "doc", "content": [ { "type": "heading", "attrs": { "level": 2 }, "content": [ { "type": "text", "text": "Hello " }, { "type": "text", "text": "world", "marks": [{ "type": "bold" }] } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "A link", "marks": [{ "type": "link", "attrs": { "href": "https://domternal.dev" } }] } ] } ]}Helper utilities
Section titled “Helper utilities”These functions are exported from @domternal/core for use in custom extensions, commands, and plugins.
Document queries
Section titled “Document queries”| Function | Signature | Description |
|---|---|---|
findParentNode | (predicate) => (selection) => FindParentNodeResult | undefined | Curried. Finds the closest parent node matching a predicate. Returns { node, pos, start, depth }. |
findChildren | (node, predicate) => FindChildResult[] | Finds all direct children of a node matching a predicate. Returns { node, pos }[]. |
getMarkRange | ($pos, type) => MarkRange | undefined | Gets the contiguous range { from, to } of a mark around a resolved position. |
defaultBlockAt | (match) => NodeType | null | Finds the first textblock type that can be created at a given ContentMatch position. |
Content checks
Section titled “Content checks”| Function | Signature | Description |
|---|---|---|
isNodeEmpty | (node, options?) => boolean | Checks if a ProseMirror node is empty. Options: checkChildren (recursive), ignoreHardBreaks. |
isDocumentEmpty | (doc) => boolean | Checks if a document has no meaningful content (only empty paragraphs). |
isValidUrl | (url, options?) => boolean | Validates a URL string. Options: protocols (default: ['http:', 'https:']). Prevents javascript: URLs. |
Usage example
Section titled “Usage example”import { findParentNode, getMarkRange, isNodeEmpty } from '@domternal/core';
// Find the nearest list item ancestorconst listItem = findParentNode((node) => node.type.name === 'listItem')(editor.state.selection);if (listItem) { console.log('Inside list item at position', listItem.pos);}
// Get the range of a link mark at the cursorconst $pos = editor.state.selection.$from;const linkType = editor.schema.marks['link'];const range = getMarkRange($pos, linkType);if (range) { console.log('Link from', range.from, 'to', range.to);}TypeScript
Section titled “TypeScript”All commands are fully typed through module augmentation. Each extension declares its commands:
declare module '@domternal/core' { interface RawCommands { toggleBold: CommandSpec; setHeading: CommandSpec<[attributes?: { level?: number }]>; }}This gives you autocomplete and type checking on:
editor.commands.*- returnsbooleaneditor.chain().*- returnsChainedCommandsfor chainingeditor.can().*- returnsboolean
Key types
Section titled “Key types”import type { Editor, EditorOptions, EditorEvents, Content, JSONContent, JSONMark, FocusPosition, Command, CommandSpec, CommandProps, SingleCommands, ChainedCommands, CanCommands, CanChainedCommands, ChainFailure, PasteEventProps, DropEventProps, DeleteEventProps, AnyExtension, ToolbarItem, ToolbarButton, ToolbarDropdown, ToolbarSeparator, ToolbarLayoutEntry, ToolbarGroup, InlineStyleOverrides, FindParentNodeResult, FindChildResult, MarkRange, IsNodeEmptyOptions, IsValidUrlOptions,} from '@domternal/core';