@beyondwork/docx-react-component 1.0.21 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +682 -0
- package/package.json +25 -36
package/README.md
CHANGED
|
@@ -135,3 +135,685 @@ Shared platform and planned xlsx docs:
|
|
|
135
135
|
This repo is not trying to become a generic office clone.
|
|
136
136
|
|
|
137
137
|
It is building fidelity-first office-document runtimes with explicit preservation and calm, reviewable UI.
|
|
138
|
+
|
|
139
|
+
## Using the package
|
|
140
|
+
|
|
141
|
+
### WordReviewEditor
|
|
142
|
+
|
|
143
|
+
`WordReviewEditor` is a React component for loading, editing, and exporting `.docx` files with full comment and tracked-change (redline) support. It is exported from `@beyondwork/docx-react-component`.
|
|
144
|
+
|
|
145
|
+
### Installation
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import {
|
|
149
|
+
WordReviewEditor,
|
|
150
|
+
type WordReviewEditorRef,
|
|
151
|
+
type WordReviewEditorProps,
|
|
152
|
+
type WordReviewEditorEvent,
|
|
153
|
+
} from "@beyondwork/docx-react-component";
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Basic mount
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { useRef } from "react";
|
|
160
|
+
import { WordReviewEditor, type WordReviewEditorRef } from "@beyondwork/docx-react-component";
|
|
161
|
+
|
|
162
|
+
export function MyEditor({ docxBytes }: { docxBytes: Uint8Array }) {
|
|
163
|
+
const editorRef = useRef<WordReviewEditorRef>(null);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<WordReviewEditor
|
|
167
|
+
ref={editorRef}
|
|
168
|
+
documentId="doc-001"
|
|
169
|
+
currentUser={{ userId: "u1", displayName: "Alice" }}
|
|
170
|
+
initialDocx={docxBytes}
|
|
171
|
+
onEvent={(event) => console.log(event)}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Props reference
|
|
180
|
+
|
|
181
|
+
| Prop | Type | Description |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `documentId` | `string` | **Required.** Stable identifier for this document. |
|
|
184
|
+
| `currentUser` | `EditorUser` | **Required.** The user performing edits and adding comments. |
|
|
185
|
+
| `initialDocx` | `Uint8Array \| ArrayBuffer` | Raw `.docx` bytes to load on first mount. |
|
|
186
|
+
| `initialSessionState` | `EditorSessionState` | Previously saved session state to restore. |
|
|
187
|
+
| `initialSnapshot` | `PersistedEditorSnapshot` | Previously saved snapshot to restore. |
|
|
188
|
+
| `externalDocSource` | `ExternalDocumentSource` | Alternative source with explicit `kind` (`"docx"`, `"session"`, `"snapshot"`). |
|
|
189
|
+
| `readOnly` | `boolean` | When `true`, all editing commands are disabled. |
|
|
190
|
+
| `reviewMode` | `"editing" \| "review"` | Shell layout hint — affects toolbar/panel arrangement but not editing authority. |
|
|
191
|
+
| `markupDisplay` | `"clean" \| "simple" \| "all"` | Controls tracked-change visibility. |
|
|
192
|
+
| `showReviewPanel` | `boolean` | Shows or hides the right-side comment and tracked-change panel. |
|
|
193
|
+
| `autosave` | `AutosaveConfig` | Enables automatic saving. |
|
|
194
|
+
| `hostAdapter` | `EditorHostAdapter` | Callbacks for `load`, `saveSession`, `saveExport`. |
|
|
195
|
+
| `datastore` | `EditorDatastoreAdapter` | Alternative persistence adapter with `load`, `saveSnapshot`. |
|
|
196
|
+
| `onEvent` | `(event: WordReviewEditorEvent) => void` | Unified event handler (see [Events](#events)). |
|
|
197
|
+
| `onWarning` | `(warning: EditorWarning) => void` | Fired for non-fatal warnings. |
|
|
198
|
+
| `onError` | `(error: EditorError) => void` | Fired for fatal errors. |
|
|
199
|
+
|
|
200
|
+
#### EditorUser
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
interface EditorUser {
|
|
204
|
+
userId: string;
|
|
205
|
+
displayName: string;
|
|
206
|
+
email?: string;
|
|
207
|
+
avatarUrl?: string;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### AutosaveConfig
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
interface AutosaveConfig {
|
|
215
|
+
enabled?: boolean;
|
|
216
|
+
debounceMs?: number; // default: 2000
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### Show / hide UI regions
|
|
223
|
+
|
|
224
|
+
#### Review panel
|
|
225
|
+
|
|
226
|
+
The right-side panel lists comment threads and tracked changes.
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
<WordReviewEditor showReviewPanel={false} ... /> // hide panel
|
|
230
|
+
<WordReviewEditor showReviewPanel={true} ... /> // show panel (default)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Tracked-change display mode
|
|
234
|
+
|
|
235
|
+
`markupDisplay` controls how tracked changes appear in the document body.
|
|
236
|
+
|
|
237
|
+
| Value | Behaviour |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `"clean"` | Show the accepted version — insertions visible, deletions hidden. |
|
|
240
|
+
| `"simple"` | Show a simplified view of changes without inline markup. |
|
|
241
|
+
| `"all"` | Show all insertion and deletion marks inline (Word's "Show Markup" mode). |
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
<WordReviewEditor markupDisplay="clean" ... />
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
You can also change the display mode at runtime:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// no ref method for markupDisplay — pass as a prop; React re-renders propagate the change.
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Document mode
|
|
254
|
+
|
|
255
|
+
`DocumentMode` controls editing authority, not just appearance.
|
|
256
|
+
|
|
257
|
+
| Mode | Effect |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `"editing"` | Edits are applied directly (no tracking). |
|
|
260
|
+
| `"suggesting"` | Every edit is automatically wrapped in a tracked change. |
|
|
261
|
+
| `"viewing"` | Document is read-only regardless of the `readOnly` prop. |
|
|
262
|
+
|
|
263
|
+
Set via ref:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
editorRef.current.setDocumentMode("suggesting");
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Or pass `reviewMode="review"` as a prop to start in a review-friendly shell layout (the component internally maps this to `"suggesting"` document mode).
|
|
270
|
+
|
|
271
|
+
#### Read-only mode
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
<WordReviewEditor readOnly={true} ... />
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
All editing, commenting, and tracked-change commands are blocked. The toolbar is still rendered but all buttons are disabled.
|
|
278
|
+
|
|
279
|
+
#### Workspace layout
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
editorRef.current.setWorkspaceMode("canvas"); // continuous scroll
|
|
283
|
+
editorRef.current.setWorkspaceMode("page"); // paginated view
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### Imperative ref
|
|
289
|
+
|
|
290
|
+
Obtain the ref via `useRef<WordReviewEditorRef>()`:
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
const editorRef = useRef<WordReviewEditorRef>(null);
|
|
294
|
+
<WordReviewEditor ref={editorRef} ... />
|
|
295
|
+
|
|
296
|
+
// then:
|
|
297
|
+
editorRef.current?.addComment({ body: "Needs revision" });
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### Comment operations
|
|
303
|
+
|
|
304
|
+
#### Add a comment
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
addComment(params: AddCommentParams): string
|
|
308
|
+
// returns the new commentId
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
interface AddCommentParams {
|
|
313
|
+
anchor?: EditorAnchorProjection; // defaults to the current selection
|
|
314
|
+
body?: string;
|
|
315
|
+
authorId?: string; // defaults to currentUser.userId
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Important**: if you want the comment to land on a specific text selection, capture the anchor *before* opening any draft UI (e.g. a modal or popover), because opening a modal typically collapses the editor selection.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
// 1. Capture anchor while text is still selected
|
|
323
|
+
const snapshot = editorRef.current.getRenderSnapshot();
|
|
324
|
+
const anchor = snapshot.selection.activeRange;
|
|
325
|
+
|
|
326
|
+
// 2. Open your draft UI, let user type a message...
|
|
327
|
+
// 3. On submit:
|
|
328
|
+
const commentId = editorRef.current.addComment({ anchor, body: draftText });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### Resolve a comment
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
editorRef.current.resolveComment(commentId);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Marks the thread as resolved. The comment remains in the document and can be exported; it is moved to the resolved list in the sidebar.
|
|
338
|
+
|
|
339
|
+
#### Reopen a resolved comment
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
editorRef.current.reopenComment(commentId);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### Delete a comment permanently
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
editorRef.current.deleteComment(commentId);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Removes the comment entirely. Use this to clean up failed or unwanted drafts.
|
|
352
|
+
|
|
353
|
+
#### Add a reply to an existing thread
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
editorRef.current.addCommentReply(commentId, "Reply text here");
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### Edit a comment body
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
editorRef.current.editCommentBody(commentId, "Updated text");
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### Scroll to a comment
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
editorRef.current.scrollToComment(commentId);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
#### Open (focus) a comment in the sidebar
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
editorRef.current.openComment(commentId);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### Get all comments
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
const sidebar: CommentSidebarSnapshot = editorRef.current.getComments();
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
interface CommentSidebarSnapshot {
|
|
385
|
+
activeCommentId?: string;
|
|
386
|
+
openCommentIds: string[];
|
|
387
|
+
resolvedCommentIds: string[];
|
|
388
|
+
detachedCommentIds: string[];
|
|
389
|
+
totalCount: number;
|
|
390
|
+
threads: CommentSidebarThreadSnapshot[];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
interface CommentSidebarThreadSnapshot {
|
|
394
|
+
commentId: string;
|
|
395
|
+
status: "open" | "resolved" | "detached";
|
|
396
|
+
anchor: EditorAnchorProjection;
|
|
397
|
+
excerpt: string; // the anchored text snippet
|
|
398
|
+
entries: CommentSidebarThreadEntrySnapshot[];
|
|
399
|
+
entryCount: number;
|
|
400
|
+
createdAt: string; // ISO 8601
|
|
401
|
+
createdBy: string; // userId
|
|
402
|
+
resolvedAt?: string;
|
|
403
|
+
resolvedBy?: string;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### Detached comments
|
|
408
|
+
|
|
409
|
+
A comment becomes **detached** when the text it was anchored to is deleted. Detached comments:
|
|
410
|
+
|
|
411
|
+
- Still appear in `sidebar.detachedCommentIds` and have `status: "detached"`.
|
|
412
|
+
- Have `anchor.kind === "detached"` with a `lastKnownRange` and a `reason` (`"deleted"`, `"invalidatedByStructureChange"`, or `"importAmbiguity"`).
|
|
413
|
+
- Do **not** block DOCX export.
|
|
414
|
+
- Can be resolved, reopened, or deleted via the same methods above.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### Tracked-change operations
|
|
419
|
+
|
|
420
|
+
#### Get all tracked changes
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
const changes: TrackedChangesSnapshot = editorRef.current.getTrackedChanges();
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
interface TrackedChangesSnapshot {
|
|
428
|
+
pendingChangeIds: string[];
|
|
429
|
+
acceptedChangeIds: string[];
|
|
430
|
+
rejectedChangeIds: string[];
|
|
431
|
+
detachedChangeIds: string[];
|
|
432
|
+
actionableChangeIds: string[];
|
|
433
|
+
preserveOnlyChangeIds: string[];
|
|
434
|
+
totalCount: number;
|
|
435
|
+
revisions: TrackedChangeEntrySnapshot[];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
interface TrackedChangeEntrySnapshot {
|
|
439
|
+
revisionId: string;
|
|
440
|
+
kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
|
|
441
|
+
status: "active" | "accepted" | "rejected" | "detached";
|
|
442
|
+
actionability: "actionable" | "preserve-only";
|
|
443
|
+
canAccept: boolean;
|
|
444
|
+
canReject: boolean;
|
|
445
|
+
anchor: EditorAnchorProjection;
|
|
446
|
+
anchorLabel: string;
|
|
447
|
+
excerpt?: string;
|
|
448
|
+
detail?: string;
|
|
449
|
+
authorId: string;
|
|
450
|
+
createdAt: string;
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`preserve-only` revisions (`formatting`, `move`) can be displayed but cannot be individually accepted or rejected through the API.
|
|
455
|
+
|
|
456
|
+
#### Accept a single change
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
editorRef.current.acceptChange(revisionId);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
#### Reject a single change
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
editorRef.current.rejectChange(revisionId);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
#### Accept all pending changes
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
editorRef.current.acceptAllChanges();
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### Reject all pending changes
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
editorRef.current.rejectAllChanges();
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Scroll to a tracked change
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
editorRef.current.scrollToRevision(revisionId);
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### Export
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
const result = await editorRef.current.exportDocx({ fileName: "output.docx" });
|
|
492
|
+
// result.bytes is the Uint8Array of the .docx file
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Export will throw if any non-detached comment no longer maps to a serializable range in the document. To diagnose, check `getComments().threads` for entries where `status !== "detached"` but whose `anchor.kind` is unexpected.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### Events
|
|
500
|
+
|
|
501
|
+
All events are dispatched through the single `onEvent` prop. The `type` discriminator narrows the payload.
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
<WordReviewEditor
|
|
505
|
+
onEvent={(event) => {
|
|
506
|
+
if (event.type === "comment_added") {
|
|
507
|
+
console.log("New comment:", event.commentId);
|
|
508
|
+
}
|
|
509
|
+
}}
|
|
510
|
+
/>
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### `ready`
|
|
514
|
+
|
|
515
|
+
Fired once after the document finishes loading and the editor is interactive.
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
{
|
|
519
|
+
type: "ready";
|
|
520
|
+
documentId: string;
|
|
521
|
+
sessionId: string;
|
|
522
|
+
source: "docx" | "session" | "snapshot";
|
|
523
|
+
stats: DocumentStats; // storyLength, commentCount, revisionCount
|
|
524
|
+
compatibility: CompatibilityReport;
|
|
525
|
+
comments: CommentSidebarSnapshot;
|
|
526
|
+
trackedChanges: TrackedChangesSnapshot;
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
#### `comment_added`
|
|
531
|
+
|
|
532
|
+
Fired when a new comment thread is created (via `addComment` or the toolbar).
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
{
|
|
536
|
+
type: "comment_added";
|
|
537
|
+
documentId: string;
|
|
538
|
+
commentId: string;
|
|
539
|
+
anchor: EditorAnchorProjection;
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
#### `comment_resolved`
|
|
544
|
+
|
|
545
|
+
Fired when a comment is resolved (via `resolveComment` or the sidebar).
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
{
|
|
549
|
+
type: "comment_resolved";
|
|
550
|
+
documentId: string;
|
|
551
|
+
commentId: string;
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
There is no separate `comment_removed` event — deletions are silent. Query `getComments()` after a `dirty_changed` event if you need to detect deletions.
|
|
556
|
+
|
|
557
|
+
#### `change_accepted`
|
|
558
|
+
|
|
559
|
+
Fired when a tracked change is accepted.
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
{
|
|
563
|
+
type: "change_accepted";
|
|
564
|
+
documentId: string;
|
|
565
|
+
changeId: string;
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
#### `change_rejected`
|
|
570
|
+
|
|
571
|
+
Fired when a tracked change is rejected.
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
{
|
|
575
|
+
type: "change_rejected";
|
|
576
|
+
documentId: string;
|
|
577
|
+
changeId: string;
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### `selection_changed`
|
|
582
|
+
|
|
583
|
+
Fired whenever the editor cursor or selection changes.
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
{
|
|
587
|
+
type: "selection_changed";
|
|
588
|
+
documentId: string;
|
|
589
|
+
selection: SelectionSnapshot;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
interface SelectionSnapshot {
|
|
593
|
+
anchor: number;
|
|
594
|
+
head: number;
|
|
595
|
+
isCollapsed: boolean;
|
|
596
|
+
activeRange: EditorAnchorProjection;
|
|
597
|
+
storyTarget?: EditorStoryTarget;
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Use `selection.activeRange` as the `anchor` argument to `addComment` — but capture it *before* opening any modal UI.
|
|
602
|
+
|
|
603
|
+
#### `dirty_changed`
|
|
604
|
+
|
|
605
|
+
Fired when the document transitions between clean and dirty (unsaved) states.
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
{
|
|
609
|
+
type: "dirty_changed";
|
|
610
|
+
documentId: string;
|
|
611
|
+
isDirty: boolean;
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
#### `story_changed`
|
|
616
|
+
|
|
617
|
+
Fired when the user navigates between document stories (e.g. main body → header/footer).
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
{
|
|
621
|
+
type: "story_changed";
|
|
622
|
+
documentId: string;
|
|
623
|
+
activeStory: EditorStoryTarget;
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
#### `export_completed`
|
|
628
|
+
|
|
629
|
+
Fired after a successful `exportDocx` call, after the host `saveExport` callback (if any) has resolved.
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
{
|
|
633
|
+
type: "export_completed";
|
|
634
|
+
documentId: string;
|
|
635
|
+
result: ExportResult; // result.bytes, result.fileName, result.mimeType
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
#### `session_saved`
|
|
640
|
+
|
|
641
|
+
Fired after the host `saveSession` callback resolves.
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
{
|
|
645
|
+
type: "session_saved";
|
|
646
|
+
documentId: string;
|
|
647
|
+
sessionState: EditorSessionState;
|
|
648
|
+
savedAt: string;
|
|
649
|
+
isAutosave: boolean;
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
#### `snapshot_saved`
|
|
654
|
+
|
|
655
|
+
Fired after the datastore `saveSnapshot` callback resolves.
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
{
|
|
659
|
+
type: "snapshot_saved";
|
|
660
|
+
documentId: string;
|
|
661
|
+
snapshot: PersistedEditorSnapshot;
|
|
662
|
+
isAutosave: boolean;
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
#### `autosave_state`
|
|
667
|
+
|
|
668
|
+
Fired when the autosave lifecycle transitions.
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
{
|
|
672
|
+
type: "autosave_state";
|
|
673
|
+
documentId: string;
|
|
674
|
+
state: "idle" | "pending" | "saving" | "saved" | "error";
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
#### `warning_added` / `warning_cleared`
|
|
679
|
+
|
|
680
|
+
Non-fatal import or rendering warnings.
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
{ type: "warning_added"; documentId: string; warning: EditorWarning; }
|
|
684
|
+
{ type: "warning_cleared"; documentId: string; warningId: string; code: EditorWarningCode; }
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
#### `error`
|
|
688
|
+
|
|
689
|
+
Fatal editor error. The editor may be in an unrecoverable state after this event.
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
{
|
|
693
|
+
type: "error";
|
|
694
|
+
documentId: string;
|
|
695
|
+
error: EditorError; // error.code, error.message, error.isFatal
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### Workflow events
|
|
700
|
+
|
|
701
|
+
These events relate to the optional workflow overlay feature.
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
{ type: "workflow_overlay_changed"; documentId: string; snapshot: WorkflowScopeSnapshot; }
|
|
705
|
+
{ type: "workflow_active_work_item_changed"; documentId: string; activeWorkItemId: string | null; }
|
|
706
|
+
{ type: "command_blocked"; documentId: string; command: string; reasons: WorkflowBlockedCommandReason[]; }
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
### Key types
|
|
712
|
+
|
|
713
|
+
#### EditorAnchorProjection
|
|
714
|
+
|
|
715
|
+
Describes a position or range in the document. Returned by `selection.activeRange` and stored on comments/revisions.
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
type EditorAnchorProjection =
|
|
719
|
+
| { kind: "range"; from: number; to: number; assoc: { start: -1|1; end: -1|1 } }
|
|
720
|
+
| { kind: "node"; at: number; assoc: -1|1 }
|
|
721
|
+
| { kind: "detached"; lastKnownRange: { from: number; to: number };
|
|
722
|
+
reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity" };
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
A `"range"` anchor is required for `addComment` if the document contains tables or the selection spans multiple characters. The `from`/`to` positions are in the editor's internal runtime coordinate space — always capture them from `getRenderSnapshot().selection.activeRange`, never construct them manually.
|
|
726
|
+
|
|
727
|
+
#### DocumentMode
|
|
728
|
+
|
|
729
|
+
```ts
|
|
730
|
+
type DocumentMode = "editing" | "suggesting" | "viewing";
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
#### WorkspaceMode
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
type WorkspaceMode = "canvas" | "page";
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
### Common patterns
|
|
742
|
+
|
|
743
|
+
#### Full add-comment flow with custom UI
|
|
744
|
+
|
|
745
|
+
```tsx
|
|
746
|
+
function CommentButton({ editorRef }: { editorRef: React.RefObject<WordReviewEditorRef> }) {
|
|
747
|
+
const [draft, setDraft] = useState<{ anchor: EditorAnchorProjection; text: string } | null>(null);
|
|
748
|
+
|
|
749
|
+
function openDraft() {
|
|
750
|
+
// Capture anchor BEFORE the modal steals focus from the editor
|
|
751
|
+
const snapshot = editorRef.current?.getRenderSnapshot();
|
|
752
|
+
if (!snapshot) return;
|
|
753
|
+
const anchor = snapshot.selection.activeRange;
|
|
754
|
+
if (anchor.kind !== "range" || anchor.from === anchor.to) return;
|
|
755
|
+
setDraft({ anchor, text: "" });
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function submit() {
|
|
759
|
+
if (!draft) return;
|
|
760
|
+
editorRef.current?.addComment({ anchor: draft.anchor, body: draft.text });
|
|
761
|
+
setDraft(null);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return (
|
|
765
|
+
<>
|
|
766
|
+
<button onClick={openDraft}>Comment</button>
|
|
767
|
+
{draft && (
|
|
768
|
+
<dialog open>
|
|
769
|
+
<textarea value={draft.text} onChange={(e) => setDraft({ ...draft, text: e.target.value })} />
|
|
770
|
+
<button onClick={submit}>Add</button>
|
|
771
|
+
<button onClick={() => setDraft(null)}>Cancel</button>
|
|
772
|
+
</dialog>
|
|
773
|
+
)}
|
|
774
|
+
</>
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
#### Listen for comment and review-change events
|
|
780
|
+
|
|
781
|
+
```tsx
|
|
782
|
+
<WordReviewEditor
|
|
783
|
+
onEvent={(event) => {
|
|
784
|
+
switch (event.type) {
|
|
785
|
+
case "comment_added":
|
|
786
|
+
console.log("comment added:", event.commentId, event.anchor);
|
|
787
|
+
break;
|
|
788
|
+
case "comment_resolved":
|
|
789
|
+
console.log("comment resolved:", event.commentId);
|
|
790
|
+
break;
|
|
791
|
+
case "change_accepted":
|
|
792
|
+
console.log("change accepted:", event.changeId);
|
|
793
|
+
break;
|
|
794
|
+
case "change_rejected":
|
|
795
|
+
console.log("change rejected:", event.changeId);
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
}}
|
|
799
|
+
/>
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
#### Resolve all open comments programmatically
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
const { threads } = editorRef.current.getComments();
|
|
806
|
+
for (const thread of threads) {
|
|
807
|
+
if (thread.status === "open") {
|
|
808
|
+
editorRef.current.resolveComment(thread.commentId);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
#### Accept or reject all actionable changes
|
|
814
|
+
|
|
815
|
+
```ts
|
|
816
|
+
editorRef.current.acceptAllChanges();
|
|
817
|
+
// or
|
|
818
|
+
editorRef.current.rejectAllChanges();
|
|
819
|
+
```
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.22",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
-
"packageManager": "pnpm@10.30.3",
|
|
7
6
|
"type": "module",
|
|
8
7
|
"sideEffects": [
|
|
9
8
|
"**/*.css"
|
|
@@ -89,30 +88,6 @@
|
|
|
89
88
|
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
|
|
90
89
|
},
|
|
91
90
|
"types": "./src/index.ts",
|
|
92
|
-
"scripts": {
|
|
93
|
-
"build": "tsup",
|
|
94
|
-
"test": "bash scripts/run-workspace-tests.sh",
|
|
95
|
-
"test:repo": "node scripts/run-repo-tests.mjs core",
|
|
96
|
-
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
97
|
-
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
98
|
-
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
99
|
-
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
100
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
101
|
-
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
102
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
103
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
104
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
105
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
106
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
107
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
108
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
109
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
110
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
111
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
112
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
113
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
114
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
115
|
-
},
|
|
116
91
|
"keywords": [
|
|
117
92
|
"docx",
|
|
118
93
|
"word",
|
|
@@ -175,14 +150,28 @@
|
|
|
175
150
|
"tsup": "^8.3.0",
|
|
176
151
|
"tsx": "^4.21.0"
|
|
177
152
|
},
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
153
|
+
"scripts": {
|
|
154
|
+
"build": "tsup",
|
|
155
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
156
|
+
"test:repo": "node scripts/run-repo-tests.mjs core",
|
|
157
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
158
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
159
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
160
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
161
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
162
|
+
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
163
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
164
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
165
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
166
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
167
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
168
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
169
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
170
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
171
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
172
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
173
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
174
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
175
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
187
176
|
}
|
|
188
|
-
}
|
|
177
|
+
}
|