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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +877 -0
  3. package/dist/chunk-3QWXTDLY.cjs +486 -0
  4. package/dist/chunk-3QWXTDLY.cjs.map +1 -0
  5. package/dist/chunk-5F6SPYCN.cjs +180 -0
  6. package/dist/chunk-5F6SPYCN.cjs.map +1 -0
  7. package/dist/chunk-6NTSXJX4.js +174 -0
  8. package/dist/chunk-6NTSXJX4.js.map +1 -0
  9. package/dist/chunk-7VYJDBH7.js +261 -0
  10. package/dist/chunk-7VYJDBH7.js.map +1 -0
  11. package/dist/chunk-DBSFCCBG.cjs +1712 -0
  12. package/dist/chunk-DBSFCCBG.cjs.map +1 -0
  13. package/dist/chunk-EFE6RHDL.cjs +4 -0
  14. package/dist/chunk-EFE6RHDL.cjs.map +1 -0
  15. package/dist/chunk-G6YRIEK4.js +3 -0
  16. package/dist/chunk-G6YRIEK4.js.map +1 -0
  17. package/dist/chunk-GFNFJ3FL.cjs +119 -0
  18. package/dist/chunk-GFNFJ3FL.cjs.map +1 -0
  19. package/dist/chunk-IG2YLUFW.js +114 -0
  20. package/dist/chunk-IG2YLUFW.js.map +1 -0
  21. package/dist/chunk-JQXTWLHL.js +176 -0
  22. package/dist/chunk-JQXTWLHL.js.map +1 -0
  23. package/dist/chunk-NJCEHQV3.cjs +454 -0
  24. package/dist/chunk-NJCEHQV3.cjs.map +1 -0
  25. package/dist/chunk-O4GTLC3T.js +478 -0
  26. package/dist/chunk-O4GTLC3T.js.map +1 -0
  27. package/dist/chunk-ODHABIIC.cjs +82 -0
  28. package/dist/chunk-ODHABIIC.cjs.map +1 -0
  29. package/dist/chunk-PZ5AY32C.js +9 -0
  30. package/dist/chunk-PZ5AY32C.js.map +1 -0
  31. package/dist/chunk-Q7SFCCGT.cjs +11 -0
  32. package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
  33. package/dist/chunk-QIUIYBCZ.js +80 -0
  34. package/dist/chunk-QIUIYBCZ.js.map +1 -0
  35. package/dist/chunk-QROUNVQK.js +450 -0
  36. package/dist/chunk-QROUNVQK.js.map +1 -0
  37. package/dist/chunk-T6FR37IC.js +41 -0
  38. package/dist/chunk-T6FR37IC.js.map +1 -0
  39. package/dist/chunk-TI44I654.cjs +265 -0
  40. package/dist/chunk-TI44I654.cjs.map +1 -0
  41. package/dist/chunk-TXPLBAH5.cjs +47 -0
  42. package/dist/chunk-TXPLBAH5.cjs.map +1 -0
  43. package/dist/chunk-U3O54IYI.cjs +187 -0
  44. package/dist/chunk-U3O54IYI.cjs.map +1 -0
  45. package/dist/chunk-VLC7SZMT.js +1669 -0
  46. package/dist/chunk-VLC7SZMT.js.map +1 -0
  47. package/dist/core/index.cjs +232 -0
  48. package/dist/core/index.cjs.map +1 -0
  49. package/dist/core/index.d.cts +122 -0
  50. package/dist/core/index.d.ts +122 -0
  51. package/dist/core/index.js +7 -0
  52. package/dist/core/index.js.map +1 -0
  53. package/dist/defaults-EQD5QKCU.js +4 -0
  54. package/dist/defaults-EQD5QKCU.js.map +1 -0
  55. package/dist/defaults-MLYXD2BG.cjs +49 -0
  56. package/dist/defaults-MLYXD2BG.cjs.map +1 -0
  57. package/dist/docx-BUrf4PFj.d.ts +49 -0
  58. package/dist/docx-DLfSdvXm.d.cts +49 -0
  59. package/dist/docx-LDETXV3L.js +5 -0
  60. package/dist/docx-LDETXV3L.js.map +1 -0
  61. package/dist/docx-N2LKIOK3.cjs +14 -0
  62. package/dist/docx-N2LKIOK3.cjs.map +1 -0
  63. package/dist/export/index.cjs +54 -0
  64. package/dist/export/index.cjs.map +1 -0
  65. package/dist/export/index.d.cts +60 -0
  66. package/dist/export/index.d.ts +60 -0
  67. package/dist/export/index.js +9 -0
  68. package/dist/export/index.js.map +1 -0
  69. package/dist/html-5BXJPQU3.js +7 -0
  70. package/dist/html-5BXJPQU3.js.map +1 -0
  71. package/dist/html-KU2KHLRF.cjs +24 -0
  72. package/dist/html-KU2KHLRF.cjs.map +1 -0
  73. package/dist/import/index.cjs +15 -0
  74. package/dist/import/index.cjs.map +1 -0
  75. package/dist/import/index.d.cts +37 -0
  76. package/dist/import/index.d.ts +37 -0
  77. package/dist/import/index.js +6 -0
  78. package/dist/import/index.js.map +1 -0
  79. package/dist/index.cjs +1035 -0
  80. package/dist/index.cjs.map +1 -0
  81. package/dist/index.d.cts +248 -0
  82. package/dist/index.d.ts +248 -0
  83. package/dist/index.js +885 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/persistence/index.cjs +37 -0
  86. package/dist/persistence/index.cjs.map +1 -0
  87. package/dist/persistence/index.d.cts +279 -0
  88. package/dist/persistence/index.d.ts +279 -0
  89. package/dist/persistence/index.js +4 -0
  90. package/dist/persistence/index.js.map +1 -0
  91. package/dist/sanitize-7IZ-SW1f.d.ts +361 -0
  92. package/dist/sanitize-CvmgqbsA.d.cts +361 -0
  93. package/dist/server/index.cjs +400 -0
  94. package/dist/server/index.cjs.map +1 -0
  95. package/dist/server/index.d.cts +229 -0
  96. package/dist/server/index.d.ts +229 -0
  97. package/dist/server/index.js +390 -0
  98. package/dist/server/index.js.map +1 -0
  99. package/dist/styles.css +680 -0
  100. package/dist/types-B4z0Quvv.d.cts +193 -0
  101. package/dist/types-B4z0Quvv.d.ts +193 -0
  102. 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.