@mdaemon/html-editor-react
v1.6.1
Published
React wrapper for MDHTMLEditor
Maintainers
Readme
MDHTMLEditor React
A React wrapper for MDHTMLEditor — a WYSIWYG HTML editor built on TipTap. Provides a drop-in replacement for @tinymce/tinymce-react with no license key required.
Installation
npm install @mdaemon/html-editor-react @mdaemon/html-editorPeer dependencies: react and react-dom (v18 or v19).
Styles
You must import the editor stylesheet for the toolbar and UI to render correctly:
import '@mdaemon/html-editor/dist/styles.css';Quick Start
import { useRef } from 'react';
import { Editor } from '@mdaemon/html-editor-react';
import type { EditorRef } from '@mdaemon/html-editor-react';
import '@mdaemon/html-editor/dist/styles.css';
function App() {
const editorRef = useRef<EditorRef>(null);
return (
<Editor
ref={editorRef}
config={{ height: 400 }}
initialValue="<p>Hello World</p>"
onChange={(html) => console.log('Content:', html)}
onInit={(editor) => console.log('Ready!', editor)}
/>
);
}<Editor> Component
The primary way to use the editor. It accepts a config object and event callbacks, and exposes imperative methods via a ref.
Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| config | EditorConfig | Yes | Configuration object passed to the underlying HTMLEditor. See Configuration. |
| body | string | No | Initial HTML content. Takes precedence over initialValue. |
| initialValue | string | No | Initial HTML content (alias for body). |
| name | string | No | When set, renders a hidden <input> with this name containing the editor content — useful for form submission. |
| disabled | boolean | No | Visually disables the editor (pointer-events off, reduced opacity). |
| onChange | (content: string) => void | No | Called when content changes (debounced). |
| onDirty | (dirty: boolean) => void | No | Called when the dirty state changes. |
| onInit | (editor: HTMLEditor) => void | No | Called when the editor finishes initialization. Receives the editor instance. |
| onFocus | () => void | No | Called when the editor receives focus. |
| onBlur | () => void | No | Called when the editor loses focus. |
| translate | (key: string) => string | No | Sets a global translation function for all editors on the page. |
| getFileSrc | (path: string) => string | No | Sets a global file path resolver (e.g., for CDN prefixing). |
Ref Methods (EditorRef)
Attach a ref to access imperative methods:
const editorRef = useRef<EditorRef>(null);
// Later...
const html = editorRef.current?.getContent();
editorRef.current?.setContent('<p>New content</p>');
editorRef.current?.insertContent('<p>Inserted at cursor</p>');
editorRef.current?.focus();
// Access the underlying HTMLEditor instance directly
const rawEditor = editorRef.current?.getEditor();| Method | Signature | Description |
|--------|-----------|-------------|
| getEditor | () => HTMLEditor \| null | Access the raw HTMLEditor instance for advanced usage. |
| getContent | () => string | Get current HTML content. |
| setContent | (html: string) => void | Replace the editor content. |
| insertContent | (html: string) => void | Insert HTML at the current cursor position. |
| focus | () => void | Focus the editor. |
Full Example
import { useRef, useState } from 'react';
import { Editor } from '@mdaemon/html-editor-react';
import type { EditorRef } from '@mdaemon/html-editor-react';
import '@mdaemon/html-editor/dist/styles.css';
function EmailComposer() {
const editorRef = useRef<EditorRef>(null);
const [dirty, setDirty] = useState(false);
const handleSave = () => {
const content = editorRef.current?.getContent() ?? '';
console.log('Saving:', content);
};
return (
<div>
<Editor
ref={editorRef}
config={{
height: 500,
basicEditor: false,
skin: 'oxide',
fontName: 'Arial',
fontSize: '12pt',
}}
initialValue="<p>Dear recipient,</p>"
onChange={(html) => console.log('Changed:', html)}
onDirty={(d) => setDirty(d)}
onInit={() => console.log('Editor ready')}
/>
<button onClick={handleSave} disabled={!dirty}>
Save
</button>
</div>
);
}useEditor Hook
For more programmatic control, use the useEditor hook. You provide a container element via the returned containerRef.
Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| config | EditorConfig | {} | Editor configuration. |
| content | string | '' | Initial HTML content. |
| onUpdate | (html: string) => void | — | Called on content change. |
Return Value
| Property | Type | Description |
|----------|------|-------------|
| editor | HTMLEditor \| null | The raw editor instance (null until initialized). |
| containerRef | RefObject<HTMLDivElement \| null> | Attach this to your container div. |
| ready | boolean | true once the editor has fired its init event. |
| getContent | () => string | Get current HTML content. |
| setContent | (html: string) => void | Replace the editor content. |
| insertContent | (html: string) => void | Insert HTML at the cursor. |
| focus | () => void | Focus the editor. |
| isDirty | () => boolean | Check if the editor has unsaved changes. |
Example
import { useState } from 'react';
import { useEditor } from '@mdaemon/html-editor-react';
import '@mdaemon/html-editor/dist/styles.css';
function NotesEditor() {
const [lastSaved, setLastSaved] = useState('');
const { containerRef, ready, getContent, setContent, isDirty } = useEditor({
config: { height: 300, basicEditor: true },
content: '<p>Start taking notes...</p>',
onUpdate: (html) => console.log('Updated:', html),
});
const handleSave = () => {
setLastSaved(getContent());
};
const handleReset = () => {
setContent('<p>Start taking notes...</p>');
};
return (
<div>
<div ref={containerRef} />
{ready && (
<div>
<button onClick={handleSave}>Save</button>
<button onClick={handleReset}>Reset</button>
<span>{isDirty() ? 'Unsaved changes' : 'Saved'}</span>
</div>
)}
</div>
);
}Global Convenience Functions
For single-editor pages, you can get and set content without a ref:
import { getEditorContent, setEditorContent } from '@mdaemon/html-editor-react';
// Get content from the active editor
const html = getEditorContent();
// Set content on the active editor
setEditorContent('<p>New content</p>');Note: These functions operate on the most recently mounted
<Editor>instance. They are intended for pages with a single editor.
Configuration
The config prop (or useEditor's config option) accepts an EditorConfig object. All options from @mdaemon/html-editor are supported:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| basicEditor | boolean | false | Use a simplified toolbar (no images, tables, code blocks). |
| readonly | boolean | false | Start the editor in read-only mode. Toggle at runtime via getEditor()?.setReadOnly(). |
| height | string \| number | 300 | Editor height. |
| min_height | string \| number | — | Minimum editor height. |
| max_height | string \| number | — | Maximum editor height. |
| language | string | 'en' | UI language code. 31 languages built in. |
| skin | 'oxide' \| 'oxide-dark' \| 'confab' \| 'confab-dark' | 'oxide' | Toolbar and dialog theme. |
| content_css | 'default' \| 'dark' \| 'confab' \| 'confab-dark' | 'default' | Content area theme. |
| content_style | string | — | Custom CSS injected into the editing surface. |
| fontName | string | 'arial, helvetica, sans-serif' | Default font family. Inlined on every block in the exported HTML — see Fonts. |
| fontSize | string | '12pt' | Default font size. Inlined on every paragraph in the exported HTML — see Fonts. |
| font_family_formats | string | (TinyMCE defaults) | Semicolon-delimited font list (Name=family,...). |
| font_size_formats | string | '8pt 9pt 10pt 12pt 14pt 18pt 24pt 36pt' | Space-delimited size options. |
| font_names | string | — | CKEditor alias for font_family_formats. |
| fontSize_sizes | string | — | CKEditor alias for font_size_formats. |
| block_formats | string | (Paragraph + H1–H6) | Block-format dropdown definitions for the blocks button (Name=tag;...). |
| style_formats | StyleFormat[] | (subset) | Named styles for the styles dropdown (CKEditor stylesSet-compatible). |
| forced_root_block | 'p' \| 'div' | 'p' | Block element produced on Enter. 'div' gives CKEditor ENTER_DIV parity. |
| trailingNode | boolean | false | Append an empty trailing paragraph after a block node (table, image, code block) so the cursor can sit after it. |
| directionality | 'ltr' \| 'rtl' | 'ltr' | Text direction. |
| toolbar | string | (preset) | Custom toolbar layout string. |
| toolbar_mode | 'sliding' \| 'floating' \| 'wrap' | 'wrap' | Toolbar overflow behavior. |
| toolbar_sticky | boolean | true | Pin toolbar at top when scrolling. |
| toolbar_narrow_breakpoint | number | — | Pixel width below which the toolbar collapses into the overflow toggle. |
| toolbar_priority | Record<string, number> | — | Per-button overflow priority — higher values stay visible longer as the toolbar narrows. |
| menubar | boolean | false | Show a TinyMCE-style menu bar above the toolbar. |
| contextmenu | boolean \| string | '' | Enable a right-click context menu. Pass a button-list string to customize its contents. |
| quickbars_selection_toolbar | string | 'bold italic \| quicklink blockquote' | Inline floating toolbar shown over a text selection. |
| quickbars_image_toolbar | boolean | false | Inline floating toolbar shown when an image is selected. |
| quickbars_insert_toolbar | boolean | false | Inline floating toolbar shown on empty lines for quick insertion. |
| elementpath | boolean | false | Show a breadcrumb of the node path at the cursor in the status bar. |
| valid_children | string | — | TinyMCE-style rules controlling which child elements are allowed. |
| auto_focus | string | — | Auto-focus on init. |
| setFocus | string | — | CSS selector for the element to focus on init (used when auto_focus is not set). |
| plugins | string | — | Accepted for TinyMCE compatibility and ignored — all features are built in. |
| browser_spellcheck | boolean | true | Enable browser spell check. |
| entity_encoding | 'raw' \| 'named' \| 'numeric' | 'raw' | HTML entity encoding mode. |
| paste_from_office | boolean | true | Clean and preserve formatting when pasting from Microsoft Word/Excel. |
| speech_to_text | boolean | true | Enable the speechtotext and dictate toolbar buttons (requires the Web Speech API). |
| convert_unsafe_embeds | boolean | true | Sanitize embedded content. |
| format_empty_lines | boolean | true | Preserve blank lines outside the editor. On the way out (getContent(), the onChange payload, preview, and the source dialog) each empty block is filled with a <br> — bare empty blocks otherwise collapse to zero height in mail clients; on the way in (setContent(), insertContent(), templates) it is stripped back out, so setContent(getContent(x)) is stable across round-trips. Set false to pass content through unchanged in both directions. |
| includeTemplates | boolean | false | Show the template dropdown. |
| templates | Template[] | [] | Predefined HTML templates. |
| dropbox | boolean | false | Enable Dropbox integration. |
| images_upload_url | string | — | Server endpoint for image uploads. |
| images_upload_credentials | boolean | true | Include credentials with upload requests. |
| images_upload_base_path | string | '/' | Prefix for uploaded image URLs. |
| images_upload_max_size | number | 10485760 | Max upload size in bytes (10 MB). |
| images_upload_headers | Record<string, string> | — | Extra headers for upload requests. |
| images_file_types | string | (permissive) | Comma/space-separated accepted extensions (e.g. 'jpg,jpeg,png,gif'). |
| images_upload_validate | (file: File) => string \| null | — | Pre-upload hook; return a message to reject the file, or null to allow. |
| images_upload_error | (message: string) => void | — | Caller-supplied alert for drag-drop/paste upload rejections and failures. |
| setup | (editor) => void | — | Pre-init callback for registering custom toolbar buttons. |
Dark Theme
<Editor
config={{
skin: 'oxide-dark',
content_css: 'dark',
}}
/>Confab Skin
The confab / confab-dark skins integrate with the WorldClient theming system, pulling colors from the host application's CSS custom properties so the editor adapts when the app theme changes:
<Editor
config={{
skin: 'confab-dark',
content_css: 'confab-dark',
}}
/>Custom Content Styles
<Editor
config={{
content_style: 'body { font-family: Georgia, serif; font-size: 16px; line-height: 1.6; }',
}}
/>Menu Bar, Context Menu & Quick Toolbars
Opt into the additional editing surfaces exposed by @mdaemon/html-editor 1.6.0:
<Editor
config={{
menubar: true, // menu bar above the toolbar
contextmenu: true, // right-click context menu
quickbars_selection_toolbar: 'bold italic | quicklink blockquote',
quickbars_image_toolbar: true, // floating toolbar when an image is selected
elementpath: true, // breadcrumb status bar at the cursor
}}
/>contextmenu also accepts a button-list string to customize its contents, and
quickbars_insert_toolbar shows a floating insert toolbar on empty lines.
Read-Only Mode
Start the editor read-only with readonly: true, or toggle it at runtime through the underlying editor instance:
<Editor config={{ readonly: true }} ref={editorRef} />
// Toggle later
editorRef.current?.getEditor()?.setReadOnly(false);
const isReadOnly = editorRef.current?.getEditor()?.isReadOnly();The
disabledprop applies a lightweight visual lock (pointer-events off, reduced opacity). For true non-editable behavior that also dims the toolbar, preferreadonly/setReadOnly().
Fonts
fontName and fontSize set the editor's default font. The defaults are written directly onto each block in the exported HTML (font-family on <p>/<div>/<hN>, font-size on <p>/<div>) rather than relying on the editor's stylesheet being present, so the content keeps its font when it is rendered somewhere else — an email body opened in another client, for example. Headings keep their level-based sizing and carry no block font-size.
<Editor
config={{
fontName: 'Georgia, serif',
fontSize: '14pt',
}}
/>Selecting text and picking a font or size from the fontfamily / fontsize toolbar dropdowns produces an inline <span> that overrides the block default, so a single paragraph can mix fonts and sizes.
The Toolbar Shows the Font at the Cursor
The fontfamily and fontsize toolbar buttons display the value in effect at the cursor — "Georgia", "14pt" — and update as the caret moves, instead of showing the static words "Font" and "Font size". A family is shown by its configured name (the label side of font_family_formats); a font that is not in the configured list, such as one pasted in from another editor, is shown by its first family name. Labels are width-constrained and ellipsized so the toolbar does not reflow as the caret moves, with the full text on each button's title.
Where no single value applies, the button falls back to the generic word. That happens for a selection spanning two different fonts or sizes, and for font size inside a heading.
This is styled by rules in
@mdaemon/html-editor/dist/styles.css— make sure the stylesheet import is present, and re-check any custom CSS you have layered on top of the toolbar buttons.
Reading the Current Font
getFontFamily() and getFontSize() report the font in effect at the cursor, resolving an inline <span> override first, then the block's own font, then the configured default:
const editor = editorRef.current?.getEditor();
const family = editor?.getFontFamily(); // e.g. 'Georgia, serif'
const size = editor?.getFontSize(); // e.g. '14pt'Use these to drive a custom font picker outside the toolbar. Unlike reading the inline mark directly, they still return a value when the font comes from the block or the configured default.
Both return '' when there is no single value to report:
- the selection spans more than one font family / size
- for
getFontSize(), the cursor is in a heading with no inline override (headings size by level and carry no blockfont-size)
Treat '' as "mixed / not applicable" and blank your picker, rather than falling back to a default — that is what the toolbar does.
The editor emits no selection event of its own, so subscribe on the TipTap instance to keep a readout in sync with the caret:
const handleInit = useCallback((editor: HTMLEditor) => {
const sync = () => {
setFamily(editor.getFontFamily());
setSize(editor.getFontSize());
};
sync();
editor.getTipTap()?.on('selectionUpdate', sync);
editor.getTipTap()?.on('transaction', sync);
}, []);
<Editor config={{ fontName: 'Georgia, serif', fontSize: '14pt' }} onInit={handleInit} />Setting a Block Font
To change the font on every block the selection touches — instead of adding an inline override — use the setBlockFontFamily / setBlockFontSize commands on the underlying TipTap instance:
const tiptap = editorRef.current?.getEditor()?.getTipTap();
tiptap?.chain().focus().setBlockFontFamily('Georgia, serif').run();
tiptap?.chain().focus().setBlockFontSize('14pt').run();Templates
Enable the template dropdown and provide a templates array:
import type { Template } from '@mdaemon/html-editor-react';
const templates: Template[] = [
{
title: 'Greeting',
description: 'A friendly greeting',
content: '<p>Hello! Thank you for reaching out.</p>',
},
{
title: 'Signature',
content: '<p>Best regards,<br/>Your Name</p>',
},
];
<Editor
config={{
includeTemplates: true,
templates,
}}
/>The Template interface:
interface Template {
id?: number | string;
title: string;
description?: string;
content: string;
}Custom Toolbar Buttons
Register custom buttons via the setup callback in config:
<Editor
config={{
setup: (editor) => {
editor.ui.registry.addButton('myButton', {
tooltip: 'Insert greeting',
text: 'Greet',
onAction: (api) => {
editor.insertContent('<p>Hello from a custom button!</p>');
},
onSetup: (api) => {
// api.isEnabled(), api.setEnabled(bool)
// api.isActive(), api.setActive(bool)
},
});
},
toolbar: 'bold italic | myButton',
}}
/>Button Options
| Option | Type | Description |
|--------|------|-------------|
| tooltip | string | Hover text. |
| text | string | Button label. |
| icon | string | Image URL (used instead of text). |
| disabled | boolean | Initial disabled state. |
| onSetup | (api) => void \| (() => void) | Called on creation; may return a teardown function. |
| onAction | (api) => void | Called on click. |
Button API
The api object passed to onSetup and onAction:
| Method | Description |
|--------|-------------|
| isEnabled() | Returns current enabled state. |
| setEnabled(enabled) | Enable or disable the button. |
| isActive() | Returns active/pressed state. |
| setActive(active) | Toggle active/pressed visual style. |
Custom Toolbar Layout
Provide a toolbar string to control which buttons appear and in what order. Use | to group buttons and || to create a collapsible overflow section:
<Editor
config={{
toolbar: 'bold italic underline | fontfamily fontsize || forecolor backcolor | undo redo',
}}
/>Buttons after || begin collapsed behind a toggle (...) button.
Available Toolbar Buttons
| Button | Action |
|--------|--------|
| bold | Toggle bold |
| italic | Toggle italic |
| underline | Toggle underline |
| strikethrough | Toggle strikethrough |
| subscript | Toggle subscript |
| superscript | Toggle superscript |
| bullist | Bullet list |
| numlist | Numbered list |
| outdent | Decrease indent (lifts a list item, or removes block margin-left) |
| indent | Increase indent (nests a list item, or adds block margin-left) |
| blockquote | Toggle block quote |
| fontfamily | Font family dropdown |
| fontsize | Font size dropdown |
| lineheight | Line height dropdown |
| blocks | Block format dropdown — Paragraph, Heading 1–6 (alias formatselect) |
| styles | Named styles dropdown (configurable via style_formats) |
| template | Template dropdown (requires includeTemplates: true) |
| alignleft | Left align |
| aligncenter | Center align |
| alignright | Right align |
| alignjustify | Justify |
| forecolor | Text color picker |
| backcolor | Highlight color picker |
| removeformat | Strip all formatting |
| copy | Copy selection |
| cut | Cut selection |
| paste | Paste from clipboard |
| undo | Undo |
| redo | Redo |
| image | Insert image (upload or URL) |
| table | Table dropdown (insert table + row/column/cell operations) |
| hr | Insert horizontal rule |
| charmap | Special character picker |
| emoticons | Emoji picker with search |
| code | Open HTML source code editor dialog |
| link | Insert/edit hyperlink |
| unlink | Remove the link at the cursor |
| anchor | Insert a named anchor (<a id> target) |
| codesample | Toggle code sample |
| fullscreen | Toggle fullscreen |
| preview | Preview in new window |
| searchreplace | Find & Replace dialog |
| speechtotext | Open Speech to Text dialog (requires speech_to_text + browser support) |
| dictate | Toggle inline dictation — inserts speech at the cursor (requires speech_to_text + browser support) |
| ltr | Left-to-right direction |
| rtl | Right-to-left direction |
Image Upload
The image toolbar button opens a dialog supporting drag-and-drop upload or direct URL entry. Supported formats: JPEG, PNG, GIF, WebP, SVG.
When images_upload_url is configured, files are posted as multipart/form-data. The server must return JSON with a location, url, or link field. Without an upload URL, images are embedded as base64 data URIs.
SVGs are automatically sanitized (script tags, event handlers, and dangerous elements are stripped).
<Editor
config={{
images_upload_url: '/api/upload',
images_upload_credentials: true,
images_upload_base_path: '/files/',
images_upload_max_size: 5 * 1024 * 1024,
images_upload_headers: {
'X-CSRF-Token': csrfToken,
},
}}
/>Localization
Set Language at Init
<Editor config={{ language: 'de' }} />Custom Translation Function
import { setTranslate } from '@mdaemon/html-editor-react';
setTranslate((key) => myTranslations[key] ?? key);
// Or via the Editor prop:
<Editor translate={(key) => myTranslations[key] ?? key} config={{}} />Change Language at Runtime
const editorRef = useRef<EditorRef>(null);
// Switch to French
editorRef.current?.getEditor()?.setLanguage('fr');Supported Languages
| Code | Language | Code | Language |
|------|----------|------|----------|
| en | English | nl | Nederlands |
| ar | العربية | nb | Norsk bokmal |
| ca | Catala | pl | Polski |
| zh | Chinese | pt | Portugues |
| cs | Cesky | ro | Romana |
| da | Dansk | ru | Russian |
| en-gb | English (UK) | sr | Srpski |
| fi | Suomi | sl | Slovenscina |
| fr | Francais | es | Espanol |
| fr-ca | Canadien francais | sv | Svenska |
| de | Deutsch | zh-tw | Chinese (Taiwan) |
| el | Greek | th | Thai |
| hu | Magyar | tr | Turkce |
| id | Bahasa Indonesia | vi | Tieng Viet |
| it | Italiano | | |
| ja | Japanese | | |
| ko | Korean | | |
File Source Resolver
Transform image src attributes globally, useful for CDN prefixing or relative path resolution:
import { setGetFileSrc } from '@mdaemon/html-editor-react';
setGetFileSrc((path) => `https://cdn.example.com${path}`);
// Or via the Editor prop:
<Editor getFileSrc={(path) => `https://cdn.example.com${path}`} config={{}} />Keyboard Shortcuts
| Shortcut | Action | |----------|--------| | Ctrl/Cmd + B | Bold | | Ctrl/Cmd + I | Italic | | Ctrl/Cmd + U | Underline | | Ctrl/Cmd + Z | Undo | | Ctrl/Cmd + Shift + Z | Redo | | Ctrl/Cmd + F | Find & Replace | | Tab | Indent — nests a list item, moves to the next table cell, or adds block indent | | Shift + Tab | Outdent — lifts a list item, moves to the previous table cell, or removes block indent | | Esc, then Tab | Move focus to the next element outside the editor (keyboard escape) | | Esc, then Shift + Tab | Move focus to the previous element outside the editor |
Tabis captured for indentation, so theEsc-then-Tabsequence provides a keyboard escape (WCAG 2.1.2, "No Keyboard Trap").Escarms the escape for a single key press; any other key disarms it.
Form Integration
Use the name prop to render a hidden <input> containing the editor content, useful for traditional form submission:
<form onSubmit={handleSubmit}>
<Editor
config={{}}
name="email_body"
initialValue="<p>Draft content</p>"
/>
<button type="submit">Send</button>
</form>Exports
// Components
import { Editor, MDEditor } from '@mdaemon/html-editor-react'; // MDEditor is an alias
// Hook
import { useEditor } from '@mdaemon/html-editor-react';
// Global functions
import { getEditorContent, setEditorContent } from '@mdaemon/html-editor-react';
// Utilities (re-exported from @mdaemon/html-editor)
import { fontNames, setTranslate, setGetFileSrc } from '@mdaemon/html-editor-react';
// Types
import type {
EditorProps,
EditorRef,
UseEditorOptions,
UseEditorReturn,
EditorConfig,
EditorEvents,
Template,
ToolbarButtonSpec,
ToolbarButtonAPI,
} from '@mdaemon/html-editor-react';Migration from @tinymce/tinymce-react
// Before (TinyMCE)
import { Editor } from '@tinymce/tinymce-react';
<Editor
apiKey="your-key"
init={{ height: 400, plugins: 'link image table' }}
initialValue="<p>Hello</p>"
onEditorChange={(content) => save(content)}
/>
// After (MDHTMLEditor React)
import { Editor } from '@mdaemon/html-editor-react';
import '@mdaemon/html-editor/dist/styles.css';
<Editor
config={{ height: 400 }}
initialValue="<p>Hello</p>"
onChange={(content) => save(content)}
/>Key differences:
- No
apiKeyorlicenseKeyrequired initprop is renamed toconfigonEditorChangeis renamed toonChangepluginsoption is not needed — all features are built in- Toolbar customization uses
basicEditor: true/falseor atoolbarstring
Running the Demo
A demo app is included to see the editor in action:
npm run demoThis starts a Vite dev server at http://localhost:5173 with three sections: the <Editor> component, the useEditor hook, and a Fonts panel with a live getFontFamily() / getFontSize() readout that follows the cursor and buttons for the block-font commands.
Development
The package is bundled with Vite 8 (Rolldown) and ships ES, CommonJS, and TypeScript declaration outputs.
| Script | Description |
|--------|-------------|
| npm run build | Production build to dist/ (ES + CJS + .d.ts). |
| npm run build:dev | Development build (unminified, sourcemaps). |
| npm run dev | Rebuild on change (watch mode). |
| npm test | Run the Jest test suite. |
| npm run test:coverage | Run tests with coverage. |
| npm run lint | Lint src with ESLint. |
| npm run typecheck | Type-check src and demo with tsc --noEmit. |
| npm run demo | Launch the demo app. |
Node.js: building the package requires Node
^20.19.0 || >=22.12.0(a Vite 8 requirement). Consumers of the published package are unaffected.
Lint, typecheck, and tests run automatically in CI on every push and pull request to master across Node 20, 22, 24, and 26.
License
LGPL-3.0-or-later — MDaemon Technologies, Ltd.
