@remyxjs/react 1.0.0-beta
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.
- package/README.md +2158 -0
- package/create/index.js +367 -0
- package/create/templates/jsx/index.html +12 -0
- package/create/templates/jsx/src/App.jsx +25 -0
- package/create/templates/jsx/src/main.jsx +9 -0
- package/create/templates/jsx/vite.config.js +6 -0
- package/create/templates/typescript/index.html +12 -0
- package/create/templates/typescript/src/App.tsx +25 -0
- package/create/templates/typescript/src/main.tsx +9 -0
- package/create/templates/typescript/src/vite-env.d.ts +1 -0
- package/create/templates/typescript/tsconfig.json +21 -0
- package/create/templates/typescript/vite.config.ts +6 -0
- package/dist/AttachmentModal--6T-vYJt.js +21 -0
- package/dist/AttachmentModal--6T-vYJt.js.map +1 -0
- package/dist/AttachmentModal-D-uxbXvb.cjs +2 -0
- package/dist/AttachmentModal-D-uxbXvb.cjs.map +1 -0
- package/dist/CommandPalette-DXWyTGOU.js +19 -0
- package/dist/CommandPalette-DXWyTGOU.js.map +1 -0
- package/dist/CommandPalette-t9J8JGNJ.cjs +2 -0
- package/dist/CommandPalette-t9J8JGNJ.cjs.map +1 -0
- package/dist/ContextMenu-B4K3Zbfp.cjs +2 -0
- package/dist/ContextMenu-B4K3Zbfp.cjs.map +1 -0
- package/dist/ContextMenu-D8wNSMc3.js +2 -0
- package/dist/ContextMenu-D8wNSMc3.js.map +1 -0
- package/dist/EmbedModal-Bh2Tcow9.cjs +2 -0
- package/dist/EmbedModal-Bh2Tcow9.cjs.map +1 -0
- package/dist/EmbedModal-cxE4maD2.js +11 -0
- package/dist/EmbedModal-cxE4maD2.js.map +1 -0
- package/dist/ExportModal-Cf1uE4r_.js +13 -0
- package/dist/ExportModal-Cf1uE4r_.js.map +1 -0
- package/dist/ExportModal-DwQVsrZE.cjs +2 -0
- package/dist/ExportModal-DwQVsrZE.cjs.map +1 -0
- package/dist/FindReplaceModal-DYL_2z8U.cjs +2 -0
- package/dist/FindReplaceModal-DYL_2z8U.cjs.map +1 -0
- package/dist/FindReplaceModal-Dgt_MrWb.js +17 -0
- package/dist/FindReplaceModal-Dgt_MrWb.js.map +1 -0
- package/dist/ImageModal-D39ywxqI.cjs +2 -0
- package/dist/ImageModal-D39ywxqI.cjs.map +1 -0
- package/dist/ImageModal-DqScpPrc.js +23 -0
- package/dist/ImageModal-DqScpPrc.js.map +1 -0
- package/dist/ImportDocumentModal-BKqMxO3z.js +22 -0
- package/dist/ImportDocumentModal-BKqMxO3z.js.map +1 -0
- package/dist/ImportDocumentModal-Bev9hp_J.cjs +2 -0
- package/dist/ImportDocumentModal-Bev9hp_J.cjs.map +1 -0
- package/dist/LinkModal-B-igSa-g.cjs +2 -0
- package/dist/LinkModal-B-igSa-g.cjs.map +1 -0
- package/dist/LinkModal-k9IeDtAb.js +15 -0
- package/dist/LinkModal-k9IeDtAb.js.map +1 -0
- package/dist/MenuBar-B-ZAX9rH.cjs +2 -0
- package/dist/MenuBar-B-ZAX9rH.cjs.map +1 -0
- package/dist/MenuBar-DWxJNHmb.js +12 -0
- package/dist/MenuBar-DWxJNHmb.js.map +1 -0
- package/dist/ModalOverlay-BDsGgv3_.cjs +2 -0
- package/dist/ModalOverlay-BDsGgv3_.cjs.map +1 -0
- package/dist/ModalOverlay-CLvRNHmp.js +6 -0
- package/dist/ModalOverlay-CLvRNHmp.js.map +1 -0
- package/dist/SourceModal-BNI_i4iW.cjs +2 -0
- package/dist/SourceModal-BNI_i4iW.cjs.map +1 -0
- package/dist/SourceModal-MdTGK3Uf.js +12 -0
- package/dist/SourceModal-MdTGK3Uf.js.map +1 -0
- package/dist/TablePickerModal-DYODWEA1.js +17 -0
- package/dist/TablePickerModal-DYODWEA1.js.map +1 -0
- package/dist/TablePickerModal-Do1QyoyM.cjs +2 -0
- package/dist/TablePickerModal-Do1QyoyM.cjs.map +1 -0
- package/dist/index-C720tbJA.js +359 -0
- package/dist/index-C720tbJA.js.map +1 -0
- package/dist/index-Dc63uIP0.cjs +2 -0
- package/dist/index-Dc63uIP0.cjs.map +1 -0
- package/dist/remyx-react.cjs +2 -0
- package/dist/remyx-react.cjs.map +1 -0
- package/dist/remyx-react.css +1 -0
- package/dist/remyx-react.js +2 -0
- package/dist/remyx-react.js.map +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,2158 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# @remyxjs/react
|
|
4
|
+
|
|
5
|
+
A feature-rich WYSIWYG editor for React, built on the framework-agnostic [`@remyxjs/core`](../remyx-core/) engine. Configurable toolbar, menu bar, markdown support, theming, file uploads, and a plugin system.
|
|
6
|
+
|
|
7
|
+
## Packages
|
|
8
|
+
|
|
9
|
+
| Package | Version | Description |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| [`@remyxjs/core`](../remyx-core/) | 1.0.0-beta | Framework-agnostic engine, commands, plugins, utilities, and CSS themes |
|
|
12
|
+
| [`@remyxjs/react`](./) | 1.0.0-beta | React components, hooks, and TypeScript declarations |
|
|
13
|
+
|
|
14
|
+
Use `@remyxjs/core` directly if building a wrapper for another framework (Vue, Svelte, Angular, vanilla JS).
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Features](#features)
|
|
21
|
+
- [Props](#props)
|
|
22
|
+
- [Config File](#config-file)
|
|
23
|
+
- [Toolbar](#toolbar)
|
|
24
|
+
- [Custom Toolbar](#custom-toolbar-via-props)
|
|
25
|
+
- [Presets](#toolbar-presets)
|
|
26
|
+
- [Helper Functions](#toolbar-helper-functions)
|
|
27
|
+
- [Available Items](#available-toolbar-items)
|
|
28
|
+
- [Menu Bar](#menu-bar)
|
|
29
|
+
- [Multiple Editors](#multiple-editors)
|
|
30
|
+
- [Fonts](#font-configuration)
|
|
31
|
+
- [Status Bar](#status-bar)
|
|
32
|
+
- [Theming](#theming)
|
|
33
|
+
- [Custom Themes](#custom-themes)
|
|
34
|
+
- [Theme Presets](#theme-presets)
|
|
35
|
+
- [Theme Variables](#available-theme-variables)
|
|
36
|
+
- [Per-Item Toolbar Theming](#per-item-toolbar-theming)
|
|
37
|
+
- [Paste Handling](#paste--auto-conversion)
|
|
38
|
+
- [File Uploads](#file-uploads)
|
|
39
|
+
- [Output Formats](#output-formats)
|
|
40
|
+
- [Attaching to Existing Elements](#attaching-to-existing-elements)
|
|
41
|
+
- [Plugins](#plugins)
|
|
42
|
+
- [Read-Only Mode](#read-only-mode)
|
|
43
|
+
- [Error Handling](#error-handling)
|
|
44
|
+
- [Engine Access](#engine-access)
|
|
45
|
+
- [Import & Export Documents](#import--export-documents)
|
|
46
|
+
- [Sanitization](#sanitization)
|
|
47
|
+
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
|
48
|
+
- [Heading Level Offset](#heading-level-offset)
|
|
49
|
+
- [Floating Toolbar & Context Menu](#floating-toolbar--context-menu)
|
|
50
|
+
- [Autosave](#autosave)
|
|
51
|
+
- [UX/UI Features](#uxui-features)
|
|
52
|
+
- [Collaboration](#collaboration)
|
|
53
|
+
- [Form Integration](#form-integration)
|
|
54
|
+
- [Exports](#exports)
|
|
55
|
+
- [TypeScript](#typescript)
|
|
56
|
+
- [Using `@remyxjs/core` Directly](#using-remyxcore-directly)
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install @remyxjs/core @remyxjs/react
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Import both stylesheets in your app entry point:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import '@remyxjs/core/style.css'; // theme variables, light/dark themes
|
|
68
|
+
import '@remyxjs/react/style.css'; // component styles (toolbar, modals, etc.)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
import { useState } from 'react';
|
|
75
|
+
import { RemyxEditor } from '@remyxjs/react';
|
|
76
|
+
import '@remyxjs/core/style.css';
|
|
77
|
+
import '@remyxjs/react/style.css';
|
|
78
|
+
|
|
79
|
+
function App() {
|
|
80
|
+
const [content, setContent] = useState('');
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<RemyxEditor
|
|
84
|
+
value={content}
|
|
85
|
+
onChange={setContent}
|
|
86
|
+
placeholder="Start typing..."
|
|
87
|
+
height={400}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Uncontrolled Mode
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
<RemyxEditor
|
|
97
|
+
defaultValue="<p>Initial content</p>"
|
|
98
|
+
onChange={(html) => console.log(html)}
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Markdown Mode
|
|
103
|
+
|
|
104
|
+
```jsx
|
|
105
|
+
const [markdown, setMarkdown] = useState('# Hello\n\nStart typing...');
|
|
106
|
+
|
|
107
|
+
<RemyxEditor
|
|
108
|
+
value={markdown}
|
|
109
|
+
onChange={setMarkdown}
|
|
110
|
+
outputFormat="markdown"
|
|
111
|
+
/>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### With Upload Handler
|
|
115
|
+
|
|
116
|
+
```jsx
|
|
117
|
+
<RemyxEditor
|
|
118
|
+
value={content}
|
|
119
|
+
onChange={setContent}
|
|
120
|
+
uploadHandler={async (file) => {
|
|
121
|
+
const formData = new FormData();
|
|
122
|
+
formData.append('file', file);
|
|
123
|
+
const res = await fetch('/api/upload', { method: 'POST', body: formData });
|
|
124
|
+
const { url } = await res.json();
|
|
125
|
+
return url;
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Dark Theme with Google Fonts
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
<RemyxEditor
|
|
134
|
+
theme="dark"
|
|
135
|
+
googleFonts={['Inter', 'Fira Code', 'Merriweather']}
|
|
136
|
+
height={500}
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Features
|
|
141
|
+
|
|
142
|
+
- **Rich text editing** — bold, italic, underline, strikethrough, subscript, superscript, headings, lists, blockquotes, code blocks, tables (with sorting, filtering, formulas, cell formatting, resize handles, and clipboard interop), and horizontal rules
|
|
143
|
+
- **Toolbar** — fully configurable with presets, helper functions, and per-item theming
|
|
144
|
+
- **Menu bar** — application-style menus (File, Edit, View, Insert, Format) with submenus and auto-deduplication
|
|
145
|
+
- **Markdown** — bidirectional HTML/Markdown conversion, toggle between modes, and auto-detection on paste
|
|
146
|
+
- **Theming** — light/dark mode, 4 built-in presets (Ocean, Forest, Sunset, Rose), full CSS variable customization
|
|
147
|
+
- **File uploads** — images and attachments via drag-and-drop, paste, or toolbar with pluggable upload handlers (S3, R2, GCS, custom)
|
|
148
|
+
- **Fonts** — custom font lists, Google Fonts auto-loading, and helper functions
|
|
149
|
+
- **Code block syntax highlighting** — 11 languages with auto-detection, theme-aware colors, language selector dropdown, line numbers toggle, copy-to-clipboard, inline code highlighting, and extensible language registry
|
|
150
|
+
- **Enhanced tables** — sortable columns, multi-column sort, filterable rows, inline formulas, cell formatting, column/row resize, sticky headers, and Excel/Sheets clipboard interop via `TablePlugin`
|
|
151
|
+
- **Template system** — `{{merge_tag}}` chips, `{{#if}}`/`{{#each}}` blocks, live preview, 5 pre-built templates, custom template library via `TemplatePlugin`
|
|
152
|
+
- **Keyboard-first editing** — Vim (normal/insert/visual), Emacs keybindings, custom key map, multi-cursor (`Cmd+D`), smart auto-pairing, jump-to-heading via `KeyboardPlugin`
|
|
153
|
+
- **Drag-and-drop content** — Drop zones, cross-editor drag, file/image/rich-text drops, block reorder with ghost preview via `DragDropPlugin`
|
|
154
|
+
- **Advanced link management** — Link previews, broken link detection, auto-linking, click analytics, bookmark anchors via `LinkPlugin`
|
|
155
|
+
- **Callouts & alerts** — 7 built-in callout types with custom type registration, collapsible toggle, nested content, and GitHub-flavored alert syntax auto-conversion via `CalloutPlugin`
|
|
156
|
+
- **Comments & annotations** — Inline comment threads via `CommentsPlugin`, `CommentsPanel` sidebar with thread cards/replies/actions, `useComments` hook for reactive state, @mention parsing, resolved/unresolved state, comment-only mode, import/export
|
|
157
|
+
- **Real-time collaboration** — CRDT-based co-editing with live cursors, presence indicators, offline-first sync, configurable transport (WebSocket, WebRTC, or custom) via `CollaborationPlugin`, `useCollaboration` hook, `CollaborationBar` component
|
|
158
|
+
- **Plugins** — `createPlugin()` API with hooks for commands, toolbar items, status bar items, and context menus
|
|
159
|
+
- **Config file** — centralized `defineConfig()` with named editor configurations and provider-based sharing
|
|
160
|
+
- **Block-based editing** — block-level toolbar with type conversion, drag-to-reorder, collapsible sections, block grouping, `BlockTemplatePlugin` with built-in templates
|
|
161
|
+
- **Mobile & touch** — touch floating toolbar, swipe indent/outdent, long-press context menu with haptic feedback, pinch-to-zoom, responsive toolbar overflow, virtual keyboard-aware layout
|
|
162
|
+
- **Multi-instance** — unlimited editors per page with full isolation (state, events, DOM, modals), `EditorBus` for inter-editor communication, `SharedResources` for memory-efficient shared schemas and icons
|
|
163
|
+
- **Import/Export** — PDF, DOCX, Markdown, CSV, and HTML
|
|
164
|
+
- **Paste cleaning** — intelligent cleanup from Word, Google Docs, LibreOffice, Pages, and raw markdown
|
|
165
|
+
- **Command palette** — searchable overlay with all editor commands, opened via `Mod+Shift+P` or toolbar button
|
|
166
|
+
- **Keyboard shortcuts** — customizable with sensible defaults
|
|
167
|
+
- **Accessibility** — semantic HTML, ARIA attributes, keyboard navigation, and focus management
|
|
168
|
+
|
|
169
|
+
## Props
|
|
170
|
+
|
|
171
|
+
| Prop | Type | Default | Description |
|
|
172
|
+
| --- | --- | --- | --- |
|
|
173
|
+
| `config` | `string` | — | Named editor config from `defineConfig()` |
|
|
174
|
+
| `value` | `string` | — | Controlled content (HTML or Markdown) |
|
|
175
|
+
| `defaultValue` | `string` | — | Initial content for uncontrolled mode |
|
|
176
|
+
| `onChange` | `(content: string) => void` | — | Called when content changes |
|
|
177
|
+
| `outputFormat` | `'html' \| 'markdown'` | `'html'` | Format passed to `onChange` |
|
|
178
|
+
| `toolbar` | `string[][]` | Full toolbar | Custom toolbar layout |
|
|
179
|
+
| `menuBar` | `boolean \| MenuBarConfig[]` | — | Enable menu bar |
|
|
180
|
+
| `theme` | `'light' \| 'dark' \| 'ocean' \| 'forest' \| 'sunset' \| 'rose'` | `'light'` | Editor theme |
|
|
181
|
+
| `placeholder` | `string` | `''` | Placeholder text |
|
|
182
|
+
| `height` | `number` | `300` | Editor height in px |
|
|
183
|
+
| `minHeight` | `number` | — | Minimum height |
|
|
184
|
+
| `maxHeight` | `number` | — | Maximum height (scrolls) |
|
|
185
|
+
| `readOnly` | `boolean` | `false` | Disable editing |
|
|
186
|
+
| `fonts` | `string[]` | Built-in list | Custom font families |
|
|
187
|
+
| `googleFonts` | `string[]` | — | Google Font families to auto-load |
|
|
188
|
+
| `statusBar` | `'bottom' \| 'top' \| 'popup' \| false` | `'bottom'` | Word/character count position |
|
|
189
|
+
| `customTheme` | `object` | — | CSS variable overrides |
|
|
190
|
+
| `toolbarItemTheme` | `object` | — | Per-item toolbar styling |
|
|
191
|
+
| `floatingToolbar` | `boolean` | `true` | Show toolbar on text selection |
|
|
192
|
+
| `contextMenu` | `boolean` | `true` | Show right-click context menu |
|
|
193
|
+
| `commandPalette` | `boolean` | `true` | Enable command palette (Mod+Shift+P or toolbar button) |
|
|
194
|
+
| `autosave` | `boolean \| AutosaveConfig` | `false` | Enable autosave with optional config (storage provider, interval, key) |
|
|
195
|
+
| `emptyState` | `boolean \| ReactNode` | `false` | Show empty state when editor has no content (true for default, or custom React node) |
|
|
196
|
+
| `breadcrumb` | `boolean` | `false` | Show breadcrumb bar with DOM path to current selection |
|
|
197
|
+
| `minimap` | `boolean` | `false` | Show minimap preview on right edge |
|
|
198
|
+
| `splitViewFormat` | `'html' \| 'markdown'` | `'html'` | Format for split view preview pane |
|
|
199
|
+
| `customizableToolbar` | `boolean` | `false` | Enable drag-and-drop toolbar button rearrangement |
|
|
200
|
+
| `onToolbarChange` | `(order: string[]) => void` | — | Called when toolbar order changes via drag |
|
|
201
|
+
| `plugins` | `Plugin[]` | — | Custom plugins |
|
|
202
|
+
| `uploadHandler` | `(file: File) => Promise<string>` | — | File upload handler |
|
|
203
|
+
| `shortcuts` | `object` | — | Keyboard shortcut overrides |
|
|
204
|
+
| `sanitize` | `object` | — | HTML sanitization options |
|
|
205
|
+
| `attachTo` | `React.RefObject` | — | Attach to an existing element |
|
|
206
|
+
| `onReady` | `(engine) => void` | — | Called when editor initializes |
|
|
207
|
+
| `onFocus` | `() => void` | — | Called on focus |
|
|
208
|
+
| `onBlur` | `() => void` | — | Called on blur |
|
|
209
|
+
| `className` | `string` | `''` | CSS class for wrapper |
|
|
210
|
+
| `style` | `object` | — | Inline styles for wrapper |
|
|
211
|
+
|
|
212
|
+
## Config File
|
|
213
|
+
|
|
214
|
+
Define shared defaults and named editor configurations in a centralized config file instead of repeating props.
|
|
215
|
+
|
|
216
|
+
**`remyx.config.js`**
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
import { defineConfig } from '@remyxjs/react';
|
|
220
|
+
|
|
221
|
+
export default defineConfig({
|
|
222
|
+
theme: 'dark',
|
|
223
|
+
placeholder: 'Start writing...',
|
|
224
|
+
height: 400,
|
|
225
|
+
|
|
226
|
+
editors: {
|
|
227
|
+
minimal: {
|
|
228
|
+
toolbar: [['bold', 'italic', 'underline'], ['link']],
|
|
229
|
+
floatingToolbar: false,
|
|
230
|
+
height: 200,
|
|
231
|
+
},
|
|
232
|
+
comments: {
|
|
233
|
+
toolbar: [['bold', 'italic', 'strikethrough'], ['orderedList', 'unorderedList'], ['link']],
|
|
234
|
+
statusBar: false,
|
|
235
|
+
height: 150,
|
|
236
|
+
placeholder: 'Write a comment...',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Usage**
|
|
243
|
+
|
|
244
|
+
```jsx
|
|
245
|
+
import { RemyxEditor, RemyxConfigProvider } from '@remyxjs/react';
|
|
246
|
+
import config from './remyx.config.js';
|
|
247
|
+
|
|
248
|
+
function App() {
|
|
249
|
+
return (
|
|
250
|
+
<RemyxConfigProvider config={config}>
|
|
251
|
+
<RemyxEditor /> {/* default config */}
|
|
252
|
+
<RemyxEditor config="minimal" /> {/* named config */}
|
|
253
|
+
<RemyxEditor config="minimal" theme="light" /> {/* prop overrides config */}
|
|
254
|
+
</RemyxConfigProvider>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Config Resolution Priority
|
|
260
|
+
|
|
261
|
+
Values merge with this priority (highest first):
|
|
262
|
+
|
|
263
|
+
1. **Component props** — always win
|
|
264
|
+
2. **Named editor config** — `editors.minimal`, `editors.comments`, etc.
|
|
265
|
+
3. **Default config** — top-level keys in the config file
|
|
266
|
+
4. **Built-in defaults** — `theme: 'light'`, `height: 300`, etc.
|
|
267
|
+
|
|
268
|
+
Editors work identically without a `RemyxConfigProvider` — the provider is optional.
|
|
269
|
+
|
|
270
|
+
### External Configuration (JSON/YAML URL)
|
|
271
|
+
|
|
272
|
+
Load the entire editor config from a remote JSON or YAML file — no code changes needed to update the editor:
|
|
273
|
+
|
|
274
|
+
```jsx
|
|
275
|
+
import { RemyxEditorFromConfig } from '@remyxjs/react';
|
|
276
|
+
|
|
277
|
+
function App() {
|
|
278
|
+
return (
|
|
279
|
+
<RemyxEditorFromConfig
|
|
280
|
+
url="/editor-config.json"
|
|
281
|
+
env="production"
|
|
282
|
+
value={content}
|
|
283
|
+
onChange={setContent}
|
|
284
|
+
loadingFallback={<div>Loading editor...</div>}
|
|
285
|
+
errorFallback={({ error, reload }) => (
|
|
286
|
+
<div>
|
|
287
|
+
<p>Config failed to load: {error.message}</p>
|
|
288
|
+
<button onClick={reload}>Retry</button>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Prop | Type | Description |
|
|
297
|
+
| --- | --- | --- |
|
|
298
|
+
| `url` | `string` | URL to a JSON or YAML config file |
|
|
299
|
+
| `env` | `string` | Environment name for config merging (e.g., `'production'`) |
|
|
300
|
+
| `fetchHeaders` | `Record<string, string>` | Custom headers for authenticated config endpoints |
|
|
301
|
+
| `pollInterval` | `number` | Auto-reload interval in ms (0 = disabled) |
|
|
302
|
+
| `onConfigLoad` | `(config) => void` | Callback when config loads successfully |
|
|
303
|
+
| `onConfigError` | `(error) => void` | Callback on load error |
|
|
304
|
+
| `loadingFallback` | `ReactNode` | UI shown while loading (default: null) |
|
|
305
|
+
| `errorFallback` | `ReactNode \| ({ error, reload }) => ReactNode` | UI shown on error (default: retry button) |
|
|
306
|
+
| `...rest` | — | All other props forwarded to `<RemyxEditor />` (overrides loaded config) |
|
|
307
|
+
|
|
308
|
+
#### useExternalConfig hook
|
|
309
|
+
|
|
310
|
+
For more control, use the hook directly:
|
|
311
|
+
|
|
312
|
+
```jsx
|
|
313
|
+
import { useExternalConfig, RemyxEditor } from '@remyxjs/react';
|
|
314
|
+
|
|
315
|
+
function MyEditor() {
|
|
316
|
+
const { config, loading, error, reload } = useExternalConfig('/editor-config.yaml', {
|
|
317
|
+
env: process.env.NODE_ENV,
|
|
318
|
+
pollInterval: 60000, // Auto-reload every 60s
|
|
319
|
+
onLoad: (cfg) => console.log('Config loaded:', cfg),
|
|
320
|
+
onError: (err) => console.error('Config error:', err),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (loading) return <div>Loading...</div>;
|
|
324
|
+
if (error) return <button onClick={reload}>Retry</button>;
|
|
325
|
+
|
|
326
|
+
return <RemyxEditor {...config} value={content} onChange={setContent} />;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
| Return value | Type | Description |
|
|
331
|
+
| --- | --- | --- |
|
|
332
|
+
| `config` | `object \| null` | The loaded configuration, or null before first load |
|
|
333
|
+
| `loading` | `boolean` | True while fetching |
|
|
334
|
+
| `error` | `Error \| null` | Fetch or parse error, or null |
|
|
335
|
+
| `reload` | `() => Promise<void>` | Re-fetch the config (cancels any in-flight request) |
|
|
336
|
+
|
|
337
|
+
## Toolbar
|
|
338
|
+
|
|
339
|
+
### Default Toolbar
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
[
|
|
343
|
+
['undo', 'redo'],
|
|
344
|
+
['headings', 'fontFamily', 'fontSize'],
|
|
345
|
+
['bold', 'italic', 'underline', 'strikethrough'],
|
|
346
|
+
['foreColor', 'backColor'],
|
|
347
|
+
['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'],
|
|
348
|
+
['orderedList', 'unorderedList', 'taskList'],
|
|
349
|
+
['outdent', 'indent'],
|
|
350
|
+
['link', 'image', 'table', 'embedMedia', 'blockquote', 'codeBlock', 'horizontalRule'],
|
|
351
|
+
['subscript', 'superscript'],
|
|
352
|
+
['findReplace', 'toggleMarkdown', 'sourceMode', 'export', 'fullscreen'],
|
|
353
|
+
]
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Each inner array is a visual group separated by a divider.
|
|
357
|
+
|
|
358
|
+
### Custom Toolbar via Props
|
|
359
|
+
|
|
360
|
+
```jsx
|
|
361
|
+
<RemyxEditor
|
|
362
|
+
toolbar={[
|
|
363
|
+
['bold', 'italic', 'underline'],
|
|
364
|
+
['orderedList', 'unorderedList'],
|
|
365
|
+
['link', 'image'],
|
|
366
|
+
]}
|
|
367
|
+
/>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Toolbar Presets
|
|
371
|
+
|
|
372
|
+
```jsx
|
|
373
|
+
import { TOOLBAR_PRESETS } from '@remyxjs/react';
|
|
374
|
+
|
|
375
|
+
<RemyxEditor toolbar={TOOLBAR_PRESETS.full} /> // all items including plugin commands (default)
|
|
376
|
+
<RemyxEditor toolbar={TOOLBAR_PRESETS.rich} /> // all features with callout, math, toc, bookmark, merge tag
|
|
377
|
+
<RemyxEditor toolbar={TOOLBAR_PRESETS.standard} /> // common editing without plugins
|
|
378
|
+
<RemyxEditor toolbar={TOOLBAR_PRESETS.minimal} /> // headings, basics, lists, links, images
|
|
379
|
+
<RemyxEditor toolbar={TOOLBAR_PRESETS.bare} /> // bold, italic, underline only
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Toolbar Helper Functions
|
|
383
|
+
|
|
384
|
+
#### `removeToolbarItems(config, itemsToRemove)`
|
|
385
|
+
|
|
386
|
+
```jsx
|
|
387
|
+
import { DEFAULT_TOOLBAR, removeToolbarItems } from '@remyxjs/react';
|
|
388
|
+
|
|
389
|
+
const toolbar = removeToolbarItems(DEFAULT_TOOLBAR, ['image', 'table', 'embedMedia', 'export']);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### `addToolbarItems(config, items, options?)`
|
|
393
|
+
|
|
394
|
+
```jsx
|
|
395
|
+
import { TOOLBAR_PRESETS, addToolbarItems } from '@remyxjs/react';
|
|
396
|
+
|
|
397
|
+
addToolbarItems(TOOLBAR_PRESETS.minimal, ['fullscreen']) // append as new group
|
|
398
|
+
addToolbarItems(TOOLBAR_PRESETS.minimal, 'fullscreen', { group: -1 }) // add to last group
|
|
399
|
+
addToolbarItems(TOOLBAR_PRESETS.minimal, 'taskList', { after: 'unorderedList' })
|
|
400
|
+
addToolbarItems(TOOLBAR_PRESETS.minimal, 'strikethrough', { before: 'underline' })
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### `createToolbar(items)`
|
|
404
|
+
|
|
405
|
+
Build a toolbar from a flat list — items are auto-grouped by category:
|
|
406
|
+
|
|
407
|
+
```jsx
|
|
408
|
+
import { createToolbar } from '@remyxjs/react';
|
|
409
|
+
|
|
410
|
+
const toolbar = createToolbar([
|
|
411
|
+
'bold', 'italic', 'underline', 'headings',
|
|
412
|
+
'link', 'image', 'orderedList', 'unorderedList', 'fullscreen',
|
|
413
|
+
]);
|
|
414
|
+
// Groups by category: [['headings'], ['bold', 'italic', 'underline'], ['orderedList', 'unorderedList'], ['link', 'image'], ['fullscreen']]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Available Toolbar Items
|
|
418
|
+
|
|
419
|
+
| Item | Type | Description |
|
|
420
|
+
| --- | --- | --- |
|
|
421
|
+
| `undo` | Button | Undo last action |
|
|
422
|
+
| `redo` | Button | Redo last action |
|
|
423
|
+
| `headings` | Dropdown | Block type (Normal, H1–H6) |
|
|
424
|
+
| `fontFamily` | Dropdown | Font family selector |
|
|
425
|
+
| `fontSize` | Dropdown | Font size selector |
|
|
426
|
+
| `bold` | Button | Bold |
|
|
427
|
+
| `italic` | Button | Italic |
|
|
428
|
+
| `underline` | Button | Underline |
|
|
429
|
+
| `strikethrough` | Button | Strikethrough |
|
|
430
|
+
| `subscript` | Button | Subscript |
|
|
431
|
+
| `superscript` | Button | Superscript |
|
|
432
|
+
| `foreColor` | Color Picker | Text color |
|
|
433
|
+
| `backColor` | Color Picker | Background color |
|
|
434
|
+
| `alignLeft` | Button | Align left |
|
|
435
|
+
| `alignCenter` | Button | Align center |
|
|
436
|
+
| `alignRight` | Button | Align right |
|
|
437
|
+
| `alignJustify` | Button | Justify |
|
|
438
|
+
| `orderedList` | Button | Numbered list |
|
|
439
|
+
| `unorderedList` | Button | Bulleted list |
|
|
440
|
+
| `taskList` | Button | Task/checkbox list |
|
|
441
|
+
| `indent` | Button | Increase indent |
|
|
442
|
+
| `outdent` | Button | Decrease indent |
|
|
443
|
+
| `link` | Button | Insert/edit link |
|
|
444
|
+
| `image` | Button | Insert image |
|
|
445
|
+
| `attachment` | Button | Attach file |
|
|
446
|
+
| `importDocument` | Button | Import (PDF, DOCX, MD, CSV) |
|
|
447
|
+
| `table` | Button | Insert table |
|
|
448
|
+
| `embedMedia` | Button | Embed video/media |
|
|
449
|
+
| `blockquote` | Button | Block quote |
|
|
450
|
+
| `codeBlock` | Button | Code block |
|
|
451
|
+
| `horizontalRule` | Button | Horizontal divider |
|
|
452
|
+
| `findReplace` | Button | Find and replace |
|
|
453
|
+
| `toggleMarkdown` | Button | Toggle markdown mode |
|
|
454
|
+
| `sourceMode` | Button | View/edit HTML source |
|
|
455
|
+
| `export` | Button | Export (PDF, MD, DOCX) |
|
|
456
|
+
| `commandPalette` | Button | Open command palette |
|
|
457
|
+
| `fullscreen` | Button | Toggle fullscreen |
|
|
458
|
+
| `insertCallout` | Button | Insert callout block |
|
|
459
|
+
| `insertMath` | Button | Insert math equation |
|
|
460
|
+
| `insertToc` | Button | Insert table of contents |
|
|
461
|
+
| `insertBookmark` | Button | Insert bookmark anchor |
|
|
462
|
+
| `insertMergeTag` | Button | Insert merge tag |
|
|
463
|
+
| `toggleAnalytics` | Button | Toggle analytics panel |
|
|
464
|
+
| `toggleSpellcheck` | Button | Toggle spellcheck |
|
|
465
|
+
| `checkGrammar` | Button | Run grammar check |
|
|
466
|
+
| `addComment` | Button | Add inline comment on selection |
|
|
467
|
+
| `removeFormat` | Button | Remove all inline formatting |
|
|
468
|
+
| `distractionFree` | Button | Toggle distraction-free mode (Mod+Shift+D) |
|
|
469
|
+
| `toggleSplitView` | Button | Toggle side-by-side preview (Mod+Shift+V) |
|
|
470
|
+
| `typography` | Dropdown | Typography controls (line height, letter spacing, paragraph spacing) |
|
|
471
|
+
| `lineHeight` | Dropdown | Line height adjustment |
|
|
472
|
+
| `letterSpacing` | Dropdown | Letter spacing adjustment |
|
|
473
|
+
| `paragraphSpacing` | Dropdown | Paragraph spacing adjustment |
|
|
474
|
+
| `startCollaboration` | Button | Start real-time collaboration session |
|
|
475
|
+
| `stopCollaboration` | Button | Stop collaboration session |
|
|
476
|
+
|
|
477
|
+
## Menu Bar
|
|
478
|
+
|
|
479
|
+
Add an application-style menu bar above the toolbar. Both the toolbar and menu bar display all their items — the menu bar is an additional navigation layer, not a replacement for the toolbar.
|
|
480
|
+
|
|
481
|
+
### Enable
|
|
482
|
+
|
|
483
|
+
```jsx
|
|
484
|
+
<RemyxEditor menuBar={true} />
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Default menus:**
|
|
488
|
+
|
|
489
|
+
| Menu | Items |
|
|
490
|
+
| --- | --- |
|
|
491
|
+
| **File** | Import Document, Export Document |
|
|
492
|
+
| **Edit** | Undo, Redo, Find & Replace |
|
|
493
|
+
| **View** | Fullscreen, Distraction-Free Mode, Split View, Toggle Markdown, Source Mode, Toggle Analytics |
|
|
494
|
+
| **Insert** | Link, Image, Table, Attachment, Embed Media, Blockquote, Code Block, Horizontal Rule, Insert Callout, Insert Math, Insert TOC, Insert Bookmark, Insert Merge Tag, Add Comment |
|
|
495
|
+
| **Format** | Bold, Italic, Underline, Strikethrough, Subscript, Superscript, Alignment, Lists, Colors, Typography (Line Height, Letter Spacing, Paragraph Spacing), Remove Formatting |
|
|
496
|
+
|
|
497
|
+
### Custom Menu Bar
|
|
498
|
+
|
|
499
|
+
```jsx
|
|
500
|
+
<RemyxEditor
|
|
501
|
+
menuBar={[
|
|
502
|
+
{ label: 'File', items: ['importDocument', 'export'] },
|
|
503
|
+
{ label: 'Edit', items: ['undo', 'redo', '---', 'findReplace'] },
|
|
504
|
+
{ label: 'Format', items: [
|
|
505
|
+
'bold', 'italic', 'underline',
|
|
506
|
+
'---',
|
|
507
|
+
{ label: 'Alignment', items: ['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'] },
|
|
508
|
+
]},
|
|
509
|
+
]}
|
|
510
|
+
/>
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Each menu object has a `label` (string) and `items` array of command names, `'---'` separators, or nested `{ label, items }` submenu objects. Any command from the [Available Toolbar Items](#available-toolbar-items) table works as a menu item.
|
|
514
|
+
|
|
515
|
+
### Menu Bar with Config File
|
|
516
|
+
|
|
517
|
+
```js
|
|
518
|
+
export default defineConfig({
|
|
519
|
+
menuBar: true,
|
|
520
|
+
editors: {
|
|
521
|
+
full: { menuBar: true },
|
|
522
|
+
minimal: {
|
|
523
|
+
menuBar: [
|
|
524
|
+
{ label: 'Edit', items: ['undo', 'redo'] },
|
|
525
|
+
{ label: 'Format', items: ['bold', 'italic', 'underline'] },
|
|
526
|
+
],
|
|
527
|
+
toolbar: [['headings', 'fontFamily', 'fontSize']],
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Toolbar and Menu Bar
|
|
534
|
+
|
|
535
|
+
When both the toolbar and menu bar are enabled, the full toolbar is preserved. The menu bar provides an additional navigation layer — it does not remove items from the toolbar. Both surfaces can be used together to give users maximum flexibility.
|
|
536
|
+
|
|
537
|
+
### Menu Bar Behavior
|
|
538
|
+
|
|
539
|
+
- **Click** a trigger to open, **hover** to switch between open menus
|
|
540
|
+
- **Escape** or click outside to close
|
|
541
|
+
- Submenus open on hover with a chevron
|
|
542
|
+
- Toggle commands show active state, modal commands open their dialogs
|
|
543
|
+
- Inherits the editor's theme and custom theme variables
|
|
544
|
+
|
|
545
|
+
## Multiple Editors
|
|
546
|
+
|
|
547
|
+
Multiple `<RemyxEditor />` instances work on the same page without any additional setup. Each editor is fully isolated.
|
|
548
|
+
|
|
549
|
+
### Basic Usage
|
|
550
|
+
|
|
551
|
+
```jsx
|
|
552
|
+
<RemyxEditor placeholder="Editor 1..." height={300} />
|
|
553
|
+
<RemyxEditor placeholder="Editor 2..." height={300} />
|
|
554
|
+
<RemyxEditor placeholder="Editor 3..." height={200} />
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### What's Isolated Per Instance
|
|
558
|
+
|
|
559
|
+
| Feature | Isolation |
|
|
560
|
+
| --- | --- |
|
|
561
|
+
| **Content & state** | Separate undo/redo history, content, and selection |
|
|
562
|
+
| **Toolbar** | Independent dropdowns, color pickers, and active states |
|
|
563
|
+
| **Menu bar** | Menus open/close independently per editor |
|
|
564
|
+
| **Modals** | Each editor has its own modals (link, image, table, find/replace) |
|
|
565
|
+
| **Floating toolbar** | Appears only for the editor with an active selection |
|
|
566
|
+
| **Context menu** | Scoped to the clicked editor |
|
|
567
|
+
| **Fullscreen** | Each editor enters/exits fullscreen independently |
|
|
568
|
+
| **Plugins** | Per-editor plugin instances with separate state |
|
|
569
|
+
| **Events** | Separate `EventBus` per instance |
|
|
570
|
+
| **Keyboard shortcuts** | Fire only for the focused editor |
|
|
571
|
+
|
|
572
|
+
### Mixed Configurations
|
|
573
|
+
|
|
574
|
+
```jsx
|
|
575
|
+
<RemyxConfigProvider config={config}>
|
|
576
|
+
<RemyxEditor menuBar={true} height={400} />
|
|
577
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
|
578
|
+
<RemyxEditor config="minimal" />
|
|
579
|
+
<RemyxEditor config="comments" />
|
|
580
|
+
</div>
|
|
581
|
+
<RemyxEditor theme="light" placeholder="Standalone editor" />
|
|
582
|
+
</RemyxConfigProvider>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
No hard limit on editor count. Performance scales linearly. Editors in a `RemyxConfigProvider` share the config (read-only) but all runtime state is per-instance.
|
|
586
|
+
|
|
587
|
+
## Font Configuration
|
|
588
|
+
|
|
589
|
+
### Custom Font List
|
|
590
|
+
|
|
591
|
+
```jsx
|
|
592
|
+
<RemyxEditor fonts={['Arial', 'Georgia', 'Courier New', 'My Custom Font']} />
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Google Fonts
|
|
596
|
+
|
|
597
|
+
```jsx
|
|
598
|
+
<RemyxEditor googleFonts={['Roboto', 'Open Sans', 'Lato', 'Montserrat']} />
|
|
599
|
+
|
|
600
|
+
// With custom fonts
|
|
601
|
+
<RemyxEditor fonts={['Arial', 'Georgia']} googleFonts={['Poppins', 'Inter']} />
|
|
602
|
+
|
|
603
|
+
// Specific weights
|
|
604
|
+
<RemyxEditor googleFonts={['Roboto:wght@400;700']} />
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Google Fonts are auto-loaded via CDN and merged into the font dropdown.
|
|
608
|
+
|
|
609
|
+
### Font Helper Functions
|
|
610
|
+
|
|
611
|
+
```jsx
|
|
612
|
+
import { DEFAULT_FONTS, removeFonts, addFonts, loadGoogleFonts } from '@remyxjs/react';
|
|
613
|
+
|
|
614
|
+
// Remove fonts
|
|
615
|
+
const fonts = removeFonts(DEFAULT_FONTS, ['Comic Sans MS', 'Impact']);
|
|
616
|
+
|
|
617
|
+
// Add fonts (append by default, or { position: 'start' } to prepend)
|
|
618
|
+
const fonts = addFonts(DEFAULT_FONTS, ['My Custom Font']);
|
|
619
|
+
|
|
620
|
+
// Load Google Fonts programmatically
|
|
621
|
+
loadGoogleFonts(['Roboto', 'Lato']);
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
## Status Bar
|
|
625
|
+
|
|
626
|
+
The status bar displays word and character counts.
|
|
627
|
+
|
|
628
|
+
```jsx
|
|
629
|
+
<RemyxEditor statusBar="bottom" /> {/* below content (default) */}
|
|
630
|
+
<RemyxEditor statusBar="top" /> {/* above content */}
|
|
631
|
+
<RemyxEditor statusBar="popup" /> {/* toolbar button with popover */}
|
|
632
|
+
<RemyxEditor statusBar={false} /> {/* hidden */}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## Theming
|
|
636
|
+
|
|
637
|
+
```jsx
|
|
638
|
+
<RemyxEditor theme="light" /> // default
|
|
639
|
+
<RemyxEditor theme="dark" />
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Custom Themes
|
|
643
|
+
|
|
644
|
+
Override CSS variables using `createTheme()` or raw variable names. Custom themes layer on top of the base theme — only specify what you want to change.
|
|
645
|
+
|
|
646
|
+
```jsx
|
|
647
|
+
import { createTheme } from '@remyxjs/react';
|
|
648
|
+
|
|
649
|
+
const brandTheme = createTheme({
|
|
650
|
+
bg: '#1a1a2e',
|
|
651
|
+
text: '#e0e0e0',
|
|
652
|
+
primary: '#e94560',
|
|
653
|
+
primaryHover: '#c81e45',
|
|
654
|
+
toolbarBg: '#16213e',
|
|
655
|
+
toolbarIcon: '#94a3b8',
|
|
656
|
+
toolbarIconActive: '#e94560',
|
|
657
|
+
radius: '12px',
|
|
658
|
+
contentFontSize: '18px',
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
<RemyxEditor theme="dark" customTheme={brandTheme} />
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Or pass raw CSS variables directly:
|
|
665
|
+
|
|
666
|
+
```jsx
|
|
667
|
+
<RemyxEditor customTheme={{ '--rmx-primary': '#e94560', '--rmx-bg': '#1a1a2e' }} />
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Theme Presets
|
|
671
|
+
|
|
672
|
+
All 6 built-in themes are available via the `theme` prop:
|
|
673
|
+
|
|
674
|
+
```jsx
|
|
675
|
+
<RemyxEditor theme="light" /> {/* Default — clean white */}
|
|
676
|
+
<RemyxEditor theme="dark" /> {/* Neutral dark */}
|
|
677
|
+
<RemyxEditor theme="ocean" /> {/* Deep blue */}
|
|
678
|
+
<RemyxEditor theme="forest" /> {/* Green earth-tone */}
|
|
679
|
+
<RemyxEditor theme="sunset" /> {/* Warm orange/amber */}
|
|
680
|
+
<RemyxEditor theme="rose" /> {/* Soft pink */}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Override individual variables on top of any theme with `customTheme`:
|
|
684
|
+
|
|
685
|
+
```jsx
|
|
686
|
+
import { createTheme } from '@remyxjs/react';
|
|
687
|
+
|
|
688
|
+
<RemyxEditor theme="ocean" customTheme={createTheme({ primary: '#ff6b6b' })} />
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Available Theme Variables
|
|
692
|
+
|
|
693
|
+
| Key | CSS Variable | Description |
|
|
694
|
+
| --- | --- | --- |
|
|
695
|
+
| `bg` | `--rmx-bg` | Editor background |
|
|
696
|
+
| `text` | `--rmx-text` | Primary text color |
|
|
697
|
+
| `textSecondary` | `--rmx-text-secondary` | Muted text color |
|
|
698
|
+
| `border` | `--rmx-border` | Border color |
|
|
699
|
+
| `borderSubtle` | `--rmx-border-subtle` | Subtle border color |
|
|
700
|
+
| `toolbarBg` | `--rmx-toolbar-bg` | Toolbar background |
|
|
701
|
+
| `toolbarBorder` | `--rmx-toolbar-border` | Toolbar border |
|
|
702
|
+
| `toolbarButtonHover` | `--rmx-toolbar-button-hover` | Button hover background |
|
|
703
|
+
| `toolbarButtonActive` | `--rmx-toolbar-button-active` | Button active background |
|
|
704
|
+
| `toolbarIcon` | `--rmx-toolbar-icon` | Icon color |
|
|
705
|
+
| `toolbarIconActive` | `--rmx-toolbar-icon-active` | Active icon color |
|
|
706
|
+
| `primary` | `--rmx-primary` | Primary accent color |
|
|
707
|
+
| `primaryHover` | `--rmx-primary-hover` | Primary hover color |
|
|
708
|
+
| `primaryLight` | `--rmx-primary-light` | Light primary (backgrounds) |
|
|
709
|
+
| `focusRing` | `--rmx-focus-ring` | Focus outline color |
|
|
710
|
+
| `selection` | `--rmx-selection` | Text selection color |
|
|
711
|
+
| `danger` | `--rmx-danger` | Error/danger color |
|
|
712
|
+
| `dangerLight` | `--rmx-danger-light` | Light danger color |
|
|
713
|
+
| `placeholder` | `--rmx-placeholder` | Placeholder text color |
|
|
714
|
+
| `modalBg` | `--rmx-modal-bg` | Modal background |
|
|
715
|
+
| `modalOverlay` | `--rmx-modal-overlay` | Modal overlay |
|
|
716
|
+
| `statusbarBg` | `--rmx-statusbar-bg` | Status bar background |
|
|
717
|
+
| `statusbarText` | `--rmx-statusbar-text` | Status bar text |
|
|
718
|
+
| `fontFamily` | `--rmx-font-family` | UI font stack |
|
|
719
|
+
| `fontSize` | `--rmx-font-size` | UI font size |
|
|
720
|
+
| `contentFontSize` | `--rmx-content-font-size` | Content font size |
|
|
721
|
+
| `contentLineHeight` | `--rmx-content-line-height` | Content line height |
|
|
722
|
+
| `radius` | `--rmx-radius` | Border radius |
|
|
723
|
+
| `radiusSm` | `--rmx-radius-sm` | Small border radius |
|
|
724
|
+
| `spacingXs` | `--rmx-spacing-xs` | Extra small spacing |
|
|
725
|
+
| `spacingSm` | `--rmx-spacing-sm` | Small spacing |
|
|
726
|
+
| `spacingMd` | `--rmx-spacing-md` | Medium spacing |
|
|
727
|
+
|
|
728
|
+
### Per-Item Toolbar Theming
|
|
729
|
+
|
|
730
|
+
Style individual toolbar buttons independently using `toolbarItemTheme`.
|
|
731
|
+
|
|
732
|
+
```jsx
|
|
733
|
+
import { createToolbarItemTheme } from '@remyxjs/react';
|
|
734
|
+
|
|
735
|
+
const itemTheme = createToolbarItemTheme({
|
|
736
|
+
bold: { color: '#e11d48', activeColor: '#be123c', activeBackground: '#ffe4e6', borderRadius: '50%' },
|
|
737
|
+
italic: { color: '#7c3aed', activeColor: '#6d28d9', activeBackground: '#ede9fe' },
|
|
738
|
+
underline: { color: '#0891b2', activeColor: '#0e7490', activeBackground: '#cffafe' },
|
|
739
|
+
_separator: { color: '#c4b5fd', width: '2px' },
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
<RemyxEditor toolbarItemTheme={itemTheme} />
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Both `customTheme` and `toolbarItemTheme` can be used together — `customTheme` sets global styles, `toolbarItemTheme` overrides specific items.
|
|
746
|
+
|
|
747
|
+
**Per-item style properties:**
|
|
748
|
+
|
|
749
|
+
| Key | CSS Variable | Description |
|
|
750
|
+
| --- | --- | --- |
|
|
751
|
+
| `color` | `--rmx-tb-color` | Icon/text color |
|
|
752
|
+
| `background` | `--rmx-tb-bg` | Default background |
|
|
753
|
+
| `hoverColor` | `--rmx-tb-hover-color` | Color on hover |
|
|
754
|
+
| `hoverBackground` | `--rmx-tb-hover-bg` | Background on hover |
|
|
755
|
+
| `activeColor` | `--rmx-tb-active-color` | Color when active |
|
|
756
|
+
| `activeBackground` | `--rmx-tb-active-bg` | Background when active |
|
|
757
|
+
| `border` | `--rmx-tb-border` | Border shorthand |
|
|
758
|
+
| `borderRadius` | `--rmx-tb-radius` | Border radius |
|
|
759
|
+
| `size` | `--rmx-tb-size` | Button width & height |
|
|
760
|
+
| `iconSize` | `--rmx-tb-icon-size` | Icon size |
|
|
761
|
+
| `padding` | `--rmx-tb-padding` | Button padding |
|
|
762
|
+
| `opacity` | `--rmx-tb-opacity` | Button opacity |
|
|
763
|
+
|
|
764
|
+
**Separator properties** (via `_separator` key):
|
|
765
|
+
|
|
766
|
+
| Key | CSS Variable | Description |
|
|
767
|
+
| --- | --- | --- |
|
|
768
|
+
| `color` | `--rmx-tb-sep-color` | Separator color |
|
|
769
|
+
| `width` | `--rmx-tb-sep-width` | Separator width |
|
|
770
|
+
| `height` | `--rmx-tb-sep-height` | Separator height |
|
|
771
|
+
| `margin` | `--rmx-tb-sep-margin` | Separator margin |
|
|
772
|
+
|
|
773
|
+
## Paste & Auto-Conversion
|
|
774
|
+
|
|
775
|
+
The editor cleans and normalizes pasted content automatically.
|
|
776
|
+
|
|
777
|
+
| Source | Handling |
|
|
778
|
+
| --- | --- |
|
|
779
|
+
| **Microsoft Word** | Strips Office XML, `mso-*` styles, conditional comments. Converts Word-style lists to proper `<ul>`/`<ol>`. |
|
|
780
|
+
| **Google Docs** | Removes internal IDs/classes. Converts styled spans to semantic tags (`<strong>`, `<em>`, `<s>`). |
|
|
781
|
+
| **LibreOffice** | Strips namespace tags and auto-generated class names. |
|
|
782
|
+
| **Apple Pages** | Removes iWork-specific attributes. |
|
|
783
|
+
| **Markdown** | Auto-detects and converts to rich HTML (headings, lists, bold, links, code fences, etc.). |
|
|
784
|
+
| **Plain text** | Wraps in `<p>` tags with `<br>` for line breaks. |
|
|
785
|
+
| **Images** | Inserted as base64 data URIs, or uploaded via `uploadHandler` if configured. |
|
|
786
|
+
|
|
787
|
+
All paste paths (keyboard, drag-and-drop, context menu) share the same pipeline: HTML goes through `cleanPastedHTML()` then `Sanitizer.sanitize()`. Plain text is checked by `looksLikeMarkdown()` and converted if detected.
|
|
788
|
+
|
|
789
|
+
```js
|
|
790
|
+
import { cleanPastedHTML, looksLikeMarkdown } from '@remyxjs/react';
|
|
791
|
+
|
|
792
|
+
const clean = cleanPastedHTML(dirtyHtml);
|
|
793
|
+
const isMarkdown = looksLikeMarkdown(text);
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## File Uploads
|
|
797
|
+
|
|
798
|
+
The `uploadHandler` prop controls where images and file attachments are stored. It receives a `File` and returns a `Promise<string>` with the file URL.
|
|
799
|
+
|
|
800
|
+
Without an `uploadHandler`, images are inserted as base64 data URIs and the attachment upload tab is disabled.
|
|
801
|
+
|
|
802
|
+
### Custom Server
|
|
803
|
+
|
|
804
|
+
```jsx
|
|
805
|
+
<RemyxEditor
|
|
806
|
+
uploadHandler={async (file) => {
|
|
807
|
+
const formData = new FormData();
|
|
808
|
+
formData.append('file', file);
|
|
809
|
+
const res = await fetch('/api/upload', { method: 'POST', body: formData });
|
|
810
|
+
const { url } = await res.json();
|
|
811
|
+
return url;
|
|
812
|
+
}}
|
|
813
|
+
/>
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### S3 / R2 / GCS (Pre-signed URL)
|
|
817
|
+
|
|
818
|
+
```jsx
|
|
819
|
+
<RemyxEditor
|
|
820
|
+
uploadHandler={async (file) => {
|
|
821
|
+
const res = await fetch('/api/presign', {
|
|
822
|
+
method: 'POST',
|
|
823
|
+
headers: { 'Content-Type': 'application/json' },
|
|
824
|
+
body: JSON.stringify({ filename: file.name, contentType: file.type }),
|
|
825
|
+
});
|
|
826
|
+
const { uploadUrl, publicUrl } = await res.json();
|
|
827
|
+
await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file });
|
|
828
|
+
return publicUrl;
|
|
829
|
+
}}
|
|
830
|
+
/>
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Upload Paths
|
|
834
|
+
|
|
835
|
+
The handler is called consistently across all upload surfaces:
|
|
836
|
+
|
|
837
|
+
| Path | Result |
|
|
838
|
+
| --- | --- |
|
|
839
|
+
| Image toolbar (Upload tab) | Inserted as `<img>` |
|
|
840
|
+
| Attachment toolbar (Upload tab) | Inserted as attachment chip |
|
|
841
|
+
| Drag & drop image/file | Image as `<img>`, others as attachment |
|
|
842
|
+
| Paste image/file | Same as drag & drop |
|
|
843
|
+
|
|
844
|
+
## Output Formats
|
|
845
|
+
|
|
846
|
+
```jsx
|
|
847
|
+
<RemyxEditor outputFormat="html" onChange={(html) => console.log(html)} />
|
|
848
|
+
<RemyxEditor outputFormat="markdown" onChange={(md) => console.log(md)} />
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
Conversion utilities for standalone use:
|
|
852
|
+
|
|
853
|
+
```js
|
|
854
|
+
import { htmlToMarkdown, markdownToHtml } from '@remyxjs/react';
|
|
855
|
+
|
|
856
|
+
const md = htmlToMarkdown('<h1>Hello</h1><p>World</p>');
|
|
857
|
+
const html = markdownToHtml('# Hello\n\nWorld');
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
## Attaching to Existing Elements
|
|
861
|
+
|
|
862
|
+
Enhance an existing `<textarea>` or `<div>` with a full WYSIWYG editor.
|
|
863
|
+
|
|
864
|
+
### Textarea
|
|
865
|
+
|
|
866
|
+
```jsx
|
|
867
|
+
const textareaRef = useRef(null);
|
|
868
|
+
|
|
869
|
+
<textarea ref={textareaRef} defaultValue="Initial content" />
|
|
870
|
+
<RemyxEditor attachTo={textareaRef} outputFormat="markdown" height={400} />
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
The textarea is hidden and its value stays in sync for form submission.
|
|
874
|
+
|
|
875
|
+
### Div
|
|
876
|
+
|
|
877
|
+
```jsx
|
|
878
|
+
const divRef = useRef(null);
|
|
879
|
+
|
|
880
|
+
<div ref={divRef}>
|
|
881
|
+
<h2>Existing content</h2>
|
|
882
|
+
<p>This will become editable.</p>
|
|
883
|
+
</div>
|
|
884
|
+
<RemyxEditor attachTo={divRef} theme="dark" height={400} />
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### `useRemyxEditor` Hook
|
|
888
|
+
|
|
889
|
+
For lower-level control:
|
|
890
|
+
|
|
891
|
+
```jsx
|
|
892
|
+
import { useRemyxEditor } from '@remyxjs/react';
|
|
893
|
+
|
|
894
|
+
function MyEditor() {
|
|
895
|
+
const targetRef = useRef(null);
|
|
896
|
+
const { engine, containerRef, editableRef, ready } = useRemyxEditor(targetRef, {
|
|
897
|
+
onChange: (content) => console.log(content),
|
|
898
|
+
placeholder: 'Type here...',
|
|
899
|
+
theme: 'light',
|
|
900
|
+
height: 400,
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
return <textarea ref={targetRef} />;
|
|
904
|
+
}
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
Returns `engine` (EditorEngine), `containerRef`, `editableRef`, and `ready` (boolean).
|
|
908
|
+
|
|
909
|
+
### `useEditorEngine` Hook
|
|
910
|
+
|
|
911
|
+
The lowest-level hook — manages just the engine lifecycle:
|
|
912
|
+
|
|
913
|
+
```jsx
|
|
914
|
+
import { useEditorEngine } from '@remyxjs/react';
|
|
915
|
+
|
|
916
|
+
function CustomEditor() {
|
|
917
|
+
const editorRef = useRef(null);
|
|
918
|
+
const engine = useEditorEngine(editorRef, {
|
|
919
|
+
outputFormat: 'html',
|
|
920
|
+
plugins: [],
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
return <div ref={editorRef} contentEditable />;
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
## Plugins
|
|
928
|
+
|
|
929
|
+
### Built-in Plugins
|
|
930
|
+
|
|
931
|
+
Fifteen built-in plugins are available:
|
|
932
|
+
|
|
933
|
+
- **WordCountPlugin** — word/character counts in the status bar
|
|
934
|
+
- **PlaceholderPlugin** — placeholder text when empty
|
|
935
|
+
- **AutolinkPlugin** — auto-converts typed URLs to links
|
|
936
|
+
- **SyntaxHighlightPlugin** — automatic syntax highlighting for code blocks with language detection, theme-aware colors, `setCodeLanguage`/`getCodeLanguage`/`toggleLineNumbers` commands, copy-to-clipboard button, inline `<code>` mini-highlighting, and extensible language registry (`registerLanguage`/`unregisterLanguage`). When active, a language selector dropdown appears on focused code blocks.
|
|
937
|
+
- **TablePlugin** — enhanced table features including column/row resize handles, click-to-sort on header cells (single + multi-column with Shift), per-column filter dropdowns, inline cell formulas (SUM, AVERAGE, COUNT, MIN, MAX, IF, CONCAT), cell formatting (number, currency, percentage, date), and sticky header rows. Adds 6 new commands: `toggleHeaderRow`, `sortTable`, `filterTable`, `clearTableFilters`, `formatCell`, `evaluateFormulas`. Context menu adds Toggle Header Row, Format Cell options, and Clear Filters when right-clicking in a table.
|
|
938
|
+
|
|
939
|
+
- **CalloutPlugin** — styled callout blocks with 7 built-in types (info, warning, error, success, tip, note, question), custom type registration, collapsible toggle, nested content, GFM alert syntax auto-conversion. Adds 4 commands: `insertCallout`, `removeCallout`, `changeCalloutType`, `toggleCalloutCollapse`. Context menu adds "Insert Callout" and "Remove Callout".
|
|
940
|
+
- **CommentsPlugin** — inline comment threads on selected text, resolved/unresolved state with visual indicators, @mention parsing, reply threads, comment-only mode (read-only editor with annotation support), import/export, DOM sync via MutationObserver. Adds 6 new commands: `addComment`, `deleteComment`, `resolveComment`, `replyToComment`, `editComment`, `navigateToComment`. Context menu adds "Add Comment" when text is selected.
|
|
941
|
+
- **LinkPlugin** — link preview tooltips (via `onUnfurl` callback), broken link detection with periodic scanning and `rmx-link-broken` indicators, auto-linking of typed URLs/emails/phone numbers on Space/Enter, link click analytics (`onLinkClick`), bookmark anchors (`insertBookmark`, `linkToBookmark`, `getBookmarks`, `removeBookmark`), `scanBrokenLinks`/`getBrokenLinks` commands.
|
|
942
|
+
- **TemplatePlugin** — `{{merge_tag}}` syntax with visual chips, `{{#if condition}}...{{/if}}` conditional blocks, `{{#each items}}...{{/each}}` repeatable sections, live preview with sample data, 5 pre-built templates (email, invoice, letter, report, newsletter), `registerTemplate`/`unregisterTemplate`/`getTemplateLibrary`/`getTemplate` APIs, `exportTemplate` JSON export. Adds 6 commands: `insertMergeTag`, `loadTemplate`, `previewTemplate`, `exitPreview`, `exportTemplate`, `getTemplateTags`.
|
|
943
|
+
- **KeyboardPlugin** — Vim mode (normal/insert/visual with h/j/k/l/w/b motions), Emacs keybindings (Ctrl+A/E/F/B/K/Y), custom keybinding map, smart bracket/quote auto-pairing with wrap-selection, multi-cursor (`Cmd+D`), jump-to-heading (`Cmd+Shift+G`). Adds 5 commands: `setKeyboardMode`, `getVimMode`, `jumpToHeading`, `getHeadings`, `selectNextOccurrence`.
|
|
944
|
+
- **DragDropPlugin** — drop zone overlays with visual guides, cross-editor drag via `text/x-remyx-block` transfer, external file/image/rich-text drops, block reorder with ghost preview and drop indicator, `onDrop`/`onFileDrop` callbacks. Adds 2 commands: `moveBlockUp`, `moveBlockDown` (`Cmd+Shift+Arrow`).
|
|
945
|
+
- **MathPlugin** — LaTeX/KaTeX math rendering with `$...$` inline and `$$...$$` block syntax, pluggable `renderMath(latex, displayMode)` callback, 40+ symbol palette across 4 categories (Greek, Operators, Arrows, Common), auto equation numbering, `latexToMathML()` conversion, `parseMathExpressions()` detection. Adds 6 commands: `insertMath`, `editMath`, `insertSymbol`, `getSymbolPalette`, `getMathElements`, `copyMathAs`.
|
|
946
|
+
- **TocPlugin** — auto-generated table of contents from H1-H6 heading hierarchy, section numbering (1, 1.1, 1.2), `insertToc` renders linked `<nav>` into document, `scrollToHeading` with smooth scroll, `validateHeadings` detects skipped levels, `onOutlineChange` callback + `toc:change` event. Adds 4 commands: `getOutline`, `insertToc`, `scrollToHeading`, `validateHeadings`.
|
|
947
|
+
- **AnalyticsPlugin** — real-time readability scores (Flesch-Kincaid, Gunning Fog, Coleman-Liau, Flesch Reading Ease), reading time estimate, vocabulary level (basic/intermediate/advanced), sentence/paragraph length warnings, goal-based word count with progress tracking, keyword density, SEO hints. `onAnalytics` callback + `analytics:update` event. Adds 3 commands: `getAnalytics`, `getSeoAnalysis`, `getKeywordDensity`.
|
|
948
|
+
- **SpellcheckPlugin** — built-in grammar engine with passive voice detection, wordiness patterns, cliche detection, and punctuation checks. Inline red wavy (spelling), blue wavy (grammar), and green dotted (style) underlines. Right-click context menu with correction suggestions, "Ignore", and "Add to Dictionary". Writing-style presets (formal, casual, technical, academic). BCP 47 multi-language support. Optional `customService` interface for LanguageTool/Grammarly. Persistent dictionary via localStorage. `onError`/`onCorrection` callbacks + `spellcheck:update`/`grammar:check` events. Adds 6 commands: `toggleSpellcheck`, `checkGrammar`, `addToDictionary`, `ignoreWord`, `setWritingStyle`, `getSpellcheckStats`.
|
|
949
|
+
|
|
950
|
+
```jsx
|
|
951
|
+
import { SyntaxHighlightPlugin, TablePlugin, CommentsPlugin, CalloutPlugin, LinkPlugin, TemplatePlugin, KeyboardPlugin, DragDropPlugin, MathPlugin, TocPlugin, AnalyticsPlugin, SpellcheckPlugin } from '@remyxjs/react';
|
|
952
|
+
|
|
953
|
+
<RemyxEditor
|
|
954
|
+
plugins={[SyntaxHighlightPlugin(), TablePlugin(), CommentsPlugin(), CalloutPlugin(), LinkPlugin(), TemplatePlugin(), KeyboardPlugin(), DragDropPlugin(), MathPlugin(), TocPlugin(), AnalyticsPlugin(), SpellcheckPlugin()]}
|
|
955
|
+
/>
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
#### TablePlugin in depth
|
|
959
|
+
|
|
960
|
+
When `TablePlugin()` is active, every `<table class="rmx-table">` in the editor automatically gains:
|
|
961
|
+
|
|
962
|
+
| Feature | How it works |
|
|
963
|
+
| --- | --- |
|
|
964
|
+
| **Sortable columns** | Click any `<th>` to cycle through ascending → descending → unsorted. Rows are physically reordered in the DOM so the sort persists in HTML output. |
|
|
965
|
+
| **Multi-column sort** | Hold **Shift** and click additional headers. A small priority number appears on each sorted header. |
|
|
966
|
+
| **Sort data types** | The sort auto-detects numeric, date, or alphabetical data. Pass `dataType` explicitly or provide a custom comparator via the `tableSortComparator` engine option. |
|
|
967
|
+
| **Sort indicators** | ▲/▼ triangles rendered via CSS `::after` on `<th>` elements with `data-sort-dir` attributes. |
|
|
968
|
+
| **Filterable rows** | A small ▽ icon appears in each header cell. Click it to open a filter dropdown with a text input. Rows not matching the filter are hidden (non-destructive — they reappear when the filter is cleared). Multiple columns can be filtered simultaneously (AND logic). |
|
|
969
|
+
| **Column/row resize** | Invisible 6px drag handles appear at column and row borders. Drag to resize. The resize is smooth (rAF-driven) and creates an undo snapshot on mouseup. |
|
|
970
|
+
| **Inline formulas** | Type `=SUM(A1:A5)` in any cell. On blur, the formula is stored in a `data-formula` attribute and the computed result is displayed. On focus, the formula text is shown for editing. Supports SUM, AVERAGE, COUNT, MIN, MAX, IF, CONCAT, cell references (A1 notation), ranges, arithmetic, and comparison operators. Circular references are detected and display `#CIRC!`. |
|
|
971
|
+
| **Cell formatting** | Right-click a cell to format as Number, Currency, Percentage, or Date. The raw value is preserved in `data-raw-value`; the display uses `Intl.NumberFormat` / `Intl.DateTimeFormat`. |
|
|
972
|
+
| **Sticky header** | `<thead><th>` cells use `position: sticky` so the header row stays visible when scrolling tall tables. |
|
|
973
|
+
| **Clipboard interop** | Copying table content produces both HTML and TSV (tab-separated values). Pasting TSV or HTML tables from Excel / Google Sheets inserts data into the grid starting at the caret cell, auto-expanding rows and columns as needed. |
|
|
974
|
+
|
|
975
|
+
**Custom sort comparator:**
|
|
976
|
+
|
|
977
|
+
```jsx
|
|
978
|
+
<RemyxEditor
|
|
979
|
+
plugins={[TablePlugin()]}
|
|
980
|
+
engineOptions={{
|
|
981
|
+
tableSortComparator: (a, b, dataType, columnIndex) => {
|
|
982
|
+
// Custom comparison logic — return negative, zero, or positive
|
|
983
|
+
return a.localeCompare(b);
|
|
984
|
+
},
|
|
985
|
+
}}
|
|
986
|
+
/>
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**Programmatic commands (via engine ref):**
|
|
990
|
+
|
|
991
|
+
```jsx
|
|
992
|
+
import { useRef } from 'react';
|
|
993
|
+
|
|
994
|
+
function MyEditor() {
|
|
995
|
+
const engineRef = useRef(null);
|
|
996
|
+
|
|
997
|
+
const handleSort = () => {
|
|
998
|
+
engineRef.current?.executeCommand('sortTable', {
|
|
999
|
+
keys: [
|
|
1000
|
+
{ columnIndex: 0, direction: 'asc' },
|
|
1001
|
+
{ columnIndex: 2, direction: 'desc', dataType: 'numeric' },
|
|
1002
|
+
],
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
const handleFilter = () => {
|
|
1007
|
+
engineRef.current?.executeCommand('filterTable', {
|
|
1008
|
+
columnIndex: 1,
|
|
1009
|
+
filterValue: 'active',
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
return (
|
|
1014
|
+
<>
|
|
1015
|
+
<button onClick={handleSort}>Sort by Name then Score</button>
|
|
1016
|
+
<button onClick={handleFilter}>Show active only</button>
|
|
1017
|
+
<RemyxEditor
|
|
1018
|
+
plugins={[TablePlugin()]}
|
|
1019
|
+
onReady={(engine) => { engineRef.current = engine; }}
|
|
1020
|
+
/>
|
|
1021
|
+
</>
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
**Context menu additions** (appear when right-clicking inside a table):
|
|
1027
|
+
- Toggle Header Row
|
|
1028
|
+
- Format as Number / Currency / Percentage / Date
|
|
1029
|
+
- Clear Filters
|
|
1030
|
+
|
|
1031
|
+
### CalloutPlugin
|
|
1032
|
+
|
|
1033
|
+
The `CalloutPlugin` adds styled callout/alert/admonition blocks with collapsible toggle and GFM alert auto-conversion:
|
|
1034
|
+
|
|
1035
|
+
```jsx
|
|
1036
|
+
import { CalloutPlugin } from '@remyxjs/react';
|
|
1037
|
+
|
|
1038
|
+
<RemyxEditor plugins={[CalloutPlugin()]} />
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
**Insert callouts programmatically:**
|
|
1042
|
+
|
|
1043
|
+
```jsx
|
|
1044
|
+
// Basic callout
|
|
1045
|
+
engine.executeCommand('insertCallout', { type: 'warning' });
|
|
1046
|
+
|
|
1047
|
+
// Collapsible with custom title
|
|
1048
|
+
engine.executeCommand('insertCallout', { type: 'tip', collapsible: true, title: 'Pro tip' });
|
|
1049
|
+
|
|
1050
|
+
// With pre-populated content
|
|
1051
|
+
engine.executeCommand('insertCallout', {
|
|
1052
|
+
type: 'info',
|
|
1053
|
+
content: '<ul><li>Step 1</li><li>Step 2</li></ul>',
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// Change type of the focused callout
|
|
1057
|
+
engine.executeCommand('changeCalloutType', 'error');
|
|
1058
|
+
|
|
1059
|
+
// Register a custom callout type
|
|
1060
|
+
import { registerCalloutType } from '@remyxjs/react';
|
|
1061
|
+
registerCalloutType({ type: 'security', label: 'Security', icon: '🔒', color: '#dc2626' });
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
**GitHub-flavored alerts** — blockquotes with `> [!NOTE]`, `> [!WARNING]`, etc. are automatically converted to callout blocks on paste or typing.
|
|
1065
|
+
|
|
1066
|
+
**Available types:** `info`, `warning`, `error`, `success`, `tip`, `note`, `question` (plus any custom types).
|
|
1067
|
+
|
|
1068
|
+
### CommentsPlugin
|
|
1069
|
+
|
|
1070
|
+
The `CommentsPlugin` adds inline comment threads — highlight text, attach discussion threads, resolve/reopen, reply, and @mention other users.
|
|
1071
|
+
|
|
1072
|
+
```jsx
|
|
1073
|
+
import { CommentsPlugin } from '@remyxjs/react';
|
|
1074
|
+
|
|
1075
|
+
<RemyxEditor
|
|
1076
|
+
plugins={[CommentsPlugin({
|
|
1077
|
+
onComment: (thread) => saveToServer(thread),
|
|
1078
|
+
onResolve: ({ thread, resolved }) => updateServer(thread),
|
|
1079
|
+
onDelete: (thread) => deleteFromServer(thread),
|
|
1080
|
+
onReply: ({ thread, reply }) => notifyUser(reply),
|
|
1081
|
+
mentionUsers: ['alice', 'bob', 'charlie'],
|
|
1082
|
+
})]}
|
|
1083
|
+
/>
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
**Comment-only mode** — make the editor read-only but still allow annotations:
|
|
1087
|
+
|
|
1088
|
+
```jsx
|
|
1089
|
+
<RemyxEditor
|
|
1090
|
+
plugins={[CommentsPlugin({ commentOnly: true })]}
|
|
1091
|
+
/>
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
#### useComments hook
|
|
1095
|
+
|
|
1096
|
+
The `useComments` hook provides reactive state and convenience methods:
|
|
1097
|
+
|
|
1098
|
+
```jsx
|
|
1099
|
+
import { useComments, CommentsPanel } from '@remyxjs/react';
|
|
1100
|
+
|
|
1101
|
+
function AnnotatedEditor() {
|
|
1102
|
+
const engineRef = useRef(null);
|
|
1103
|
+
const {
|
|
1104
|
+
threads, activeThread, addComment, deleteComment,
|
|
1105
|
+
resolveComment, replyToComment, navigateToComment,
|
|
1106
|
+
exportThreads, importThreads,
|
|
1107
|
+
} = useComments(engineRef.current);
|
|
1108
|
+
|
|
1109
|
+
return (
|
|
1110
|
+
<div style={{ display: 'flex' }}>
|
|
1111
|
+
<RemyxEditor
|
|
1112
|
+
plugins={[CommentsPlugin({ mentionUsers: ['alice', 'bob'] })]}
|
|
1113
|
+
onReady={(engine) => { engineRef.current = engine; }}
|
|
1114
|
+
style={{ flex: 1 }}
|
|
1115
|
+
/>
|
|
1116
|
+
<CommentsPanel
|
|
1117
|
+
threads={threads}
|
|
1118
|
+
activeThread={activeThread}
|
|
1119
|
+
onNavigate={navigateToComment}
|
|
1120
|
+
onResolve={(id, resolved) => resolveComment(id, resolved)}
|
|
1121
|
+
onDelete={deleteComment}
|
|
1122
|
+
onReply={(id, params) => replyToComment(id, params)}
|
|
1123
|
+
filter="all" {/* 'all' | 'open' | 'resolved' */}
|
|
1124
|
+
/>
|
|
1125
|
+
</div>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
#### CommentsPanel props
|
|
1131
|
+
|
|
1132
|
+
| Prop | Type | Description |
|
|
1133
|
+
| --- | --- | --- |
|
|
1134
|
+
| `threads` | `CommentThread[]` | All threads (from `useComments`) |
|
|
1135
|
+
| `activeThread` | `CommentThread \| null` | Currently focused thread |
|
|
1136
|
+
| `onNavigate` | `(threadId) => void` | Called when a thread card is clicked |
|
|
1137
|
+
| `onResolve` | `(threadId, resolved) => void` | Called when Resolve/Reopen is clicked |
|
|
1138
|
+
| `onDelete` | `(threadId) => void` | Called when Delete is clicked |
|
|
1139
|
+
| `onReply` | `(threadId, { author, body }) => void` | Called when a reply is submitted |
|
|
1140
|
+
| `filter` | `'all' \| 'open' \| 'resolved'` | Which threads to display |
|
|
1141
|
+
| `className` | `string` | Additional CSS class |
|
|
1142
|
+
|
|
1143
|
+
#### Engine events
|
|
1144
|
+
|
|
1145
|
+
| Event | Payload | When |
|
|
1146
|
+
| --- | --- | --- |
|
|
1147
|
+
| `comment:created` | `{ thread }` | New comment added |
|
|
1148
|
+
| `comment:resolved` | `{ thread, resolved }` | Thread resolved or reopened |
|
|
1149
|
+
| `comment:deleted` | `{ thread }` | Thread deleted |
|
|
1150
|
+
| `comment:replied` | `{ thread, reply }` | Reply added to thread |
|
|
1151
|
+
| `comment:updated` | `{ thread }` | Thread body edited |
|
|
1152
|
+
| `comment:clicked` | `{ thread, element }` | Comment highlight clicked |
|
|
1153
|
+
| `comment:navigated` | `{ threadId }` | Scrolled to comment |
|
|
1154
|
+
| `comment:imported` | `{ count }` | Threads imported |
|
|
1155
|
+
|
|
1156
|
+
### Custom Plugins
|
|
1157
|
+
|
|
1158
|
+
```js
|
|
1159
|
+
import { createPlugin } from '@remyxjs/react';
|
|
1160
|
+
|
|
1161
|
+
const MyPlugin = createPlugin({
|
|
1162
|
+
name: 'my-plugin',
|
|
1163
|
+
version: '1.0.0',
|
|
1164
|
+
description: 'Example plugin with lifecycle hooks',
|
|
1165
|
+
author: 'You',
|
|
1166
|
+
|
|
1167
|
+
// Lifecycle hooks (auto-wired, sandboxed)
|
|
1168
|
+
onContentChange(api) {
|
|
1169
|
+
console.log('Content changed:', api.getText().length, 'chars');
|
|
1170
|
+
},
|
|
1171
|
+
onSelectionChange(api) {
|
|
1172
|
+
console.log('Selection:', api.getActiveFormats());
|
|
1173
|
+
},
|
|
1174
|
+
|
|
1175
|
+
// Traditional init/destroy
|
|
1176
|
+
init(api) { /* called once on mount */ },
|
|
1177
|
+
destroy(api) { /* called on unmount */ },
|
|
1178
|
+
|
|
1179
|
+
// Dependencies — ensures 'other-plugin' is initialized first
|
|
1180
|
+
dependencies: ['other-plugin'],
|
|
1181
|
+
|
|
1182
|
+
// Scoped settings with validation
|
|
1183
|
+
settingsSchema: [
|
|
1184
|
+
{ key: 'maxLength', type: 'number', label: 'Max Length', defaultValue: 5000, validate: (v) => v > 0 },
|
|
1185
|
+
{ key: 'mode', type: 'select', label: 'Mode', defaultValue: 'auto', options: [
|
|
1186
|
+
{ label: 'Auto', value: 'auto' }, { label: 'Manual', value: 'manual' },
|
|
1187
|
+
]},
|
|
1188
|
+
],
|
|
1189
|
+
defaultSettings: { maxLength: 5000, mode: 'auto' },
|
|
1190
|
+
});
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
```jsx
|
|
1194
|
+
<RemyxEditor plugins={[MyPlugin]} />
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
Access plugin settings from outside:
|
|
1198
|
+
|
|
1199
|
+
```jsx
|
|
1200
|
+
<RemyxEditor
|
|
1201
|
+
plugins={[MyPlugin]}
|
|
1202
|
+
onReady={(engine) => {
|
|
1203
|
+
engine.plugins.getPluginSetting('my-plugin', 'maxLength'); // 5000
|
|
1204
|
+
engine.plugins.setPluginSetting('my-plugin', 'maxLength', 3000); // validates + updates
|
|
1205
|
+
}}
|
|
1206
|
+
/>
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
### Plugin with Custom Commands
|
|
1210
|
+
|
|
1211
|
+
```js
|
|
1212
|
+
const HighlightPlugin = createPlugin({
|
|
1213
|
+
name: 'highlight',
|
|
1214
|
+
init(engine) {
|
|
1215
|
+
engine.executeCommand('highlight');
|
|
1216
|
+
},
|
|
1217
|
+
commands: [
|
|
1218
|
+
{
|
|
1219
|
+
name: 'highlight',
|
|
1220
|
+
execute(eng) {
|
|
1221
|
+
document.execCommand('backColor', false, '#ffeb3b');
|
|
1222
|
+
},
|
|
1223
|
+
},
|
|
1224
|
+
],
|
|
1225
|
+
});
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
## Read-Only Mode
|
|
1229
|
+
|
|
1230
|
+
Disable editing while keeping the full rendered output visible.
|
|
1231
|
+
|
|
1232
|
+
```jsx
|
|
1233
|
+
<RemyxEditor value={content} readOnly={true} />
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
Combine with a toggle for preview mode:
|
|
1237
|
+
|
|
1238
|
+
```jsx
|
|
1239
|
+
const [editing, setEditing] = useState(true);
|
|
1240
|
+
|
|
1241
|
+
<button onClick={() => setEditing(!editing)}>
|
|
1242
|
+
{editing ? 'Preview' : 'Edit'}
|
|
1243
|
+
</button>
|
|
1244
|
+
<RemyxEditor value={content} onChange={setContent} readOnly={!editing} />
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
## Error Handling
|
|
1248
|
+
|
|
1249
|
+
### onError Callback
|
|
1250
|
+
|
|
1251
|
+
Catch errors from plugins, the engine, and file uploads without crashing the app:
|
|
1252
|
+
|
|
1253
|
+
```jsx
|
|
1254
|
+
<RemyxEditor
|
|
1255
|
+
onError={(error, info) => {
|
|
1256
|
+
if (info.source === 'plugin') {
|
|
1257
|
+
console.warn(`Plugin "${info.pluginName}" failed:`, error);
|
|
1258
|
+
} else if (info.source === 'upload') {
|
|
1259
|
+
alert(`Upload failed: ${error.message}`);
|
|
1260
|
+
} else if (info.source === 'engine') {
|
|
1261
|
+
console.error(`Engine error in ${info.phase}:`, error);
|
|
1262
|
+
}
|
|
1263
|
+
}}
|
|
1264
|
+
/>
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
Error sources:
|
|
1268
|
+
|
|
1269
|
+
| Source | Info Fields | Description |
|
|
1270
|
+
|---|---|---|
|
|
1271
|
+
| `'plugin'` | `pluginName` | Plugin init/destroy failure |
|
|
1272
|
+
| `'engine'` | `phase` | Engine initialization error |
|
|
1273
|
+
| `'upload'` | `file` | Upload handler rejection |
|
|
1274
|
+
|
|
1275
|
+
### Custom Error Fallback
|
|
1276
|
+
|
|
1277
|
+
Replace the default error UI with your own component:
|
|
1278
|
+
|
|
1279
|
+
```jsx
|
|
1280
|
+
<RemyxEditor
|
|
1281
|
+
errorFallback={
|
|
1282
|
+
<div className="editor-error">
|
|
1283
|
+
<p>Something went wrong. Please refresh.</p>
|
|
1284
|
+
</div>
|
|
1285
|
+
}
|
|
1286
|
+
/>
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
## Engine Access
|
|
1290
|
+
|
|
1291
|
+
Use the `onReady` callback to access the `EditorEngine` instance for programmatic control.
|
|
1292
|
+
|
|
1293
|
+
### Execute Commands
|
|
1294
|
+
|
|
1295
|
+
```jsx
|
|
1296
|
+
function App() {
|
|
1297
|
+
const engineRef = useRef(null);
|
|
1298
|
+
|
|
1299
|
+
return (
|
|
1300
|
+
<>
|
|
1301
|
+
<button onClick={() => engineRef.current?.executeCommand('bold')}>
|
|
1302
|
+
Bold
|
|
1303
|
+
</button>
|
|
1304
|
+
<button onClick={() => engineRef.current?.executeCommand('insertLink', 'https://example.com')}>
|
|
1305
|
+
Insert Link
|
|
1306
|
+
</button>
|
|
1307
|
+
<RemyxEditor onReady={(engine) => { engineRef.current = engine }} />
|
|
1308
|
+
</>
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
### Listen to Events
|
|
1314
|
+
|
|
1315
|
+
```jsx
|
|
1316
|
+
<RemyxEditor
|
|
1317
|
+
onReady={(engine) => {
|
|
1318
|
+
engine.on('selection:change', ({ formats }) => {
|
|
1319
|
+
console.log('Active formats:', formats); // { bold: true, italic: false, ... }
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
engine.on('command:executed', ({ name }) => {
|
|
1323
|
+
console.log('Command run:', name);
|
|
1324
|
+
});
|
|
1325
|
+
}}
|
|
1326
|
+
onFocus={() => console.log('Editor focused')}
|
|
1327
|
+
onBlur={() => console.log('Editor blurred')}
|
|
1328
|
+
/>
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
### Read Content Programmatically
|
|
1332
|
+
|
|
1333
|
+
```jsx
|
|
1334
|
+
<RemyxEditor
|
|
1335
|
+
onReady={(engine) => {
|
|
1336
|
+
// HTML content
|
|
1337
|
+
const html = engine.getHTML();
|
|
1338
|
+
|
|
1339
|
+
// Plain text
|
|
1340
|
+
const text = engine.getText();
|
|
1341
|
+
|
|
1342
|
+
// Stats
|
|
1343
|
+
const words = engine.getWordCount();
|
|
1344
|
+
const chars = engine.getCharCount();
|
|
1345
|
+
const empty = engine.isEmpty();
|
|
1346
|
+
}}
|
|
1347
|
+
/>
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
### Engine Methods
|
|
1351
|
+
|
|
1352
|
+
| Method | Returns | Description |
|
|
1353
|
+
|---|---|---|
|
|
1354
|
+
| `getHTML()` | `string` | Sanitized HTML content |
|
|
1355
|
+
| `setHTML(html)` | `void` | Set editor content |
|
|
1356
|
+
| `getText()` | `string` | Plain text content |
|
|
1357
|
+
| `isEmpty()` | `boolean` | True if no meaningful text |
|
|
1358
|
+
| `focus()` | `void` | Focus the editor |
|
|
1359
|
+
| `blur()` | `void` | Blur the editor |
|
|
1360
|
+
| `executeCommand(name, ...args)` | `any` | Execute a registered command |
|
|
1361
|
+
| `on(event, handler)` | `Function` | Subscribe (returns unsubscribe fn) |
|
|
1362
|
+
| `off(event, handler)` | `void` | Unsubscribe from event |
|
|
1363
|
+
| `getWordCount()` | `number` | Word count |
|
|
1364
|
+
| `getCharCount()` | `number` | Character count |
|
|
1365
|
+
| `destroy()` | `void` | Clean up the engine |
|
|
1366
|
+
|
|
1367
|
+
### Engine Events
|
|
1368
|
+
|
|
1369
|
+
| Event | Payload | When |
|
|
1370
|
+
|---|---|---|
|
|
1371
|
+
| `'content:change'` | — | Content was modified |
|
|
1372
|
+
| `'selection:change'` | `{ formats }` | Selection or formatting changed |
|
|
1373
|
+
| `'focus'` | — | Editor gained focus |
|
|
1374
|
+
| `'blur'` | — | Editor lost focus |
|
|
1375
|
+
| `'mode:change'` | — | Switched markdown/source mode |
|
|
1376
|
+
| `'command:executed'` | `{ name }` | After a command runs |
|
|
1377
|
+
| `'plugin:error'` | `{ name, error }` | Plugin failed |
|
|
1378
|
+
| `'editor:error'` | `{ phase, error }` | Engine error |
|
|
1379
|
+
| `'upload:error'` | `{ file, error }` | Upload failed |
|
|
1380
|
+
|
|
1381
|
+
## Import & Export Documents
|
|
1382
|
+
|
|
1383
|
+
### Programmatic Export
|
|
1384
|
+
|
|
1385
|
+
```jsx
|
|
1386
|
+
import { exportAsPDF, exportAsDocx, exportAsMarkdown } from '@remyxjs/react';
|
|
1387
|
+
|
|
1388
|
+
function ExportButtons({ content }) {
|
|
1389
|
+
return (
|
|
1390
|
+
<>
|
|
1391
|
+
<button onClick={() => exportAsPDF(content, 'My Document')}>
|
|
1392
|
+
Export PDF
|
|
1393
|
+
</button>
|
|
1394
|
+
<button onClick={() => exportAsDocx(content, 'document.doc')}>
|
|
1395
|
+
Export Word
|
|
1396
|
+
</button>
|
|
1397
|
+
<button onClick={() => exportAsMarkdown(content, 'document.md')}>
|
|
1398
|
+
Export Markdown
|
|
1399
|
+
</button>
|
|
1400
|
+
</>
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
PDF export opens the browser print dialog. Word and Markdown exports trigger file downloads.
|
|
1406
|
+
|
|
1407
|
+
### Programmatic Import
|
|
1408
|
+
|
|
1409
|
+
```jsx
|
|
1410
|
+
import { convertDocument, isImportableFile } from '@remyxjs/react';
|
|
1411
|
+
|
|
1412
|
+
async function handleFileSelect(file, engine) {
|
|
1413
|
+
if (!isImportableFile(file)) {
|
|
1414
|
+
alert('Unsupported format');
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const html = await convertDocument(file);
|
|
1418
|
+
|
|
1419
|
+
// Insert at cursor
|
|
1420
|
+
engine.executeCommand('importDocument', { html, mode: 'insert' });
|
|
1421
|
+
|
|
1422
|
+
// Or replace entire content
|
|
1423
|
+
engine.executeCommand('importDocument', { html, mode: 'replace' });
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
Supported import formats: PDF, DOCX, Markdown, HTML, TXT, CSV, TSV, RTF.
|
|
1428
|
+
|
|
1429
|
+
### Toolbar Export & Import
|
|
1430
|
+
|
|
1431
|
+
The editor ships with built-in toolbar buttons for both:
|
|
1432
|
+
|
|
1433
|
+
```jsx
|
|
1434
|
+
<RemyxEditor
|
|
1435
|
+
toolbar={[
|
|
1436
|
+
['bold', 'italic', 'underline'],
|
|
1437
|
+
['importDocument', 'export'], // import & export buttons
|
|
1438
|
+
]}
|
|
1439
|
+
/>
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
The `importDocument` button opens a modal for file selection. The `export` button opens a modal with format options.
|
|
1443
|
+
|
|
1444
|
+
## Sanitization
|
|
1445
|
+
|
|
1446
|
+
Control which HTML tags, attributes, and CSS properties are allowed in editor content.
|
|
1447
|
+
|
|
1448
|
+
### Custom Allowlists
|
|
1449
|
+
|
|
1450
|
+
```jsx
|
|
1451
|
+
<RemyxEditor
|
|
1452
|
+
sanitize={{
|
|
1453
|
+
allowedTags: {
|
|
1454
|
+
// Tag name → array of allowed attributes
|
|
1455
|
+
p: ['class', 'style'],
|
|
1456
|
+
strong: [],
|
|
1457
|
+
em: [],
|
|
1458
|
+
a: ['href', 'target', 'rel', 'title'],
|
|
1459
|
+
img: ['src', 'alt', 'width', 'height'],
|
|
1460
|
+
h1: ['class'], h2: ['class'], h3: ['class'],
|
|
1461
|
+
ul: [], ol: [], li: [],
|
|
1462
|
+
blockquote: ['class'],
|
|
1463
|
+
code: ['class'],
|
|
1464
|
+
pre: ['class'],
|
|
1465
|
+
},
|
|
1466
|
+
allowedStyles: [
|
|
1467
|
+
'color', 'background-color', 'font-size', 'text-align',
|
|
1468
|
+
'font-weight', 'font-style',
|
|
1469
|
+
],
|
|
1470
|
+
}}
|
|
1471
|
+
/>
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
### Default Security
|
|
1475
|
+
|
|
1476
|
+
Without custom sanitization, the editor applies these protections by default:
|
|
1477
|
+
|
|
1478
|
+
- Strips `<script>`, `<style>`, `<svg>`, `<math>`, `<form>`, `<object>`, `<embed>` tags entirely
|
|
1479
|
+
- Blocks all `on*` event handler attributes (e.g., `onclick`, `onerror`)
|
|
1480
|
+
- Validates URL protocols — blocks `javascript:`, `vbscript:`, `data:text/html`
|
|
1481
|
+
- Blocks CSS injection (`expression()`, `@import`, `behavior:`)
|
|
1482
|
+
- Restricts `<input>` to `type="checkbox"` only
|
|
1483
|
+
- Sandboxes `<iframe>` elements
|
|
1484
|
+
|
|
1485
|
+
## Keyboard Shortcuts
|
|
1486
|
+
|
|
1487
|
+
`mod` = `Cmd` on Mac, `Ctrl` on Windows/Linux.
|
|
1488
|
+
|
|
1489
|
+
| Shortcut | Action |
|
|
1490
|
+
| --- | --- |
|
|
1491
|
+
| `mod+B` | Bold |
|
|
1492
|
+
| `mod+I` | Italic |
|
|
1493
|
+
| `mod+U` | Underline |
|
|
1494
|
+
| `mod+Shift+X` | Strikethrough |
|
|
1495
|
+
| `mod+Z` | Undo |
|
|
1496
|
+
| `mod+Shift+Z` | Redo |
|
|
1497
|
+
| `mod+K` | Insert link |
|
|
1498
|
+
| `mod+F` | Find & replace |
|
|
1499
|
+
| `mod+Shift+7` | Numbered list |
|
|
1500
|
+
| `mod+Shift+8` | Bulleted list |
|
|
1501
|
+
| `mod+Shift+9` | Blockquote |
|
|
1502
|
+
| `mod+Shift+C` | Code block |
|
|
1503
|
+
| `Tab` | Indent |
|
|
1504
|
+
| `Shift+Tab` | Outdent |
|
|
1505
|
+
| `mod+Shift+F` | Fullscreen |
|
|
1506
|
+
| `mod+Shift+U` | Source mode |
|
|
1507
|
+
| `mod+Shift+P` | Command palette |
|
|
1508
|
+
| `mod+,` | Subscript |
|
|
1509
|
+
| `mod+.` | Superscript |
|
|
1510
|
+
|
|
1511
|
+
### Custom Shortcuts
|
|
1512
|
+
|
|
1513
|
+
Override default shortcuts by passing a `shortcuts` object:
|
|
1514
|
+
|
|
1515
|
+
```jsx
|
|
1516
|
+
<RemyxEditor
|
|
1517
|
+
shortcuts={{
|
|
1518
|
+
bold: 'mod+shift+b', // remap bold
|
|
1519
|
+
insertLink: 'mod+l', // remap link
|
|
1520
|
+
}}
|
|
1521
|
+
/>
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
Register shortcuts programmatically via the engine:
|
|
1525
|
+
|
|
1526
|
+
```jsx
|
|
1527
|
+
<RemyxEditor
|
|
1528
|
+
onReady={(engine) => {
|
|
1529
|
+
engine.keyboard.register('mod+shift+h', 'highlight');
|
|
1530
|
+
|
|
1531
|
+
// Get platform-specific label for display
|
|
1532
|
+
const label = engine.keyboard.getShortcutLabel('mod+b');
|
|
1533
|
+
// → '⌘B' on Mac, 'Ctrl+B' on Windows
|
|
1534
|
+
}}
|
|
1535
|
+
/>
|
|
1536
|
+
```
|
|
1537
|
+
|
|
1538
|
+
## Heading Level Offset
|
|
1539
|
+
|
|
1540
|
+
When embedding the editor in a page that already has an `<h1>`, use `baseHeadingLevel` to offset heading levels so the editor's headings fit the page hierarchy:
|
|
1541
|
+
|
|
1542
|
+
```jsx
|
|
1543
|
+
// Page has its own <h1>, so editor H1 renders as <h2>
|
|
1544
|
+
<h1>My Page Title</h1>
|
|
1545
|
+
<RemyxEditor baseHeadingLevel={2} />
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
| `baseHeadingLevel` | Editor H1 | Editor H2 | Editor H3 |
|
|
1549
|
+
|---|---|---|---|
|
|
1550
|
+
| `1` (default) | `<h1>` | `<h2>` | `<h3>` |
|
|
1551
|
+
| `2` | `<h2>` | `<h3>` | `<h4>` |
|
|
1552
|
+
| `3` | `<h3>` | `<h4>` | `<h5>` |
|
|
1553
|
+
|
|
1554
|
+
All heading levels are clamped to a maximum of `<h6>`.
|
|
1555
|
+
|
|
1556
|
+
## Floating Toolbar & Context Menu
|
|
1557
|
+
|
|
1558
|
+
### Floating Toolbar
|
|
1559
|
+
|
|
1560
|
+
A compact toolbar appears when text is selected, providing quick access to formatting:
|
|
1561
|
+
|
|
1562
|
+
```jsx
|
|
1563
|
+
<RemyxEditor floatingToolbar={true} /> {/* enabled (default) */}
|
|
1564
|
+
<RemyxEditor floatingToolbar={false} /> {/* disabled */}
|
|
1565
|
+
```
|
|
1566
|
+
|
|
1567
|
+
The floating toolbar shows bold, italic, underline, strikethrough, link, highlight, and heading options near the selection.
|
|
1568
|
+
|
|
1569
|
+
### Context Menu
|
|
1570
|
+
|
|
1571
|
+
Right-click on the editor content to access a context menu with relevant actions:
|
|
1572
|
+
|
|
1573
|
+
```jsx
|
|
1574
|
+
<RemyxEditor contextMenu={true} /> {/* enabled (default) */}
|
|
1575
|
+
<RemyxEditor contextMenu={false} /> {/* disabled */}
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
The context menu adapts to the clicked element — links show edit/remove options, images show resize options, tables show row/column operations.
|
|
1579
|
+
|
|
1580
|
+
### Combining with Minimal Toolbar
|
|
1581
|
+
|
|
1582
|
+
For a clean writing experience, hide the main toolbar and rely on the floating toolbar and context menu:
|
|
1583
|
+
|
|
1584
|
+
```jsx
|
|
1585
|
+
<RemyxEditor
|
|
1586
|
+
toolbar={[]}
|
|
1587
|
+
floatingToolbar={true}
|
|
1588
|
+
contextMenu={true}
|
|
1589
|
+
menuBar={false}
|
|
1590
|
+
statusBar={false}
|
|
1591
|
+
placeholder="Just start writing..."
|
|
1592
|
+
height={500}
|
|
1593
|
+
/>
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
## Command Palette
|
|
1597
|
+
|
|
1598
|
+
The command palette provides a searchable overlay listing all available editor commands. Open it via the toolbar button or `Mod+Shift+P` (`Cmd+Shift+P` on Mac, `Ctrl+Shift+P` on Windows/Linux).
|
|
1599
|
+
|
|
1600
|
+
```jsx
|
|
1601
|
+
<RemyxEditor commandPalette={true} /> {/* enabled (default) */}
|
|
1602
|
+
<RemyxEditor commandPalette={false} /> {/* disabled */}
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
Commands are organized by category (Text, Lists, Media, Layout, Advanced, Commands) and support fuzzy search. The palette includes all built-in commands, any registered via the engine's command registry, and custom items added with `registerCommandItems()`.
|
|
1606
|
+
|
|
1607
|
+
**Recently-used commands** are automatically pinned to the top of the palette under a "Recent" category. The last 5 executed commands are tracked in `localStorage`. This works for both the command palette (Mod+Shift+P) and the inline slash command menu (Mod+/).
|
|
1608
|
+
|
|
1609
|
+
**Custom command items** can be registered from anywhere in your application:
|
|
1610
|
+
|
|
1611
|
+
```jsx
|
|
1612
|
+
import { registerCommandItems, unregisterCommandItem } from '@remyxjs/react';
|
|
1613
|
+
|
|
1614
|
+
// Register custom commands that appear in the palette
|
|
1615
|
+
registerCommandItems({
|
|
1616
|
+
id: 'saveToCloud',
|
|
1617
|
+
label: 'Save to Cloud',
|
|
1618
|
+
description: 'Upload current document',
|
|
1619
|
+
icon: '☁️',
|
|
1620
|
+
keywords: ['save', 'cloud', 'upload'],
|
|
1621
|
+
category: 'Custom',
|
|
1622
|
+
action: (engine) => uploadToCloud(engine.getHTML()),
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// Clean up when no longer needed
|
|
1626
|
+
unregisterCommandItem('saveToCloud');
|
|
1627
|
+
```
|
|
1628
|
+
|
|
1629
|
+
To add the command palette button to a custom toolbar:
|
|
1630
|
+
|
|
1631
|
+
```jsx
|
|
1632
|
+
<RemyxEditor
|
|
1633
|
+
toolbar={[
|
|
1634
|
+
['bold', 'italic', 'underline'],
|
|
1635
|
+
['commandPalette'],
|
|
1636
|
+
]}
|
|
1637
|
+
/>
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
## Autosave
|
|
1641
|
+
|
|
1642
|
+
Enable autosave with the `autosave` prop. Pass `true` for localStorage defaults, or an object for full control.
|
|
1643
|
+
|
|
1644
|
+
### Basic (localStorage)
|
|
1645
|
+
|
|
1646
|
+
```jsx
|
|
1647
|
+
<RemyxEditor value={content} onChange={setContent} autosave />
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
This saves to `localStorage` every 30 seconds and 2 seconds after each content change. A save-status indicator appears in the status bar (Saved / Saving... / Unsaved / Save failed), and a recovery banner appears on reload if unsaved content is detected.
|
|
1651
|
+
|
|
1652
|
+
### Cloud Storage (AWS S3 / GCP / custom API)
|
|
1653
|
+
|
|
1654
|
+
```jsx
|
|
1655
|
+
<RemyxEditor
|
|
1656
|
+
value={content}
|
|
1657
|
+
onChange={setContent}
|
|
1658
|
+
autosave={{
|
|
1659
|
+
provider: {
|
|
1660
|
+
endpoint: 'https://api.example.com/autosave',
|
|
1661
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1662
|
+
},
|
|
1663
|
+
key: 'doc-123',
|
|
1664
|
+
interval: 60000, // save every 60s
|
|
1665
|
+
debounce: 3000, // 3s after last edit
|
|
1666
|
+
}}
|
|
1667
|
+
/>
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
For S3 presigned URLs or GCP signed URLs, use `buildUrl`:
|
|
1671
|
+
|
|
1672
|
+
```jsx
|
|
1673
|
+
autosave={{
|
|
1674
|
+
provider: {
|
|
1675
|
+
endpoint: 'https://my-bucket.s3.amazonaws.com',
|
|
1676
|
+
buildUrl: (key) => getPresignedUrl(key),
|
|
1677
|
+
method: 'PUT',
|
|
1678
|
+
},
|
|
1679
|
+
key: `user-${userId}/draft`,
|
|
1680
|
+
}}
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
### Filesystem (Electron / Tauri / Node)
|
|
1684
|
+
|
|
1685
|
+
```jsx
|
|
1686
|
+
autosave={{
|
|
1687
|
+
provider: {
|
|
1688
|
+
writeFn: async (key, data) => window.electron.writeFile(`/saves/${key}.json`, data),
|
|
1689
|
+
readFn: async (key) => window.electron.readFile(`/saves/${key}.json`),
|
|
1690
|
+
deleteFn: async (key) => window.electron.deleteFile(`/saves/${key}.json`),
|
|
1691
|
+
},
|
|
1692
|
+
key: 'my-document',
|
|
1693
|
+
}}
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
### Custom Provider
|
|
1697
|
+
|
|
1698
|
+
```jsx
|
|
1699
|
+
autosave={{
|
|
1700
|
+
provider: {
|
|
1701
|
+
save: async (key, content, metadata) => { /* your save logic */ },
|
|
1702
|
+
load: async (key) => { /* return { content, timestamp } or null */ },
|
|
1703
|
+
clear: async (key) => { /* your delete logic */ },
|
|
1704
|
+
},
|
|
1705
|
+
}}
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
### AutosaveConfig Options
|
|
1709
|
+
|
|
1710
|
+
| Option | Type | Default | Description |
|
|
1711
|
+
| --- | --- | --- | --- |
|
|
1712
|
+
| `enabled` | `boolean` | `true` | Toggle autosave on/off |
|
|
1713
|
+
| `interval` | `number` | `30000` | Periodic save interval in ms |
|
|
1714
|
+
| `debounce` | `number` | `2000` | Debounce delay after content change in ms |
|
|
1715
|
+
| `provider` | `string \| object` | `'localStorage'` | Storage provider config |
|
|
1716
|
+
| `key` | `string` | `'rmx-default'` | Storage key for this editor instance |
|
|
1717
|
+
| `onRecover` | `(data) => void` | — | Callback when recovery data is found |
|
|
1718
|
+
| `showRecoveryBanner` | `boolean` | `true` | Show the recovery banner UI |
|
|
1719
|
+
| `showSaveStatus` | `boolean` | `true` | Show save status in the status bar |
|
|
1720
|
+
|
|
1721
|
+
### useAutosave Hook
|
|
1722
|
+
|
|
1723
|
+
For custom UIs, use the `useAutosave` hook directly:
|
|
1724
|
+
|
|
1725
|
+
```jsx
|
|
1726
|
+
import { useAutosave } from '@remyxjs/react';
|
|
1727
|
+
|
|
1728
|
+
function MyEditor({ engine }) {
|
|
1729
|
+
const { saveStatus, lastSaved, recoveryData, recoverContent, dismissRecovery } =
|
|
1730
|
+
useAutosave(engine, { enabled: true, key: 'doc-123' });
|
|
1731
|
+
|
|
1732
|
+
return (
|
|
1733
|
+
<div>
|
|
1734
|
+
<span>Status: {saveStatus}</span>
|
|
1735
|
+
{recoveryData && (
|
|
1736
|
+
<div>
|
|
1737
|
+
<button onClick={recoverContent}>Restore</button>
|
|
1738
|
+
<button onClick={dismissRecovery}>Dismiss</button>
|
|
1739
|
+
</div>
|
|
1740
|
+
)}
|
|
1741
|
+
</div>
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
```
|
|
1745
|
+
|
|
1746
|
+
## UX/UI Features
|
|
1747
|
+
|
|
1748
|
+
### Empty State
|
|
1749
|
+
|
|
1750
|
+
Show a configurable empty state when the editor has no content:
|
|
1751
|
+
```jsx
|
|
1752
|
+
// Default illustration + "Start typing..." message
|
|
1753
|
+
<RemyxEditor emptyState />
|
|
1754
|
+
|
|
1755
|
+
// Custom empty state component
|
|
1756
|
+
<RemyxEditor emptyState={<div>Drop content here or start typing...</div>} />
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
### Breadcrumb Bar
|
|
1760
|
+
|
|
1761
|
+
Display the DOM path to the current cursor position:
|
|
1762
|
+
```jsx
|
|
1763
|
+
<RemyxEditor breadcrumb />
|
|
1764
|
+
```
|
|
1765
|
+
Shows a bar like "Blockquote > List > Item 2" or "Table > Row 3 > Cell 1" that updates on every selection change.
|
|
1766
|
+
|
|
1767
|
+
### Minimap
|
|
1768
|
+
|
|
1769
|
+
Show a scaled-down document preview on the right edge:
|
|
1770
|
+
```jsx
|
|
1771
|
+
<RemyxEditor minimap />
|
|
1772
|
+
```
|
|
1773
|
+
Click anywhere on the minimap to scroll to that position. Updates automatically on content changes.
|
|
1774
|
+
|
|
1775
|
+
### Split View
|
|
1776
|
+
|
|
1777
|
+
Side-by-side edit + preview pane:
|
|
1778
|
+
```jsx
|
|
1779
|
+
<RemyxEditor splitViewFormat="html" /> // HTML preview
|
|
1780
|
+
<RemyxEditor splitViewFormat="markdown" /> // Markdown preview
|
|
1781
|
+
```
|
|
1782
|
+
Toggle via `Mod+Shift+V` or the toolbar button. The preview updates in real time.
|
|
1783
|
+
|
|
1784
|
+
### Distraction-Free Mode
|
|
1785
|
+
|
|
1786
|
+
Hide all editor chrome (toolbar, menu bar, status bar) for focused writing:
|
|
1787
|
+
```jsx
|
|
1788
|
+
// Trigger via command
|
|
1789
|
+
engine.executeCommand('distractionFree');
|
|
1790
|
+
```
|
|
1791
|
+
Shortcut: `Mod+Shift+D`. Chrome reappears on mouse movement and auto-hides after 3 seconds.
|
|
1792
|
+
|
|
1793
|
+
### Toolbar Customization
|
|
1794
|
+
|
|
1795
|
+
Let users drag-and-drop toolbar buttons to rearrange them:
|
|
1796
|
+
```jsx
|
|
1797
|
+
<RemyxEditor
|
|
1798
|
+
customizableToolbar
|
|
1799
|
+
onToolbarChange={(newOrder) => console.log('Toolbar reordered:', newOrder)}
|
|
1800
|
+
/>
|
|
1801
|
+
```
|
|
1802
|
+
The custom order is persisted in `localStorage` and restored on next load.
|
|
1803
|
+
|
|
1804
|
+
### Typography Controls
|
|
1805
|
+
|
|
1806
|
+
Fine-tune line height, letter spacing, and paragraph spacing via the `typography` toolbar dropdown or programmatically:
|
|
1807
|
+
```jsx
|
|
1808
|
+
// Add 'typography' to your toolbar config
|
|
1809
|
+
<RemyxEditor toolbar={[['bold', 'italic'], ['typography']]} />
|
|
1810
|
+
```
|
|
1811
|
+
|
|
1812
|
+
### Color Palette Presets
|
|
1813
|
+
|
|
1814
|
+
Save and reuse custom color palettes. A "Save Preset" button appears in the color picker dropdown. Presets are stored in `localStorage`.
|
|
1815
|
+
|
|
1816
|
+
## Collaboration
|
|
1817
|
+
|
|
1818
|
+
### useSpellcheck Hook
|
|
1819
|
+
|
|
1820
|
+
For reactive spellcheck/grammar state, use the `useSpellcheck` hook:
|
|
1821
|
+
|
|
1822
|
+
```jsx
|
|
1823
|
+
import { useSpellcheck, SpellcheckPlugin } from '@remyxjs/react';
|
|
1824
|
+
|
|
1825
|
+
function MyEditor() {
|
|
1826
|
+
const engineRef = useRef(null);
|
|
1827
|
+
const {
|
|
1828
|
+
errors, // Array of current spellcheck/grammar errors
|
|
1829
|
+
stats, // { total, grammar, style, byRule }
|
|
1830
|
+
enabled, // boolean — whether spellcheck is active
|
|
1831
|
+
stylePreset, // current writing style preset
|
|
1832
|
+
language, // current BCP 47 language tag
|
|
1833
|
+
toggleSpellcheck, // () => boolean — toggle on/off
|
|
1834
|
+
checkGrammar, // () => Promise<Array> — run grammar check
|
|
1835
|
+
addToDictionary, // (word) => void
|
|
1836
|
+
ignoreWord, // (word) => void
|
|
1837
|
+
setWritingStyle, // ('formal'|'casual'|'technical'|'academic') => void
|
|
1838
|
+
setLanguage, // (lang) => void
|
|
1839
|
+
getDictionary, // () => string[]
|
|
1840
|
+
} = useSpellcheck(engineRef.current);
|
|
1841
|
+
|
|
1842
|
+
return (
|
|
1843
|
+
<div>
|
|
1844
|
+
<span>{stats.total} issues found</span>
|
|
1845
|
+
<button onClick={toggleSpellcheck}>{enabled ? 'Disable' : 'Enable'}</button>
|
|
1846
|
+
<RemyxEditor
|
|
1847
|
+
plugins={[SpellcheckPlugin({ stylePreset: 'formal' })]}
|
|
1848
|
+
onReady={(engine) => { engineRef.current = engine }}
|
|
1849
|
+
/>
|
|
1850
|
+
</div>
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
### useCollaboration Hook
|
|
1856
|
+
|
|
1857
|
+
For real-time collaborative editing, use the `useCollaboration` hook:
|
|
1858
|
+
|
|
1859
|
+
```jsx
|
|
1860
|
+
import { useCollaboration, CollaborationBar, CollaborationPlugin } from '@remyxjs/react';
|
|
1861
|
+
|
|
1862
|
+
function CollaborativeEditor() {
|
|
1863
|
+
const engineRef = useRef(null);
|
|
1864
|
+
const { status, peers, connect, disconnect } = useCollaboration(engineRef.current);
|
|
1865
|
+
|
|
1866
|
+
return (
|
|
1867
|
+
<div>
|
|
1868
|
+
<CollaborationBar status={status} peers={peers} />
|
|
1869
|
+
<button onClick={() => connect({ serverUrl: 'wss://signal.example.com', roomId: 'room-1', userName: 'Alice' })}>
|
|
1870
|
+
Join
|
|
1871
|
+
</button>
|
|
1872
|
+
<button onClick={disconnect}>Leave</button>
|
|
1873
|
+
<RemyxEditor
|
|
1874
|
+
plugins={[CollaborationPlugin({ autoConnect: false })]}
|
|
1875
|
+
onReady={(engine) => { engineRef.current = engine; }}
|
|
1876
|
+
/>
|
|
1877
|
+
</div>
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
#### useCollaboration return value
|
|
1883
|
+
|
|
1884
|
+
| Property | Type | Description |
|
|
1885
|
+
| --- | --- | --- |
|
|
1886
|
+
| `status` | `'connected' \| 'disconnected' \| 'connecting' \| 'error'` | Current connection state |
|
|
1887
|
+
| `peers` | `Array<{ id, name, color, cursor, isActive }>` | List of connected peers |
|
|
1888
|
+
| `connect` | `(opts: { serverUrl, roomId, userName }) => void` | Connect to a collaboration room |
|
|
1889
|
+
| `disconnect` | `() => void` | Disconnect from the room |
|
|
1890
|
+
|
|
1891
|
+
#### CollaborationBar props
|
|
1892
|
+
|
|
1893
|
+
| Prop | Type | Description |
|
|
1894
|
+
| --- | --- | --- |
|
|
1895
|
+
| `status` | `string` | Connection status (from `useCollaboration`) |
|
|
1896
|
+
| `peers` | `Array` | Peer list (from `useCollaboration`) |
|
|
1897
|
+
| `className` | `string` | Additional CSS class |
|
|
1898
|
+
| `showAvatars` | `boolean` | Show peer avatar circles (default: `true`) |
|
|
1899
|
+
|
|
1900
|
+
## Form Integration
|
|
1901
|
+
|
|
1902
|
+
### Next.js / Remix Form
|
|
1903
|
+
|
|
1904
|
+
```jsx
|
|
1905
|
+
function ArticleForm() {
|
|
1906
|
+
const [content, setContent] = useState('');
|
|
1907
|
+
|
|
1908
|
+
return (
|
|
1909
|
+
<form method="post" action="/api/articles">
|
|
1910
|
+
<input type="text" name="title" placeholder="Title" />
|
|
1911
|
+
<RemyxEditor value={content} onChange={setContent} height={400} />
|
|
1912
|
+
<input type="hidden" name="body" value={content} />
|
|
1913
|
+
<button type="submit">Publish</button>
|
|
1914
|
+
</form>
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
```
|
|
1918
|
+
|
|
1919
|
+
### Textarea Sync
|
|
1920
|
+
|
|
1921
|
+
Attach to a `<textarea>` for native form submission — the textarea value stays in sync automatically:
|
|
1922
|
+
|
|
1923
|
+
```jsx
|
|
1924
|
+
function CommentForm() {
|
|
1925
|
+
const textareaRef = useRef(null);
|
|
1926
|
+
|
|
1927
|
+
return (
|
|
1928
|
+
<form onSubmit={handleSubmit}>
|
|
1929
|
+
<textarea ref={textareaRef} name="comment" hidden />
|
|
1930
|
+
<RemyxEditor
|
|
1931
|
+
attachTo={textareaRef}
|
|
1932
|
+
toolbar={[['bold', 'italic', 'link']]}
|
|
1933
|
+
statusBar={false}
|
|
1934
|
+
height={150}
|
|
1935
|
+
placeholder="Write a comment..."
|
|
1936
|
+
/>
|
|
1937
|
+
<button type="submit">Post</button>
|
|
1938
|
+
</form>
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
```
|
|
1942
|
+
|
|
1943
|
+
### Controlled with Validation
|
|
1944
|
+
|
|
1945
|
+
```jsx
|
|
1946
|
+
function ValidatedEditor() {
|
|
1947
|
+
const [content, setContent] = useState('');
|
|
1948
|
+
const [error, setError] = useState('');
|
|
1949
|
+
const engineRef = useRef(null);
|
|
1950
|
+
|
|
1951
|
+
const validate = () => {
|
|
1952
|
+
const words = engineRef.current?.getWordCount() || 0;
|
|
1953
|
+
if (words < 10) {
|
|
1954
|
+
setError('Please write at least 10 words');
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
if (words > 5000) {
|
|
1958
|
+
setError('Maximum 5000 words allowed');
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
setError('');
|
|
1962
|
+
return true;
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
return (
|
|
1966
|
+
<div>
|
|
1967
|
+
<RemyxEditor
|
|
1968
|
+
value={content}
|
|
1969
|
+
onChange={(html) => { setContent(html); setError(''); }}
|
|
1970
|
+
onReady={(engine) => { engineRef.current = engine }}
|
|
1971
|
+
onBlur={validate}
|
|
1972
|
+
/>
|
|
1973
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
1974
|
+
<button onClick={() => validate() && submitForm(content)}>
|
|
1975
|
+
Submit
|
|
1976
|
+
</button>
|
|
1977
|
+
</div>
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
## Exports
|
|
1983
|
+
|
|
1984
|
+
`@remyxjs/react` re-exports everything from `@remyxjs/core` for convenience. You only need one import source:
|
|
1985
|
+
|
|
1986
|
+
```js
|
|
1987
|
+
// React components & hooks
|
|
1988
|
+
import { RemyxEditor, useRemyxEditor, useEditorEngine } from '@remyxjs/react';
|
|
1989
|
+
import { RemyxConfigProvider, useRemyxConfig } from '@remyxjs/react';
|
|
1990
|
+
|
|
1991
|
+
// External config
|
|
1992
|
+
import { RemyxEditorFromConfig, useExternalConfig } from '@remyxjs/react';
|
|
1993
|
+
import { loadConfig } from '@remyxjs/react'; // re-exported from core
|
|
1994
|
+
|
|
1995
|
+
// Toolbar
|
|
1996
|
+
import { TOOLBAR_PRESETS, DEFAULT_TOOLBAR, removeToolbarItems, addToolbarItems, createToolbar } from '@remyxjs/react';
|
|
1997
|
+
|
|
1998
|
+
// Defaults
|
|
1999
|
+
import { DEFAULT_FONTS, DEFAULT_FONT_SIZES, DEFAULT_COLORS, DEFAULT_KEYBINDINGS, DEFAULT_MENU_BAR } from '@remyxjs/react';
|
|
2000
|
+
|
|
2001
|
+
// Fonts
|
|
2002
|
+
import { removeFonts, addFonts, loadGoogleFonts } from '@remyxjs/react';
|
|
2003
|
+
|
|
2004
|
+
// Theming
|
|
2005
|
+
import { createTheme, THEME_VARIABLES, THEME_PRESETS } from '@remyxjs/react';
|
|
2006
|
+
import { createToolbarItemTheme, resolveToolbarItemStyle, TOOLBAR_ITEM_STYLE_KEYS } from '@remyxjs/react';
|
|
2007
|
+
|
|
2008
|
+
// Markdown & paste
|
|
2009
|
+
import { htmlToMarkdown, markdownToHtml, cleanPastedHTML, looksLikeMarkdown } from '@remyxjs/react';
|
|
2010
|
+
|
|
2011
|
+
// Document conversion
|
|
2012
|
+
import { convertDocument, isImportableFile, getSupportedExtensions, getSupportedFormatNames } from '@remyxjs/react';
|
|
2013
|
+
|
|
2014
|
+
// Export
|
|
2015
|
+
import { exportAsPDF, exportAsDocx, exportAsMarkdown } from '@remyxjs/react';
|
|
2016
|
+
|
|
2017
|
+
// Plugins
|
|
2018
|
+
import { createPlugin, WordCountPlugin, AutolinkPlugin, PlaceholderPlugin, SyntaxHighlightPlugin } from '@remyxjs/react';
|
|
2019
|
+
|
|
2020
|
+
// Syntax highlighting utilities
|
|
2021
|
+
import { SUPPORTED_LANGUAGES, LANGUAGE_MAP, detectLanguage, tokenize } from '@remyxjs/react';
|
|
2022
|
+
|
|
2023
|
+
// Command palette
|
|
2024
|
+
import { SLASH_COMMAND_ITEMS, filterSlashItems } from '@remyxjs/react';
|
|
2025
|
+
|
|
2026
|
+
// Advanced Link Management
|
|
2027
|
+
import { LinkPlugin, detectLinks, slugify } from '@remyxjs/react';
|
|
2028
|
+
|
|
2029
|
+
// Templates, Keyboard, Drag & Drop
|
|
2030
|
+
import { TemplatePlugin, renderTemplate, getTemplateLibrary } from '@remyxjs/react';
|
|
2031
|
+
import { KeyboardPlugin, getHeadings } from '@remyxjs/react';
|
|
2032
|
+
import { DragDropPlugin } from '@remyxjs/react';
|
|
2033
|
+
|
|
2034
|
+
// Math & Equations
|
|
2035
|
+
import { MathPlugin, getSymbolPalette, parseMathExpressions, latexToMathML } from '@remyxjs/react';
|
|
2036
|
+
|
|
2037
|
+
// Table of Contents
|
|
2038
|
+
import { TocPlugin, buildOutline, renderTocHTML, validateHeadingHierarchy } from '@remyxjs/react';
|
|
2039
|
+
|
|
2040
|
+
// Content Analytics
|
|
2041
|
+
import { AnalyticsPlugin, analyzeContent, keywordDensity, seoAnalysis } from '@remyxjs/react';
|
|
2042
|
+
|
|
2043
|
+
// Callouts & Alerts
|
|
2044
|
+
import { CalloutPlugin, registerCalloutType, getCalloutTypes, parseGFMAlert } from '@remyxjs/react';
|
|
2045
|
+
|
|
2046
|
+
// Comments & Annotations
|
|
2047
|
+
import { CommentsPlugin, parseMentions } from '@remyxjs/react'; // core exports
|
|
2048
|
+
import { useComments, CommentsPanel } from '@remyxjs/react'; // React-specific
|
|
2049
|
+
|
|
2050
|
+
// Spelling & Grammar
|
|
2051
|
+
import { SpellcheckPlugin, analyzeGrammar, STYLE_PRESETS } from '@remyxjs/react'; // core exports
|
|
2052
|
+
import { useSpellcheck } from '@remyxjs/react'; // React-specific
|
|
2053
|
+
|
|
2054
|
+
// Collaboration
|
|
2055
|
+
import { CollaborationPlugin } from '@remyxjs/react'; // core export
|
|
2056
|
+
import { useCollaboration, CollaborationBar } from '@remyxjs/react'; // React-specific
|
|
2057
|
+
|
|
2058
|
+
// Config
|
|
2059
|
+
import { defineConfig } from '@remyxjs/react';
|
|
2060
|
+
|
|
2061
|
+
// Core engine (advanced)
|
|
2062
|
+
import { EditorEngine, EventBus, CommandRegistry, Selection, History } from '@remyxjs/react';
|
|
2063
|
+
```
|
|
2064
|
+
|
|
2065
|
+
## TypeScript
|
|
2066
|
+
|
|
2067
|
+
This package ships with TypeScript declarations. Key types:
|
|
2068
|
+
|
|
2069
|
+
```ts
|
|
2070
|
+
import type {
|
|
2071
|
+
RemyxEditorProps,
|
|
2072
|
+
MenuBarConfig,
|
|
2073
|
+
Plugin,
|
|
2074
|
+
UseRemyxEditorReturn,
|
|
2075
|
+
UseEditorEngineReturn,
|
|
2076
|
+
} from '@remyxjs/react';
|
|
2077
|
+
```
|
|
2078
|
+
|
|
2079
|
+
### RemyxEditorProps
|
|
2080
|
+
|
|
2081
|
+
Full type definition for all `<RemyxEditor>` props including `value`, `onChange`, `toolbar`, `menuBar`, `theme`, `plugins`, `uploadHandler`, and more.
|
|
2082
|
+
|
|
2083
|
+
### EditorEngine
|
|
2084
|
+
|
|
2085
|
+
The core engine type with methods like `getHTML()`, `setHTML()`, `executeCommand()`, `on()`, `off()`, and `destroy()`.
|
|
2086
|
+
|
|
2087
|
+
## Using `@remyxjs/core` Directly
|
|
2088
|
+
|
|
2089
|
+
For non-React integrations (Vue, Svelte, Angular, vanilla JS, or Node.js), use the core package directly:
|
|
2090
|
+
|
|
2091
|
+
```bash
|
|
2092
|
+
npm install @remyxjs/core
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
```js
|
|
2096
|
+
import { EditorEngine, EventBus } from '@remyxjs/core';
|
|
2097
|
+
import '@remyxjs/core/style.css';
|
|
2098
|
+
|
|
2099
|
+
// Create an editor on any DOM element
|
|
2100
|
+
const element = document.querySelector('#editor');
|
|
2101
|
+
const engine = new EditorEngine(element, { outputFormat: 'html' });
|
|
2102
|
+
|
|
2103
|
+
// Register commands
|
|
2104
|
+
import { registerFormattingCommands, registerListCommands } from '@remyxjs/core';
|
|
2105
|
+
registerFormattingCommands(engine);
|
|
2106
|
+
registerListCommands(engine);
|
|
2107
|
+
|
|
2108
|
+
// Initialize
|
|
2109
|
+
engine.init();
|
|
2110
|
+
|
|
2111
|
+
// Listen for changes
|
|
2112
|
+
engine.on('content:change', () => {
|
|
2113
|
+
console.log(engine.getHTML());
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
// Cleanup
|
|
2117
|
+
engine.destroy();
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
`@remyxjs/core` exports 90+ named symbols — the full engine, all 17 command register functions, plugin system (including syntax highlighting), autosave providers, utilities (markdown, paste cleaning, export, fonts, themes, toolbar config), constants, and `defineConfig()`. See the [core package README](../remyx-core/README.md) for the full API.
|
|
2121
|
+
|
|
2122
|
+
## Architecture
|
|
2123
|
+
|
|
2124
|
+
```
|
|
2125
|
+
@remyxjs/react
|
|
2126
|
+
components/
|
|
2127
|
+
RemyxEditor.jsx Main editor component
|
|
2128
|
+
Toolbar/ Toolbar buttons, dropdowns, color pickers
|
|
2129
|
+
MenuBar/ Application-style menu system
|
|
2130
|
+
Modals/ Link, image, table, export, import modals
|
|
2131
|
+
EditArea/ Content area, floating toolbar, image resize
|
|
2132
|
+
StatusBar/ Word/character count
|
|
2133
|
+
ContextMenu/ Right-click menu
|
|
2134
|
+
hooks/
|
|
2135
|
+
useEditorEngine.js Low-level engine lifecycle hook
|
|
2136
|
+
useRemyxEditor.js High-level editor setup hook
|
|
2137
|
+
useSelection.js Selection state tracking
|
|
2138
|
+
useModal.js Modal open/close state
|
|
2139
|
+
useContextMenu.js Context menu positioning
|
|
2140
|
+
config/
|
|
2141
|
+
RemyxConfigProvider.jsx React context for shared config
|
|
2142
|
+
icons/
|
|
2143
|
+
index.jsx SVG icon components
|
|
2144
|
+
types/
|
|
2145
|
+
index.d.ts TypeScript declarations
|
|
2146
|
+
```
|
|
2147
|
+
|
|
2148
|
+
## Peer Dependencies
|
|
2149
|
+
|
|
2150
|
+
| Package | Version |
|
|
2151
|
+
| --- | --- |
|
|
2152
|
+
| `@remyxjs/core` | >= 0.34.0 |
|
|
2153
|
+
| `react` | >= 18.0.0 |
|
|
2154
|
+
| `react-dom` | >= 18.0.0 |
|
|
2155
|
+
|
|
2156
|
+
## License
|
|
2157
|
+
|
|
2158
|
+
MIT
|