react-next-editor-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +877 -0
- package/dist/chunk-3QWXTDLY.cjs +486 -0
- package/dist/chunk-3QWXTDLY.cjs.map +1 -0
- package/dist/chunk-5F6SPYCN.cjs +180 -0
- package/dist/chunk-5F6SPYCN.cjs.map +1 -0
- package/dist/chunk-6NTSXJX4.js +174 -0
- package/dist/chunk-6NTSXJX4.js.map +1 -0
- package/dist/chunk-7VYJDBH7.js +261 -0
- package/dist/chunk-7VYJDBH7.js.map +1 -0
- package/dist/chunk-DBSFCCBG.cjs +1712 -0
- package/dist/chunk-DBSFCCBG.cjs.map +1 -0
- package/dist/chunk-EFE6RHDL.cjs +4 -0
- package/dist/chunk-EFE6RHDL.cjs.map +1 -0
- package/dist/chunk-G6YRIEK4.js +3 -0
- package/dist/chunk-G6YRIEK4.js.map +1 -0
- package/dist/chunk-GFNFJ3FL.cjs +119 -0
- package/dist/chunk-GFNFJ3FL.cjs.map +1 -0
- package/dist/chunk-IG2YLUFW.js +114 -0
- package/dist/chunk-IG2YLUFW.js.map +1 -0
- package/dist/chunk-JQXTWLHL.js +176 -0
- package/dist/chunk-JQXTWLHL.js.map +1 -0
- package/dist/chunk-NJCEHQV3.cjs +454 -0
- package/dist/chunk-NJCEHQV3.cjs.map +1 -0
- package/dist/chunk-O4GTLC3T.js +478 -0
- package/dist/chunk-O4GTLC3T.js.map +1 -0
- package/dist/chunk-ODHABIIC.cjs +82 -0
- package/dist/chunk-ODHABIIC.cjs.map +1 -0
- package/dist/chunk-PZ5AY32C.js +9 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-Q7SFCCGT.cjs +11 -0
- package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
- package/dist/chunk-QIUIYBCZ.js +80 -0
- package/dist/chunk-QIUIYBCZ.js.map +1 -0
- package/dist/chunk-QROUNVQK.js +450 -0
- package/dist/chunk-QROUNVQK.js.map +1 -0
- package/dist/chunk-T6FR37IC.js +41 -0
- package/dist/chunk-T6FR37IC.js.map +1 -0
- package/dist/chunk-TI44I654.cjs +265 -0
- package/dist/chunk-TI44I654.cjs.map +1 -0
- package/dist/chunk-TXPLBAH5.cjs +47 -0
- package/dist/chunk-TXPLBAH5.cjs.map +1 -0
- package/dist/chunk-U3O54IYI.cjs +187 -0
- package/dist/chunk-U3O54IYI.cjs.map +1 -0
- package/dist/chunk-VLC7SZMT.js +1669 -0
- package/dist/chunk-VLC7SZMT.js.map +1 -0
- package/dist/core/index.cjs +232 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +122 -0
- package/dist/core/index.d.ts +122 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/defaults-EQD5QKCU.js +4 -0
- package/dist/defaults-EQD5QKCU.js.map +1 -0
- package/dist/defaults-MLYXD2BG.cjs +49 -0
- package/dist/defaults-MLYXD2BG.cjs.map +1 -0
- package/dist/docx-BUrf4PFj.d.ts +49 -0
- package/dist/docx-DLfSdvXm.d.cts +49 -0
- package/dist/docx-LDETXV3L.js +5 -0
- package/dist/docx-LDETXV3L.js.map +1 -0
- package/dist/docx-N2LKIOK3.cjs +14 -0
- package/dist/docx-N2LKIOK3.cjs.map +1 -0
- package/dist/export/index.cjs +54 -0
- package/dist/export/index.cjs.map +1 -0
- package/dist/export/index.d.cts +60 -0
- package/dist/export/index.d.ts +60 -0
- package/dist/export/index.js +9 -0
- package/dist/export/index.js.map +1 -0
- package/dist/html-5BXJPQU3.js +7 -0
- package/dist/html-5BXJPQU3.js.map +1 -0
- package/dist/html-KU2KHLRF.cjs +24 -0
- package/dist/html-KU2KHLRF.cjs.map +1 -0
- package/dist/import/index.cjs +15 -0
- package/dist/import/index.cjs.map +1 -0
- package/dist/import/index.d.cts +37 -0
- package/dist/import/index.d.ts +37 -0
- package/dist/import/index.js +6 -0
- package/dist/import/index.js.map +1 -0
- package/dist/index.cjs +1035 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +248 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.js +885 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/index.cjs +37 -0
- package/dist/persistence/index.cjs.map +1 -0
- package/dist/persistence/index.d.cts +279 -0
- package/dist/persistence/index.d.ts +279 -0
- package/dist/persistence/index.js +4 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/sanitize-7IZ-SW1f.d.ts +361 -0
- package/dist/sanitize-CvmgqbsA.d.cts +361 -0
- package/dist/server/index.cjs +400 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +229 -0
- package/dist/server/index.d.ts +229 -0
- package/dist/server/index.js +390 -0
- package/dist/server/index.js.map +1 -0
- package/dist/styles.css +680 -0
- package/dist/types-B4z0Quvv.d.cts +193 -0
- package/dist/types-B4z0Quvv.d.ts +193 -0
- package/package.json +183 -0
package/README.md
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
# react-next-editor-js
|
|
2
|
+
|
|
3
|
+
A comprehensive, performant, secure, configurable, customizable, reusable, and
|
|
4
|
+
pluggable **Word-style rich document editor** for React and Next.js, built
|
|
5
|
+
directly on [ProseMirror](https://prosemirror.net).
|
|
6
|
+
|
|
7
|
+
It provides a familiar word-processor authoring experience, works fully offline,
|
|
8
|
+
synchronizes to your own REST API, and produces shareable **DOCX, PDF, and plain
|
|
9
|
+
text** — all without any external document-rendering server. It is written
|
|
10
|
+
entirely in TypeScript and ships ESM + CJS builds with complete type
|
|
11
|
+
definitions.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import dynamic from 'next/dynamic';
|
|
15
|
+
import 'react-next-editor-js/styles.css';
|
|
16
|
+
|
|
17
|
+
const Editor = dynamic(() => import('react-next-editor-js').then((m) => m.Editor), {
|
|
18
|
+
ssr: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
<Editor documentId="doc-2024-08" placeholder="Start typing…" />;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Table of contents
|
|
27
|
+
|
|
28
|
+
- [Highlights](#highlights)
|
|
29
|
+
- [Design principles](#design-principles)
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Quick start (Next.js App Router)](#quick-start-nextjs-app-router)
|
|
32
|
+
- [Usage patterns](#usage-patterns)
|
|
33
|
+
- [Configuration](#configuration)
|
|
34
|
+
- [Props reference](#props-reference)
|
|
35
|
+
- [Feature flags](#feature-flags)
|
|
36
|
+
- [Page configuration](#page-configuration)
|
|
37
|
+
- [Toolbar](#toolbar)
|
|
38
|
+
- [Theming](#theming)
|
|
39
|
+
- [Localization](#localization)
|
|
40
|
+
- [Imperative API (ref)](#imperative-api-ref)
|
|
41
|
+
- [Events](#events)
|
|
42
|
+
- [Custom toolbars & panels](#custom-toolbars--panels)
|
|
43
|
+
- [Visual pagination](#visual-pagination)
|
|
44
|
+
- [DOCX import](#docx-import)
|
|
45
|
+
- [Export](#export)
|
|
46
|
+
- [Client-side export](#client-side-export)
|
|
47
|
+
- [Programmatic export service (server)](#programmatic-export-service-server)
|
|
48
|
+
- [Offline-first persistence & sync](#offline-first-persistence--sync)
|
|
49
|
+
- [Extensibility](#extensibility)
|
|
50
|
+
- [Security](#security)
|
|
51
|
+
- [Accessibility & internationalization](#accessibility--internationalization)
|
|
52
|
+
- [Subpath entry points](#subpath-entry-points)
|
|
53
|
+
- [SSR & browser support](#ssr--browser-support)
|
|
54
|
+
- [TypeScript](#typescript)
|
|
55
|
+
- [Architecture](#architecture)
|
|
56
|
+
- [Limitations & non-goals](#limitations--non-goals)
|
|
57
|
+
- [Development](#development)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Highlights
|
|
63
|
+
|
|
64
|
+
- **Rich text** — bold, italic, underline, strikethrough, superscript,
|
|
65
|
+
subscript, inline code, font family, font size, text color, highlight, and
|
|
66
|
+
clear-formatting.
|
|
67
|
+
- **Block & structural** — headings (H1–H6), text alignment, indentation, line
|
|
68
|
+
spacing, bulleted / numbered / **task** lists, blockquotes, horizontal rules,
|
|
69
|
+
**tables** (insert, add/remove rows & columns, merge/split cells, cell
|
|
70
|
+
background & alignment, column resizing), images (URL / paste / data-URI,
|
|
71
|
+
resize), hyperlinks, and manual page breaks.
|
|
72
|
+
- **Word-like page surface** — A4 / Letter / Legal / A5 / custom sizes,
|
|
73
|
+
configurable margins and orientation. Document-styled single flow by default,
|
|
74
|
+
or **true visual pagination** with discrete on-screen page sheets, repeating
|
|
75
|
+
headers/footers, and live page numbers.
|
|
76
|
+
- **Offline-first** — durable IndexedDB persistence, debounced autosave,
|
|
77
|
+
crash/reload recovery, a durable outbox, connectivity detection (real
|
|
78
|
+
reachability, not just `navigator.onLine`), and a sync engine with exponential
|
|
79
|
+
backoff and a version-guard conflict path. Offline edits upload automatically
|
|
80
|
+
on reconnect.
|
|
81
|
+
- **Export** — isomorphic converters that run **identically in the browser and
|
|
82
|
+
Node**: DOCX (via `docx`), PDF (browser print or a headless-browser renderer),
|
|
83
|
+
plain text, and HTML. An optional server export service renders stored JSON to
|
|
84
|
+
files and writes them to storage.
|
|
85
|
+
- **Import** — semantic `.docx` import (via `mammoth`): structure and common
|
|
86
|
+
styles are sanitized and mapped into the schema (see
|
|
87
|
+
[Import fidelity](#import-fidelity)).
|
|
88
|
+
- **Configurable & extensible** — a single documented props object, per-feature
|
|
89
|
+
toggles, a data-driven customizable toolbar, CSS-variable theming, injectable
|
|
90
|
+
localized strings, custom ProseMirror plugins, custom DOCX node mappings, and
|
|
91
|
+
injectable persistence / sync / asset adapters.
|
|
92
|
+
- **Robust & secure** — schema-enforced document validity, sanitized
|
|
93
|
+
paste/URL/image ingress with no active content, render-time CSS sanitization,
|
|
94
|
+
a React error boundary that contains failures, and a release dependency tree
|
|
95
|
+
with no known vulnerabilities.
|
|
96
|
+
- **Accessible & responsive** — keyboard-navigable, ARIA-labeled toolbar with
|
|
97
|
+
arrow-key navigation; RTL aware; fully responsive from mobile to desktop.
|
|
98
|
+
|
|
99
|
+
## Design principles
|
|
100
|
+
|
|
101
|
+
- **ProseMirror owns the DOM.** The React layer mounts and disposes the
|
|
102
|
+
`EditorView` but never re-renders the editing surface, which avoids the most
|
|
103
|
+
common class of integration bugs.
|
|
104
|
+
- **Core vs. adapters.** The editing core is backend-agnostic. Persistence,
|
|
105
|
+
sync, and asset upload are injected as adapter interfaces, so the same editor
|
|
106
|
+
works against any backend.
|
|
107
|
+
- **Offline-first.** The local store is the source of truth during editing; the
|
|
108
|
+
network is best-effort and never in the critical path.
|
|
109
|
+
- **One schema, shared serializers.** The document schema underpins the editor,
|
|
110
|
+
persistence, and every exporter, so on-screen, downloaded, and API-rendered
|
|
111
|
+
output stay consistent.
|
|
112
|
+
|
|
113
|
+
## Installation
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm install react-next-editor-js
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`react` and `react-dom` (`^18.2` or `^19`) are **peer dependencies**.
|
|
120
|
+
|
|
121
|
+
Two optional dependencies are lazily imported only when their feature is used —
|
|
122
|
+
install them where you need them:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install docx # DOCX export (client + server)
|
|
126
|
+
npm install mammoth # DOCX import
|
|
127
|
+
# Server PDF rendering (optional): one of
|
|
128
|
+
npm install playwright # or: npm install puppeteer
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Import the stylesheet once in your app:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import 'react-next-editor-js/styles.css';
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Quick start (Next.js App Router)
|
|
138
|
+
|
|
139
|
+
The editor is **client-only** — it requires the DOM and must not be
|
|
140
|
+
server-rendered. Load it with `next/dynamic` and `{ ssr: false }`.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
'use client';
|
|
144
|
+
|
|
145
|
+
import dynamic from 'next/dynamic';
|
|
146
|
+
import { useRef } from 'react';
|
|
147
|
+
import type { EditorRef, DocumentJSON } from 'react-next-editor-js';
|
|
148
|
+
import 'react-next-editor-js/styles.css';
|
|
149
|
+
|
|
150
|
+
const Editor = dynamic(() => import('react-next-editor-js').then((m) => m.Editor), {
|
|
151
|
+
ssr: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export default function MyEditor() {
|
|
155
|
+
const ref = useRef<EditorRef>(null);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div style={{ height: '80vh' }}>
|
|
159
|
+
<Editor
|
|
160
|
+
ref={ref}
|
|
161
|
+
documentId="doc-2024-08"
|
|
162
|
+
placeholder="Start typing…"
|
|
163
|
+
onChange={(json: DocumentJSON) => {
|
|
164
|
+
/* persist / lift state */
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Give the editor a sized container (e.g. a fixed height or a flex parent): it
|
|
173
|
+
fills its parent and scrolls its own canvas.
|
|
174
|
+
|
|
175
|
+
## Usage patterns
|
|
176
|
+
|
|
177
|
+
**Uncontrolled (recommended).** Provide `initialContent`; read changes via
|
|
178
|
+
`onChange` or the `ref`.
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<Editor initialContent={docJson} onChange={(json) => save(json)} />
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Controlled.** Provide `value` (ProseMirror JSON) together with `onChange`. The
|
|
185
|
+
editor reconciles external value changes without disturbing the cursor when the
|
|
186
|
+
content is unchanged.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<Editor value={value} onChange={(json) => setValue(json)} />
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Read-only / view mode.**
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<Editor initialContent={docJson} readOnly /> // or mode="readonly"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Plain-text or empty start.** `initialContent` also accepts a plain string
|
|
199
|
+
(split into paragraphs) or `null` (empty document).
|
|
200
|
+
|
|
201
|
+
### Saving to your backend
|
|
202
|
+
|
|
203
|
+
`onChange` fires on every keystroke, so debounce writes to your API. For
|
|
204
|
+
full offline-first behaviour (queue offline, upload on reconnect) prefer the
|
|
205
|
+
[`sync` adapter](#offline-first-persistence--sync) instead of saving manually.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useMemo, useRef } from 'react';
|
|
209
|
+
import type { DocumentJSON } from 'react-next-editor-js';
|
|
210
|
+
|
|
211
|
+
function useDebouncedSave(documentId: string, wait = 800) {
|
|
212
|
+
const timer = useRef<ReturnType<typeof setTimeout>>();
|
|
213
|
+
return useMemo(
|
|
214
|
+
() => (json: DocumentJSON) => {
|
|
215
|
+
clearTimeout(timer.current);
|
|
216
|
+
timer.current = setTimeout(() => {
|
|
217
|
+
void fetch(`/api/documents/${documentId}`, {
|
|
218
|
+
method: 'PUT',
|
|
219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
220
|
+
body: JSON.stringify({ doc: json }),
|
|
221
|
+
});
|
|
222
|
+
}, wait);
|
|
223
|
+
},
|
|
224
|
+
[documentId, wait],
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function MyEditor({ id }: { id: string }) {
|
|
229
|
+
const save = useDebouncedSave(id);
|
|
230
|
+
return <Editor documentId={id} onChange={(json) => save(json)} />;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Configuration
|
|
235
|
+
|
|
236
|
+
Everything is driven by a single props object. Every field is optional; sensible
|
|
237
|
+
defaults apply.
|
|
238
|
+
|
|
239
|
+
### Props reference
|
|
240
|
+
|
|
241
|
+
| Prop | Type | Default | Description |
|
|
242
|
+
|------|------|---------|-------------|
|
|
243
|
+
| `documentId` | `string` | — | Stable id used for local persistence and sync. |
|
|
244
|
+
| `initialContent` | `DocumentJSON \| string \| null` | empty | Initial content for uncontrolled usage. |
|
|
245
|
+
| `value` | `DocumentJSON \| null` | — | Controlled value (use with `onChange`). |
|
|
246
|
+
| `mode` | `'edit' \| 'readonly'` | `'edit'` | Editing mode. |
|
|
247
|
+
| `readOnly` | `boolean` | `false` | Convenience alias for read-only. |
|
|
248
|
+
| `placeholder` | `string` | — | Placeholder for an empty document. |
|
|
249
|
+
| `features` | `Partial<FeatureFlags>` | all on | Per-feature toggles. |
|
|
250
|
+
| `page` | `Partial<PageConfig>` | A4 | Size, orientation, margins, chrome, pagination, header/footer. |
|
|
251
|
+
| `toolbar` | `ToolbarConfig \| false` | default | Toolbar layout, or `false` to hide. |
|
|
252
|
+
| `statusBar` | `boolean` | `true` | Show the word/character + sync status bar. |
|
|
253
|
+
| `theme` | `ThemeTokens` | — | Design tokens (CSS variables). |
|
|
254
|
+
| `strings` | `Partial<EditorStrings>` | English | Localized UI strings. |
|
|
255
|
+
| `fontFamilies` | `string[]` | built-in | Font picker options. |
|
|
256
|
+
| `fontSizes` | `number[]` (pt) | built-in | Size picker options. |
|
|
257
|
+
| `colorPalette` | `string[]` | built-in | Color/highlight palette. |
|
|
258
|
+
| `extensions` | `EditorExtensions` | — | Custom plugins and custom DOCX mappings. |
|
|
259
|
+
| `persistence` | `PersistenceConfig` | auto | Local store, autosave, store adapter. |
|
|
260
|
+
| `sync` | `SyncConfig` | — | REST adapter; auto-upload on reconnect. |
|
|
261
|
+
| `metadata` | `Record<string, unknown>` | — | Per-document metadata stored alongside content. |
|
|
262
|
+
| `dir` | `'ltr' \| 'rtl' \| 'auto'` | `'ltr'` | Text direction (RTL aware). |
|
|
263
|
+
| `ariaLabel` | `string` | `'Document editor'` | Accessible label for the editing region. |
|
|
264
|
+
| `className` | `string` | — | Class added to the root element. |
|
|
265
|
+
| `style` | `React.CSSProperties` | — | Inline style on the root element. |
|
|
266
|
+
| `children` | `React.ReactNode` | — | Custom UI rendered inside the editor context (see [Custom toolbars & panels](#custom-toolbars--panels)). |
|
|
267
|
+
| `onReady` | `(ref: EditorRef) => void` | — | Fired once the editor is mounted. |
|
|
268
|
+
| `onChange` | `(json: DocumentJSON, ref: EditorRef) => void` | — | Fired on every document change. |
|
|
269
|
+
| `onSelectionChange` | `(state: EditorState) => void` | — | Fired on selection change. |
|
|
270
|
+
| `onSaveStatusChange` | `(status: SaveStatus, detail?) => void` | — | Fired on save/sync transitions. |
|
|
271
|
+
| `onError` | `(error: Error) => void` | — | Fired when the error boundary contains a failure. |
|
|
272
|
+
|
|
273
|
+
### Feature flags
|
|
274
|
+
|
|
275
|
+
Every feature can be toggled. Disabling one removes its schema node/mark,
|
|
276
|
+
commands, input rules, and toolbar item together.
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
<Editor features={{ table: false, image: false, taskList: false }} />
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Available flags: `bold`, `italic`, `underline`, `strikethrough`, `superscript`,
|
|
283
|
+
`subscript`, `code`, `fontFamily`, `fontSize`, `textColor`, `highlight`,
|
|
284
|
+
`clearFormatting`, `headings`, `alignment`, `lineSpacing`, `indentation`,
|
|
285
|
+
`bulletList`, `orderedList`, `taskList`, `blockquote`, `horizontalRule`,
|
|
286
|
+
`table`, `image`, `link`, `pageBreak`, `history`, `wordCount`, `docxImport`.
|
|
287
|
+
|
|
288
|
+
### Page configuration
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
interface PageConfig {
|
|
292
|
+
size: 'A4' | 'Letter' | 'Legal' | 'A5' | 'custom';
|
|
293
|
+
widthMm?: number; // when size === 'custom'
|
|
294
|
+
heightMm?: number; // when size === 'custom'
|
|
295
|
+
orientation: 'portrait' | 'landscape';
|
|
296
|
+
margins: { top: number; right: number; bottom: number; left: number }; // mm
|
|
297
|
+
showPageChrome: boolean; // white sheet on a canvas (single-flow)
|
|
298
|
+
pagination?: 'none' | 'visual'; // see "Visual pagination"
|
|
299
|
+
header?: PageRunningElement; // visual pagination only
|
|
300
|
+
footer?: PageFooterElement; // visual pagination only
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
<Editor page={{ size: 'Letter', orientation: 'portrait', margins: { top: 25.4, right: 25.4, bottom: 25.4, left: 25.4 } }} />
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Toolbar
|
|
309
|
+
|
|
310
|
+
The built-in toolbar is data-driven: define ordered **groups** of item ids to
|
|
311
|
+
reorder or remove controls, toggle `sticky`, or hide it with `toolbar={false}`.
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<Editor
|
|
315
|
+
toolbar={{
|
|
316
|
+
sticky: true,
|
|
317
|
+
groups: [
|
|
318
|
+
['undo', 'redo'],
|
|
319
|
+
['paragraphStyle', 'fontFamily', 'fontSize'],
|
|
320
|
+
['bold', 'italic', 'underline', 'textColor', 'highlight'],
|
|
321
|
+
['bulletList', 'orderedList', 'link', 'image', 'table'],
|
|
322
|
+
],
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The available item ids are exported as the `ToolbarItemId` union, and the default
|
|
328
|
+
layout is `DEFAULT_TOOLBAR_GROUPS`. Items whose feature is disabled are filtered
|
|
329
|
+
out automatically.
|
|
330
|
+
|
|
331
|
+
To go beyond reordering — adding your own buttons, dropdowns, or panels — hide
|
|
332
|
+
the built-in toolbar and render your own controls as `children`, reading live
|
|
333
|
+
editor state through `useEditorContext()`. See
|
|
334
|
+
[Custom toolbars & panels](#custom-toolbars--panels).
|
|
335
|
+
|
|
336
|
+
### Theming
|
|
337
|
+
|
|
338
|
+
Every visual aspect is a CSS custom property scoped under `.rne-root`. Override
|
|
339
|
+
any `--rne-*` token in your stylesheet, or pass the `theme` prop — no forking.
|
|
340
|
+
|
|
341
|
+
```css
|
|
342
|
+
.rne-root {
|
|
343
|
+
--rne-accent: #2563eb;
|
|
344
|
+
--rne-page-background: #ffffff;
|
|
345
|
+
--rne-canvas-background: #f3f4f6;
|
|
346
|
+
--rne-toolbar-background: #ffffff;
|
|
347
|
+
--rne-border-radius: 6px;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
<Editor theme={{ accent: '#0b5cad', pageBackground: '#fff' }} />
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Common tokens: `fontFamily`, `fontSize`, `textColor`, `background`,
|
|
356
|
+
`canvasBackground`, `pageBackground`, `accent`, `toolbarBackground`,
|
|
357
|
+
`toolbarColor`, `toolbarActiveBackground`, `borderColor`, `borderRadius`,
|
|
358
|
+
`selectionColor`.
|
|
359
|
+
|
|
360
|
+
### Localization
|
|
361
|
+
|
|
362
|
+
All UI strings are externalized and overridable (`EditorStrings`). The default
|
|
363
|
+
set is English.
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
<Editor strings={{ bold: 'Gras', italic: 'Italique', link: 'Lien' }} />
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Imperative API (ref)
|
|
370
|
+
|
|
371
|
+
A `ref` of type `EditorRef` exposes an imperative handle.
|
|
372
|
+
|
|
373
|
+
| Method | Returns | Description |
|
|
374
|
+
|--------|---------|-------------|
|
|
375
|
+
| `getJSON()` | `DocumentJSON` | Current document as ProseMirror JSON. |
|
|
376
|
+
| `getText(options?)` | `string` | Document as plain text. |
|
|
377
|
+
| `getHTML()` | `string` | Document as an HTML fragment. |
|
|
378
|
+
| `setContent(content)` | `void` | Replace content (`DocumentJSON \| string \| null`). |
|
|
379
|
+
| `importDocx(file)` | `Promise<{ warnings }>` | Import a `.docx`, replacing content (undoable). |
|
|
380
|
+
| `focus()` | `void` | Focus the editing surface. |
|
|
381
|
+
| `isDirty()` | `boolean` | Whether there are unsynced local changes. |
|
|
382
|
+
| `save()` | `Promise<void>` | Force an immediate local save. |
|
|
383
|
+
| `clearLocalData()` | `Promise<void>` | Purge this document's local data. |
|
|
384
|
+
| `exportAs(format, filename?)` | `Promise<void>` | Download/print (`'docx' \| 'pdf' \| 'txt' \| 'html'`). |
|
|
385
|
+
| `getView()` | `EditorView \| null` | Escape hatch: the ProseMirror view. |
|
|
386
|
+
| `getState()` | `EditorState \| null` | Escape hatch: the editor state. |
|
|
387
|
+
| `getSchema()` | `Schema \| null` | The active schema. |
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
const ref = useRef<EditorRef>(null);
|
|
391
|
+
// …
|
|
392
|
+
await ref.current?.exportAs('docx', 'doc-2024-08');
|
|
393
|
+
const text = ref.current?.getText();
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Events
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
<Editor
|
|
400
|
+
onReady={(ref) => console.log('ready')}
|
|
401
|
+
onChange={(json, ref) => persist(json)}
|
|
402
|
+
onSelectionChange={(state) => updateInspector(state)}
|
|
403
|
+
onSaveStatusChange={(status, detail) => setBadge(status)} // 'savingLocal' | 'savedLocal' | 'syncing' | 'synced' | 'syncFailed' | 'offline' | 'idle'
|
|
404
|
+
onError={(error) => report(error)}
|
|
405
|
+
/>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Custom toolbars & panels
|
|
409
|
+
|
|
410
|
+
Render your own UI as `children` of `<Editor>`; those components run inside the
|
|
411
|
+
editor's context and can call `useEditorContext()` to read live state and
|
|
412
|
+
dispatch commands. This is the way to build a fully custom toolbar, a slash menu,
|
|
413
|
+
a word-count badge, or an inspector panel.
|
|
414
|
+
|
|
415
|
+
`useEditorContext()` returns:
|
|
416
|
+
|
|
417
|
+
| Field | Description |
|
|
418
|
+
|-------|-------------|
|
|
419
|
+
| `state` | The current `EditorState` (re-renders on every change). |
|
|
420
|
+
| `view` | The live `EditorView` (or `null` before mount). |
|
|
421
|
+
| `schema` | The active schema. |
|
|
422
|
+
| `commands` | The command set: `registry` (toolbar commands), `marks`, `blocks`, `links`, `insert`. |
|
|
423
|
+
| `run(command)` | Dispatch a ProseMirror command against the view and refocus. |
|
|
424
|
+
| `importDocx(file)` | Import a `.docx`, replacing content. |
|
|
425
|
+
| `editable` | Whether the editor is currently editable. |
|
|
426
|
+
| `strings`, `features`, `fontFamilies`, `fontSizes`, `colorPalette` | Resolved config. |
|
|
427
|
+
|
|
428
|
+
A custom bold button that reflects active state:
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
'use client';
|
|
432
|
+
import { useEditorContext } from 'react-next-editor-js';
|
|
433
|
+
|
|
434
|
+
function BoldButton() {
|
|
435
|
+
const { commands, run, state } = useEditorContext();
|
|
436
|
+
const active = state ? commands.registry.bold.isActive?.(state) : false;
|
|
437
|
+
const enabled = state ? commands.registry.bold.isEnabled?.(state) ?? true : false;
|
|
438
|
+
return (
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
aria-pressed={active}
|
|
442
|
+
disabled={!enabled}
|
|
443
|
+
onMouseDown={(e) => e.preventDefault()} // keep selection
|
|
444
|
+
onClick={() => run(commands.registry.bold.run)}
|
|
445
|
+
style={{ fontWeight: active ? 700 : 400 }}
|
|
446
|
+
>
|
|
447
|
+
B
|
|
448
|
+
</button>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Compose it into a custom toolbar and hide the built-in one with `toolbar={false}`:
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { useEditorContext } from 'react-next-editor-js';
|
|
457
|
+
|
|
458
|
+
function MyToolbar() {
|
|
459
|
+
const { commands, run } = useEditorContext();
|
|
460
|
+
return (
|
|
461
|
+
<div className="my-toolbar">
|
|
462
|
+
<BoldButton />
|
|
463
|
+
<button onMouseDown={(e) => e.preventDefault()} onClick={() => run(commands.blocks.setHeading(1))}>
|
|
464
|
+
H1
|
|
465
|
+
</button>
|
|
466
|
+
<button onMouseDown={(e) => e.preventDefault()} onClick={() => run(commands.insert.table(3, 3, true))}>
|
|
467
|
+
Table
|
|
468
|
+
</button>
|
|
469
|
+
<button onClick={() => run(commands.links.setLink({ href: 'https://example.com' }))}>
|
|
470
|
+
Link
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
<Editor initialContent={docJson} toolbar={false}>
|
|
477
|
+
<MyToolbar />
|
|
478
|
+
</Editor>;
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Command groups available on `commands`:
|
|
482
|
+
|
|
483
|
+
- `registry[id]` — every built-in toolbar command (`bold`, `italic`, `alignLeft`,
|
|
484
|
+
`bulletList`, `addRowAfter`, …) as `{ run, isActive?, isEnabled? }`.
|
|
485
|
+
- `marks` — parametric mark commands: `setFontFamily(name)`, `setFontSize(pt)`,
|
|
486
|
+
`setTextColor(hex)`, `setHighlight(hex)`, and `getActive*` readers.
|
|
487
|
+
- `blocks` — `setParagraph()`, `setHeading(level)`, `setAlign(a)`, `setLineHeight(n)`.
|
|
488
|
+
- `links` — `setLink({ href })`, `removeLink`, `getActiveLink(state)`.
|
|
489
|
+
- `insert` — `image({ src, alt })`, `table(rows, cols, withHeaderRow)`.
|
|
490
|
+
|
|
491
|
+
> `useEditorContext()` must be called from a component rendered as a child of
|
|
492
|
+
> `<Editor>`. Outside that subtree it throws.
|
|
493
|
+
|
|
494
|
+
## Visual pagination
|
|
495
|
+
|
|
496
|
+
By default the editor renders a single document-styled flow (cheap and robust;
|
|
497
|
+
print and PDF paginate naturally). Opt into **true visual pagination** to split
|
|
498
|
+
content across discrete on-screen page sheets with repeating headers/footers and
|
|
499
|
+
live page numbers:
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
<Editor
|
|
503
|
+
page={{
|
|
504
|
+
size: 'A4',
|
|
505
|
+
pagination: 'visual',
|
|
506
|
+
header: { show: true, text: 'Confidential', align: 'left' },
|
|
507
|
+
footer: { pageNumbers: true }, // "Page X of Y"
|
|
508
|
+
// or a custom footer: footer: { show: true, text: '{page} / {pages}', align: 'center' }
|
|
509
|
+
}}
|
|
510
|
+
/>
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
`{page}` and `{pages}` in header/footer text are replaced with the live page
|
|
514
|
+
number and total. Pagination is **purely visual**: it measures content heights
|
|
515
|
+
and inserts spacer decorations plus a page-sheet background layer — it **never
|
|
516
|
+
mutates the document**, so content integrity is guaranteed even if measurement is
|
|
517
|
+
imperfect. It re-measures on edits, resize, and image load.
|
|
518
|
+
|
|
519
|
+
**Line-level splitting.** A tall paragraph, heading, list, or blockquote is split
|
|
520
|
+
at a **line boundary** so it flows naturally across pages — it is not pushed
|
|
521
|
+
whole to the next page or left to overflow. Tables and leaf atoms (images,
|
|
522
|
+
horizontal rules) are not divided: they move to the next page if they fit there,
|
|
523
|
+
and the only content that overflows a page is a *single* line or atom (e.g. an
|
|
524
|
+
image, or one table row) taller than a whole page — which cannot be split by
|
|
525
|
+
definition. Set `pagination` at mount time.
|
|
526
|
+
|
|
527
|
+
## DOCX import
|
|
528
|
+
|
|
529
|
+
Import external `.docx` files (`mammoth` converts to HTML, which is sanitized and
|
|
530
|
+
parsed into the schema). Structure and common styles are preserved; see
|
|
531
|
+
[Import fidelity](#import-fidelity) for exactly what maps across. Available as a
|
|
532
|
+
toolbar button (enabled by the `docxImport` feature) and imperatively:
|
|
533
|
+
|
|
534
|
+
```tsx
|
|
535
|
+
const input = e.target as HTMLInputElement;
|
|
536
|
+
const file = input.files?.[0];
|
|
537
|
+
if (file) {
|
|
538
|
+
const { warnings } = await ref.current!.importDocx(file); // File | ArrayBuffer | Uint8Array
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Requires the optional `mammoth` dependency. The lower-level converter is also
|
|
543
|
+
available, and returns conversion `warnings` plus the intermediate `html`:
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
import { importDocx } from 'react-next-editor-js/import';
|
|
547
|
+
const { doc, warnings, html } = await importDocx(arrayBuffer, schema, {
|
|
548
|
+
// Optional extra mammoth style mappings (merged with the built-in defaults):
|
|
549
|
+
styleMap: ["p[style-name='Legal Heading'] => h2:fresh"],
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Import fidelity
|
|
554
|
+
|
|
555
|
+
Import is a **semantic** conversion (Word → HTML → schema), not a byte-for-byte
|
|
556
|
+
reproduction. This is intentional: lossless round-tripping of *arbitrary*
|
|
557
|
+
externally-authored Word documents is an explicit non-goal (it would require a
|
|
558
|
+
full Office layout engine). What maps across is well-defined:
|
|
559
|
+
|
|
560
|
+
| Word construct | Imported as | Notes |
|
|
561
|
+
|----------------|-------------|-------|
|
|
562
|
+
| Headings 1–6 (and 7–9, Title, Subtitle) | `heading` (h1–h6) | 7–9 fold to H6; Title→H1, Subtitle→H2. |
|
|
563
|
+
| Bold, italic | `strong`, `em` | Including the Strong/Emphasis character styles. |
|
|
564
|
+
| Bulleted / numbered lists (nested) | `bullet_list` / `ordered_list` | Nesting preserved. |
|
|
565
|
+
| Tables | `table` | Cells, header row; merged cells best-effort. |
|
|
566
|
+
| Hyperlinks | `link` | URLs sanitized. |
|
|
567
|
+
| Images | inline `image` | Embedded as data URIs. |
|
|
568
|
+
| Blockquotes (Quote styles) | `blockquote` | |
|
|
569
|
+
| Empty paragraphs | preserved | Word's spacing-by-blank-line is kept. |
|
|
570
|
+
| Custom named paragraph styles | mapped via `styleMap` | Supply your own mappings. |
|
|
571
|
+
|
|
572
|
+
Constructs **not** reproduced (dropped or normalized): direct character
|
|
573
|
+
formatting that Word stores outside named styles — underline, text/highlight
|
|
574
|
+
color, font family/size, and explicit alignment — as well as headers/footers,
|
|
575
|
+
footnotes, comments, fields, text boxes, and section/column layout. Provide a
|
|
576
|
+
custom `styleMap` to capture document-specific named styles. For the canonical,
|
|
577
|
+
loss-free format, persist and reload the editor's own JSON (`onChange` /
|
|
578
|
+
`getJSON`), which round-trips every supported feature exactly.
|
|
579
|
+
|
|
580
|
+
## Export
|
|
581
|
+
|
|
582
|
+
All converters are isomorphic and share one implementation, so browser download,
|
|
583
|
+
client print, and server rendering produce consistent output.
|
|
584
|
+
|
|
585
|
+
### Client-side export
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import {
|
|
589
|
+
exportDocument, // high-level: download (docx/txt/html) or print (pdf)
|
|
590
|
+
documentToText, // DocumentJSON -> string
|
|
591
|
+
documentToHtml, // DocumentJSON -> HTML fragment
|
|
592
|
+
documentToDocxBlob, // DocumentJSON -> Blob (browser)
|
|
593
|
+
printDocumentToPdf, // open the print dialog with a print stylesheet
|
|
594
|
+
buildPrintDocument, // standalone print HTML (shared with the server PDF path)
|
|
595
|
+
downloadBlob, downloadText,
|
|
596
|
+
} from 'react-next-editor-js/export';
|
|
597
|
+
|
|
598
|
+
await exportDocument(doc, 'docx', { filename: 'report', page });
|
|
599
|
+
await printDocumentToPdf(doc, { page, title: 'Report' });
|
|
600
|
+
const txt = documentToText(doc, { includeLinkUrls: true });
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
The simplest path is `ref.current.exportAs('docx' | 'pdf' | 'txt' | 'html')`.
|
|
604
|
+
|
|
605
|
+
### Programmatic export service (server)
|
|
606
|
+
|
|
607
|
+
`react-next-editor-js/server` is an **optional, Node-only** service that converts
|
|
608
|
+
stored or inline document JSON to DOCX/PDF/text/HTML using the same converters,
|
|
609
|
+
optionally writes results to storage, and enforces access control via an injected
|
|
610
|
+
hook. The editor's offline/client export does not depend on it.
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
import {
|
|
614
|
+
createExportService,
|
|
615
|
+
createExportHandler,
|
|
616
|
+
FilesystemStorage,
|
|
617
|
+
createPlaywrightPdfRenderer, // optional; requires `playwright` (or use createPuppeteerPdfRenderer)
|
|
618
|
+
} from 'react-next-editor-js/server';
|
|
619
|
+
|
|
620
|
+
const service = createExportService({
|
|
621
|
+
store: { loadDocument: (id) => db.loadDocJson(id) }, // read stored JSON by id
|
|
622
|
+
storage: new FilesystemStorage({ baseDir: '/var/exports', baseUrl: '/exports' }),
|
|
623
|
+
pdfRenderer: createPlaywrightPdfRenderer(), // server PDF
|
|
624
|
+
authorize: (req, ctx) => canAccess(ctx.token, req.documentId),
|
|
625
|
+
nodeConverters: { /* custom node -> DOCX mappings, matching the client */ },
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const single = await service.export({ documentId: 'doc-1', format: 'docx', store: true });
|
|
629
|
+
const batch = await service.exportBatch([{ documentId: 'a', format: 'pdf' }, /* … */]);
|
|
630
|
+
const { jobId } = service.enqueue([/* … */]); // async; poll service.getJob(jobId)
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Use it directly as a Next.js App Router route handler (it is a standard
|
|
634
|
+
`(Request) => Promise<Response>`):
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
// app/api/export/route.ts
|
|
638
|
+
import { createExportService, createExportHandler } from 'react-next-editor-js/server';
|
|
639
|
+
|
|
640
|
+
export const runtime = 'nodejs'; // DOCX/PDF need Node
|
|
641
|
+
const handle = createExportHandler(createExportService(/* …adapters… */));
|
|
642
|
+
export const POST = handle;
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Errors are reported as `status: 'error'` per document — the service never emits a
|
|
646
|
+
malformed file silently. Storage and PDF rendering are pluggable
|
|
647
|
+
(`StorageAdapter`, `PdfRenderer`); a `MemoryStorage` is provided for tests.
|
|
648
|
+
|
|
649
|
+
## Offline-first persistence & sync
|
|
650
|
+
|
|
651
|
+
When given a `documentId`, the editor autosaves to a durable local store
|
|
652
|
+
(IndexedDB by default), recovers the latest state after a crash/reload, and — if
|
|
653
|
+
a `sync.remote` adapter is provided — uploads queued changes automatically when
|
|
654
|
+
connectivity returns.
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
import { ConflictError } from 'react-next-editor-js';
|
|
658
|
+
import type { RemoteSyncAdapter } from 'react-next-editor-js';
|
|
659
|
+
|
|
660
|
+
const remote: RemoteSyncAdapter = {
|
|
661
|
+
async save(record, signal) {
|
|
662
|
+
const res = await fetch(`/api/docs/${record.id}`, {
|
|
663
|
+
method: 'PUT',
|
|
664
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
665
|
+
body: JSON.stringify({ doc: record.doc, baseVersion: record.baseVersion }),
|
|
666
|
+
signal,
|
|
667
|
+
});
|
|
668
|
+
if (res.status === 409) throw new ConflictError('stale', await res.json());
|
|
669
|
+
return { version: (await res.json()).version };
|
|
670
|
+
},
|
|
671
|
+
ping: async () => (await fetch('/api/health')).ok,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
<Editor
|
|
675
|
+
documentId="doc-2024-08"
|
|
676
|
+
persistence={{ enabled: true }} // IndexedDB autosave (default when documentId is set)
|
|
677
|
+
sync={{ remote, onConflict: (local, remote) => promptUser(local, remote) }}
|
|
678
|
+
/>;
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
How it works:
|
|
682
|
+
|
|
683
|
+
- **Local persistence** (`PersistenceConfig`) — debounced autosave of
|
|
684
|
+
`doc.toJSON()` to a `LocalStoreAdapter`; the default is `IndexedDBStore` (with
|
|
685
|
+
an in-memory fallback). Configure `store`, `debounceMs`, `requestPersistent`.
|
|
686
|
+
- **Outbox** — every local save is recorded in a durable outbox that survives
|
|
687
|
+
reloads and restarts.
|
|
688
|
+
- **Connectivity** (`ConnectivityMonitor`) — listens to `online`/`offline` and,
|
|
689
|
+
when a `ping` is provided, confirms real API reachability rather than trusting
|
|
690
|
+
`navigator.onLine`.
|
|
691
|
+
- **Sync engine** (`SyncEngine`) — on reconnect and after each local save, flushes
|
|
692
|
+
the outbox with idempotent uploads and exponential backoff. On a version
|
|
693
|
+
conflict (throw `ConflictError`), the document is parked and `onConflict` fires;
|
|
694
|
+
edits are never silently lost.
|
|
695
|
+
|
|
696
|
+
Adapters are injectable, so the same editor works against any backend. Provide a
|
|
697
|
+
custom local store via `persistence.store` and a remote via `sync.remote`:
|
|
698
|
+
|
|
699
|
+
```tsx
|
|
700
|
+
import { IndexedDBStore } from 'react-next-editor-js/persistence';
|
|
701
|
+
import type { LocalStoreAdapter, StoredDocument } from 'react-next-editor-js';
|
|
702
|
+
|
|
703
|
+
// Example: wrap the built-in store to encrypt documents at rest.
|
|
704
|
+
class EncryptedStore implements LocalStoreAdapter {
|
|
705
|
+
constructor(private readonly inner = new IndexedDBStore()) {}
|
|
706
|
+
async putDocument(r: StoredDocument) {
|
|
707
|
+
return this.inner.putDocument({ ...r, doc: encrypt(r.doc) as never });
|
|
708
|
+
}
|
|
709
|
+
async getDocument(id: string) {
|
|
710
|
+
const r = await this.inner.getDocument(id);
|
|
711
|
+
return r ? { ...r, doc: decrypt(r.doc) } : null;
|
|
712
|
+
}
|
|
713
|
+
// delegate the rest…
|
|
714
|
+
listDocuments = (...a: never[]) => this.inner.listDocuments(...(a as []));
|
|
715
|
+
deleteDocument = (id: string) => this.inner.deleteDocument(id);
|
|
716
|
+
enqueue = this.inner.enqueue.bind(this.inner);
|
|
717
|
+
dequeue = this.inner.dequeue.bind(this.inner);
|
|
718
|
+
listOutbox = this.inner.listOutbox.bind(this.inner);
|
|
719
|
+
clear = this.inner.clear.bind(this.inner);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
<Editor documentId="doc-2024-08" persistence={{ store: new EncryptedStore() }} sync={{ remote }} />;
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`LocalStoreAdapter`, `RemoteSyncAdapter`, and `AssetUploadAdapter` are exported
|
|
726
|
+
from `react-next-editor-js/persistence` (and the package root). The editor wires
|
|
727
|
+
`LocalStoreAdapter` (via `persistence.store`) and `RemoteSyncAdapter` (via
|
|
728
|
+
`sync.remote`); `AssetUploadAdapter` is provided as an interface for building your
|
|
729
|
+
own image/asset upload pipeline. Auth tokens are supplied through your adapter and
|
|
730
|
+
are never embedded in the editor; all network access must use HTTPS.
|
|
731
|
+
|
|
732
|
+
## Extensibility
|
|
733
|
+
|
|
734
|
+
Register custom ProseMirror plugins and matching DOCX mappings without forking:
|
|
735
|
+
|
|
736
|
+
```tsx
|
|
737
|
+
<Editor
|
|
738
|
+
extensions={{
|
|
739
|
+
plugins: [myPlugin], // any prosemirror-state Plugin[]
|
|
740
|
+
docxNodeConverters: {
|
|
741
|
+
signature: (node, ctx) => [
|
|
742
|
+
new ctx.docx.Paragraph({
|
|
743
|
+
children: [new ctx.docx.TextRun({ text: `Signed: ${node.attrs?.name}` })],
|
|
744
|
+
}),
|
|
745
|
+
],
|
|
746
|
+
},
|
|
747
|
+
}}
|
|
748
|
+
/>
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
For deeper control, the framework-agnostic core is exported from
|
|
752
|
+
`react-next-editor-js/core` (`buildSchema`, `createCommands`, `buildPlugins`,
|
|
753
|
+
`createEditorState`, `countDocument`, …), and `ref.getView()` / `getState()` /
|
|
754
|
+
`getSchema()` provide direct access to the underlying ProseMirror objects.
|
|
755
|
+
|
|
756
|
+
## Security
|
|
757
|
+
|
|
758
|
+
The editor follows a defense-in-depth posture:
|
|
759
|
+
|
|
760
|
+
- All pasted, imported, or loaded content is sanitized; `<script>`, inline event
|
|
761
|
+
handlers, and other active content are stripped.
|
|
762
|
+
- Link and image URLs are validated; `javascript:`/`vbscript:`/`data:text/html`
|
|
763
|
+
and SVG/script data-URIs are rejected, and oversized data-URIs are capped.
|
|
764
|
+
- Inline `style` values from document JSON are re-validated at render time, so a
|
|
765
|
+
crafted attribute (e.g. `align: "left;background:url(...)"`) cannot inject CSS.
|
|
766
|
+
- The schema enforces document validity, so the document cannot enter an invalid
|
|
767
|
+
or unrenderable state.
|
|
768
|
+
- A React error boundary contains failures so a fault in the editor cannot bring
|
|
769
|
+
down the host app.
|
|
770
|
+
|
|
771
|
+
Helpers `sanitizeUrl`, `sanitizeImageSrc`, and `sanitizeHtml` are exported for
|
|
772
|
+
reuse.
|
|
773
|
+
|
|
774
|
+
**Content Security Policy.** Formatting (alignment, color, font, highlight) uses
|
|
775
|
+
inline `style` *attributes*, so the editor requires `style-src 'unsafe-inline'`
|
|
776
|
+
(or `style-src-attr 'unsafe-inline'`). It uses no inline `<script>` or `eval`, so
|
|
777
|
+
`script-src` can remain strict (nonce/hash based).
|
|
778
|
+
|
|
779
|
+
**Integrator responsibilities.** Backend authentication/authorization, transport
|
|
780
|
+
(HTTPS), CSP, and storage policy are the host's responsibility. At-rest
|
|
781
|
+
encryption of the local IndexedDB store is not built in; for sensitive
|
|
782
|
+
deployments, wrap the injected `LocalStoreAdapter` to encrypt values, and use
|
|
783
|
+
`clearLocalData()` (e.g. on logout) to purge.
|
|
784
|
+
|
|
785
|
+
## Accessibility & internationalization
|
|
786
|
+
|
|
787
|
+
- Toolbar controls are keyboard-navigable with ARIA labels, active/pressed state,
|
|
788
|
+
and arrow-key (Home/End/←/→) movement between buttons.
|
|
789
|
+
- The editing region is an ARIA `textbox`; provide an `ariaLabel`.
|
|
790
|
+
- Color popovers close on `Escape`; image insertion prompts for alt text.
|
|
791
|
+
- RTL is supported via the `dir` prop; all UI strings are externalized for
|
|
792
|
+
localization.
|
|
793
|
+
|
|
794
|
+
## Subpath entry points
|
|
795
|
+
|
|
796
|
+
Import only what you need to keep bundles lean.
|
|
797
|
+
|
|
798
|
+
| Entry | Contents |
|
|
799
|
+
|-------|----------|
|
|
800
|
+
| `react-next-editor-js` | React component, hooks, and the full public API (default). |
|
|
801
|
+
| `react-next-editor-js/core` | Framework-agnostic schema, commands, plugins (incl. pagination), and state factory. |
|
|
802
|
+
| `react-next-editor-js/export` | Isomorphic DOCX/PDF/text/HTML converters and download helpers. |
|
|
803
|
+
| `react-next-editor-js/import` | Best-effort `.docx` importer. |
|
|
804
|
+
| `react-next-editor-js/persistence` | Adapters, IndexedDB/memory stores, autosave, connectivity, sync engine. |
|
|
805
|
+
| `react-next-editor-js/server` | Node-only programmatic export service and route handler. |
|
|
806
|
+
| `react-next-editor-js/styles.css` | The stylesheet. |
|
|
807
|
+
|
|
808
|
+
## SSR & browser support
|
|
809
|
+
|
|
810
|
+
The editor requires the DOM and must be loaded client-side only — use
|
|
811
|
+
`next/dynamic` with `{ ssr: false }` (or a `'use client'` boundary). The package
|
|
812
|
+
guards DOM access so importing it on the server does not crash, but the component
|
|
813
|
+
itself renders only on the client.
|
|
814
|
+
|
|
815
|
+
Supported browsers: the latest two versions of Chrome, Edge, Firefox, and Safari.
|
|
816
|
+
|
|
817
|
+
## TypeScript
|
|
818
|
+
|
|
819
|
+
The package ships complete type definitions for every public API. React is a peer
|
|
820
|
+
dependency and is kept external so a single React instance is used.
|
|
821
|
+
|
|
822
|
+
```ts
|
|
823
|
+
import type {
|
|
824
|
+
EditorProps, EditorRef, DocumentJSON, FeatureFlags, PageConfig,
|
|
825
|
+
ThemeTokens, ToolbarConfig, EditorStrings, SaveStatus,
|
|
826
|
+
PersistenceConfig, SyncConfig, RemoteSyncAdapter, LocalStoreAdapter,
|
|
827
|
+
} from 'react-next-editor-js';
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
## Architecture
|
|
831
|
+
|
|
832
|
+
```
|
|
833
|
+
src/
|
|
834
|
+
core/ schema (nodes/marks), commands, plugins, state, pagination
|
|
835
|
+
react/ Editor component, toolbar, status bar, error boundary, context
|
|
836
|
+
export/ isomorphic text / html / docx / pdf converters
|
|
837
|
+
import/ best-effort docx import
|
|
838
|
+
persistence/ adapter interfaces, IndexedDB + memory stores, autosave
|
|
839
|
+
sync/ connectivity monitor, sync engine
|
|
840
|
+
server/ programmatic export service, storage, PDF renderers, route handler
|
|
841
|
+
security/ URL / image / HTML / CSS sanitization
|
|
842
|
+
config/ types and defaults
|
|
843
|
+
styles/ editor.css
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
The document schema is the single source of truth: nodes/marks, commands,
|
|
847
|
+
persistence, and every serializer derive from it.
|
|
848
|
+
|
|
849
|
+
## Limitations & non-goals
|
|
850
|
+
|
|
851
|
+
- No separate self-hosted document-rendering server (by design).
|
|
852
|
+
- No real-time multi-user collaboration (the architecture leaves room for it).
|
|
853
|
+
- **DOCX is a semantic, not byte-perfect, interchange format.** Export reproduces
|
|
854
|
+
every supported schema feature; import maps the structures listed in
|
|
855
|
+
[Import fidelity](#import-fidelity). Lossless round-tripping of *arbitrary*
|
|
856
|
+
externally-authored Word documents is an explicit non-goal — the editor's own
|
|
857
|
+
JSON is the canonical, exact format. Direct character formatting and
|
|
858
|
+
page/section layout from imported files are normalized, not preserved.
|
|
859
|
+
- **Visual pagination splits paragraphs, headings, lists, and blockquotes at line
|
|
860
|
+
boundaries**, so tall content flows across pages. The only content that
|
|
861
|
+
overflows a page is a single line or unsplittable atom (an image, or one table
|
|
862
|
+
row) that is itself taller than a whole page. Pagination is on-screen only —
|
|
863
|
+
print/PDF use the browser's native page breaking.
|
|
864
|
+
|
|
865
|
+
## Development
|
|
866
|
+
|
|
867
|
+
```bash
|
|
868
|
+
npm run build # bundle (tsup): ESM + CJS + .d.ts + styles.css
|
|
869
|
+
npm run type-check # tsc --noEmit
|
|
870
|
+
npm run lint # eslint
|
|
871
|
+
npm test # vitest
|
|
872
|
+
npm run verify # type-check + lint + test
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
## License
|
|
876
|
+
|
|
877
|
+
MIT.
|