@mdaemon/html-editor-react 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +812 -771
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,771 +1,812 @@
1
- [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmdaemon-technologies%2FMDHTMLEditor-react%2Fmaster%2Fpackage.json&query=%24.version&prefix=v&label=npm&color=blue)](https://www.npmjs.com/package/@mdaemon/html-editor-react) [![install size](https://packagephobia.com/badge?p=@mdaemon/html-editor-react)](https://packagephobia.com/result?p=@mdaemon/html-editor-react) [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmdaemon-technologies%2FMDHTMLEditor-react%2Fmaster%2Fpackage.json&query=%24.license&prefix=v&label=license&color=green)](https://github.com/mdaemon-technologies/MDHTMLEditor-react/blob/master/LICENSE) [![Node.js CI](https://github.com/mdaemon-technologies/MDHTMLEditor-react/actions/workflows/ci.yml/badge.svg)](https://github.com/mdaemon-technologies/MDHTMLEditor-react/actions/workflows/ci.yml)
2
-
3
- # MDHTMLEditor React
4
-
5
- A React wrapper for [MDHTMLEditor](https://github.com/mdaemon-technologies/MDHTMLEditor) — a WYSIWYG HTML editor built on TipTap. Provides a drop-in replacement for `@tinymce/tinymce-react` with no license key required.
6
-
7
- ## Installation
8
-
9
- ```bash
10
- npm install @mdaemon/html-editor-react @mdaemon/html-editor
11
- ```
12
-
13
- **Peer dependencies:** `react` and `react-dom` (v18 or v19).
14
-
15
- ## Styles
16
-
17
- You must import the editor stylesheet for the toolbar and UI to render correctly:
18
-
19
- ```tsx
20
- import '@mdaemon/html-editor/dist/styles.css';
21
- ```
22
-
23
- ## Quick Start
24
-
25
- ```tsx
26
- import { useRef } from 'react';
27
- import { Editor } from '@mdaemon/html-editor-react';
28
- import type { EditorRef } from '@mdaemon/html-editor-react';
29
- import '@mdaemon/html-editor/dist/styles.css';
30
-
31
- function App() {
32
- const editorRef = useRef<EditorRef>(null);
33
-
34
- return (
35
- <Editor
36
- ref={editorRef}
37
- config={{ height: 400 }}
38
- initialValue="<p>Hello World</p>"
39
- onChange={(html) => console.log('Content:', html)}
40
- onInit={(editor) => console.log('Ready!', editor)}
41
- />
42
- );
43
- }
44
- ```
45
-
46
- ## `<Editor>` Component
47
-
48
- The primary way to use the editor. It accepts a `config` object and event callbacks, and exposes imperative methods via a ref.
49
-
50
- ### Props
51
-
52
- | Prop | Type | Required | Description |
53
- |------|------|----------|-------------|
54
- | `config` | `EditorConfig` | Yes | Configuration object passed to the underlying HTMLEditor. See [Configuration](#configuration). |
55
- | `body` | `string` | No | Initial HTML content. Takes precedence over `initialValue`. |
56
- | `initialValue` | `string` | No | Initial HTML content (alias for `body`). |
57
- | `name` | `string` | No | When set, renders a hidden `<input>` with this name containing the editor content &mdash; useful for form submission. |
58
- | `disabled` | `boolean` | No | Visually disables the editor (pointer-events off, reduced opacity). |
59
- | `onChange` | `(content: string) => void` | No | Called when content changes (debounced). |
60
- | `onDirty` | `(dirty: boolean) => void` | No | Called when the dirty state changes. |
61
- | `onInit` | `(editor: HTMLEditor) => void` | No | Called when the editor finishes initialization. Receives the editor instance. |
62
- | `onFocus` | `() => void` | No | Called when the editor receives focus. |
63
- | `onBlur` | `() => void` | No | Called when the editor loses focus. |
64
- | `translate` | `(key: string) => string` | No | Sets a global translation function for all editors on the page. |
65
- | `getFileSrc` | `(path: string) => string` | No | Sets a global file path resolver (e.g., for CDN prefixing). |
66
-
67
- ### Ref Methods (`EditorRef`)
68
-
69
- Attach a ref to access imperative methods:
70
-
71
- ```tsx
72
- const editorRef = useRef<EditorRef>(null);
73
-
74
- // Later...
75
- const html = editorRef.current?.getContent();
76
- editorRef.current?.setContent('<p>New content</p>');
77
- editorRef.current?.insertContent('<p>Inserted at cursor</p>');
78
- editorRef.current?.focus();
79
-
80
- // Access the underlying HTMLEditor instance directly
81
- const rawEditor = editorRef.current?.getEditor();
82
- ```
83
-
84
- | Method | Signature | Description |
85
- |--------|-----------|-------------|
86
- | `getEditor` | `() => HTMLEditor \| null` | Access the raw HTMLEditor instance for advanced usage. |
87
- | `getContent` | `() => string` | Get current HTML content. |
88
- | `setContent` | `(html: string) => void` | Replace the editor content. |
89
- | `insertContent` | `(html: string) => void` | Insert HTML at the current cursor position. |
90
- | `focus` | `() => void` | Focus the editor. |
91
-
92
- ### Full Example
93
-
94
- ```tsx
95
- import { useRef, useState } from 'react';
96
- import { Editor } from '@mdaemon/html-editor-react';
97
- import type { EditorRef } from '@mdaemon/html-editor-react';
98
- import '@mdaemon/html-editor/dist/styles.css';
99
-
100
- function EmailComposer() {
101
- const editorRef = useRef<EditorRef>(null);
102
- const [dirty, setDirty] = useState(false);
103
-
104
- const handleSave = () => {
105
- const content = editorRef.current?.getContent() ?? '';
106
- console.log('Saving:', content);
107
- };
108
-
109
- return (
110
- <div>
111
- <Editor
112
- ref={editorRef}
113
- config={{
114
- height: 500,
115
- basicEditor: false,
116
- skin: 'oxide',
117
- fontName: 'Arial',
118
- fontSize: '12pt',
119
- }}
120
- initialValue="<p>Dear recipient,</p>"
121
- onChange={(html) => console.log('Changed:', html)}
122
- onDirty={(d) => setDirty(d)}
123
- onInit={() => console.log('Editor ready')}
124
- />
125
- <button onClick={handleSave} disabled={!dirty}>
126
- Save
127
- </button>
128
- </div>
129
- );
130
- }
131
- ```
132
-
133
- ## `useEditor` Hook
134
-
135
- For more programmatic control, use the `useEditor` hook. You provide a container element via the returned `containerRef`.
136
-
137
- ### Options
138
-
139
- | Option | Type | Default | Description |
140
- |--------|------|---------|-------------|
141
- | `config` | `EditorConfig` | `{}` | Editor configuration. |
142
- | `content` | `string` | `''` | Initial HTML content. |
143
- | `onUpdate` | `(html: string) => void` | &mdash; | Called on content change. |
144
-
145
- ### Return Value
146
-
147
- | Property | Type | Description |
148
- |----------|------|-------------|
149
- | `editor` | `HTMLEditor \| null` | The raw editor instance (`null` until initialized). |
150
- | `containerRef` | `RefObject<HTMLDivElement \| null>` | Attach this to your container div. |
151
- | `ready` | `boolean` | `true` once the editor has fired its `init` event. |
152
- | `getContent` | `() => string` | Get current HTML content. |
153
- | `setContent` | `(html: string) => void` | Replace the editor content. |
154
- | `insertContent` | `(html: string) => void` | Insert HTML at the cursor. |
155
- | `focus` | `() => void` | Focus the editor. |
156
- | `isDirty` | `() => boolean` | Check if the editor has unsaved changes. |
157
-
158
- ### Example
159
-
160
- ```tsx
161
- import { useState } from 'react';
162
- import { useEditor } from '@mdaemon/html-editor-react';
163
- import '@mdaemon/html-editor/dist/styles.css';
164
-
165
- function NotesEditor() {
166
- const [lastSaved, setLastSaved] = useState('');
167
- const { containerRef, ready, getContent, setContent, isDirty } = useEditor({
168
- config: { height: 300, basicEditor: true },
169
- content: '<p>Start taking notes...</p>',
170
- onUpdate: (html) => console.log('Updated:', html),
171
- });
172
-
173
- const handleSave = () => {
174
- setLastSaved(getContent());
175
- };
176
-
177
- const handleReset = () => {
178
- setContent('<p>Start taking notes...</p>');
179
- };
180
-
181
- return (
182
- <div>
183
- <div ref={containerRef} />
184
- {ready && (
185
- <div>
186
- <button onClick={handleSave}>Save</button>
187
- <button onClick={handleReset}>Reset</button>
188
- <span>{isDirty() ? 'Unsaved changes' : 'Saved'}</span>
189
- </div>
190
- )}
191
- </div>
192
- );
193
- }
194
- ```
195
-
196
- ## Global Convenience Functions
197
-
198
- For single-editor pages, you can get and set content without a ref:
199
-
200
- ```tsx
201
- import { getEditorContent, setEditorContent } from '@mdaemon/html-editor-react';
202
-
203
- // Get content from the active editor
204
- const html = getEditorContent();
205
-
206
- // Set content on the active editor
207
- setEditorContent('<p>New content</p>');
208
- ```
209
-
210
- > **Note:** These functions operate on the most recently mounted `<Editor>` instance. They are intended for pages with a single editor.
211
-
212
- ## Configuration
213
-
214
- The `config` prop (or `useEditor`'s `config` option) accepts an `EditorConfig` object. All options from `@mdaemon/html-editor` are supported:
215
-
216
- | Option | Type | Default | Description |
217
- |--------|------|---------|-------------|
218
- | `basicEditor` | `boolean` | `false` | Use a simplified toolbar (no images, tables, code blocks). |
219
- | `readonly` | `boolean` | `false` | Start the editor in read-only mode. Toggle at runtime via `getEditor()?.setReadOnly()`. |
220
- | `height` | `string \| number` | `300` | Editor height. |
221
- | `min_height` | `string \| number` | &mdash; | Minimum editor height. |
222
- | `max_height` | `string \| number` | &mdash; | Maximum editor height. |
223
- | `language` | `string` | `'en'` | UI language code. 31 languages built in. |
224
- | `skin` | `'oxide' \| 'oxide-dark' \| 'confab' \| 'confab-dark'` | `'oxide'` | Toolbar and dialog theme. |
225
- | `content_css` | `'default' \| 'dark' \| 'confab' \| 'confab-dark'` | `'default'` | Content area theme. |
226
- | `content_style` | `string` | &mdash; | Custom CSS injected into the editing surface. |
227
- | `fontName` | `string` | `'arial, helvetica, sans-serif'` | Default font family. Inlined on every block in the exported HTML &mdash; see [Fonts](#fonts). |
228
- | `fontSize` | `string` | `'12pt'` | Default font size. Inlined on every paragraph in the exported HTML &mdash; see [Fonts](#fonts). |
229
- | `font_family_formats` | `string` | *(TinyMCE defaults)* | Semicolon-delimited font list (`Name=family,...`). |
230
- | `font_size_formats` | `string` | `'8pt 9pt 10pt 12pt 14pt 18pt 24pt 36pt'` | Space-delimited size options. |
231
- | `font_names` | `string` | &mdash; | CKEditor alias for `font_family_formats`. |
232
- | `fontSize_sizes` | `string` | &mdash; | CKEditor alias for `font_size_formats`. |
233
- | `block_formats` | `string` | *(Paragraph + H1&ndash;H6)* | Block-format dropdown definitions for the `blocks` button (`Name=tag;...`). |
234
- | `style_formats` | `StyleFormat[]` | *(subset)* | Named styles for the `styles` dropdown (CKEditor `stylesSet`-compatible). |
235
- | `forced_root_block` | `'p' \| 'div'` | `'p'` | Block element produced on Enter. `'div'` gives CKEditor `ENTER_DIV` parity. |
236
- | `trailingNode` | `boolean` | `false` | Append an empty trailing paragraph after a block node (table, image, code block) so the cursor can sit after it. |
237
- | `directionality` | `'ltr' \| 'rtl'` | `'ltr'` | Text direction. |
238
- | `toolbar` | `string` | *(preset)* | Custom toolbar layout string. |
239
- | `toolbar_mode` | `'sliding' \| 'floating' \| 'wrap'` | `'wrap'` | Toolbar overflow behavior. |
240
- | `toolbar_sticky` | `boolean` | `true` | Pin toolbar at top when scrolling. |
241
- | `toolbar_narrow_breakpoint` | `number` | &mdash; | Pixel width below which the toolbar collapses into the overflow toggle. |
242
- | `toolbar_priority` | `Record<string, number>` | &mdash; | Per-button overflow priority &mdash; higher values stay visible longer as the toolbar narrows. |
243
- | `menubar` | `boolean` | `false` | Show a TinyMCE-style menu bar above the toolbar. |
244
- | `contextmenu` | `boolean \| string` | `''` | Enable a right-click context menu. Pass a button-list string to customize its contents. |
245
- | `quickbars_selection_toolbar` | `string` | `'bold italic \| quicklink blockquote'` | Inline floating toolbar shown over a text selection. |
246
- | `quickbars_image_toolbar` | `boolean` | `false` | Inline floating toolbar shown when an image is selected. |
247
- | `quickbars_insert_toolbar` | `boolean` | `false` | Inline floating toolbar shown on empty lines for quick insertion. |
248
- | `elementpath` | `boolean` | `false` | Show a breadcrumb of the node path at the cursor in the status bar. |
249
- | `valid_children` | `string` | &mdash; | TinyMCE-style rules controlling which child elements are allowed. |
250
- | `auto_focus` | `string` | &mdash; | Auto-focus on init. |
251
- | `setFocus` | `string` | &mdash; | CSS selector for the element to focus on init (used when `auto_focus` is not set). |
252
- | `plugins` | `string` | &mdash; | Accepted for TinyMCE compatibility and ignored &mdash; all features are built in. |
253
- | `browser_spellcheck` | `boolean` | `true` | Enable browser spell check. |
254
- | `entity_encoding` | `'raw' \| 'named' \| 'numeric'` | `'raw'` | HTML entity encoding mode. |
255
- | `paste_from_office` | `boolean` | `true` | Clean and preserve formatting when pasting from Microsoft Word/Excel. |
256
- | `speech_to_text` | `boolean` | `true` | Enable the `speechtotext` and `dictate` toolbar buttons (requires the Web Speech API). |
257
- | `convert_unsafe_embeds` | `boolean` | `true` | Sanitize embedded content. |
258
- | `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>` &mdash; 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. |
259
- | `includeTemplates` | `boolean` | `false` | Show the template dropdown. |
260
- | `templates` | `Template[]` | `[]` | Predefined HTML templates. |
261
- | `dropbox` | `boolean` | `false` | Enable Dropbox integration. |
262
- | `images_upload_url` | `string` | &mdash; | Server endpoint for image uploads. |
263
- | `images_upload_credentials` | `boolean` | `true` | Include credentials with upload requests. |
264
- | `images_upload_base_path` | `string` | `'/'` | Prefix for uploaded image URLs. |
265
- | `images_upload_max_size` | `number` | `10485760` | Max upload size in bytes (10 MB). |
266
- | `images_upload_headers` | `Record<string, string>` | &mdash; | Extra headers for upload requests. |
267
- | `images_file_types` | `string` | *(permissive)* | Comma/space-separated accepted extensions (e.g. `'jpg,jpeg,png,gif'`). |
268
- | `images_upload_validate` | `(file: File) => string \| null` | &mdash; | Pre-upload hook; return a message to reject the file, or `null` to allow. |
269
- | `images_upload_error` | `(message: string) => void` | &mdash; | Caller-supplied alert for drag-drop/paste upload rejections and failures. |
270
- | `setup` | `(editor) => void` | &mdash; | Pre-init callback for registering custom toolbar buttons. |
271
-
272
- ### Dark Theme
273
-
274
- ```tsx
275
- <Editor
276
- config={{
277
- skin: 'oxide-dark',
278
- content_css: 'dark',
279
- }}
280
- />
281
- ```
282
-
283
- ### Confab Skin
284
-
285
- 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:
286
-
287
- ```tsx
288
- <Editor
289
- config={{
290
- skin: 'confab-dark',
291
- content_css: 'confab-dark',
292
- }}
293
- />
294
- ```
295
-
296
- ### Custom Content Styles
297
-
298
- ```tsx
299
- <Editor
300
- config={{
301
- content_style: 'body { font-family: Georgia, serif; font-size: 16px; line-height: 1.6; }',
302
- }}
303
- />
304
- ```
305
-
306
- ### Menu Bar, Context Menu & Quick Toolbars
307
-
308
- Opt into the additional editing surfaces exposed by `@mdaemon/html-editor` 1.6.0:
309
-
310
- ```tsx
311
- <Editor
312
- config={{
313
- menubar: true, // menu bar above the toolbar
314
- contextmenu: true, // right-click context menu
315
- quickbars_selection_toolbar: 'bold italic | quicklink blockquote',
316
- quickbars_image_toolbar: true, // floating toolbar when an image is selected
317
- elementpath: true, // breadcrumb status bar at the cursor
318
- }}
319
- />
320
- ```
321
-
322
- `contextmenu` also accepts a button-list string to customize its contents, and
323
- `quickbars_insert_toolbar` shows a floating insert toolbar on empty lines.
324
-
325
- ### Read-Only Mode
326
-
327
- Start the editor read-only with `readonly: true`, or toggle it at runtime through the underlying editor instance:
328
-
329
- ```tsx
330
- <Editor config={{ readonly: true }} ref={editorRef} />
331
-
332
- // Toggle later
333
- editorRef.current?.getEditor()?.setReadOnly(false);
334
- const isReadOnly = editorRef.current?.getEditor()?.isReadOnly();
335
- ```
336
-
337
- > The `disabled` prop applies a lightweight visual lock (pointer-events off, reduced opacity). For true non-editable behavior that also dims the toolbar, prefer `readonly` / `setReadOnly()`.
338
-
339
- ## Fonts
340
-
341
- `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 &mdash; an email body opened in another client, for example. Headings keep their level-based sizing and carry no block `font-size`.
342
-
343
- ```tsx
344
- <Editor
345
- config={{
346
- fontName: 'Georgia, serif',
347
- fontSize: '14pt',
348
- }}
349
- />
350
- ```
351
-
352
- 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.
353
-
354
- ### The Toolbar Shows the Font at the Cursor
355
-
356
- The `fontfamily` and `fontsize` toolbar buttons display the value in effect at the cursor &mdash; "Georgia", "14pt" &mdash; 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`.
357
-
358
- 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.
359
-
360
- > This is styled by rules in `@mdaemon/html-editor/dist/styles.css` &mdash; make sure the [stylesheet import](#styles) is present, and re-check any custom CSS you have layered on top of the toolbar buttons.
361
-
362
- ### Reading the Current Font
363
-
364
- `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:
365
-
366
- ```tsx
367
- const editor = editorRef.current?.getEditor();
368
-
369
- const family = editor?.getFontFamily(); // e.g. 'Georgia, serif'
370
- const size = editor?.getFontSize(); // e.g. '14pt'
371
- ```
372
-
373
- 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.
374
-
375
- Both return `''` when there is no single value to report:
376
-
377
- - the selection spans more than one font family / size
378
- - for `getFontSize()`, the cursor is in a heading with no inline override (headings size by level and carry no block `font-size`)
379
-
380
- Treat `''` as "mixed / not applicable" and blank your picker, rather than falling back to a default &mdash; that is what the toolbar does.
381
-
382
- The editor emits no selection event of its own, so subscribe on the TipTap instance to keep a readout in sync with the caret:
383
-
384
- ```tsx
385
- const handleInit = useCallback((editor: HTMLEditor) => {
386
- const sync = () => {
387
- setFamily(editor.getFontFamily());
388
- setSize(editor.getFontSize());
389
- };
390
-
391
- sync();
392
- editor.getTipTap()?.on('selectionUpdate', sync);
393
- editor.getTipTap()?.on('transaction', sync);
394
- }, []);
395
-
396
- <Editor config={{ fontName: 'Georgia, serif', fontSize: '14pt' }} onInit={handleInit} />
397
- ```
398
-
399
- ### Setting a Block Font
400
-
401
- To change the font on every block the selection touches &mdash; instead of adding an inline override &mdash; use the `setBlockFontFamily` / `setBlockFontSize` commands on the underlying TipTap instance:
402
-
403
- ```tsx
404
- const tiptap = editorRef.current?.getEditor()?.getTipTap();
405
-
406
- tiptap?.chain().focus().setBlockFontFamily('Georgia, serif').run();
407
- tiptap?.chain().focus().setBlockFontSize('14pt').run();
408
- ```
409
-
410
- ## Templates
411
-
412
- Enable the template dropdown and provide a `templates` array:
413
-
414
- ```tsx
415
- import type { Template } from '@mdaemon/html-editor-react';
416
-
417
- const templates: Template[] = [
418
- {
419
- title: 'Greeting',
420
- description: 'A friendly greeting',
421
- content: '<p>Hello! Thank you for reaching out.</p>',
422
- },
423
- {
424
- title: 'Signature',
425
- content: '<p>Best regards,<br/>Your Name</p>',
426
- },
427
- ];
428
-
429
- <Editor
430
- config={{
431
- includeTemplates: true,
432
- templates,
433
- }}
434
- />
435
- ```
436
-
437
- The `Template` interface:
438
-
439
- ```typescript
440
- interface Template {
441
- id?: number | string;
442
- title: string;
443
- description?: string;
444
- content: string;
445
- }
446
- ```
447
-
448
- ## Custom Toolbar Buttons
449
-
450
- Register custom buttons via the `setup` callback in `config`:
451
-
452
- ```tsx
453
- <Editor
454
- config={{
455
- setup: (editor) => {
456
- editor.ui.registry.addButton('myButton', {
457
- tooltip: 'Insert greeting',
458
- text: 'Greet',
459
- onAction: (api) => {
460
- editor.insertContent('<p>Hello from a custom button!</p>');
461
- },
462
- onSetup: (api) => {
463
- // api.isEnabled(), api.setEnabled(bool)
464
- // api.isActive(), api.setActive(bool)
465
- },
466
- });
467
- },
468
- toolbar: 'bold italic | myButton',
469
- }}
470
- />
471
- ```
472
-
473
- ### Button Options
474
-
475
- | Option | Type | Description |
476
- |--------|------|-------------|
477
- | `tooltip` | `string` | Hover text. |
478
- | `text` | `string` | Button label. |
479
- | `icon` | `string` | Image URL (used instead of text). |
480
- | `disabled` | `boolean` | Initial disabled state. |
481
- | `onSetup` | `(api) => void \| (() => void)` | Called on creation; may return a teardown function. |
482
- | `onAction` | `(api) => void` | Called on click. |
483
-
484
- ### Button API
485
-
486
- The `api` object passed to `onSetup` and `onAction`:
487
-
488
- | Method | Description |
489
- |--------|-------------|
490
- | `isEnabled()` | Returns current enabled state. |
491
- | `setEnabled(enabled)` | Enable or disable the button. |
492
- | `isActive()` | Returns active/pressed state. |
493
- | `setActive(active)` | Toggle active/pressed visual style. |
494
-
495
- ## Custom Toolbar Layout
496
-
497
- Provide a `toolbar` string to control which buttons appear and in what order. Use `|` to group buttons and `||` to create a collapsible overflow section:
498
-
499
- ```tsx
500
- <Editor
501
- config={{
502
- toolbar: 'bold italic underline | fontfamily fontsize || forecolor backcolor | undo redo',
503
- }}
504
- />
505
- ```
506
-
507
- Buttons after `||` begin collapsed behind a toggle (`...`) button.
508
-
509
- ### Available Toolbar Buttons
510
-
511
- | Button | Action |
512
- |--------|--------|
513
- | `bold` | Toggle bold |
514
- | `italic` | Toggle italic |
515
- | `underline` | Toggle underline |
516
- | `strikethrough` | Toggle strikethrough |
517
- | `subscript` | Toggle subscript |
518
- | `superscript` | Toggle superscript |
519
- | `bullist` | Bullet list |
520
- | `numlist` | Numbered list |
521
- | `outdent` | Decrease indent (lifts a list item, or removes block `margin-left`) |
522
- | `indent` | Increase indent (nests a list item, or adds block `margin-left`) |
523
- | `blockquote` | Toggle block quote |
524
- | `fontfamily` | Font family dropdown |
525
- | `fontsize` | Font size dropdown |
526
- | `lineheight` | Line height dropdown |
527
- | `blocks` | Block format dropdown &mdash; Paragraph, Heading 1&ndash;6 (alias `formatselect`) |
528
- | `styles` | Named styles dropdown (configurable via `style_formats`) |
529
- | `template` | Template dropdown (requires `includeTemplates: true`) |
530
- | `alignleft` | Left align |
531
- | `aligncenter` | Center align |
532
- | `alignright` | Right align |
533
- | `alignjustify` | Justify |
534
- | `forecolor` | Text color picker |
535
- | `backcolor` | Highlight color picker |
536
- | `removeformat` | Strip all formatting |
537
- | `copy` | Copy selection |
538
- | `cut` | Cut selection |
539
- | `paste` | Paste from clipboard |
540
- | `undo` | Undo |
541
- | `redo` | Redo |
542
- | `image` | Insert image (upload or URL) |
543
- | `table` | Table dropdown (insert table + row/column/cell operations) |
544
- | `hr` | Insert horizontal rule |
545
- | `charmap` | Special character picker |
546
- | `emoticons` | Emoji picker with search |
547
- | `code` | Open HTML source code editor dialog |
548
- | `link` | Insert/edit hyperlink |
549
- | `unlink` | Remove the link at the cursor |
550
- | `anchor` | Insert a named anchor (`<a id>` target) |
551
- | `codesample` | Toggle code sample |
552
- | `fullscreen` | Toggle fullscreen |
553
- | `preview` | Preview in new window |
554
- | `searchreplace` | Find & Replace dialog |
555
- | `speechtotext` | Open Speech to Text dialog (requires `speech_to_text` + browser support) |
556
- | `dictate` | Toggle inline dictation &mdash; inserts speech at the cursor (requires `speech_to_text` + browser support) |
557
- | `ltr` | Left-to-right direction |
558
- | `rtl` | Right-to-left direction |
559
-
560
- ## Image Upload
561
-
562
- The image toolbar button opens a dialog supporting drag-and-drop upload or direct URL entry. Supported formats: JPEG, PNG, GIF, WebP, SVG.
563
-
564
- 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.
565
-
566
- SVGs are automatically sanitized (script tags, event handlers, and dangerous elements are stripped).
567
-
568
- ```tsx
569
- <Editor
570
- config={{
571
- images_upload_url: '/api/upload',
572
- images_upload_credentials: true,
573
- images_upload_base_path: '/files/',
574
- images_upload_max_size: 5 * 1024 * 1024,
575
- images_upload_headers: {
576
- 'X-CSRF-Token': csrfToken,
577
- },
578
- }}
579
- />
580
- ```
581
-
582
- ## Localization
583
-
584
- ### Set Language at Init
585
-
586
- ```tsx
587
- <Editor config={{ language: 'de' }} />
588
- ```
589
-
590
- ### Custom Translation Function
591
-
592
- ```tsx
593
- import { setTranslate } from '@mdaemon/html-editor-react';
594
-
595
- setTranslate((key) => myTranslations[key] ?? key);
596
-
597
- // Or via the Editor prop:
598
- <Editor translate={(key) => myTranslations[key] ?? key} config={{}} />
599
- ```
600
-
601
- ### Change Language at Runtime
602
-
603
- ```tsx
604
- const editorRef = useRef<EditorRef>(null);
605
-
606
- // Switch to French
607
- editorRef.current?.getEditor()?.setLanguage('fr');
608
- ```
609
-
610
- ### Supported Languages
611
-
612
- | Code | Language | Code | Language |
613
- |------|----------|------|----------|
614
- | `en` | English | `nl` | Nederlands |
615
- | `ar` | العربية | `nb` | Norsk bokmal |
616
- | `ca` | Catala | `pl` | Polski |
617
- | `zh` | Chinese | `pt` | Portugues |
618
- | `cs` | Cesky | `ro` | Romana |
619
- | `da` | Dansk | `ru` | Russian |
620
- | `en-gb` | English (UK) | `sr` | Srpski |
621
- | `fi` | Suomi | `sl` | Slovenscina |
622
- | `fr` | Francais | `es` | Espanol |
623
- | `fr-ca` | Canadien francais | `sv` | Svenska |
624
- | `de` | Deutsch | `zh-tw` | Chinese (Taiwan) |
625
- | `el` | Greek | `th` | Thai |
626
- | `hu` | Magyar | `tr` | Turkce |
627
- | `id` | Bahasa Indonesia | `vi` | Tieng Viet |
628
- | `it` | Italiano | | |
629
- | `ja` | Japanese | | |
630
- | `ko` | Korean | | |
631
-
632
- ## File Source Resolver
633
-
634
- Transform image `src` attributes globally, useful for CDN prefixing or relative path resolution:
635
-
636
- ```tsx
637
- import { setGetFileSrc } from '@mdaemon/html-editor-react';
638
-
639
- setGetFileSrc((path) => `https://cdn.example.com${path}`);
640
-
641
- // Or via the Editor prop:
642
- <Editor getFileSrc={(path) => `https://cdn.example.com${path}`} config={{}} />
643
- ```
644
-
645
- ## Keyboard Shortcuts
646
-
647
- | Shortcut | Action |
648
- |----------|--------|
649
- | Ctrl/Cmd + B | Bold |
650
- | Ctrl/Cmd + I | Italic |
651
- | Ctrl/Cmd + U | Underline |
652
- | Ctrl/Cmd + Z | Undo |
653
- | Ctrl/Cmd + Shift + Z | Redo |
654
- | Ctrl/Cmd + F | Find & Replace |
655
- | Tab | Indent &mdash; nests a list item, moves to the next table cell, or adds block indent |
656
- | Shift + Tab | Outdent &mdash; lifts a list item, moves to the previous table cell, or removes block indent |
657
- | Esc, then Tab | Move focus to the next element outside the editor (keyboard escape) |
658
- | Esc, then Shift + Tab | Move focus to the previous element outside the editor |
659
-
660
- > `Tab` is captured for indentation, so the `Esc`-then-`Tab` sequence provides a
661
- > keyboard escape (WCAG 2.1.2, "No Keyboard Trap"). `Esc` arms the escape for a single
662
- > key press; any other key disarms it.
663
-
664
- ## Form Integration
665
-
666
- Use the `name` prop to render a hidden `<input>` containing the editor content, useful for traditional form submission:
667
-
668
- ```tsx
669
- <form onSubmit={handleSubmit}>
670
- <Editor
671
- config={{}}
672
- name="email_body"
673
- initialValue="<p>Draft content</p>"
674
- />
675
- <button type="submit">Send</button>
676
- </form>
677
- ```
678
-
679
- ## Exports
680
-
681
- ```tsx
682
- // Components
683
- import { Editor, MDEditor } from '@mdaemon/html-editor-react'; // MDEditor is an alias
684
-
685
- // Hook
686
- import { useEditor } from '@mdaemon/html-editor-react';
687
-
688
- // Global functions
689
- import { getEditorContent, setEditorContent } from '@mdaemon/html-editor-react';
690
-
691
- // Utilities (re-exported from @mdaemon/html-editor)
692
- import { fontNames, setTranslate, setGetFileSrc } from '@mdaemon/html-editor-react';
693
-
694
- // Types
695
- import type {
696
- EditorProps,
697
- EditorRef,
698
- UseEditorOptions,
699
- UseEditorReturn,
700
- EditorConfig,
701
- EditorEvents,
702
- Template,
703
- ToolbarButtonSpec,
704
- ToolbarButtonAPI,
705
- } from '@mdaemon/html-editor-react';
706
- ```
707
-
708
- ## Migration from `@tinymce/tinymce-react`
709
-
710
- ```tsx
711
- // Before (TinyMCE)
712
- import { Editor } from '@tinymce/tinymce-react';
713
-
714
- <Editor
715
- apiKey="your-key"
716
- init={{ height: 400, plugins: 'link image table' }}
717
- initialValue="<p>Hello</p>"
718
- onEditorChange={(content) => save(content)}
719
- />
720
-
721
- // After (MDHTMLEditor React)
722
- import { Editor } from '@mdaemon/html-editor-react';
723
- import '@mdaemon/html-editor/dist/styles.css';
724
-
725
- <Editor
726
- config={{ height: 400 }}
727
- initialValue="<p>Hello</p>"
728
- onChange={(content) => save(content)}
729
- />
730
- ```
731
-
732
- Key differences:
733
-
734
- - No `apiKey` or `licenseKey` required
735
- - `init` prop is renamed to `config`
736
- - `onEditorChange` is renamed to `onChange`
737
- - `plugins` option is not needed &mdash; all features are built in
738
- - Toolbar customization uses `basicEditor: true/false` or a `toolbar` string
739
-
740
- ## Running the Demo
741
-
742
- A demo app is included to see the editor in action:
743
-
744
- ```bash
745
- npm run demo
746
- ```
747
-
748
- This starts a Vite dev server at `http://localhost:5173` with three sections: the `<Editor>` component, the `useEditor` hook, and a [Fonts](#fonts) panel with a live `getFontFamily()` / `getFontSize()` readout that follows the cursor and buttons for the block-font commands.
749
-
750
- ## Development
751
-
752
- The package is bundled with [Vite 8](https://vite.dev/) (Rolldown) and ships ES, CommonJS, and TypeScript declaration outputs.
753
-
754
- | Script | Description |
755
- |--------|-------------|
756
- | `npm run build` | Production build to `dist/` (ES + CJS + `.d.ts`). |
757
- | `npm run build:dev` | Development build (unminified, sourcemaps). |
758
- | `npm run dev` | Rebuild on change (watch mode). |
759
- | `npm test` | Run the Jest test suite. |
760
- | `npm run test:coverage` | Run tests with coverage. |
761
- | `npm run lint` | Lint `src` with ESLint. |
762
- | `npm run typecheck` | Type-check `src` and `demo` with `tsc --noEmit`. |
763
- | `npm run demo` | Launch the demo app. |
764
-
765
- > **Node.js:** building the package requires Node `^20.19.0 || >=22.12.0` (a Vite 8 requirement). Consumers of the published package are unaffected.
766
-
767
- Lint, typecheck, and tests run automatically in [CI](.github/workflows/ci.yml) on every push and pull request to `master` across Node 20, 22, 24, and 26.
768
-
769
- ## License
770
-
771
- LGPL-3.0-or-later &mdash; [MDaemon Technologies, Ltd.](https://www.mdaemon.com)
1
+ [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmdaemon-technologies%2FMDHTMLEditor-react%2Fmaster%2Fpackage.json&query=%24.version&prefix=v&label=npm&color=blue)](https://www.npmjs.com/package/@mdaemon/html-editor-react) [![install size](https://packagephobia.com/badge?p=@mdaemon/html-editor-react)](https://packagephobia.com/result?p=@mdaemon/html-editor-react) [![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmdaemon-technologies%2FMDHTMLEditor-react%2Fmaster%2Fpackage.json&query=%24.license&prefix=v&label=license&color=green)](https://github.com/mdaemon-technologies/MDHTMLEditor-react/blob/master/LICENSE) [![Node.js CI](https://github.com/mdaemon-technologies/MDHTMLEditor-react/actions/workflows/ci.yml/badge.svg)](https://github.com/mdaemon-technologies/MDHTMLEditor-react/actions/workflows/ci.yml)
2
+
3
+ # MDHTMLEditor React
4
+
5
+ A React wrapper for [MDHTMLEditor](https://github.com/mdaemon-technologies/MDHTMLEditor) &mdash; a WYSIWYG HTML editor built on TipTap. Provides a drop-in replacement for `@tinymce/tinymce-react` with no license key required.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @mdaemon/html-editor-react @mdaemon/html-editor
11
+ ```
12
+
13
+ **Peer dependencies:** `react` and `react-dom` (v18 or v19).
14
+
15
+ ## Styles
16
+
17
+ You must import the editor stylesheet for the toolbar and UI to render correctly:
18
+
19
+ ```tsx
20
+ import '@mdaemon/html-editor/dist/styles.css';
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```tsx
26
+ import { useRef } from 'react';
27
+ import { Editor } from '@mdaemon/html-editor-react';
28
+ import type { EditorRef } from '@mdaemon/html-editor-react';
29
+ import '@mdaemon/html-editor/dist/styles.css';
30
+
31
+ function App() {
32
+ const editorRef = useRef<EditorRef>(null);
33
+
34
+ return (
35
+ <Editor
36
+ ref={editorRef}
37
+ config={{ height: 400 }}
38
+ initialValue="<p>Hello World</p>"
39
+ onChange={(html) => console.log('Content:', html)}
40
+ onInit={(editor) => console.log('Ready!', editor)}
41
+ />
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## `<Editor>` Component
47
+
48
+ The primary way to use the editor. It accepts a `config` object and event callbacks, and exposes imperative methods via a ref.
49
+
50
+ ### Props
51
+
52
+ | Prop | Type | Required | Description |
53
+ |------|------|----------|-------------|
54
+ | `config` | `EditorConfig` | Yes | Configuration object passed to the underlying HTMLEditor. See [Configuration](#configuration). |
55
+ | `body` | `string` | No | Initial HTML content. Takes precedence over `initialValue`. |
56
+ | `initialValue` | `string` | No | Initial HTML content (alias for `body`). |
57
+ | `name` | `string` | No | When set, renders a hidden `<input>` with this name containing the editor content &mdash; useful for form submission. |
58
+ | `disabled` | `boolean` | No | Visually disables the editor (pointer-events off, reduced opacity). |
59
+ | `onChange` | `(content: string) => void` | No | Called when content changes (debounced). |
60
+ | `onDirty` | `(dirty: boolean) => void` | No | Called when the dirty state changes. |
61
+ | `onInit` | `(editor: HTMLEditor) => void` | No | Called when the editor finishes initialization. Receives the editor instance. |
62
+ | `onFocus` | `() => void` | No | Called when the editor receives focus. |
63
+ | `onBlur` | `() => void` | No | Called when the editor loses focus. |
64
+ | `translate` | `(key: string) => string` | No | Sets a global translation function for all editors on the page. |
65
+ | `getFileSrc` | `(path: string) => string` | No | Sets a global file path resolver (e.g., for CDN prefixing). |
66
+
67
+ ### Ref Methods (`EditorRef`)
68
+
69
+ Attach a ref to access imperative methods:
70
+
71
+ ```tsx
72
+ const editorRef = useRef<EditorRef>(null);
73
+
74
+ // Later...
75
+ const html = editorRef.current?.getContent();
76
+ editorRef.current?.setContent('<p>New content</p>');
77
+ editorRef.current?.insertContent('<p>Inserted at cursor</p>');
78
+ editorRef.current?.focus();
79
+
80
+ // Access the underlying HTMLEditor instance directly
81
+ const rawEditor = editorRef.current?.getEditor();
82
+ ```
83
+
84
+ | Method | Signature | Description |
85
+ |--------|-----------|-------------|
86
+ | `getEditor` | `() => HTMLEditor \| null` | Access the raw HTMLEditor instance for advanced usage. |
87
+ | `getContent` | `() => string` | Get current HTML content. |
88
+ | `setContent` | `(html: string) => void` | Replace the editor content. |
89
+ | `insertContent` | `(html: string) => void` | Insert HTML at the current cursor position. |
90
+ | `focus` | `() => void` | Focus the editor. |
91
+
92
+ ### Full Example
93
+
94
+ ```tsx
95
+ import { useRef, useState } from 'react';
96
+ import { Editor } from '@mdaemon/html-editor-react';
97
+ import type { EditorRef } from '@mdaemon/html-editor-react';
98
+ import '@mdaemon/html-editor/dist/styles.css';
99
+
100
+ function EmailComposer() {
101
+ const editorRef = useRef<EditorRef>(null);
102
+ const [dirty, setDirty] = useState(false);
103
+
104
+ const handleSave = () => {
105
+ const content = editorRef.current?.getContent() ?? '';
106
+ console.log('Saving:', content);
107
+ };
108
+
109
+ return (
110
+ <div>
111
+ <Editor
112
+ ref={editorRef}
113
+ config={{
114
+ height: 500,
115
+ basicEditor: false,
116
+ skin: 'oxide',
117
+ fontName: 'Arial',
118
+ fontSize: '12pt',
119
+ }}
120
+ initialValue="<p>Dear recipient,</p>"
121
+ onChange={(html) => console.log('Changed:', html)}
122
+ onDirty={(d) => setDirty(d)}
123
+ onInit={() => console.log('Editor ready')}
124
+ />
125
+ <button onClick={handleSave} disabled={!dirty}>
126
+ Save
127
+ </button>
128
+ </div>
129
+ );
130
+ }
131
+ ```
132
+
133
+ ## `useEditor` Hook
134
+
135
+ For more programmatic control, use the `useEditor` hook. You provide a container element via the returned `containerRef`.
136
+
137
+ ### Options
138
+
139
+ | Option | Type | Default | Description |
140
+ |--------|------|---------|-------------|
141
+ | `config` | `EditorConfig` | `{}` | Editor configuration. |
142
+ | `content` | `string` | `''` | Initial HTML content. |
143
+ | `onUpdate` | `(html: string) => void` | &mdash; | Called on content change. |
144
+
145
+ ### Return Value
146
+
147
+ | Property | Type | Description |
148
+ |----------|------|-------------|
149
+ | `editor` | `HTMLEditor \| null` | The raw editor instance (`null` until initialized). |
150
+ | `containerRef` | `RefObject<HTMLDivElement \| null>` | Attach this to your container div. |
151
+ | `ready` | `boolean` | `true` once the editor has fired its `init` event. |
152
+ | `getContent` | `() => string` | Get current HTML content. |
153
+ | `setContent` | `(html: string) => void` | Replace the editor content. |
154
+ | `insertContent` | `(html: string) => void` | Insert HTML at the cursor. |
155
+ | `focus` | `() => void` | Focus the editor. |
156
+ | `isDirty` | `() => boolean` | Check if the editor has unsaved changes. |
157
+
158
+ ### Example
159
+
160
+ ```tsx
161
+ import { useState } from 'react';
162
+ import { useEditor } from '@mdaemon/html-editor-react';
163
+ import '@mdaemon/html-editor/dist/styles.css';
164
+
165
+ function NotesEditor() {
166
+ const [lastSaved, setLastSaved] = useState('');
167
+ const { containerRef, ready, getContent, setContent, isDirty } = useEditor({
168
+ config: { height: 300, basicEditor: true },
169
+ content: '<p>Start taking notes...</p>',
170
+ onUpdate: (html) => console.log('Updated:', html),
171
+ });
172
+
173
+ const handleSave = () => {
174
+ setLastSaved(getContent());
175
+ };
176
+
177
+ const handleReset = () => {
178
+ setContent('<p>Start taking notes...</p>');
179
+ };
180
+
181
+ return (
182
+ <div>
183
+ <div ref={containerRef} />
184
+ {ready && (
185
+ <div>
186
+ <button onClick={handleSave}>Save</button>
187
+ <button onClick={handleReset}>Reset</button>
188
+ <span>{isDirty() ? 'Unsaved changes' : 'Saved'}</span>
189
+ </div>
190
+ )}
191
+ </div>
192
+ );
193
+ }
194
+ ```
195
+
196
+ ## Global Convenience Functions
197
+
198
+ For single-editor pages, you can get and set content without a ref:
199
+
200
+ ```tsx
201
+ import { getEditorContent, setEditorContent } from '@mdaemon/html-editor-react';
202
+
203
+ // Get content from the active editor
204
+ const html = getEditorContent();
205
+
206
+ // Set content on the active editor
207
+ setEditorContent('<p>New content</p>');
208
+ ```
209
+
210
+ > **Note:** These functions operate on the most recently mounted `<Editor>` instance. They are intended for pages with a single editor.
211
+
212
+ ## Configuration
213
+
214
+ The `config` prop (or `useEditor`'s `config` option) accepts an `EditorConfig` object. All options from `@mdaemon/html-editor` are supported:
215
+
216
+ | Option | Type | Default | Description |
217
+ |--------|------|---------|-------------|
218
+ | `basicEditor` | `boolean` | `false` | Use a simplified toolbar (no images, tables, code blocks). |
219
+ | `readonly` | `boolean` | `false` | Start the editor in read-only mode. Toggle at runtime via `getEditor()?.setReadOnly()`. |
220
+ | `height` | `string \| number` | `300` | Editor height. |
221
+ | `min_height` | `string \| number` | &mdash; | Minimum editor height. |
222
+ | `max_height` | `string \| number` | &mdash; | Maximum editor height. |
223
+ | `language` | `string` | `'en'` | UI language code. 31 languages built in. |
224
+ | `skin` | `'oxide' \| 'oxide-dark' \| 'confab' \| 'confab-dark'` | `'oxide'` | Toolbar and dialog theme. |
225
+ | `content_css` | `'default' \| 'dark' \| 'confab' \| 'confab-dark'` | `'default'` | Content area theme. |
226
+ | `content_style` | `string` | &mdash; | Custom CSS injected into the editing surface. |
227
+ | `fontName` | `string` | `'arial, helvetica, sans-serif'` | Default font family. Inlined on every block in the exported HTML &mdash; see [Fonts](#fonts). |
228
+ | `fontSize` | `string` | `'12pt'` | Default font size. Inlined on every paragraph in the exported HTML &mdash; see [Fonts](#fonts). |
229
+ | `font_family_formats` | `string` | *(TinyMCE defaults)* | Semicolon-delimited font list (`Name=family,...`). |
230
+ | `font_size_formats` | `string` | `'8pt 9pt 10pt 12pt 14pt 18pt 24pt 36pt'` | Space-delimited size options. |
231
+ | `font_names` | `string` | &mdash; | CKEditor alias for `font_family_formats`. |
232
+ | `fontSize_sizes` | `string` | &mdash; | CKEditor alias for `font_size_formats`. |
233
+ | `block_formats` | `string` | *(Paragraph + H1&ndash;H6)* | Block-format dropdown definitions for the `blocks` button (`Name=tag;...`). |
234
+ | `style_formats` | `StyleFormat[]` | *(subset)* | Named styles for the `styles` dropdown (CKEditor `stylesSet`-compatible). |
235
+ | `forced_root_block` | `'p' \| 'div'` | `'p'` | Block element produced on Enter. `'div'` gives CKEditor `ENTER_DIV` parity. |
236
+ | `trailingNode` | `boolean` | `false` | Append an empty trailing paragraph after a block node (table, image, code block) so the cursor can sit after it. |
237
+ | `directionality` | `'ltr' \| 'rtl'` | `'ltr'` | Text direction. |
238
+ | `toolbar` | `string` | *(preset)* | Custom toolbar layout string. |
239
+ | `toolbar_mode` | `'sliding' \| 'floating' \| 'wrap'` | `'wrap'` | Toolbar overflow behavior. |
240
+ | `toolbar_sticky` | `boolean` | `true` | Pin toolbar at top when scrolling. |
241
+ | `toolbar_narrow_breakpoint` | `number` | &mdash; | Pixel width below which the toolbar collapses into the overflow toggle. |
242
+ | `toolbar_priority` | `Record<string, number>` | &mdash; | Per-button overflow priority &mdash; higher values stay visible longer as the toolbar narrows. |
243
+ | `menubar` | `boolean` | `false` | Show a TinyMCE-style menu bar above the toolbar. |
244
+ | `contextmenu` | `boolean \| string` | `''` | Enable a right-click context menu. Pass a button-list string to customize its contents. |
245
+ | `quickbars_selection_toolbar` | `string` | `'bold italic \| quicklink blockquote'` | Inline floating toolbar shown over a text selection. |
246
+ | `quickbars_image_toolbar` | `boolean` | `false` | Inline floating toolbar shown when an image is selected. |
247
+ | `quickbars_insert_toolbar` | `boolean` | `false` | Inline floating toolbar shown on empty lines for quick insertion. |
248
+ | `elementpath` | `boolean` | `false` | Show a breadcrumb of the node path at the cursor in the status bar. |
249
+ | `valid_children` | `string` | &mdash; | TinyMCE-style rules controlling which child elements are allowed. |
250
+ | `auto_focus` | `string` | &mdash; | Auto-focus on init. |
251
+ | `setFocus` | `string` | &mdash; | CSS selector for the element to focus on init (used when `auto_focus` is not set). |
252
+ | `plugins` | `string` | &mdash; | Accepted for TinyMCE compatibility and ignored &mdash; all features are built in. |
253
+ | `browser_spellcheck` | `boolean` | `true` | Enable browser spell check. |
254
+ | `entity_encoding` | `'raw' \| 'named' \| 'numeric'` | `'raw'` | HTML entity encoding mode. |
255
+ | `paste_from_office` | `boolean` | `true` | Clean and preserve formatting when pasting from Microsoft Word/Excel. Pasted list indentation is normalized either way &mdash; see [Lists & Indentation](#lists--indentation). |
256
+ | `speech_to_text` | `boolean` | `true` | Enable the `speechtotext` and `dictate` toolbar buttons (requires the Web Speech API). |
257
+ | `convert_unsafe_embeds` | `boolean` | `true` | Sanitize embedded content. |
258
+ | `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>` &mdash; 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. |
259
+ | `includeTemplates` | `boolean` | `false` | Show the template dropdown. |
260
+ | `templates` | `Template[]` | `[]` | Predefined HTML templates. |
261
+ | `dropbox` | `boolean` | `false` | Enable Dropbox integration. |
262
+ | `images_upload_url` | `string` | &mdash; | Server endpoint for image uploads. |
263
+ | `images_upload_credentials` | `boolean` | `true` | Include credentials with upload requests. |
264
+ | `images_upload_base_path` | `string` | `'/'` | Prefix for uploaded image URLs. |
265
+ | `images_upload_max_size` | `number` | `10485760` | Max upload size in bytes (10 MB). |
266
+ | `images_upload_headers` | `Record<string, string>` | &mdash; | Extra headers for upload requests. |
267
+ | `images_file_types` | `string` | *(permissive)* | Comma/space-separated accepted extensions (e.g. `'jpg,jpeg,png,gif'`). |
268
+ | `images_upload_validate` | `(file: File) => string \| null` | &mdash; | Pre-upload hook; return a message to reject the file, or `null` to allow. |
269
+ | `images_upload_error` | `(message: string) => void` | &mdash; | Caller-supplied alert for drag-drop/paste upload rejections and failures. |
270
+ | `setup` | `(editor) => void` | &mdash; | Pre-init callback for registering custom toolbar buttons. |
271
+
272
+ ### Dark Theme
273
+
274
+ ```tsx
275
+ <Editor
276
+ config={{
277
+ skin: 'oxide-dark',
278
+ content_css: 'dark',
279
+ }}
280
+ />
281
+ ```
282
+
283
+ ### Confab Skin
284
+
285
+ 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:
286
+
287
+ ```tsx
288
+ <Editor
289
+ config={{
290
+ skin: 'confab-dark',
291
+ content_css: 'confab-dark',
292
+ }}
293
+ />
294
+ ```
295
+
296
+ ### Custom Content Styles
297
+
298
+ ```tsx
299
+ <Editor
300
+ config={{
301
+ content_style: 'body { font-family: Georgia, serif; font-size: 16px; line-height: 1.6; }',
302
+ }}
303
+ />
304
+ ```
305
+
306
+ ### Menu Bar, Context Menu & Quick Toolbars
307
+
308
+ Opt into the additional editing surfaces exposed by `@mdaemon/html-editor` 1.6.0:
309
+
310
+ ```tsx
311
+ <Editor
312
+ config={{
313
+ menubar: true, // menu bar above the toolbar
314
+ contextmenu: true, // right-click context menu
315
+ quickbars_selection_toolbar: 'bold italic | quicklink blockquote',
316
+ quickbars_image_toolbar: true, // floating toolbar when an image is selected
317
+ elementpath: true, // breadcrumb status bar at the cursor
318
+ }}
319
+ />
320
+ ```
321
+
322
+ `contextmenu` also accepts a button-list string to customize its contents, and
323
+ `quickbars_insert_toolbar` shows a floating insert toolbar on empty lines.
324
+
325
+ ### Read-Only Mode
326
+
327
+ Start the editor read-only with `readonly: true`, or toggle it at runtime through the underlying editor instance:
328
+
329
+ ```tsx
330
+ <Editor config={{ readonly: true }} ref={editorRef} />
331
+
332
+ // Toggle later
333
+ editorRef.current?.getEditor()?.setReadOnly(false);
334
+ const isReadOnly = editorRef.current?.getEditor()?.isReadOnly();
335
+ ```
336
+
337
+ > The `disabled` prop applies a lightweight visual lock (pointer-events off, reduced opacity). For true non-editable behavior that also dims the toolbar, prefer `readonly` / `setReadOnly()`.
338
+
339
+ ## Fonts
340
+
341
+ `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 &mdash; an email body opened in another client, for example. Headings keep their level-based sizing and carry no block `font-size`.
342
+
343
+ ```tsx
344
+ <Editor
345
+ config={{
346
+ fontName: 'Georgia, serif',
347
+ fontSize: '14pt',
348
+ }}
349
+ />
350
+ ```
351
+
352
+ 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.
353
+
354
+ ### The Toolbar Shows the Font at the Cursor
355
+
356
+ The `fontfamily` and `fontsize` toolbar buttons display the value in effect at the cursor &mdash; "Georgia", "14pt" &mdash; 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`.
357
+
358
+ 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.
359
+
360
+ > This is styled by rules in `@mdaemon/html-editor/dist/styles.css` &mdash; make sure the [stylesheet import](#styles) is present, and re-check any custom CSS you have layered on top of the toolbar buttons.
361
+
362
+ ### Reading the Current Font
363
+
364
+ `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:
365
+
366
+ ```tsx
367
+ const editor = editorRef.current?.getEditor();
368
+
369
+ const family = editor?.getFontFamily(); // e.g. 'Georgia, serif'
370
+ const size = editor?.getFontSize(); // e.g. '14pt'
371
+ ```
372
+
373
+ 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.
374
+
375
+ Both return `''` when there is no single value to report:
376
+
377
+ - the selection spans more than one font family / size
378
+ - for `getFontSize()`, the cursor is in a heading with no inline override (headings size by level and carry no block `font-size`)
379
+
380
+ Treat `''` as "mixed / not applicable" and blank your picker, rather than falling back to a default &mdash; that is what the toolbar does.
381
+
382
+ The editor emits no selection event of its own, so subscribe on the TipTap instance to keep a readout in sync with the caret:
383
+
384
+ ```tsx
385
+ const handleInit = useCallback((editor: HTMLEditor) => {
386
+ const sync = () => {
387
+ setFamily(editor.getFontFamily());
388
+ setSize(editor.getFontSize());
389
+ };
390
+
391
+ sync();
392
+ editor.getTipTap()?.on('selectionUpdate', sync);
393
+ editor.getTipTap()?.on('transaction', sync);
394
+ }, []);
395
+
396
+ <Editor config={{ fontName: 'Georgia, serif', fontSize: '14pt' }} onInit={handleInit} />
397
+ ```
398
+
399
+ ### Setting a Block Font
400
+
401
+ To change the font on every block the selection touches &mdash; instead of adding an inline override &mdash; use the `setBlockFontFamily` / `setBlockFontSize` commands on the underlying TipTap instance:
402
+
403
+ ```tsx
404
+ const tiptap = editorRef.current?.getEditor()?.getTipTap();
405
+
406
+ tiptap?.chain().focus().setBlockFontFamily('Georgia, serif').run();
407
+ tiptap?.chain().focus().setBlockFontSize('14pt').run();
408
+ ```
409
+
410
+ ## Lists & Indentation
411
+
412
+ ### Increase / Decrease Indent
413
+
414
+ The `indent` / `outdent` toolbar buttons, `execCommand('indent' | 'outdent')`, and the <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> keys all resolve to the same behavior, chosen by what is under the cursor:
415
+
416
+ | Context | Increase indent | Decrease indent |
417
+ |---------|-----------------|-----------------|
418
+ | List item with a previous sibling | Nests it one level deeper | Reduces its own indent, then un-nests it one level |
419
+ | First list item at its level | Adds a `margin-left` to the `<li>` | Reduces that margin, then lifts the item out of the list |
420
+ | Paragraph, heading | Adds a `margin-left` in 40px steps (max 400px) | Removes one step |
421
+ | Code block | Inserts a literal tab | Nothing |
422
+
423
+ Inside a list the indent is written to the `<li>` rather than to the paragraph inside it, so the bullet or number moves with the text. Because it is an inline `margin-left`, it survives in the exported HTML &mdash; in a mail client, say &mdash; without the editor's stylesheet. In a table, <kbd>Tab</kbd> still moves between cells; the toolbar buttons indent the paragraph in the cell.
424
+
425
+ ### Ordered List Numbering
426
+
427
+ An ordered list's numbering style is preserved rather than normalized to `1, 2, 3`. `<ol type="A">`, `type="a"`, `type="I"`, `type="i"` and `start="5"` round-trip through `setContent()` / `getContent()` and render as written. A list that states its style with CSS instead (`<ol style="list-style-type:upper-alpha">`) is read the same way and exported as `<ol type="A">`.
428
+
429
+ ```tsx
430
+ <Editor
431
+ config={{}}
432
+ initialValue='<ol type="A" start="3"><li><p>Third</p></li><li><p>Fourth</p></li></ol>'
433
+ />
434
+ // renders as C. Third / D. Fourth
435
+ ```
436
+
437
+ Lists that declare nothing keep the editor's nesting defaults: decimal, then lower-alpha, then lower-roman for ordered lists; disc, circle, then square for bullets.
438
+
439
+ > If you layer your own CSS on the editing surface (`content_style`, or a stylesheet in the host app), avoid a blanket `ol { list-style-type: ... }` reset &mdash; it outranks the `type` attribute and flattens typed lists back to decimal.
440
+
441
+ ### Pasting a List
442
+
443
+ Lists pasted from Word, Outlook, Google Docs, another editor, or a web page are normalized on the way in:
444
+
445
+ - **Indentation is dropped.** Those sources position a list with inline `margin-left` / `padding-left` / `text-indent` from their own page layout. Carried over, that stacks on top of the editor's list indent, so a pasted list sits a level further right than one built with the toolbar. Nesting is structural (`<ol><li><ol>`), so nothing is lost by dropping it. Indentation on non-list blocks (paragraphs, blockquotes) is left alone.
446
+ - **A list that is already a real `<ol>`/`<ul>` stays one list.** Word and Outlook email HTML often marks up a genuine list whose `<li>`s also carry Word's `MsoListParagraph` class and `mso-list` metadata; those items are cleaned in place instead of being wrapped in a second list.
447
+ - **Word's numbering style is kept.** `mso-level-number-format` (and `mso-level-start-at`) become the list's `type` and `start`, so a lettered or roman Word list is not renumbered. When the clipboard carries no `@list` rules at all &mdash; common &mdash; the style is inferred from the marker text Word inlines (`A.`, `iv.`, `1.`, `&middot;`), which also keeps a numbered list from arriving as a bullet list.
448
+
449
+ Word/Excel cleaning as a whole can be turned off with [`paste_from_office: false`](#configuration); list indentation is normalized either way.
450
+
451
+ ## Templates
452
+
453
+ Enable the template dropdown and provide a `templates` array:
454
+
455
+ ```tsx
456
+ import type { Template } from '@mdaemon/html-editor-react';
457
+
458
+ const templates: Template[] = [
459
+ {
460
+ title: 'Greeting',
461
+ description: 'A friendly greeting',
462
+ content: '<p>Hello! Thank you for reaching out.</p>',
463
+ },
464
+ {
465
+ title: 'Signature',
466
+ content: '<p>Best regards,<br/>Your Name</p>',
467
+ },
468
+ ];
469
+
470
+ <Editor
471
+ config={{
472
+ includeTemplates: true,
473
+ templates,
474
+ }}
475
+ />
476
+ ```
477
+
478
+ The `Template` interface:
479
+
480
+ ```typescript
481
+ interface Template {
482
+ id?: number | string;
483
+ title: string;
484
+ description?: string;
485
+ content: string;
486
+ }
487
+ ```
488
+
489
+ ## Custom Toolbar Buttons
490
+
491
+ Register custom buttons via the `setup` callback in `config`:
492
+
493
+ ```tsx
494
+ <Editor
495
+ config={{
496
+ setup: (editor) => {
497
+ editor.ui.registry.addButton('myButton', {
498
+ tooltip: 'Insert greeting',
499
+ text: 'Greet',
500
+ onAction: (api) => {
501
+ editor.insertContent('<p>Hello from a custom button!</p>');
502
+ },
503
+ onSetup: (api) => {
504
+ // api.isEnabled(), api.setEnabled(bool)
505
+ // api.isActive(), api.setActive(bool)
506
+ },
507
+ });
508
+ },
509
+ toolbar: 'bold italic | myButton',
510
+ }}
511
+ />
512
+ ```
513
+
514
+ ### Button Options
515
+
516
+ | Option | Type | Description |
517
+ |--------|------|-------------|
518
+ | `tooltip` | `string` | Hover text. |
519
+ | `text` | `string` | Button label. |
520
+ | `icon` | `string` | Image URL (used instead of text). |
521
+ | `disabled` | `boolean` | Initial disabled state. |
522
+ | `onSetup` | `(api) => void \| (() => void)` | Called on creation; may return a teardown function. |
523
+ | `onAction` | `(api) => void` | Called on click. |
524
+
525
+ ### Button API
526
+
527
+ The `api` object passed to `onSetup` and `onAction`:
528
+
529
+ | Method | Description |
530
+ |--------|-------------|
531
+ | `isEnabled()` | Returns current enabled state. |
532
+ | `setEnabled(enabled)` | Enable or disable the button. |
533
+ | `isActive()` | Returns active/pressed state. |
534
+ | `setActive(active)` | Toggle active/pressed visual style. |
535
+
536
+ ## Custom Toolbar Layout
537
+
538
+ Provide a `toolbar` string to control which buttons appear and in what order. Use `|` to group buttons and `||` to create a collapsible overflow section:
539
+
540
+ ```tsx
541
+ <Editor
542
+ config={{
543
+ toolbar: 'bold italic underline | fontfamily fontsize || forecolor backcolor | undo redo',
544
+ }}
545
+ />
546
+ ```
547
+
548
+ Buttons after `||` begin collapsed behind a toggle (`...`) button.
549
+
550
+ ### Available Toolbar Buttons
551
+
552
+ | Button | Action |
553
+ |--------|--------|
554
+ | `bold` | Toggle bold |
555
+ | `italic` | Toggle italic |
556
+ | `underline` | Toggle underline |
557
+ | `strikethrough` | Toggle strikethrough |
558
+ | `subscript` | Toggle subscript |
559
+ | `superscript` | Toggle superscript |
560
+ | `bullist` | Bullet list |
561
+ | `numlist` | Numbered list |
562
+ | `outdent` | Decrease indent &mdash; un-nests a list item, or removes block `margin-left` (see [Lists & Indentation](#lists--indentation)) |
563
+ | `indent` | Increase indent &mdash; nests a list item, or adds block `margin-left` (see [Lists & Indentation](#lists--indentation)) |
564
+ | `blockquote` | Toggle block quote |
565
+ | `fontfamily` | Font family dropdown |
566
+ | `fontsize` | Font size dropdown |
567
+ | `lineheight` | Line height dropdown |
568
+ | `blocks` | Block format dropdown &mdash; Paragraph, Heading 1&ndash;6 (alias `formatselect`) |
569
+ | `styles` | Named styles dropdown (configurable via `style_formats`) |
570
+ | `template` | Template dropdown (requires `includeTemplates: true`) |
571
+ | `alignleft` | Left align |
572
+ | `aligncenter` | Center align |
573
+ | `alignright` | Right align |
574
+ | `alignjustify` | Justify |
575
+ | `forecolor` | Text color picker |
576
+ | `backcolor` | Highlight color picker |
577
+ | `removeformat` | Strip all formatting |
578
+ | `copy` | Copy selection |
579
+ | `cut` | Cut selection |
580
+ | `paste` | Paste from clipboard |
581
+ | `undo` | Undo |
582
+ | `redo` | Redo |
583
+ | `image` | Insert image (upload or URL) |
584
+ | `table` | Table dropdown (insert table + row/column/cell operations) |
585
+ | `hr` | Insert horizontal rule |
586
+ | `charmap` | Special character picker |
587
+ | `emoticons` | Emoji picker with search |
588
+ | `code` | Open HTML source code editor dialog |
589
+ | `link` | Insert/edit hyperlink |
590
+ | `unlink` | Remove the link at the cursor |
591
+ | `anchor` | Insert a named anchor (`<a id>` target) |
592
+ | `codesample` | Toggle code sample |
593
+ | `fullscreen` | Toggle fullscreen |
594
+ | `preview` | Preview in new window |
595
+ | `searchreplace` | Find & Replace dialog |
596
+ | `speechtotext` | Open Speech to Text dialog (requires `speech_to_text` + browser support) |
597
+ | `dictate` | Toggle inline dictation &mdash; inserts speech at the cursor (requires `speech_to_text` + browser support) |
598
+ | `ltr` | Left-to-right direction |
599
+ | `rtl` | Right-to-left direction |
600
+
601
+ ## Image Upload
602
+
603
+ The image toolbar button opens a dialog supporting drag-and-drop upload or direct URL entry. Supported formats: JPEG, PNG, GIF, WebP, SVG.
604
+
605
+ 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.
606
+
607
+ SVGs are automatically sanitized (script tags, event handlers, and dangerous elements are stripped).
608
+
609
+ ```tsx
610
+ <Editor
611
+ config={{
612
+ images_upload_url: '/api/upload',
613
+ images_upload_credentials: true,
614
+ images_upload_base_path: '/files/',
615
+ images_upload_max_size: 5 * 1024 * 1024,
616
+ images_upload_headers: {
617
+ 'X-CSRF-Token': csrfToken,
618
+ },
619
+ }}
620
+ />
621
+ ```
622
+
623
+ ## Localization
624
+
625
+ ### Set Language at Init
626
+
627
+ ```tsx
628
+ <Editor config={{ language: 'de' }} />
629
+ ```
630
+
631
+ ### Custom Translation Function
632
+
633
+ ```tsx
634
+ import { setTranslate } from '@mdaemon/html-editor-react';
635
+
636
+ setTranslate((key) => myTranslations[key] ?? key);
637
+
638
+ // Or via the Editor prop:
639
+ <Editor translate={(key) => myTranslations[key] ?? key} config={{}} />
640
+ ```
641
+
642
+ ### Change Language at Runtime
643
+
644
+ ```tsx
645
+ const editorRef = useRef<EditorRef>(null);
646
+
647
+ // Switch to French
648
+ editorRef.current?.getEditor()?.setLanguage('fr');
649
+ ```
650
+
651
+ ### Supported Languages
652
+
653
+ | Code | Language | Code | Language |
654
+ |------|----------|------|----------|
655
+ | `en` | English | `nl` | Nederlands |
656
+ | `ar` | العربية | `nb` | Norsk bokmal |
657
+ | `ca` | Catala | `pl` | Polski |
658
+ | `zh` | Chinese | `pt` | Portugues |
659
+ | `cs` | Cesky | `ro` | Romana |
660
+ | `da` | Dansk | `ru` | Russian |
661
+ | `en-gb` | English (UK) | `sr` | Srpski |
662
+ | `fi` | Suomi | `sl` | Slovenscina |
663
+ | `fr` | Francais | `es` | Espanol |
664
+ | `fr-ca` | Canadien francais | `sv` | Svenska |
665
+ | `de` | Deutsch | `zh-tw` | Chinese (Taiwan) |
666
+ | `el` | Greek | `th` | Thai |
667
+ | `hu` | Magyar | `tr` | Turkce |
668
+ | `id` | Bahasa Indonesia | `vi` | Tieng Viet |
669
+ | `it` | Italiano | | |
670
+ | `ja` | Japanese | | |
671
+ | `ko` | Korean | | |
672
+
673
+ ## File Source Resolver
674
+
675
+ Transform image `src` attributes globally, useful for CDN prefixing or relative path resolution:
676
+
677
+ ```tsx
678
+ import { setGetFileSrc } from '@mdaemon/html-editor-react';
679
+
680
+ setGetFileSrc((path) => `https://cdn.example.com${path}`);
681
+
682
+ // Or via the Editor prop:
683
+ <Editor getFileSrc={(path) => `https://cdn.example.com${path}`} config={{}} />
684
+ ```
685
+
686
+ ## Keyboard Shortcuts
687
+
688
+ | Shortcut | Action |
689
+ |----------|--------|
690
+ | Ctrl/Cmd + B | Bold |
691
+ | Ctrl/Cmd + I | Italic |
692
+ | Ctrl/Cmd + U | Underline |
693
+ | Ctrl/Cmd + Z | Undo |
694
+ | Ctrl/Cmd + Shift + Z | Redo |
695
+ | Ctrl/Cmd + F | Find & Replace |
696
+ | Tab | Increase indent &mdash; nests or indents a list item, moves to the next table cell, inserts a tab in a code block, or indents the block ([details](#lists--indentation)) |
697
+ | Shift + Tab | Decrease indent &mdash; un-nests a list item, moves to the previous table cell, or removes block indent ([details](#lists--indentation)) |
698
+ | Esc, then Tab | Move focus to the next element outside the editor (keyboard escape) |
699
+ | Esc, then Shift + Tab | Move focus to the previous element outside the editor |
700
+
701
+ > `Tab` is captured for indentation, so the `Esc`-then-`Tab` sequence provides a
702
+ > keyboard escape (WCAG 2.1.2, "No Keyboard Trap"). `Esc` arms the escape for a single
703
+ > key press; any other key disarms it.
704
+
705
+ ## Form Integration
706
+
707
+ Use the `name` prop to render a hidden `<input>` containing the editor content, useful for traditional form submission:
708
+
709
+ ```tsx
710
+ <form onSubmit={handleSubmit}>
711
+ <Editor
712
+ config={{}}
713
+ name="email_body"
714
+ initialValue="<p>Draft content</p>"
715
+ />
716
+ <button type="submit">Send</button>
717
+ </form>
718
+ ```
719
+
720
+ ## Exports
721
+
722
+ ```tsx
723
+ // Components
724
+ import { Editor, MDEditor } from '@mdaemon/html-editor-react'; // MDEditor is an alias
725
+
726
+ // Hook
727
+ import { useEditor } from '@mdaemon/html-editor-react';
728
+
729
+ // Global functions
730
+ import { getEditorContent, setEditorContent } from '@mdaemon/html-editor-react';
731
+
732
+ // Utilities (re-exported from @mdaemon/html-editor)
733
+ import { fontNames, setTranslate, setGetFileSrc } from '@mdaemon/html-editor-react';
734
+
735
+ // Types
736
+ import type {
737
+ EditorProps,
738
+ EditorRef,
739
+ UseEditorOptions,
740
+ UseEditorReturn,
741
+ EditorConfig,
742
+ EditorEvents,
743
+ Template,
744
+ ToolbarButtonSpec,
745
+ ToolbarButtonAPI,
746
+ } from '@mdaemon/html-editor-react';
747
+ ```
748
+
749
+ ## Migration from `@tinymce/tinymce-react`
750
+
751
+ ```tsx
752
+ // Before (TinyMCE)
753
+ import { Editor } from '@tinymce/tinymce-react';
754
+
755
+ <Editor
756
+ apiKey="your-key"
757
+ init={{ height: 400, plugins: 'link image table' }}
758
+ initialValue="<p>Hello</p>"
759
+ onEditorChange={(content) => save(content)}
760
+ />
761
+
762
+ // After (MDHTMLEditor React)
763
+ import { Editor } from '@mdaemon/html-editor-react';
764
+ import '@mdaemon/html-editor/dist/styles.css';
765
+
766
+ <Editor
767
+ config={{ height: 400 }}
768
+ initialValue="<p>Hello</p>"
769
+ onChange={(content) => save(content)}
770
+ />
771
+ ```
772
+
773
+ Key differences:
774
+
775
+ - No `apiKey` or `licenseKey` required
776
+ - `init` prop is renamed to `config`
777
+ - `onEditorChange` is renamed to `onChange`
778
+ - `plugins` option is not needed &mdash; all features are built in
779
+ - Toolbar customization uses `basicEditor: true/false` or a `toolbar` string
780
+
781
+ ## Running the Demo
782
+
783
+ A demo app is included to see the editor in action:
784
+
785
+ ```bash
786
+ npm run demo
787
+ ```
788
+
789
+ This starts a Vite dev server at `http://localhost:5173` with three sections: the `<Editor>` component, the `useEditor` hook, and a [Fonts](#fonts) panel with a live `getFontFamily()` / `getFontSize()` readout that follows the cursor and buttons for the block-font commands.
790
+
791
+ ## Development
792
+
793
+ The package is bundled with [Vite 8](https://vite.dev/) (Rolldown) and ships ES, CommonJS, and TypeScript declaration outputs.
794
+
795
+ | Script | Description |
796
+ |--------|-------------|
797
+ | `npm run build` | Production build to `dist/` (ES + CJS + `.d.ts`). |
798
+ | `npm run build:dev` | Development build (unminified, sourcemaps). |
799
+ | `npm run dev` | Rebuild on change (watch mode). |
800
+ | `npm test` | Run the Jest test suite. |
801
+ | `npm run test:coverage` | Run tests with coverage. |
802
+ | `npm run lint` | Lint `src` with ESLint. |
803
+ | `npm run typecheck` | Type-check `src` and `demo` with `tsc --noEmit`. |
804
+ | `npm run demo` | Launch the demo app. |
805
+
806
+ > **Node.js:** building the package requires Node `^20.19.0 || >=22.12.0` (a Vite 8 requirement). Consumers of the published package are unaffected.
807
+
808
+ Lint, typecheck, and tests run automatically in [CI](.github/workflows/ci.yml) on every push and pull request to `master` across Node 20, 22, 24, and 26.
809
+
810
+ ## License
811
+
812
+ LGPL-3.0-or-later &mdash; [MDaemon Technologies, Ltd.](https://www.mdaemon.com)