@beyondwork/docx-react-component 1.0.21 → 1.0.23
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 +763 -38
- package/package.json +25 -36
- package/src/api/public-types.ts +66 -1
- package/src/core/commands/index.ts +574 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +661 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
package/README.md
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: React OOXML Office
|
|
3
|
+
summary: Shipped docx package landing page and primary router into consumer, wrapper, agent, and maintainer documentation.
|
|
4
|
+
audience: consumer, wrapper, agent, maintainer
|
|
5
|
+
stability: main-path
|
|
6
|
+
docRole: main-path
|
|
7
|
+
canonical: true
|
|
8
|
+
---
|
|
9
|
+
|
|
1
10
|
# React OOXML Office
|
|
2
11
|
|
|
3
12
|
[](https://github.com/bwllaming/React-OOXML-Office/actions/workflows/ci.yml)
|
|
4
13
|
|
|
5
|
-
`@beyondwork/docx-react-component` is the shipped product in this repository:
|
|
14
|
+
`@beyondwork/docx-react-component` is the shipped product in this repository: a fidelity-first React docx editor centered on `WordReviewEditor`.
|
|
15
|
+
|
|
16
|
+
## Use This README For
|
|
17
|
+
|
|
18
|
+
- confirming what is shipped today
|
|
19
|
+
- installing the package
|
|
20
|
+
- finding the right documentation lane quickly
|
|
21
|
+
|
|
22
|
+
Do not use this README as the full package contract. The canonical consumer path starts in `docs/README.md`, `docs/reference/public-api.md`, and `docs/reference/integration-guide.md`.
|
|
6
23
|
|
|
7
|
-
|
|
24
|
+
## Current Package Reality
|
|
25
|
+
|
|
26
|
+
The repository may also carry broader branch-local work, but the shipped contract today is still docx-first:
|
|
8
27
|
|
|
9
28
|
- `docx` is the only implemented and shipped runtime contract
|
|
10
|
-
- `xlsx` is the
|
|
11
|
-
- `pdf`
|
|
29
|
+
- `xlsx` is planned work, not part of the current package contract
|
|
30
|
+
- `pdf` remains future work outside the current package boundary
|
|
12
31
|
|
|
13
32
|
## Install
|
|
14
33
|
|
|
@@ -54,12 +73,27 @@ Snapshot/export note:
|
|
|
54
73
|
- when a session starts from a real `.docx`, persisted snapshots now carry embedded source-package provenance so later snapshot-origin `.docx` export can use the same package-backed exporter
|
|
55
74
|
- legacy snapshots without that provenance still load, but `.docx` export is intentionally blocked rather than falling back to a lossy minimal package
|
|
56
75
|
|
|
57
|
-
The current
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
- `@beyondwork/docx-react-component
|
|
61
|
-
- `@beyondwork/docx-react-component/
|
|
62
|
-
- `@beyondwork/docx-react-component/ui-tailwind
|
|
76
|
+
The current shipped ESM exports include:
|
|
77
|
+
|
|
78
|
+
- stable default entrypoints:
|
|
79
|
+
- `@beyondwork/docx-react-component`
|
|
80
|
+
- `@beyondwork/docx-react-component/public-types`
|
|
81
|
+
- `@beyondwork/docx-react-component/ui-tailwind`
|
|
82
|
+
- `@beyondwork/docx-react-component/legal`
|
|
83
|
+
- `@beyondwork/docx-react-component/compare`
|
|
84
|
+
- `@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css`
|
|
85
|
+
- advanced-supported subpaths for wrapper teams and package-adjacent integrations:
|
|
86
|
+
- `@beyondwork/docx-react-component/runtime/document-runtime`
|
|
87
|
+
- `@beyondwork/docx-react-component/io/docx-session`
|
|
88
|
+
- `@beyondwork/docx-react-component/core/commands/*`
|
|
89
|
+
- `@beyondwork/docx-react-component/core/selection/mapping`
|
|
90
|
+
- `@beyondwork/docx-react-component/core/state/editor-state`
|
|
91
|
+
- `@beyondwork/docx-react-component/ui-tailwind/editor-surface/search-plugin`
|
|
92
|
+
- alias subpaths:
|
|
93
|
+
- `@beyondwork/docx-react-component/tailwind`
|
|
94
|
+
- `@beyondwork/docx-react-component/api/public-types`
|
|
95
|
+
|
|
96
|
+
Use `docs/reference/public-api.md` and `docs/reference/public-api.manifest.json` for the current contract inventory and stability guidance.
|
|
63
97
|
|
|
64
98
|
## Product Contract
|
|
65
99
|
|
|
@@ -74,48 +108,56 @@ For the current shipped `docx` implementation, that specifically means:
|
|
|
74
108
|
- preserve unsupported but preservable OOXML
|
|
75
109
|
- remain editable in Word after export
|
|
76
110
|
|
|
77
|
-
|
|
111
|
+
For the normative API and host contract, use:
|
|
78
112
|
|
|
79
|
-
|
|
113
|
+
- [`docs/reference/public-api.md`](docs/reference/public-api.md)
|
|
114
|
+
- [`docs/reference/integration-guide.md`](docs/reference/integration-guide.md)
|
|
115
|
+
- [`docs/reference/ooxml-compliance.md`](docs/reference/ooxml-compliance.md)
|
|
80
116
|
|
|
81
|
-
|
|
82
|
-
- format-specific runtimes with explicit capability boundaries
|
|
83
|
-
- host-facing review and editing surfaces for each supported format
|
|
117
|
+
## Documentation Map
|
|
84
118
|
|
|
85
|
-
|
|
119
|
+
### Start Here
|
|
86
120
|
|
|
87
|
-
|
|
121
|
+
- [`docs/README.md`](docs/README.md)
|
|
88
122
|
|
|
89
|
-
|
|
123
|
+
### Main Consumer Path
|
|
90
124
|
|
|
91
|
-
- `
|
|
92
|
-
- `
|
|
93
|
-
- `docs/
|
|
94
|
-
- `docs/
|
|
95
|
-
- `docs/plans/current-state.md`
|
|
96
|
-
- `docs/plans/master-plan.md`
|
|
125
|
+
- [`docs/reference/quick-start.md`](docs/reference/quick-start.md)
|
|
126
|
+
- [`docs/reference/consumer-matrix.md`](docs/reference/consumer-matrix.md)
|
|
127
|
+
- [`docs/reference/public-api.md`](docs/reference/public-api.md)
|
|
128
|
+
- [`docs/reference/integration-guide.md`](docs/reference/integration-guide.md)
|
|
97
129
|
|
|
98
|
-
|
|
130
|
+
### Wrapper And Agent Path
|
|
99
131
|
|
|
100
|
-
- `docs/reference/
|
|
101
|
-
|
|
102
|
-
- `docs/reference/
|
|
103
|
-
- `docs/reference/
|
|
104
|
-
- `docs/reference/
|
|
132
|
+
- [`docs/reference/consumer-matrix.md`](docs/reference/consumer-matrix.md)
|
|
133
|
+
- [`docs/reference/agent-integration-guide.md`](docs/reference/agent-integration-guide.md)
|
|
134
|
+
- [`docs/reference/public-api.md`](docs/reference/public-api.md)
|
|
135
|
+
- [`docs/reference/service-wrapper-guidance.md`](docs/reference/service-wrapper-guidance.md)
|
|
136
|
+
- [`docs/reference/agent-capability-map.md`](docs/reference/agent-capability-map.md)
|
|
137
|
+
- [`docs/reference/agent-read-models-and-snapshots.md`](docs/reference/agent-read-models-and-snapshots.md)
|
|
138
|
+
- [`docs/reference/agent-workflow-and-suggestions.md`](docs/reference/agent-workflow-and-suggestions.md)
|
|
139
|
+
- [`docs/reference/scope-aware-selection-tooling.md`](docs/reference/scope-aware-selection-tooling.md)
|
|
140
|
+
- [`docs/reference/editor-integration-style.md`](docs/reference/editor-integration-style.md)
|
|
141
|
+
- [`docs/reference/beyondwork-runtime-environment.md`](docs/reference/beyondwork-runtime-environment.md)
|
|
105
142
|
|
|
106
|
-
|
|
143
|
+
Wrapper and agent docs stay on `main`, but they are downstream integration guidance rather than the canonical package contract.
|
|
107
144
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- `
|
|
111
|
-
- `
|
|
112
|
-
-
|
|
113
|
-
|
|
145
|
+
Current integration honesty:
|
|
146
|
+
|
|
147
|
+
- the shipped React host surface is still `WordReviewEditor`
|
|
148
|
+
- `showReviewPanel={false}` only hides the bundled review rail
|
|
149
|
+
- a distinct public chromeless React surface is not yet shipped
|
|
150
|
+
|
|
151
|
+
### Maintainer Path
|
|
152
|
+
|
|
153
|
+
- [`docs/maintainers/README.md`](docs/maintainers/README.md)
|
|
154
|
+
|
|
155
|
+
Maintainer prompts, operator runbooks, and proof/closure material remain important, but they are not the first reading path for package consumers.
|
|
114
156
|
|
|
115
157
|
## Packaging And Release
|
|
116
158
|
|
|
117
159
|
- `.github/workflows/publish.yml` publishes on `v*` tags after verifying the tag matches `package.json`
|
|
118
|
-
- `pnpm pack --dry-run` is the baseline package proof
|
|
160
|
+
- `pnpm pack --dry-run` is the baseline package proof
|
|
119
161
|
- npm provenance is enabled in `publishConfig` and in the publish workflow invocation
|
|
120
162
|
- the published package currently ships source ESM entry points plus TypeScript source-backed `types` exports
|
|
121
163
|
- the Microsoft Open XML SDK remains CI/internal-service only, never part of the shipped browser runtime
|
|
@@ -129,9 +171,692 @@ Shared platform and planned xlsx docs:
|
|
|
129
171
|
- keep docs honest about shipped versus planned behavior
|
|
130
172
|
- add or extend fixtures for compatibility-critical changes
|
|
131
173
|
- `bash scripts/validate-fixtures.sh` now uses the Railway validator service and requires `OPENXML_VALIDATOR_AUTH_TOKEN` when hitting the public domain
|
|
174
|
+
- `node scripts/validate-docs-navigation.mjs` enforces the core docs catalog, required indexes, and frontmatter contract
|
|
132
175
|
|
|
133
176
|
## Guiding Principle
|
|
134
177
|
|
|
135
178
|
This repo is not trying to become a generic office clone.
|
|
136
179
|
|
|
137
180
|
It is building fidelity-first office-document runtimes with explicit preservation and calm, reviewable UI.
|
|
181
|
+
|
|
182
|
+
## Using the package
|
|
183
|
+
|
|
184
|
+
### WordReviewEditor
|
|
185
|
+
|
|
186
|
+
`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`.
|
|
187
|
+
|
|
188
|
+
### Installation
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import {
|
|
192
|
+
WordReviewEditor,
|
|
193
|
+
type WordReviewEditorRef,
|
|
194
|
+
type WordReviewEditorProps,
|
|
195
|
+
type WordReviewEditorEvent,
|
|
196
|
+
} from "@beyondwork/docx-react-component";
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Basic mount
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
import { useRef } from "react";
|
|
203
|
+
import { WordReviewEditor, type WordReviewEditorRef } from "@beyondwork/docx-react-component";
|
|
204
|
+
|
|
205
|
+
export function MyEditor({ docxBytes }: { docxBytes: Uint8Array }) {
|
|
206
|
+
const editorRef = useRef<WordReviewEditorRef>(null);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<WordReviewEditor
|
|
210
|
+
ref={editorRef}
|
|
211
|
+
documentId="doc-001"
|
|
212
|
+
currentUser={{ userId: "u1", displayName: "Alice" }}
|
|
213
|
+
initialDocx={docxBytes}
|
|
214
|
+
onEvent={(event) => console.log(event)}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### Props reference
|
|
223
|
+
|
|
224
|
+
| Prop | Type | Description |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `documentId` | `string` | **Required.** Stable identifier for this document. |
|
|
227
|
+
| `currentUser` | `EditorUser` | **Required.** The user performing edits and adding comments. |
|
|
228
|
+
| `initialDocx` | `Uint8Array \| ArrayBuffer` | Raw `.docx` bytes to load on first mount. |
|
|
229
|
+
| `initialSessionState` | `EditorSessionState` | Previously saved session state to restore. |
|
|
230
|
+
| `initialSnapshot` | `PersistedEditorSnapshot` | Previously saved snapshot to restore. |
|
|
231
|
+
| `externalDocSource` | `ExternalDocumentSource` | Alternative source with explicit `kind` (`"docx"`, `"session"`, `"snapshot"`). |
|
|
232
|
+
| `readOnly` | `boolean` | When `true`, all editing commands are disabled. |
|
|
233
|
+
| `reviewMode` | `"editing" \| "review"` | Shell layout hint — affects toolbar/panel arrangement but not editing authority. |
|
|
234
|
+
| `markupDisplay` | `"clean" \| "simple" \| "all"` | Controls tracked-change visibility. |
|
|
235
|
+
| `showReviewPanel` | `boolean` | Shows or hides the right-side comment and tracked-change panel. |
|
|
236
|
+
| `autosave` | `AutosaveConfig` | Enables automatic saving. |
|
|
237
|
+
| `hostAdapter` | `EditorHostAdapter` | Callbacks for `load`, `saveSession`, `saveExport`. |
|
|
238
|
+
| `datastore` | `EditorDatastoreAdapter` | Alternative persistence adapter with `load`, `saveSnapshot`. |
|
|
239
|
+
| `onEvent` | `(event: WordReviewEditorEvent) => void` | Unified event handler (see [Events](#events)). |
|
|
240
|
+
| `onWarning` | `(warning: EditorWarning) => void` | Fired for non-fatal warnings. |
|
|
241
|
+
| `onError` | `(error: EditorError) => void` | Fired for fatal errors. |
|
|
242
|
+
|
|
243
|
+
#### EditorUser
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
interface EditorUser {
|
|
247
|
+
userId: string;
|
|
248
|
+
displayName: string;
|
|
249
|
+
email?: string;
|
|
250
|
+
avatarUrl?: string;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### AutosaveConfig
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
interface AutosaveConfig {
|
|
258
|
+
enabled?: boolean;
|
|
259
|
+
debounceMs?: number; // default: 2000
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
### Show / hide UI regions
|
|
266
|
+
|
|
267
|
+
#### Review panel
|
|
268
|
+
|
|
269
|
+
The right-side panel lists comment threads and tracked changes.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
<WordReviewEditor showReviewPanel={false} ... /> // hide panel
|
|
273
|
+
<WordReviewEditor showReviewPanel={true} ... /> // show panel (default)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### Tracked-change display mode
|
|
277
|
+
|
|
278
|
+
`markupDisplay` controls how tracked changes appear in the document body.
|
|
279
|
+
|
|
280
|
+
| Value | Behaviour |
|
|
281
|
+
|---|---|
|
|
282
|
+
| `"clean"` | Show the accepted version — insertions visible, deletions hidden. |
|
|
283
|
+
| `"simple"` | Show a simplified view of changes without inline markup. |
|
|
284
|
+
| `"all"` | Show all insertion and deletion marks inline (Word's "Show Markup" mode). |
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
<WordReviewEditor markupDisplay="clean" ... />
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
You can also change the display mode at runtime:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// no ref method for markupDisplay — pass as a prop; React re-renders propagate the change.
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Document mode
|
|
297
|
+
|
|
298
|
+
`DocumentMode` controls editing authority, not just appearance.
|
|
299
|
+
|
|
300
|
+
| Mode | Effect |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `"editing"` | Edits are applied directly (no tracking). |
|
|
303
|
+
| `"suggesting"` | Every edit is automatically wrapped in a tracked change. |
|
|
304
|
+
| `"viewing"` | Document is read-only regardless of the `readOnly` prop. |
|
|
305
|
+
|
|
306
|
+
Set via ref:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
editorRef.current.setDocumentMode("suggesting");
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Or pass `reviewMode="review"` as a prop to start in a review-friendly shell layout (the component internally maps this to `"suggesting"` document mode).
|
|
313
|
+
|
|
314
|
+
#### Read-only mode
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
<WordReviewEditor readOnly={true} ... />
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
All editing, commenting, and tracked-change commands are blocked. The toolbar is still rendered but all buttons are disabled.
|
|
321
|
+
|
|
322
|
+
#### Workspace layout
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
editorRef.current.setWorkspaceMode("canvas"); // continuous scroll
|
|
326
|
+
editorRef.current.setWorkspaceMode("page"); // paginated view
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### Imperative ref
|
|
332
|
+
|
|
333
|
+
Obtain the ref via `useRef<WordReviewEditorRef>()`:
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
const editorRef = useRef<WordReviewEditorRef>(null);
|
|
337
|
+
<WordReviewEditor ref={editorRef} ... />
|
|
338
|
+
|
|
339
|
+
// then:
|
|
340
|
+
editorRef.current?.addComment({ body: "Needs revision" });
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
### Comment operations
|
|
346
|
+
|
|
347
|
+
#### Add a comment
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
addComment(params: AddCommentParams): string
|
|
351
|
+
// returns the new commentId
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
interface AddCommentParams {
|
|
356
|
+
anchor?: EditorAnchorProjection; // defaults to the current selection
|
|
357
|
+
body?: string;
|
|
358
|
+
authorId?: string; // defaults to currentUser.userId
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**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.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
// 1. Capture anchor while text is still selected
|
|
366
|
+
const snapshot = editorRef.current.getRenderSnapshot();
|
|
367
|
+
const anchor = snapshot.selection.activeRange;
|
|
368
|
+
|
|
369
|
+
// 2. Open your draft UI, let user type a message...
|
|
370
|
+
// 3. On submit:
|
|
371
|
+
const commentId = editorRef.current.addComment({ anchor, body: draftText });
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### Resolve a comment
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
editorRef.current.resolveComment(commentId);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
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.
|
|
381
|
+
|
|
382
|
+
#### Reopen a resolved comment
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
editorRef.current.reopenComment(commentId);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Delete a comment permanently
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
editorRef.current.deleteComment(commentId);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Removes the comment entirely. Use this to clean up failed or unwanted drafts.
|
|
395
|
+
|
|
396
|
+
#### Add a reply to an existing thread
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
editorRef.current.addCommentReply(commentId, "Reply text here");
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### Edit a comment body
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
editorRef.current.editCommentBody(commentId, "Updated text");
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### Scroll to a comment
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
editorRef.current.scrollToComment(commentId);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### Open (focus) a comment in the sidebar
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
editorRef.current.openComment(commentId);
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### Get all comments
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
const sidebar: CommentSidebarSnapshot = editorRef.current.getComments();
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
interface CommentSidebarSnapshot {
|
|
428
|
+
activeCommentId?: string;
|
|
429
|
+
openCommentIds: string[];
|
|
430
|
+
resolvedCommentIds: string[];
|
|
431
|
+
detachedCommentIds: string[];
|
|
432
|
+
totalCount: number;
|
|
433
|
+
threads: CommentSidebarThreadSnapshot[];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
interface CommentSidebarThreadSnapshot {
|
|
437
|
+
commentId: string;
|
|
438
|
+
status: "open" | "resolved" | "detached";
|
|
439
|
+
anchor: EditorAnchorProjection;
|
|
440
|
+
excerpt: string; // the anchored text snippet
|
|
441
|
+
entries: CommentSidebarThreadEntrySnapshot[];
|
|
442
|
+
entryCount: number;
|
|
443
|
+
createdAt: string; // ISO 8601
|
|
444
|
+
createdBy: string; // userId
|
|
445
|
+
resolvedAt?: string;
|
|
446
|
+
resolvedBy?: string;
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
#### Detached comments
|
|
451
|
+
|
|
452
|
+
A comment becomes **detached** when the text it was anchored to is deleted. Detached comments:
|
|
453
|
+
|
|
454
|
+
- Still appear in `sidebar.detachedCommentIds` and have `status: "detached"`.
|
|
455
|
+
- Have `anchor.kind === "detached"` with a `lastKnownRange` and a `reason` (`"deleted"`, `"invalidatedByStructureChange"`, or `"importAmbiguity"`).
|
|
456
|
+
- Do **not** block DOCX export.
|
|
457
|
+
- Can be resolved, reopened, or deleted via the same methods above.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
### Tracked-change operations
|
|
462
|
+
|
|
463
|
+
#### Get all tracked changes
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
const changes: TrackedChangesSnapshot = editorRef.current.getTrackedChanges();
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
interface TrackedChangesSnapshot {
|
|
471
|
+
pendingChangeIds: string[];
|
|
472
|
+
acceptedChangeIds: string[];
|
|
473
|
+
rejectedChangeIds: string[];
|
|
474
|
+
detachedChangeIds: string[];
|
|
475
|
+
actionableChangeIds: string[];
|
|
476
|
+
preserveOnlyChangeIds: string[];
|
|
477
|
+
totalCount: number;
|
|
478
|
+
revisions: TrackedChangeEntrySnapshot[];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
interface TrackedChangeEntrySnapshot {
|
|
482
|
+
revisionId: string;
|
|
483
|
+
kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
|
|
484
|
+
status: "active" | "accepted" | "rejected" | "detached";
|
|
485
|
+
actionability: "actionable" | "preserve-only";
|
|
486
|
+
canAccept: boolean;
|
|
487
|
+
canReject: boolean;
|
|
488
|
+
anchor: EditorAnchorProjection;
|
|
489
|
+
anchorLabel: string;
|
|
490
|
+
excerpt?: string;
|
|
491
|
+
detail?: string;
|
|
492
|
+
authorId: string;
|
|
493
|
+
createdAt: string;
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`preserve-only` revisions (`formatting`, `move`) can be displayed but cannot be individually accepted or rejected through the API.
|
|
498
|
+
|
|
499
|
+
#### Accept a single change
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
editorRef.current.acceptChange(revisionId);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
#### Reject a single change
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
editorRef.current.rejectChange(revisionId);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
#### Accept all pending changes
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
editorRef.current.acceptAllChanges();
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### Reject all pending changes
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
editorRef.current.rejectAllChanges();
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### Scroll to a tracked change
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
editorRef.current.scrollToRevision(revisionId);
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
### Export
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
const result = await editorRef.current.exportDocx({ fileName: "output.docx" });
|
|
535
|
+
// result.bytes is the Uint8Array of the .docx file
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
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.
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
### Events
|
|
543
|
+
|
|
544
|
+
All events are dispatched through the single `onEvent` prop. The `type` discriminator narrows the payload.
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
<WordReviewEditor
|
|
548
|
+
onEvent={(event) => {
|
|
549
|
+
if (event.type === "comment_added") {
|
|
550
|
+
console.log("New comment:", event.commentId);
|
|
551
|
+
}
|
|
552
|
+
}}
|
|
553
|
+
/>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
#### `ready`
|
|
557
|
+
|
|
558
|
+
Fired once after the document finishes loading and the editor is interactive.
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
{
|
|
562
|
+
type: "ready";
|
|
563
|
+
documentId: string;
|
|
564
|
+
sessionId: string;
|
|
565
|
+
source: "docx" | "session" | "snapshot";
|
|
566
|
+
stats: DocumentStats; // storyLength, commentCount, revisionCount
|
|
567
|
+
compatibility: CompatibilityReport;
|
|
568
|
+
comments: CommentSidebarSnapshot;
|
|
569
|
+
trackedChanges: TrackedChangesSnapshot;
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
#### `comment_added`
|
|
574
|
+
|
|
575
|
+
Fired when a new comment thread is created (via `addComment` or the toolbar).
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
{
|
|
579
|
+
type: "comment_added";
|
|
580
|
+
documentId: string;
|
|
581
|
+
commentId: string;
|
|
582
|
+
anchor: EditorAnchorProjection;
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
#### `comment_resolved`
|
|
587
|
+
|
|
588
|
+
Fired when a comment is resolved (via `resolveComment` or the sidebar).
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
{
|
|
592
|
+
type: "comment_resolved";
|
|
593
|
+
documentId: string;
|
|
594
|
+
commentId: string;
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
There is no separate `comment_removed` event — deletions are silent. Query `getComments()` after a `dirty_changed` event if you need to detect deletions.
|
|
599
|
+
|
|
600
|
+
#### `change_accepted`
|
|
601
|
+
|
|
602
|
+
Fired when a tracked change is accepted.
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
{
|
|
606
|
+
type: "change_accepted";
|
|
607
|
+
documentId: string;
|
|
608
|
+
changeId: string;
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
#### `change_rejected`
|
|
613
|
+
|
|
614
|
+
Fired when a tracked change is rejected.
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
{
|
|
618
|
+
type: "change_rejected";
|
|
619
|
+
documentId: string;
|
|
620
|
+
changeId: string;
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
#### `selection_changed`
|
|
625
|
+
|
|
626
|
+
Fired whenever the editor cursor or selection changes.
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
{
|
|
630
|
+
type: "selection_changed";
|
|
631
|
+
documentId: string;
|
|
632
|
+
selection: SelectionSnapshot;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
interface SelectionSnapshot {
|
|
636
|
+
anchor: number;
|
|
637
|
+
head: number;
|
|
638
|
+
isCollapsed: boolean;
|
|
639
|
+
activeRange: EditorAnchorProjection;
|
|
640
|
+
storyTarget?: EditorStoryTarget;
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Use `selection.activeRange` as the `anchor` argument to `addComment` — but capture it *before* opening any modal UI.
|
|
645
|
+
|
|
646
|
+
#### `dirty_changed`
|
|
647
|
+
|
|
648
|
+
Fired when the document transitions between clean and dirty (unsaved) states.
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
{
|
|
652
|
+
type: "dirty_changed";
|
|
653
|
+
documentId: string;
|
|
654
|
+
isDirty: boolean;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
#### `story_changed`
|
|
659
|
+
|
|
660
|
+
Fired when the user navigates between document stories (e.g. main body → header/footer).
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
{
|
|
664
|
+
type: "story_changed";
|
|
665
|
+
documentId: string;
|
|
666
|
+
activeStory: EditorStoryTarget;
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
#### `export_completed`
|
|
671
|
+
|
|
672
|
+
Fired after a successful `exportDocx` call, after the host `saveExport` callback (if any) has resolved.
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
{
|
|
676
|
+
type: "export_completed";
|
|
677
|
+
documentId: string;
|
|
678
|
+
result: ExportResult; // result.bytes, result.fileName, result.mimeType
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
#### `session_saved`
|
|
683
|
+
|
|
684
|
+
Fired after the host `saveSession` callback resolves.
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
{
|
|
688
|
+
type: "session_saved";
|
|
689
|
+
documentId: string;
|
|
690
|
+
sessionState: EditorSessionState;
|
|
691
|
+
savedAt: string;
|
|
692
|
+
isAutosave: boolean;
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
#### `snapshot_saved`
|
|
697
|
+
|
|
698
|
+
Fired after the datastore `saveSnapshot` callback resolves.
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
{
|
|
702
|
+
type: "snapshot_saved";
|
|
703
|
+
documentId: string;
|
|
704
|
+
snapshot: PersistedEditorSnapshot;
|
|
705
|
+
isAutosave: boolean;
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
#### `autosave_state`
|
|
710
|
+
|
|
711
|
+
Fired when the autosave lifecycle transitions.
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
{
|
|
715
|
+
type: "autosave_state";
|
|
716
|
+
documentId: string;
|
|
717
|
+
state: "idle" | "pending" | "saving" | "saved" | "error";
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
#### `warning_added` / `warning_cleared`
|
|
722
|
+
|
|
723
|
+
Non-fatal import or rendering warnings.
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
{ type: "warning_added"; documentId: string; warning: EditorWarning; }
|
|
727
|
+
{ type: "warning_cleared"; documentId: string; warningId: string; code: EditorWarningCode; }
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
#### `error`
|
|
731
|
+
|
|
732
|
+
Fatal editor error. The editor may be in an unrecoverable state after this event.
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
{
|
|
736
|
+
type: "error";
|
|
737
|
+
documentId: string;
|
|
738
|
+
error: EditorError; // error.code, error.message, error.isFatal
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
#### Workflow events
|
|
743
|
+
|
|
744
|
+
These events relate to the optional workflow overlay feature.
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
{ type: "workflow_overlay_changed"; documentId: string; snapshot: WorkflowScopeSnapshot; }
|
|
748
|
+
{ type: "workflow_active_work_item_changed"; documentId: string; activeWorkItemId: string | null; }
|
|
749
|
+
{ type: "command_blocked"; documentId: string; command: string; reasons: WorkflowBlockedCommandReason[]; }
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
### Key types
|
|
755
|
+
|
|
756
|
+
#### EditorAnchorProjection
|
|
757
|
+
|
|
758
|
+
Describes a position or range in the document. Returned by `selection.activeRange` and stored on comments/revisions.
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
type EditorAnchorProjection =
|
|
762
|
+
| { kind: "range"; from: number; to: number; assoc: { start: -1|1; end: -1|1 } }
|
|
763
|
+
| { kind: "node"; at: number; assoc: -1|1 }
|
|
764
|
+
| { kind: "detached"; lastKnownRange: { from: number; to: number };
|
|
765
|
+
reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity" };
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
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.
|
|
769
|
+
|
|
770
|
+
#### DocumentMode
|
|
771
|
+
|
|
772
|
+
```ts
|
|
773
|
+
type DocumentMode = "editing" | "suggesting" | "viewing";
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
#### WorkspaceMode
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
type WorkspaceMode = "canvas" | "page";
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
### Common patterns
|
|
785
|
+
|
|
786
|
+
#### Full add-comment flow with custom UI
|
|
787
|
+
|
|
788
|
+
```tsx
|
|
789
|
+
function CommentButton({ editorRef }: { editorRef: React.RefObject<WordReviewEditorRef> }) {
|
|
790
|
+
const [draft, setDraft] = useState<{ anchor: EditorAnchorProjection; text: string } | null>(null);
|
|
791
|
+
|
|
792
|
+
function openDraft() {
|
|
793
|
+
// Capture anchor BEFORE the modal steals focus from the editor
|
|
794
|
+
const snapshot = editorRef.current?.getRenderSnapshot();
|
|
795
|
+
if (!snapshot) return;
|
|
796
|
+
const anchor = snapshot.selection.activeRange;
|
|
797
|
+
if (anchor.kind !== "range" || anchor.from === anchor.to) return;
|
|
798
|
+
setDraft({ anchor, text: "" });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function submit() {
|
|
802
|
+
if (!draft) return;
|
|
803
|
+
editorRef.current?.addComment({ anchor: draft.anchor, body: draft.text });
|
|
804
|
+
setDraft(null);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return (
|
|
808
|
+
<>
|
|
809
|
+
<button onClick={openDraft}>Comment</button>
|
|
810
|
+
{draft && (
|
|
811
|
+
<dialog open>
|
|
812
|
+
<textarea value={draft.text} onChange={(e) => setDraft({ ...draft, text: e.target.value })} />
|
|
813
|
+
<button onClick={submit}>Add</button>
|
|
814
|
+
<button onClick={() => setDraft(null)}>Cancel</button>
|
|
815
|
+
</dialog>
|
|
816
|
+
)}
|
|
817
|
+
</>
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
#### Listen for comment and review-change events
|
|
823
|
+
|
|
824
|
+
```tsx
|
|
825
|
+
<WordReviewEditor
|
|
826
|
+
onEvent={(event) => {
|
|
827
|
+
switch (event.type) {
|
|
828
|
+
case "comment_added":
|
|
829
|
+
console.log("comment added:", event.commentId, event.anchor);
|
|
830
|
+
break;
|
|
831
|
+
case "comment_resolved":
|
|
832
|
+
console.log("comment resolved:", event.commentId);
|
|
833
|
+
break;
|
|
834
|
+
case "change_accepted":
|
|
835
|
+
console.log("change accepted:", event.changeId);
|
|
836
|
+
break;
|
|
837
|
+
case "change_rejected":
|
|
838
|
+
console.log("change rejected:", event.changeId);
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}}
|
|
842
|
+
/>
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
#### Resolve all open comments programmatically
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
const { threads } = editorRef.current.getComments();
|
|
849
|
+
for (const thread of threads) {
|
|
850
|
+
if (thread.status === "open") {
|
|
851
|
+
editorRef.current.resolveComment(thread.commentId);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
#### Accept or reject all actionable changes
|
|
857
|
+
|
|
858
|
+
```ts
|
|
859
|
+
editorRef.current.acceptAllChanges();
|
|
860
|
+
// or
|
|
861
|
+
editorRef.current.rejectAllChanges();
|
|
862
|
+
```
|