@beyondwork/docx-react-component 1.0.17 → 1.0.19
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 +8 -2
- package/package.json +32 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +487 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
forwardRef,
|
|
3
|
+
type FocusEvent,
|
|
4
|
+
useCallback,
|
|
3
5
|
useEffect,
|
|
4
6
|
useImperativeHandle,
|
|
5
7
|
useMemo,
|
|
@@ -10,41 +12,91 @@ import React, {
|
|
|
10
12
|
|
|
11
13
|
import type {
|
|
12
14
|
AutosaveState,
|
|
13
|
-
EditorDatastoreAdapter,
|
|
14
15
|
CompatibilityReport,
|
|
16
|
+
EditorDatastoreAdapter,
|
|
17
|
+
EditorHostAdapter,
|
|
18
|
+
EditorSessionState,
|
|
19
|
+
ExportResult,
|
|
15
20
|
EditorError,
|
|
21
|
+
EditorStoryTarget,
|
|
16
22
|
EditorWarning,
|
|
17
|
-
FormattingAlignment,
|
|
18
23
|
ExportDocxOptions,
|
|
24
|
+
FormattingAlignment,
|
|
25
|
+
FormattingStateSnapshot,
|
|
26
|
+
HeaderFooterLinkPatch,
|
|
19
27
|
InsertImageOptions,
|
|
20
28
|
InsertTableOptions,
|
|
29
|
+
PageLayoutSnapshot,
|
|
21
30
|
PersistedEditorSnapshot,
|
|
22
31
|
RuntimeRenderSnapshot,
|
|
32
|
+
SectionBreakType,
|
|
33
|
+
SectionLayoutPatch,
|
|
34
|
+
SectionPageNumberingPatch,
|
|
23
35
|
SearchOptions,
|
|
24
36
|
SearchResultSnapshot,
|
|
25
37
|
SelectionSnapshot as PublicSelectionSnapshot,
|
|
26
|
-
|
|
38
|
+
StyleCatalogSnapshot,
|
|
39
|
+
SurfaceBlockSnapshot,
|
|
40
|
+
ViewMode as EditorViewMode,
|
|
27
41
|
WordReviewEditorEvent,
|
|
28
42
|
WordReviewEditorProps,
|
|
29
43
|
WordReviewEditorRef,
|
|
44
|
+
WorkspaceMode,
|
|
45
|
+
ZoomLevel,
|
|
30
46
|
} from "../api/public-types";
|
|
47
|
+
import { editorSessionStateFromPersistedSnapshot } from "../api/session-state.ts";
|
|
31
48
|
import {
|
|
32
49
|
createDetachedAnchor,
|
|
33
50
|
createNodeAnchor,
|
|
34
51
|
createRangeAnchor,
|
|
52
|
+
storyTargetsEqual,
|
|
35
53
|
type TransactionMapping,
|
|
36
54
|
} from "../core/selection/mapping.ts";
|
|
37
55
|
import {
|
|
38
56
|
applyFormattingOperationToDocument,
|
|
39
57
|
getFormattingStateFromRenderSnapshot,
|
|
40
58
|
} from "../core/commands/formatting-commands.ts";
|
|
41
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
applyParagraphStyleToDocument,
|
|
61
|
+
applyTableStyleToDocument,
|
|
62
|
+
} from "../core/commands/style-commands.ts";
|
|
63
|
+
import {
|
|
64
|
+
continueNumbering as continueListNumbering,
|
|
65
|
+
backspaceAtListStart,
|
|
66
|
+
indentListItems,
|
|
67
|
+
outdentListItems,
|
|
68
|
+
restartNumbering as restartListNumbering,
|
|
69
|
+
splitListParagraph,
|
|
70
|
+
} from "../core/commands/list-commands.ts";
|
|
71
|
+
import {
|
|
72
|
+
resolveActiveParagraphIndex,
|
|
73
|
+
setActiveParagraphIndentation,
|
|
74
|
+
setActiveParagraphTabStops,
|
|
75
|
+
} from "../core/commands/paragraph-layout-commands.ts";
|
|
76
|
+
import {
|
|
77
|
+
deleteSectionBreakAtSectionIndex,
|
|
78
|
+
insertSectionBreakAfterSectionIndex,
|
|
79
|
+
setHeaderFooterLinkAtSectionIndex,
|
|
80
|
+
setSectionPageNumberingAtSectionIndex,
|
|
81
|
+
updateSectionLayoutAtSectionIndex,
|
|
82
|
+
} from "../core/commands/section-layout-commands.ts";
|
|
83
|
+
import {
|
|
84
|
+
insertImage as insertImageInDocument,
|
|
85
|
+
resizeImage as resizeImageInCatalog,
|
|
86
|
+
repositionFloatingImage as repositionFloatingImageInDocument,
|
|
87
|
+
} from "../core/commands/image-commands.ts";
|
|
42
88
|
import {
|
|
43
89
|
applyTableStructureOperation,
|
|
44
90
|
} from "../core/commands/table-structure-commands.ts";
|
|
45
91
|
import {
|
|
92
|
+
deleteSelectionOrBackward,
|
|
93
|
+
deleteSelectionOrForward,
|
|
94
|
+
insertHardBreak as insertHardBreakInDocument,
|
|
46
95
|
insertPageBreak as insertPageBreakInDocument,
|
|
96
|
+
insertTab as insertTabInDocument,
|
|
97
|
+
insertText as insertTextInDocument,
|
|
47
98
|
insertTable as insertTableInDocument,
|
|
99
|
+
splitParagraph as splitParagraphInDocument,
|
|
48
100
|
} from "../core/commands/text-commands.ts";
|
|
49
101
|
import {
|
|
50
102
|
createCanonicalDocumentId,
|
|
@@ -54,29 +106,38 @@ import {
|
|
|
54
106
|
createDocumentRuntime,
|
|
55
107
|
type DocumentRuntime,
|
|
56
108
|
} from "../runtime/document-runtime.ts";
|
|
109
|
+
import {
|
|
110
|
+
getStoryBlocks,
|
|
111
|
+
replaceStoryBlocks,
|
|
112
|
+
} from "../runtime/story-targeting.ts";
|
|
57
113
|
import { loadDocxEditorSession } from "../io/docx-session.ts";
|
|
58
114
|
import {
|
|
59
115
|
decodePersistedSourcePackageBytes,
|
|
60
116
|
hasValidPersistedSourcePackageDigest,
|
|
61
117
|
} from "../io/source-package-provenance.ts";
|
|
62
118
|
import { deriveCapabilities } from "../runtime/session-capabilities";
|
|
63
|
-
import {
|
|
64
|
-
createSearchExcerpt,
|
|
65
|
-
findSearchMatches,
|
|
66
|
-
} from "../ui-tailwind/editor-surface/search-plugin";
|
|
119
|
+
import { searchDocument } from "../runtime/document-search.ts";
|
|
67
120
|
import {
|
|
68
121
|
TwProseMirrorSurface,
|
|
69
122
|
type TwProseMirrorSurfaceRef,
|
|
70
123
|
} from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
|
|
71
124
|
import { TwReviewWorkspace } from "../ui-tailwind/tw-review-workspace";
|
|
72
125
|
import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail";
|
|
73
|
-
import type { ViewMode } from "../ui-tailwind/toolbar/tw-toolbar";
|
|
74
126
|
import type { MarkupDisplay } from "./headless/comment-decoration-model";
|
|
127
|
+
import type {
|
|
128
|
+
SelectionToolbarAnchor,
|
|
129
|
+
SelectionToolbarModel,
|
|
130
|
+
} from "./headless/selection-toolbar-model";
|
|
131
|
+
import {
|
|
132
|
+
downloadExportResult,
|
|
133
|
+
withExportDelivery,
|
|
134
|
+
} from "./browser-export";
|
|
75
135
|
|
|
76
136
|
interface ResolvedSource {
|
|
77
|
-
source: "docx" | "
|
|
137
|
+
source: "docx" | "session" | "snapshot";
|
|
78
138
|
sourceLabel?: string;
|
|
79
139
|
initialDocx?: Uint8Array | ArrayBuffer;
|
|
140
|
+
initialSessionState?: EditorSessionState;
|
|
80
141
|
initialSnapshot?: PersistedEditorSnapshot;
|
|
81
142
|
}
|
|
82
143
|
|
|
@@ -84,6 +145,11 @@ interface CreateRuntimeArgs {
|
|
|
84
145
|
documentId: string;
|
|
85
146
|
readOnly: boolean;
|
|
86
147
|
source: ResolvedSource;
|
|
148
|
+
initialViewState?: {
|
|
149
|
+
workspaceMode?: WorkspaceMode;
|
|
150
|
+
zoomLevel?: ZoomLevel;
|
|
151
|
+
};
|
|
152
|
+
hostAdapter?: EditorHostAdapter;
|
|
87
153
|
datastore?: EditorDatastoreAdapter;
|
|
88
154
|
currentUserId?: string;
|
|
89
155
|
}
|
|
@@ -129,6 +195,12 @@ const ACCESSIBLE_REGION_ORDER = [
|
|
|
129
195
|
|
|
130
196
|
type AccessibleRegionId = (typeof ACCESSIBLE_REGION_ORDER)[number];
|
|
131
197
|
|
|
198
|
+
type SelectionToolbarDismissReason =
|
|
199
|
+
| "blur"
|
|
200
|
+
| "chrome-action"
|
|
201
|
+
| "comment-action"
|
|
202
|
+
| "escape";
|
|
203
|
+
|
|
132
204
|
export function __createWordReviewEditorRefBridge(
|
|
133
205
|
runtime: WordReviewEditorRuntime,
|
|
134
206
|
mountedSurface?: TwProseMirrorSurfaceRef | null,
|
|
@@ -138,6 +210,7 @@ export function __createWordReviewEditorRefBridge(
|
|
|
138
210
|
blur: () => runtime.blur(),
|
|
139
211
|
undo: () => runtime.undo(),
|
|
140
212
|
redo: () => runtime.redo(),
|
|
213
|
+
replaceText: (text, target) => runtime.replaceText(text, target),
|
|
141
214
|
addComment: (params) => runtime.addComment(params),
|
|
142
215
|
openComment: (commentId) => runtime.openComment(commentId),
|
|
143
216
|
resolveComment: (commentId) => runtime.resolveComment(commentId),
|
|
@@ -152,6 +225,7 @@ export function __createWordReviewEditorRefBridge(
|
|
|
152
225
|
acceptAllChanges: () => runtime.acceptAllChanges(),
|
|
153
226
|
rejectAllChanges: () => runtime.rejectAllChanges(),
|
|
154
227
|
exportDocx: (options) => runtime.exportDocx(options),
|
|
228
|
+
getSessionState: () => runtime.getSessionState(),
|
|
155
229
|
getSnapshot: () => runtime.getPersistedSnapshot(),
|
|
156
230
|
getRenderSnapshot: () => clonePublicValue(runtime.getRenderSnapshot()),
|
|
157
231
|
getCompatibilityReport: () => runtime.getCompatibilityReport(),
|
|
@@ -165,7 +239,7 @@ export function __createWordReviewEditorRefBridge(
|
|
|
165
239
|
clonePublicValue(runtime.getRenderSnapshot().trackedChanges),
|
|
166
240
|
isDirty: () => runtime.getRenderSnapshot().isDirty,
|
|
167
241
|
getFormattingState: () => getFormattingStateFromRenderSnapshot(runtime.getRenderSnapshot()),
|
|
168
|
-
|
|
242
|
+
getStyleCatalog: () => getRuntimeStyleCatalog(runtime),
|
|
169
243
|
toggleBold: () => {
|
|
170
244
|
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "bold" });
|
|
171
245
|
},
|
|
@@ -214,6 +288,12 @@ export function __createWordReviewEditorRefBridge(
|
|
|
214
288
|
alignment,
|
|
215
289
|
});
|
|
216
290
|
},
|
|
291
|
+
setParagraphStyle: (styleId) => {
|
|
292
|
+
applyRuntimeParagraphStyle(runtime, styleId);
|
|
293
|
+
},
|
|
294
|
+
setTableStyle: (styleId) => {
|
|
295
|
+
applyRuntimeTableStyle(runtime, styleId);
|
|
296
|
+
},
|
|
217
297
|
indent: () => {
|
|
218
298
|
applyRuntimeFormattingOperation(runtime, { type: "indent" });
|
|
219
299
|
},
|
|
@@ -281,38 +361,71 @@ export function __createWordReviewEditorRefBridge(
|
|
|
281
361
|
});
|
|
282
362
|
},
|
|
283
363
|
search: (query, options) =>
|
|
284
|
-
mountedSurface
|
|
285
|
-
searchSnapshotSurface(runtime.getRenderSnapshot(), query, options),
|
|
364
|
+
searchRuntimeDocument(runtime, mountedSurface ?? null, query, options),
|
|
286
365
|
clearSearch: () => {
|
|
287
366
|
mountedSurface?.clearSearch();
|
|
288
367
|
},
|
|
289
368
|
setSelection: (selection) => {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
selection
|
|
293
|
-
|
|
294
|
-
),
|
|
295
|
-
});
|
|
369
|
+
applyRuntimeSelection(
|
|
370
|
+
runtime,
|
|
371
|
+
normalizeRequestedSelection(runtime.getRenderSnapshot(), selection),
|
|
372
|
+
);
|
|
296
373
|
},
|
|
297
374
|
scrollToRevision: (revisionId: string) => {
|
|
298
375
|
const revision = runtime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
299
376
|
(r) => r.revisionId === revisionId,
|
|
300
377
|
);
|
|
301
378
|
if (!revision || revision.anchor.kind === "detached") return;
|
|
302
|
-
runtime.
|
|
303
|
-
type: "selection.set",
|
|
304
|
-
selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(revision.anchor)),
|
|
305
|
-
});
|
|
379
|
+
applyRuntimeSelection(runtime, createSelectionFromAnchor(revision.anchor));
|
|
306
380
|
},
|
|
307
381
|
scrollToComment: (commentId: string) => {
|
|
308
382
|
const comment = runtime.getRenderSnapshot().comments.threads.find(
|
|
309
383
|
(t) => t.commentId === commentId,
|
|
310
384
|
);
|
|
311
385
|
if (!comment || comment.anchor.kind === "detached") return;
|
|
312
|
-
runtime.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
386
|
+
applyRuntimeSelection(runtime, createSelectionFromAnchor(comment.anchor));
|
|
387
|
+
},
|
|
388
|
+
openStory: (target: EditorStoryTarget) => {
|
|
389
|
+
runtime.openStory(target);
|
|
390
|
+
},
|
|
391
|
+
closeStory: () => {
|
|
392
|
+
runtime.closeStory();
|
|
393
|
+
},
|
|
394
|
+
getPageLayoutSnapshot: () => {
|
|
395
|
+
return runtime.getPageLayoutSnapshot();
|
|
396
|
+
},
|
|
397
|
+
getDocumentNavigationSnapshot: () => {
|
|
398
|
+
return runtime.getDocumentNavigationSnapshot();
|
|
399
|
+
},
|
|
400
|
+
getViewState: () => {
|
|
401
|
+
return runtime.getViewState();
|
|
402
|
+
},
|
|
403
|
+
setWorkspaceMode: (mode) => {
|
|
404
|
+
runtime.setWorkspaceMode(mode);
|
|
405
|
+
},
|
|
406
|
+
setZoom: (level) => {
|
|
407
|
+
runtime.setZoom(level);
|
|
408
|
+
},
|
|
409
|
+
insertSectionBreak: (type, options) => {
|
|
410
|
+
applyRuntimeInsertSectionBreak(runtime, type, options);
|
|
411
|
+
},
|
|
412
|
+
deleteSectionBreak: (sectionIndex) => {
|
|
413
|
+
applyRuntimeDeleteSectionBreak(runtime, sectionIndex);
|
|
414
|
+
},
|
|
415
|
+
updateSectionLayout: (sectionIndex, patch) => {
|
|
416
|
+
applyRuntimeUpdateSectionLayout(runtime, sectionIndex, patch);
|
|
417
|
+
},
|
|
418
|
+
setSectionPageNumbering: (sectionIndex, patch) => {
|
|
419
|
+
applyRuntimeSetSectionPageNumbering(runtime, sectionIndex, patch);
|
|
420
|
+
},
|
|
421
|
+
setHeaderFooterLink: (sectionIndex, params) => {
|
|
422
|
+
applyRuntimeSetHeaderFooterLink(runtime, sectionIndex, params);
|
|
423
|
+
},
|
|
424
|
+
setImageLayout: (mediaId, dimensions) => {
|
|
425
|
+
applyRuntimeImageResize(runtime, mediaId, dimensions);
|
|
426
|
+
},
|
|
427
|
+
setImageFrame: (mediaId, offsets) => {
|
|
428
|
+
applyRuntimeImageReposition(runtime, mediaId, offsets);
|
|
316
429
|
},
|
|
317
430
|
};
|
|
318
431
|
}
|
|
@@ -321,31 +434,61 @@ export async function __resolveWordReviewEditorSource(
|
|
|
321
434
|
props: Pick<
|
|
322
435
|
WordReviewEditorProps,
|
|
323
436
|
| "documentId"
|
|
437
|
+
| "hostAdapter"
|
|
324
438
|
| "datastore"
|
|
325
439
|
| "externalDocSource"
|
|
326
440
|
| "initialDocx"
|
|
441
|
+
| "initialSessionState"
|
|
327
442
|
| "initialSnapshot"
|
|
328
443
|
| "initialSourceLabel"
|
|
444
|
+
| "loadRevision"
|
|
445
|
+
| "loadSourcePolicy"
|
|
329
446
|
>,
|
|
330
447
|
): Promise<ResolvedSource> {
|
|
331
448
|
const explicitInitialCount =
|
|
332
|
-
Number(Boolean(props.initialDocx)) +
|
|
449
|
+
Number(Boolean(props.initialDocx)) +
|
|
450
|
+
Number(Boolean(props.initialSessionState)) +
|
|
451
|
+
Number(Boolean(props.initialSnapshot));
|
|
333
452
|
if (explicitInitialCount > 1) {
|
|
334
|
-
throw new Error(
|
|
453
|
+
throw new Error(
|
|
454
|
+
"Provide exactly one of initialDocx, initialSessionState, or initialSnapshot.",
|
|
455
|
+
);
|
|
335
456
|
}
|
|
336
457
|
|
|
337
458
|
if (props.externalDocSource) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
459
|
+
if (props.externalDocSource.kind === "docx") {
|
|
460
|
+
return {
|
|
461
|
+
source: "docx",
|
|
462
|
+
initialDocx: props.externalDocSource.bytes,
|
|
463
|
+
sourceLabel: props.externalDocSource.sourceLabel,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (props.externalDocSource.kind === "session") {
|
|
468
|
+
return {
|
|
469
|
+
source: "session",
|
|
470
|
+
initialSessionState: props.externalDocSource.sessionState,
|
|
471
|
+
sourceLabel:
|
|
472
|
+
props.externalDocSource.sourceLabel ??
|
|
473
|
+
props.externalDocSource.sessionState.sourcePackage?.sourceLabel,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
source: "snapshot",
|
|
479
|
+
initialSnapshot: props.externalDocSource.snapshot,
|
|
480
|
+
sourceLabel: props.externalDocSource.sourceLabel,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (props.initialSessionState) {
|
|
485
|
+
return {
|
|
486
|
+
source: "session",
|
|
487
|
+
initialSessionState: props.initialSessionState,
|
|
488
|
+
sourceLabel:
|
|
489
|
+
props.initialSourceLabel ??
|
|
490
|
+
props.initialSessionState.sourcePackage?.sourceLabel,
|
|
491
|
+
};
|
|
349
492
|
}
|
|
350
493
|
|
|
351
494
|
if (props.initialSnapshot) {
|
|
@@ -364,31 +507,48 @@ export async function __resolveWordReviewEditorSource(
|
|
|
364
507
|
};
|
|
365
508
|
}
|
|
366
509
|
|
|
367
|
-
|
|
510
|
+
const loader = props.hostAdapter?.load ?? props.datastore?.load;
|
|
511
|
+
if (!loader) {
|
|
368
512
|
throw new Error(
|
|
369
|
-
`WordReviewEditor ${props.documentId} needs initialDocx, initialSnapshot, or datastore
|
|
513
|
+
`WordReviewEditor ${props.documentId} needs initialDocx, initialSessionState, initialSnapshot, or a host/datastore load source.`,
|
|
370
514
|
);
|
|
371
515
|
}
|
|
372
516
|
|
|
373
|
-
const loadResult = await
|
|
517
|
+
const loadResult = await loader({
|
|
374
518
|
documentId: props.documentId,
|
|
519
|
+
loadRevision: props.loadRevision,
|
|
520
|
+
loadSourcePolicy: props.loadSourcePolicy,
|
|
375
521
|
});
|
|
376
522
|
|
|
377
523
|
if (!loadResult.source) {
|
|
378
|
-
throw new Error(
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Host or datastore loader did not return a loadable source for ${props.documentId}.`,
|
|
526
|
+
);
|
|
379
527
|
}
|
|
380
528
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
529
|
+
if (loadResult.source.kind === "docx") {
|
|
530
|
+
return {
|
|
531
|
+
source: "docx",
|
|
532
|
+
initialDocx: loadResult.source.bytes,
|
|
533
|
+
sourceLabel: loadResult.source.sourceLabel,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (loadResult.source.kind === "session") {
|
|
538
|
+
return {
|
|
539
|
+
source: "session",
|
|
540
|
+
initialSessionState: loadResult.source.sessionState,
|
|
541
|
+
sourceLabel:
|
|
542
|
+
loadResult.source.sourceLabel ??
|
|
543
|
+
loadResult.source.sessionState.sourcePackage?.sourceLabel,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
source: "snapshot",
|
|
549
|
+
initialSnapshot: loadResult.source.snapshot,
|
|
550
|
+
sourceLabel: loadResult.source.sourceLabel,
|
|
551
|
+
};
|
|
392
552
|
}
|
|
393
553
|
|
|
394
554
|
export function __createFallbackRuntime(args: CreateRuntimeArgs): WordReviewEditorRuntime {
|
|
@@ -408,34 +568,39 @@ function createRuntime(
|
|
|
408
568
|
})
|
|
409
569
|
: undefined;
|
|
410
570
|
const snapshotExportResolution = !args.source.initialDocx
|
|
411
|
-
?
|
|
571
|
+
? resolvePackageBackedExportSession(args)
|
|
412
572
|
: undefined;
|
|
413
|
-
const
|
|
414
|
-
args.source.
|
|
415
|
-
docxSession?.
|
|
416
|
-
|
|
417
|
-
args.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
573
|
+
const initialSessionState =
|
|
574
|
+
args.source.initialSessionState ??
|
|
575
|
+
docxSession?.initialSessionState ??
|
|
576
|
+
(args.source.initialSnapshot
|
|
577
|
+
? editorSessionStateFromPersistedSnapshot(args.source.initialSnapshot)
|
|
578
|
+
: editorSessionStateFromPersistedSnapshot(
|
|
579
|
+
createFallbackPersistedSnapshot(
|
|
580
|
+
args.documentId,
|
|
581
|
+
args.source.sourceLabel ?? "Generated shell snapshot",
|
|
582
|
+
),
|
|
583
|
+
));
|
|
584
|
+
const runtimeSessionState = snapshotExportResolution?.barrier
|
|
585
|
+
? applySessionExportBarrier(initialSessionState, snapshotExportResolution.barrier)
|
|
586
|
+
: initialSessionState;
|
|
423
587
|
|
|
424
588
|
return createDocumentRuntime({
|
|
425
589
|
documentId: args.documentId,
|
|
426
|
-
|
|
590
|
+
initialSessionState: runtimeSessionState,
|
|
427
591
|
sourceKind: args.source.source,
|
|
428
592
|
sourceLabel: args.source.sourceLabel,
|
|
593
|
+
initialViewState: args.initialViewState,
|
|
429
594
|
readOnly: args.readOnly || docxSession?.readOnly,
|
|
430
|
-
editorBuild:
|
|
595
|
+
editorBuild: runtimeSessionState.editorBuild,
|
|
431
596
|
fatalError: docxSession?.fatalError,
|
|
432
|
-
exportDocx: async (
|
|
597
|
+
exportDocx: async (sessionState, options) => {
|
|
433
598
|
if (docxSession) {
|
|
434
|
-
return docxSession.exportDocx(
|
|
599
|
+
return docxSession.exportDocx(sessionState, options);
|
|
435
600
|
}
|
|
436
601
|
|
|
437
602
|
if (snapshotExportResolution?.session) {
|
|
438
|
-
return snapshotExportResolution.session.exportDocx(
|
|
603
|
+
return snapshotExportResolution.session.exportDocx(sessionState, options);
|
|
439
604
|
}
|
|
440
605
|
|
|
441
606
|
throw createSnapshotExportBlockedError(
|
|
@@ -443,7 +608,7 @@ function createRuntime(
|
|
|
443
608
|
snapshotExportResolution?.barrier ?? {
|
|
444
609
|
reason: "missing_source_package_provenance",
|
|
445
610
|
message:
|
|
446
|
-
"DOCX export is blocked because this
|
|
611
|
+
"DOCX export is blocked because this session does not carry embedded source package provenance.",
|
|
447
612
|
},
|
|
448
613
|
);
|
|
449
614
|
},
|
|
@@ -457,11 +622,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
457
622
|
function WordReviewEditor(props, ref) {
|
|
458
623
|
const {
|
|
459
624
|
currentUser,
|
|
625
|
+
hostAdapter,
|
|
460
626
|
datastore,
|
|
461
627
|
documentId,
|
|
462
628
|
externalDocSource,
|
|
463
629
|
externalDocumentRevision,
|
|
464
630
|
initialDocx,
|
|
631
|
+
loadRevision,
|
|
632
|
+
loadSourcePolicy = "prefer-saved-state",
|
|
633
|
+
initialSessionState,
|
|
465
634
|
initialSnapshot,
|
|
466
635
|
initialSourceLabel,
|
|
467
636
|
markupDisplay = "simple",
|
|
@@ -475,24 +644,36 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
475
644
|
|
|
476
645
|
const [runtime, setRuntime] = useState<WordReviewEditorRuntime | null>(null);
|
|
477
646
|
const [loadError, setLoadError] = useState<EditorError | null>(null);
|
|
478
|
-
const [viewMode, setViewMode] = useState<ViewMode>("canvas");
|
|
479
|
-
const liveMarkupDisplay: MarkupDisplay = viewMode === "document" ? "all" : "clean";
|
|
480
647
|
const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
|
|
481
648
|
const [showTrackedChanges, setShowTrackedChanges] = useState(false);
|
|
482
649
|
const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
|
|
650
|
+
const [selectionToolbarAnchor, setSelectionToolbarAnchor] = useState<SelectionToolbarAnchor | null>(null);
|
|
651
|
+
const [selectionToolbarDismissedKey, setSelectionToolbarDismissedKey] = useState<string | null>(null);
|
|
652
|
+
const [selectionToolbarFocusWithin, setSelectionToolbarFocusWithin] = useState(false);
|
|
483
653
|
const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
|
|
484
654
|
const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
|
|
655
|
+
const selectionToolbarElementRef = useRef<HTMLDivElement | null>(null);
|
|
485
656
|
const shellRef = useRef<HTMLDivElement | null>(null);
|
|
657
|
+
const lastSelectionToolbarKeyRef = useRef<string | null>(null);
|
|
486
658
|
const lastAnnouncedErrorIdRef = useRef<string | null>(null);
|
|
487
659
|
const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
488
660
|
const lastSavedRevisionTokenRef = useRef<string | null>(null);
|
|
661
|
+
const hostAdapterRef = useRef(hostAdapter);
|
|
489
662
|
const datastoreRef = useRef(datastore);
|
|
490
663
|
const onEventRef = useRef(onEvent);
|
|
491
664
|
const onWarningRef = useRef(onWarning);
|
|
492
665
|
const onErrorRef = useRef(onError);
|
|
666
|
+
const runtimeViewStateSeedRef = useRef<{
|
|
667
|
+
workspaceMode: WorkspaceMode;
|
|
668
|
+
zoomLevel: ZoomLevel;
|
|
669
|
+
}>({
|
|
670
|
+
workspaceMode: "canvas",
|
|
671
|
+
zoomLevel: 100,
|
|
672
|
+
});
|
|
493
673
|
const initialSourceRef = useRef<{
|
|
494
674
|
documentId: string;
|
|
495
675
|
initialDocx?: Uint8Array | ArrayBuffer;
|
|
676
|
+
initialSessionState?: EditorSessionState;
|
|
496
677
|
initialSnapshot?: PersistedEditorSnapshot;
|
|
497
678
|
initialSourceLabel?: string;
|
|
498
679
|
} | null>(null);
|
|
@@ -501,6 +682,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
501
682
|
initialSourceRef.current = {
|
|
502
683
|
documentId,
|
|
503
684
|
initialDocx,
|
|
685
|
+
initialSessionState,
|
|
504
686
|
initialSnapshot,
|
|
505
687
|
initialSourceLabel,
|
|
506
688
|
};
|
|
@@ -509,18 +691,23 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
509
691
|
const stableInitialSource = initialSourceRef.current;
|
|
510
692
|
const sourceReloadKey = externalDocSource
|
|
511
693
|
? `external:${externalDocSource.kind}:${externalDocumentRevision ?? "static"}`
|
|
694
|
+
: stableInitialSource?.initialSessionState
|
|
695
|
+
? "initial-session"
|
|
512
696
|
: stableInitialSource?.initialSnapshot
|
|
513
697
|
? "initial-snapshot"
|
|
514
698
|
: stableInitialSource?.initialDocx
|
|
515
699
|
? "initial-docx"
|
|
516
|
-
:
|
|
700
|
+
: hostAdapter
|
|
701
|
+
? `host-adapter:${loadRevision ?? "static"}`
|
|
702
|
+
: `datastore:${loadRevision ?? "static"}`;
|
|
517
703
|
|
|
518
704
|
useEffect(() => {
|
|
705
|
+
hostAdapterRef.current = hostAdapter;
|
|
519
706
|
datastoreRef.current = datastore;
|
|
520
707
|
onEventRef.current = onEvent;
|
|
521
708
|
onWarningRef.current = onWarning;
|
|
522
709
|
onErrorRef.current = onError;
|
|
523
|
-
}, [datastore, onError, onEvent, onWarning]);
|
|
710
|
+
}, [datastore, hostAdapter, onError, onEvent, onWarning]);
|
|
524
711
|
|
|
525
712
|
useEffect(() => {
|
|
526
713
|
let cancelled = false;
|
|
@@ -531,11 +718,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
531
718
|
try {
|
|
532
719
|
const source = await __resolveWordReviewEditorSource({
|
|
533
720
|
documentId,
|
|
721
|
+
hostAdapter: hostAdapterRef.current,
|
|
534
722
|
datastore: datastoreRef.current,
|
|
535
723
|
externalDocSource,
|
|
536
724
|
initialDocx: stableInitialSource?.initialDocx,
|
|
725
|
+
initialSessionState: stableInitialSource?.initialSessionState,
|
|
537
726
|
initialSnapshot: stableInitialSource?.initialSnapshot,
|
|
538
727
|
initialSourceLabel: stableInitialSource?.initialSourceLabel,
|
|
728
|
+
loadRevision,
|
|
729
|
+
loadSourcePolicy,
|
|
539
730
|
});
|
|
540
731
|
|
|
541
732
|
if (cancelled) {
|
|
@@ -548,6 +739,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
548
739
|
documentId,
|
|
549
740
|
readOnly,
|
|
550
741
|
source,
|
|
742
|
+
initialViewState: runtimeViewStateSeedRef.current,
|
|
743
|
+
hostAdapter: hostAdapterRef.current,
|
|
551
744
|
datastore: datastoreRef.current,
|
|
552
745
|
currentUserId: currentUser.userId,
|
|
553
746
|
},
|
|
@@ -557,6 +750,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
557
750
|
},
|
|
558
751
|
);
|
|
559
752
|
emitEditorEvent({
|
|
753
|
+
hostAdapter: hostAdapterRef.current,
|
|
560
754
|
datastore: datastoreRef.current,
|
|
561
755
|
onEvent: onEventRef.current,
|
|
562
756
|
event: createReadyEvent(nextRuntime, source.source),
|
|
@@ -572,6 +766,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
572
766
|
setLoadError(normalized);
|
|
573
767
|
onErrorRef.current?.(normalized);
|
|
574
768
|
emitEditorEvent({
|
|
769
|
+
hostAdapter: hostAdapterRef.current,
|
|
575
770
|
datastore: datastoreRef.current,
|
|
576
771
|
onEvent: onEventRef.current,
|
|
577
772
|
event: {
|
|
@@ -600,7 +795,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
600
795
|
}
|
|
601
796
|
|
|
602
797
|
return runtime.subscribeToEvents((event) => {
|
|
798
|
+
if (event.type === "export_completed" || event.type === "ready") {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
603
801
|
emitEditorEvent({
|
|
802
|
+
hostAdapter: hostAdapterRef.current,
|
|
604
803
|
datastore: datastoreRef.current,
|
|
605
804
|
onEvent: onEventRef.current,
|
|
606
805
|
event,
|
|
@@ -625,20 +824,37 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
625
824
|
documentId,
|
|
626
825
|
readOnly,
|
|
627
826
|
currentUserId: currentUser.userId,
|
|
827
|
+
initialViewState: runtimeViewStateSeedRef.current,
|
|
628
828
|
source: {
|
|
629
|
-
source:
|
|
829
|
+
source:
|
|
830
|
+
initialSessionState
|
|
831
|
+
? "session"
|
|
832
|
+
: "snapshot",
|
|
833
|
+
initialSessionState:
|
|
834
|
+
initialSessionState ??
|
|
835
|
+
(initialSnapshot
|
|
836
|
+
? editorSessionStateFromPersistedSnapshot(initialSnapshot)
|
|
837
|
+
: undefined),
|
|
630
838
|
initialSnapshot:
|
|
631
839
|
initialSnapshot ?? createFallbackPersistedSnapshot(documentId, initialSourceLabel),
|
|
632
|
-
sourceLabel: guessSourceLabel(
|
|
840
|
+
sourceLabel: guessSourceLabel(
|
|
841
|
+
initialSourceLabel,
|
|
842
|
+
initialSessionState,
|
|
843
|
+
initialSnapshot,
|
|
844
|
+
externalDocSource,
|
|
845
|
+
),
|
|
633
846
|
},
|
|
847
|
+
hostAdapter: hostAdapterRef.current,
|
|
634
848
|
datastore: datastoreRef.current,
|
|
635
849
|
}),
|
|
636
850
|
[
|
|
637
851
|
currentUser.userId,
|
|
638
852
|
documentId,
|
|
853
|
+
initialSessionState,
|
|
639
854
|
initialSnapshot,
|
|
640
855
|
initialSourceLabel,
|
|
641
856
|
readOnly,
|
|
857
|
+
hostAdapter,
|
|
642
858
|
externalDocSource?.kind,
|
|
643
859
|
externalDocSource?.sourceLabel,
|
|
644
860
|
],
|
|
@@ -651,11 +867,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
651
867
|
: createLoadingSnapshot(
|
|
652
868
|
documentId,
|
|
653
869
|
readOnly,
|
|
654
|
-
guessSourceLabel(
|
|
870
|
+
guessSourceLabel(
|
|
871
|
+
initialSourceLabel,
|
|
872
|
+
initialSessionState,
|
|
873
|
+
initialSnapshot,
|
|
874
|
+
externalDocSource,
|
|
875
|
+
),
|
|
655
876
|
),
|
|
656
877
|
[
|
|
657
878
|
documentId,
|
|
658
879
|
externalDocSource,
|
|
880
|
+
initialSessionState,
|
|
659
881
|
initialSnapshot,
|
|
660
882
|
initialSourceLabel,
|
|
661
883
|
loadError,
|
|
@@ -670,6 +892,23 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
670
892
|
);
|
|
671
893
|
|
|
672
894
|
const activeRuntime = runtime ?? optimisticRuntime;
|
|
895
|
+
const viewState = activeRuntime.getViewState();
|
|
896
|
+
const isPageWorkspace = viewState.workspaceMode === "page";
|
|
897
|
+
const liveMarkupDisplay: MarkupDisplay = isPageWorkspace ? "all" : "clean";
|
|
898
|
+
const canonicalDocument = activeRuntime.getSessionState().canonicalDocument;
|
|
899
|
+
const documentNavigation = activeRuntime.getDocumentNavigationSnapshot();
|
|
900
|
+
const effectiveViewMode = deriveEditorViewMode(snapshot.readOnly, reviewMode);
|
|
901
|
+
|
|
902
|
+
useEffect(() => {
|
|
903
|
+
activeRuntime.setViewMode(effectiveViewMode);
|
|
904
|
+
}, [activeRuntime, effectiveViewMode]);
|
|
905
|
+
|
|
906
|
+
useEffect(() => {
|
|
907
|
+
runtimeViewStateSeedRef.current = {
|
|
908
|
+
workspaceMode: viewState.workspaceMode,
|
|
909
|
+
zoomLevel: viewState.zoomLevel,
|
|
910
|
+
};
|
|
911
|
+
}, [viewState.workspaceMode, viewState.zoomLevel]);
|
|
673
912
|
|
|
674
913
|
useImperativeHandle(
|
|
675
914
|
ref,
|
|
@@ -678,6 +917,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
678
917
|
blur: () => activeRuntime.blur(),
|
|
679
918
|
undo: () => activeRuntime.undo(),
|
|
680
919
|
redo: () => activeRuntime.redo(),
|
|
920
|
+
replaceText: (text, target) => activeRuntime.replaceText(text, target),
|
|
681
921
|
addComment: (params) =>
|
|
682
922
|
activeRuntime.addComment({
|
|
683
923
|
...params,
|
|
@@ -700,6 +940,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
700
940
|
exportDocx: (options) =>
|
|
701
941
|
runtime
|
|
702
942
|
? persistAndExport({
|
|
943
|
+
hostAdapter: hostAdapterRef.current,
|
|
703
944
|
datastore: datastoreRef.current,
|
|
704
945
|
documentId,
|
|
705
946
|
runtime,
|
|
@@ -711,10 +952,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
711
952
|
})
|
|
712
953
|
: rejectExportWhileLoading({
|
|
713
954
|
documentId,
|
|
955
|
+
hostAdapter: hostAdapterRef.current,
|
|
714
956
|
datastore: datastoreRef.current,
|
|
715
957
|
onError: onErrorRef.current,
|
|
716
958
|
onEvent: onEventRef.current,
|
|
717
959
|
}),
|
|
960
|
+
getSessionState: () => activeRuntime.getSessionState(),
|
|
718
961
|
getSnapshot: () => activeRuntime.getPersistedSnapshot(),
|
|
719
962
|
getRenderSnapshot: () => clonePublicValue(activeRuntime.getRenderSnapshot()),
|
|
720
963
|
getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
|
|
@@ -730,7 +973,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
730
973
|
isDirty: () => activeRuntime.getRenderSnapshot().isDirty,
|
|
731
974
|
getFormattingState: () =>
|
|
732
975
|
getFormattingStateFromRenderSnapshot(activeRuntime.getRenderSnapshot()),
|
|
733
|
-
|
|
976
|
+
getStyleCatalog: () => getRuntimeStyleCatalog(activeRuntime),
|
|
734
977
|
toggleBold: () => {
|
|
735
978
|
applyRuntimeFormattingOperation(activeRuntime, {
|
|
736
979
|
type: "toggle",
|
|
@@ -797,6 +1040,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
797
1040
|
alignment,
|
|
798
1041
|
});
|
|
799
1042
|
},
|
|
1043
|
+
setParagraphStyle: (styleId) => {
|
|
1044
|
+
applyRuntimeParagraphStyle(activeRuntime, styleId);
|
|
1045
|
+
},
|
|
1046
|
+
setTableStyle: (styleId) => {
|
|
1047
|
+
applyRuntimeTableStyle(activeRuntime, styleId);
|
|
1048
|
+
},
|
|
800
1049
|
indent: () => {
|
|
801
1050
|
applyRuntimeFormattingOperation(activeRuntime, { type: "indent" });
|
|
802
1051
|
},
|
|
@@ -864,45 +1113,85 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
864
1113
|
});
|
|
865
1114
|
},
|
|
866
1115
|
search: (query, options) =>
|
|
867
|
-
|
|
868
|
-
|
|
1116
|
+
searchRuntimeDocument(
|
|
1117
|
+
activeRuntime,
|
|
1118
|
+
surfaceRef.current,
|
|
1119
|
+
query,
|
|
1120
|
+
options,
|
|
1121
|
+
),
|
|
869
1122
|
clearSearch: () => {
|
|
870
1123
|
surfaceRef.current?.clearSearch();
|
|
871
1124
|
},
|
|
872
1125
|
setSelection: (selection) => {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
selection
|
|
876
|
-
|
|
877
|
-
),
|
|
878
|
-
});
|
|
1126
|
+
applyRuntimeSelection(
|
|
1127
|
+
activeRuntime,
|
|
1128
|
+
normalizeRequestedSelection(activeRuntime.getRenderSnapshot(), selection),
|
|
1129
|
+
);
|
|
879
1130
|
},
|
|
880
1131
|
scrollToRevision: (revisionId: string) => {
|
|
881
1132
|
const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
882
1133
|
(r) => r.revisionId === revisionId,
|
|
883
1134
|
);
|
|
884
1135
|
if (!revision || revision.anchor.kind === "detached") return;
|
|
885
|
-
activeRuntime.
|
|
886
|
-
type: "selection.set",
|
|
887
|
-
selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(revision.anchor)),
|
|
888
|
-
});
|
|
1136
|
+
applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(revision.anchor));
|
|
889
1137
|
},
|
|
890
1138
|
scrollToComment: (commentId: string) => {
|
|
891
1139
|
const comment = activeRuntime.getRenderSnapshot().comments.threads.find(
|
|
892
1140
|
(t) => t.commentId === commentId,
|
|
893
1141
|
);
|
|
894
1142
|
if (!comment || comment.anchor.kind === "detached") return;
|
|
895
|
-
activeRuntime.
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1143
|
+
applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(comment.anchor));
|
|
1144
|
+
},
|
|
1145
|
+
openStory: (target: EditorStoryTarget) => {
|
|
1146
|
+
activeRuntime.openStory(target);
|
|
1147
|
+
},
|
|
1148
|
+
closeStory: () => {
|
|
1149
|
+
activeRuntime.closeStory();
|
|
1150
|
+
},
|
|
1151
|
+
getPageLayoutSnapshot: () => {
|
|
1152
|
+
return activeRuntime.getPageLayoutSnapshot();
|
|
1153
|
+
},
|
|
1154
|
+
getDocumentNavigationSnapshot: () => {
|
|
1155
|
+
return activeRuntime.getDocumentNavigationSnapshot();
|
|
1156
|
+
},
|
|
1157
|
+
getViewState: () => {
|
|
1158
|
+
return activeRuntime.getViewState();
|
|
1159
|
+
},
|
|
1160
|
+
setWorkspaceMode: (mode) => {
|
|
1161
|
+
activeRuntime.setWorkspaceMode(mode);
|
|
1162
|
+
},
|
|
1163
|
+
setZoom: (level) => {
|
|
1164
|
+
activeRuntime.setZoom(level);
|
|
1165
|
+
},
|
|
1166
|
+
insertSectionBreak: (type, options) => {
|
|
1167
|
+
applyRuntimeInsertSectionBreak(activeRuntime, type, options);
|
|
1168
|
+
},
|
|
1169
|
+
deleteSectionBreak: (sectionIndex) => {
|
|
1170
|
+
applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex);
|
|
1171
|
+
},
|
|
1172
|
+
updateSectionLayout: (sectionIndex, patch) => {
|
|
1173
|
+
applyRuntimeUpdateSectionLayout(activeRuntime, sectionIndex, patch);
|
|
1174
|
+
},
|
|
1175
|
+
setSectionPageNumbering: (sectionIndex, patch) => {
|
|
1176
|
+
applyRuntimeSetSectionPageNumbering(activeRuntime, sectionIndex, patch);
|
|
1177
|
+
},
|
|
1178
|
+
setHeaderFooterLink: (sectionIndex, params) => {
|
|
1179
|
+
applyRuntimeSetHeaderFooterLink(activeRuntime, sectionIndex, params);
|
|
1180
|
+
},
|
|
1181
|
+
setImageLayout: (mediaId, dimensions) => {
|
|
1182
|
+
applyRuntimeImageResize(activeRuntime, mediaId, dimensions);
|
|
1183
|
+
},
|
|
1184
|
+
setImageFrame: (mediaId, offsets) => {
|
|
1185
|
+
applyRuntimeImageReposition(activeRuntime, mediaId, offsets);
|
|
899
1186
|
},
|
|
900
1187
|
}),
|
|
901
1188
|
[activeRuntime, currentUser.userId, documentId, runtime],
|
|
902
1189
|
);
|
|
903
1190
|
|
|
904
1191
|
useEffect(() => {
|
|
905
|
-
|
|
1192
|
+
const canPersist =
|
|
1193
|
+
Boolean(hostAdapterRef.current?.saveSession) || Boolean(datastoreRef.current?.saveSnapshot);
|
|
1194
|
+
if (!canPersist || props.autosave?.enabled === false || !runtime || readOnly) {
|
|
906
1195
|
if (autosaveTimerRef.current) {
|
|
907
1196
|
clearTimeout(autosaveTimerRef.current);
|
|
908
1197
|
autosaveTimerRef.current = null;
|
|
@@ -924,7 +1213,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
924
1213
|
|
|
925
1214
|
const debounceMs = props.autosave?.debounceMs ?? 800;
|
|
926
1215
|
if (debounceMs <= 0) {
|
|
927
|
-
void
|
|
1216
|
+
void persistSession({
|
|
1217
|
+
hostAdapter: hostAdapterRef.current,
|
|
928
1218
|
datastore: datastoreRef.current,
|
|
929
1219
|
documentId,
|
|
930
1220
|
runtime,
|
|
@@ -937,7 +1227,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
937
1227
|
}
|
|
938
1228
|
|
|
939
1229
|
autosaveTimerRef.current = setTimeout(() => {
|
|
940
|
-
void
|
|
1230
|
+
void persistSession({
|
|
1231
|
+
hostAdapter: hostAdapterRef.current,
|
|
941
1232
|
datastore: datastoreRef.current,
|
|
942
1233
|
documentId,
|
|
943
1234
|
runtime,
|
|
@@ -989,29 +1280,29 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
989
1280
|
if (anchor.kind === "detached") {
|
|
990
1281
|
return;
|
|
991
1282
|
}
|
|
992
|
-
|
|
993
|
-
activeRuntime.dispatch({
|
|
994
|
-
type: "selection.set",
|
|
995
|
-
selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(anchor)),
|
|
996
|
-
});
|
|
1283
|
+
applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(anchor));
|
|
997
1284
|
}
|
|
998
1285
|
|
|
999
|
-
function addReviewComment():
|
|
1286
|
+
function addReviewComment(): string | null {
|
|
1000
1287
|
try {
|
|
1001
|
-
activeRuntime.addComment({
|
|
1288
|
+
const commentId = activeRuntime.addComment({
|
|
1002
1289
|
anchor: snapshot.selection.activeRange,
|
|
1003
|
-
body: "
|
|
1290
|
+
body: "",
|
|
1004
1291
|
authorId: currentUser.userId,
|
|
1005
1292
|
});
|
|
1293
|
+
activeRuntime.openComment(commentId);
|
|
1006
1294
|
setActiveRailTab("comments");
|
|
1295
|
+
return commentId;
|
|
1007
1296
|
} catch {
|
|
1008
1297
|
// Runtime already emitted a concrete export-safety error for invalid anchors.
|
|
1298
|
+
return null;
|
|
1009
1299
|
}
|
|
1010
1300
|
}
|
|
1011
1301
|
|
|
1012
1302
|
function exportCurrentDocument(): void {
|
|
1013
1303
|
void (runtime
|
|
1014
1304
|
? persistAndExport({
|
|
1305
|
+
hostAdapter: hostAdapterRef.current,
|
|
1015
1306
|
datastore: datastoreRef.current,
|
|
1016
1307
|
documentId,
|
|
1017
1308
|
runtime,
|
|
@@ -1022,31 +1313,181 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1022
1313
|
})
|
|
1023
1314
|
: rejectExportWhileLoading({
|
|
1024
1315
|
documentId,
|
|
1316
|
+
hostAdapter: hostAdapterRef.current,
|
|
1025
1317
|
datastore: datastoreRef.current,
|
|
1026
1318
|
onError: onErrorRef.current,
|
|
1027
1319
|
onEvent: onEventRef.current,
|
|
1028
1320
|
}));
|
|
1029
1321
|
}
|
|
1030
1322
|
|
|
1031
|
-
const selectionPreview = summarizeSelectionPreview(snapshot);
|
|
1032
1323
|
const derivedCapabilities = deriveCapabilities(snapshot, reviewMode);
|
|
1033
1324
|
const capabilities = showReviewPanel
|
|
1034
1325
|
? derivedCapabilities
|
|
1035
1326
|
: { ...derivedCapabilities, reviewRailVisible: false };
|
|
1327
|
+
const formattingState = getFormattingStateFromRenderSnapshot(snapshot);
|
|
1328
|
+
const styleCatalog = useMemo(
|
|
1329
|
+
() => getRuntimeStyleCatalog(activeRuntime),
|
|
1330
|
+
[activeRuntime, snapshot.revisionToken],
|
|
1331
|
+
);
|
|
1036
1332
|
const diagnosticsModeMessage = getDiagnosticsModeMessage(loadError ?? snapshot.fatalError);
|
|
1037
1333
|
const addCommentDisabledReason =
|
|
1038
1334
|
!capabilities.canAddComment && !snapshot.selection.isCollapsed
|
|
1039
1335
|
? "Select text within one paragraph to add a DOCX comment."
|
|
1040
1336
|
: undefined;
|
|
1337
|
+
const selectionToolbar = buildSelectionToolbarModel({
|
|
1338
|
+
snapshot,
|
|
1339
|
+
viewState,
|
|
1340
|
+
capabilities,
|
|
1341
|
+
documentNavigation,
|
|
1342
|
+
styleCatalog,
|
|
1343
|
+
formattingState,
|
|
1344
|
+
addCommentDisabledReason,
|
|
1345
|
+
});
|
|
1346
|
+
const selectionToolbarSelectionKey = useMemo(
|
|
1347
|
+
() => createSelectionToolbarSelectionKey(snapshot.selection, viewState.activeStory),
|
|
1348
|
+
[snapshot.selection, viewState.activeStory],
|
|
1349
|
+
);
|
|
1350
|
+
const shouldRenderSelectionToolbar = Boolean(
|
|
1351
|
+
selectionToolbar &&
|
|
1352
|
+
selectionToolbarSelectionKey &&
|
|
1353
|
+
selectionToolbarDismissedKey !== selectionToolbarSelectionKey &&
|
|
1354
|
+
(viewState.isFocused || selectionToolbarFocusWithin),
|
|
1355
|
+
);
|
|
1041
1356
|
const accessibilityInstructionsId = `${documentId}-accessibility-instructions`;
|
|
1042
1357
|
const accessibilityStatusId = `${documentId}-accessibility-status`;
|
|
1043
1358
|
const accessibilityAlertId = `${documentId}-accessibility-alert`;
|
|
1044
1359
|
|
|
1045
1360
|
const dispatchSelection = (selection: PublicSelectionSnapshot) =>
|
|
1046
|
-
activeRuntime
|
|
1047
|
-
|
|
1048
|
-
|
|
1361
|
+
applyRuntimeSelection(activeRuntime, selection);
|
|
1362
|
+
|
|
1363
|
+
const dismissSelectionToolbar = useCallback(
|
|
1364
|
+
(_reason: SelectionToolbarDismissReason) => {
|
|
1365
|
+
if (selectionToolbarSelectionKey) {
|
|
1366
|
+
setSelectionToolbarDismissedKey(selectionToolbarSelectionKey);
|
|
1367
|
+
} else {
|
|
1368
|
+
setSelectionToolbarDismissedKey(null);
|
|
1369
|
+
}
|
|
1370
|
+
setSelectionToolbarAnchor(null);
|
|
1371
|
+
setSelectionToolbarFocusWithin(false);
|
|
1372
|
+
},
|
|
1373
|
+
[selectionToolbarSelectionKey],
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
const getDocumentSurfaceElement = useCallback((): HTMLElement | null => {
|
|
1377
|
+
return shellRef.current?.querySelector<HTMLElement>("[data-wre-document-surface='true']") ?? null;
|
|
1378
|
+
}, []);
|
|
1379
|
+
|
|
1380
|
+
const isTargetWithinSelectionToolbar = useCallback((target: EventTarget | null): boolean => {
|
|
1381
|
+
return target instanceof Node && Boolean(selectionToolbarElementRef.current?.contains(target));
|
|
1382
|
+
}, []);
|
|
1383
|
+
|
|
1384
|
+
const isTargetWithinDocumentSurface = useCallback(
|
|
1385
|
+
(target: EventTarget | null): boolean => {
|
|
1386
|
+
const surfaceElement = getDocumentSurfaceElement();
|
|
1387
|
+
return target instanceof Node && Boolean(surfaceElement?.contains(target));
|
|
1388
|
+
},
|
|
1389
|
+
[getDocumentSurfaceElement],
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
const focusDocumentSurface = useCallback((): void => {
|
|
1393
|
+
const surfaceElement = getDocumentSurfaceElement();
|
|
1394
|
+
surfaceElement?.focus();
|
|
1395
|
+
activeRuntime.focus();
|
|
1396
|
+
}, [activeRuntime, getDocumentSurfaceElement]);
|
|
1397
|
+
|
|
1398
|
+
const handleSurfaceFocus = useCallback(
|
|
1399
|
+
(_event: FocusEvent<HTMLDivElement>) => {
|
|
1400
|
+
setSelectionToolbarFocusWithin(false);
|
|
1401
|
+
activeRuntime.focus();
|
|
1402
|
+
},
|
|
1403
|
+
[activeRuntime],
|
|
1404
|
+
);
|
|
1405
|
+
|
|
1406
|
+
const handleSurfaceBlur = useCallback(
|
|
1407
|
+
(event: FocusEvent<HTMLDivElement>) => {
|
|
1408
|
+
if (isTargetWithinSelectionToolbar(event.relatedTarget)) {
|
|
1409
|
+
setSelectionToolbarFocusWithin(true);
|
|
1410
|
+
activeRuntime.focus();
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
setSelectionToolbarFocusWithin(false);
|
|
1415
|
+
activeRuntime.blur();
|
|
1416
|
+
dismissSelectionToolbar("blur");
|
|
1417
|
+
},
|
|
1418
|
+
[activeRuntime, dismissSelectionToolbar, isTargetWithinSelectionToolbar],
|
|
1419
|
+
);
|
|
1420
|
+
|
|
1421
|
+
const handleSelectionToolbarFocusCapture = useCallback(() => {
|
|
1422
|
+
setSelectionToolbarFocusWithin(true);
|
|
1423
|
+
activeRuntime.focus();
|
|
1424
|
+
}, [activeRuntime]);
|
|
1425
|
+
|
|
1426
|
+
const handleSelectionToolbarBlurCapture = useCallback(
|
|
1427
|
+
(event: FocusEvent<HTMLDivElement>) => {
|
|
1428
|
+
if (isTargetWithinSelectionToolbar(event.relatedTarget)) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (isTargetWithinDocumentSurface(event.relatedTarget)) {
|
|
1433
|
+
setSelectionToolbarFocusWithin(false);
|
|
1434
|
+
activeRuntime.focus();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
setSelectionToolbarFocusWithin(false);
|
|
1439
|
+
activeRuntime.blur();
|
|
1440
|
+
dismissSelectionToolbar("blur");
|
|
1441
|
+
},
|
|
1442
|
+
[
|
|
1443
|
+
activeRuntime,
|
|
1444
|
+
dismissSelectionToolbar,
|
|
1445
|
+
isTargetWithinDocumentSurface,
|
|
1446
|
+
isTargetWithinSelectionToolbar,
|
|
1447
|
+
],
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
const addSelectionToolbarComment = useCallback(() => {
|
|
1451
|
+
const commentId = addReviewComment();
|
|
1452
|
+
if (!commentId) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
dismissSelectionToolbar("comment-action");
|
|
1456
|
+
queueMicrotask(() => {
|
|
1457
|
+
focusDocumentSurface();
|
|
1049
1458
|
});
|
|
1459
|
+
}, [dismissSelectionToolbar, focusDocumentSurface]);
|
|
1460
|
+
|
|
1461
|
+
const handleSelectionToolbarAnchorChange = useCallback(
|
|
1462
|
+
(nextAnchor: SelectionToolbarAnchor | null) => {
|
|
1463
|
+
setSelectionToolbarAnchor((current) =>
|
|
1464
|
+
selectionToolbarAnchorsEqual(current, nextAnchor) ? current : nextAnchor);
|
|
1465
|
+
},
|
|
1466
|
+
[],
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
useEffect(() => {
|
|
1470
|
+
if (!selectionToolbarSelectionKey) {
|
|
1471
|
+
setSelectionToolbarDismissedKey(null);
|
|
1472
|
+
setSelectionToolbarFocusWithin(false);
|
|
1473
|
+
setSelectionToolbarAnchor(null);
|
|
1474
|
+
lastSelectionToolbarKeyRef.current = null;
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (lastSelectionToolbarKeyRef.current !== selectionToolbarSelectionKey) {
|
|
1479
|
+
lastSelectionToolbarKeyRef.current = selectionToolbarSelectionKey;
|
|
1480
|
+
setSelectionToolbarDismissedKey(null);
|
|
1481
|
+
setSelectionToolbarFocusWithin(false);
|
|
1482
|
+
}
|
|
1483
|
+
}, [selectionToolbarSelectionKey]);
|
|
1484
|
+
|
|
1485
|
+
useEffect(() => {
|
|
1486
|
+
if (!selectionToolbar) {
|
|
1487
|
+
setSelectionToolbarAnchor(null);
|
|
1488
|
+
setSelectionToolbarFocusWithin(false);
|
|
1489
|
+
}
|
|
1490
|
+
}, [selectionToolbar]);
|
|
1050
1491
|
|
|
1051
1492
|
useEffect(() => {
|
|
1052
1493
|
const shell = shellRef.current;
|
|
@@ -1075,6 +1516,23 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1075
1516
|
}, [loadError, snapshot.fatalError]);
|
|
1076
1517
|
|
|
1077
1518
|
function handleShellKeyDownCapture(event: React.KeyboardEvent<HTMLDivElement>): void {
|
|
1519
|
+
if (
|
|
1520
|
+
event.key === "Escape" &&
|
|
1521
|
+
shouldRenderSelectionToolbar &&
|
|
1522
|
+
(isTargetWithinDocumentSurface(event.target) || isTargetWithinSelectionToolbar(event.target))
|
|
1523
|
+
) {
|
|
1524
|
+
event.preventDefault();
|
|
1525
|
+
event.stopPropagation();
|
|
1526
|
+
const restoreSurfaceFocus = isTargetWithinSelectionToolbar(event.target);
|
|
1527
|
+
dismissSelectionToolbar("escape");
|
|
1528
|
+
if (restoreSurfaceFocus) {
|
|
1529
|
+
queueMicrotask(() => {
|
|
1530
|
+
focusDocumentSurface();
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1078
1536
|
if (event.key !== "F6") {
|
|
1079
1537
|
return;
|
|
1080
1538
|
}
|
|
@@ -1089,15 +1547,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1089
1547
|
}
|
|
1090
1548
|
|
|
1091
1549
|
const editorCallbacks = {
|
|
1092
|
-
onFocus:
|
|
1093
|
-
onBlur:
|
|
1550
|
+
onFocus: handleSurfaceFocus,
|
|
1551
|
+
onBlur: handleSurfaceBlur,
|
|
1094
1552
|
onSelectionChange: dispatchSelection,
|
|
1095
|
-
onInsertText: (text: string) => activeRuntime
|
|
1096
|
-
onDeleteBackward: () => activeRuntime
|
|
1097
|
-
onDeleteForward: () => activeRuntime
|
|
1098
|
-
onInsertTab: () => activeRuntime
|
|
1099
|
-
|
|
1100
|
-
|
|
1553
|
+
onInsertText: (text: string) => applyRuntimeTextCommand(activeRuntime, { type: "insert-text", text }),
|
|
1554
|
+
onDeleteBackward: () => applyRuntimeTextCommand(activeRuntime, { type: "delete-backward" }),
|
|
1555
|
+
onDeleteForward: () => applyRuntimeTextCommand(activeRuntime, { type: "delete-forward" }),
|
|
1556
|
+
onInsertTab: () => applyRuntimeTextCommand(activeRuntime, { type: "insert-tab" }),
|
|
1557
|
+
onOutdentTab: () => applyRuntimeTextCommand(activeRuntime, { type: "outdent-tab" }),
|
|
1558
|
+
onInsertHardBreak: () => applyRuntimeTextCommand(activeRuntime, { type: "insert-hard-break" }),
|
|
1559
|
+
onSplitParagraph: () => applyRuntimeTextCommand(activeRuntime, { type: "split-paragraph" }),
|
|
1101
1560
|
};
|
|
1102
1561
|
|
|
1103
1562
|
const reviewCallbacks = {
|
|
@@ -1145,6 +1604,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1145
1604
|
activeRuntime.rejectAllChanges();
|
|
1146
1605
|
setActiveRailTab("changes");
|
|
1147
1606
|
},
|
|
1607
|
+
onCloseStory: () => {
|
|
1608
|
+
activeRuntime.closeStory();
|
|
1609
|
+
},
|
|
1148
1610
|
};
|
|
1149
1611
|
|
|
1150
1612
|
return (
|
|
@@ -1185,29 +1647,80 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1185
1647
|
) : null}
|
|
1186
1648
|
<TwReviewWorkspace
|
|
1187
1649
|
snapshot={snapshot}
|
|
1650
|
+
viewState={viewState}
|
|
1188
1651
|
currentUserId={currentUser.userId}
|
|
1189
1652
|
capabilities={capabilities}
|
|
1653
|
+
documentNavigation={documentNavigation}
|
|
1190
1654
|
reviewMode={reviewMode}
|
|
1191
|
-
|
|
1655
|
+
workspaceMode={viewState.workspaceMode}
|
|
1656
|
+
zoomLevel={viewState.zoomLevel}
|
|
1657
|
+
formattingState={formattingState}
|
|
1658
|
+
styleCatalog={styleCatalog}
|
|
1192
1659
|
activeRailTab={activeRailTab}
|
|
1193
1660
|
activeCommentId={snapshot.comments.activeCommentId}
|
|
1194
1661
|
activeRevisionId={activeRevisionId}
|
|
1195
1662
|
showTrackedChanges={showTrackedChanges}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1663
|
+
selectionToolbar={shouldRenderSelectionToolbar ? selectionToolbar : null}
|
|
1664
|
+
selectionToolbarAnchor={shouldRenderSelectionToolbar ? selectionToolbarAnchor : null}
|
|
1665
|
+
onAddCommentFromSelection={addSelectionToolbarComment}
|
|
1666
|
+
onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
|
|
1667
|
+
onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
|
|
1668
|
+
onSelectionToolbarBlurCapture={handleSelectionToolbarBlurCapture}
|
|
1669
|
+
selectionToolbarRef={selectionToolbarElementRef}
|
|
1670
|
+
onWorkspaceModeChange={(mode) => activeRuntime.setWorkspaceMode(mode)}
|
|
1671
|
+
onZoomChange={(level) => activeRuntime.setZoom(level)}
|
|
1199
1672
|
onActiveRailTabChange={setActiveRailTab}
|
|
1200
1673
|
onShowTrackedChangesChange={setShowTrackedChanges}
|
|
1674
|
+
onToggleBold={() =>
|
|
1675
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" })}
|
|
1676
|
+
onToggleItalic={() =>
|
|
1677
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "italic" })}
|
|
1678
|
+
onToggleUnderline={() =>
|
|
1679
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "underline" })}
|
|
1680
|
+
onSetParagraphStyle={(styleId) =>
|
|
1681
|
+
applyRuntimeParagraphStyle(activeRuntime, styleId)}
|
|
1682
|
+
onOutdent={() =>
|
|
1683
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" })}
|
|
1684
|
+
onIndent={() =>
|
|
1685
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "indent" })}
|
|
1686
|
+
onOpenHeaderStory={() =>
|
|
1687
|
+
openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header")}
|
|
1688
|
+
onOpenFooterStory={() =>
|
|
1689
|
+
openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "footer")}
|
|
1690
|
+
onSetParagraphIndentation={(indentation) =>
|
|
1691
|
+
applyRuntimeParagraphIndentation(activeRuntime, indentation)
|
|
1692
|
+
}
|
|
1693
|
+
onSetParagraphTabStops={(tabStops) =>
|
|
1694
|
+
applyRuntimeParagraphTabStops(activeRuntime, tabStops)
|
|
1695
|
+
}
|
|
1696
|
+
onRestartNumbering={() => applyRuntimeNumberingFlow(activeRuntime, { type: "restart" })}
|
|
1697
|
+
onContinueNumbering={() => applyRuntimeNumberingFlow(activeRuntime, { type: "continue" })}
|
|
1698
|
+
onNavigateHeading={(headingId) => {
|
|
1699
|
+
const heading = documentNavigation.headings.find(
|
|
1700
|
+
(entry) => entry.headingId === headingId,
|
|
1701
|
+
);
|
|
1702
|
+
if (!heading) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
applyRuntimeSelection(
|
|
1706
|
+
activeRuntime,
|
|
1707
|
+
createCollapsedPublicSelection(heading.offset),
|
|
1708
|
+
);
|
|
1709
|
+
}}
|
|
1201
1710
|
{...reviewCallbacks}
|
|
1202
1711
|
document={
|
|
1203
1712
|
<TwProseMirrorSurface
|
|
1204
1713
|
ref={surfaceRef}
|
|
1205
1714
|
currentUser={currentUser}
|
|
1206
1715
|
snapshot={snapshot}
|
|
1716
|
+
canonicalDocument={canonicalDocument}
|
|
1717
|
+
documentNavigation={documentNavigation}
|
|
1207
1718
|
reviewMode={reviewMode}
|
|
1208
1719
|
markupDisplay={liveMarkupDisplay}
|
|
1209
1720
|
activeRevisionId={activeRevisionId}
|
|
1210
1721
|
showTrackedChanges={showTrackedChanges}
|
|
1722
|
+
isPageWorkspace={isPageWorkspace}
|
|
1723
|
+
onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
|
|
1211
1724
|
{...editorCallbacks}
|
|
1212
1725
|
onCommentActivated={(commentId) => {
|
|
1213
1726
|
activeRuntime.openComment(commentId);
|
|
@@ -1237,138 +1750,934 @@ function applyRuntimeFormattingOperation(
|
|
|
1237
1750
|
| { type: "indent" }
|
|
1238
1751
|
| { type: "outdent" },
|
|
1239
1752
|
): void {
|
|
1240
|
-
const
|
|
1241
|
-
if (!
|
|
1753
|
+
const context = getStoryMutationContext(runtime);
|
|
1754
|
+
if (!context) {
|
|
1242
1755
|
return;
|
|
1243
1756
|
}
|
|
1244
1757
|
|
|
1245
1758
|
const result = applyFormattingOperationToDocument(
|
|
1246
|
-
|
|
1247
|
-
|
|
1759
|
+
context.localDocument,
|
|
1760
|
+
context.localSnapshot,
|
|
1248
1761
|
operation,
|
|
1249
1762
|
);
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1763
|
+
dispatchStoryMutationResult(
|
|
1764
|
+
runtime,
|
|
1765
|
+
context,
|
|
1766
|
+
{
|
|
1767
|
+
...result,
|
|
1768
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1769
|
+
},
|
|
1770
|
+
context.timestamp,
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1253
1773
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1774
|
+
function getRuntimeStyleCatalog(
|
|
1775
|
+
runtime: WordReviewEditorRuntime,
|
|
1776
|
+
): StyleCatalogSnapshot {
|
|
1777
|
+
const styles = runtime.getSessionState().canonicalDocument.styles;
|
|
1778
|
+
const mapRecord = <
|
|
1779
|
+
T extends {
|
|
1780
|
+
styleId: string;
|
|
1781
|
+
displayName: string;
|
|
1782
|
+
kind: "paragraph" | "character" | "table";
|
|
1783
|
+
isDefault: boolean;
|
|
1784
|
+
basedOn?: string;
|
|
1785
|
+
nextStyle?: string;
|
|
1261
1786
|
},
|
|
1262
|
-
|
|
1787
|
+
>(
|
|
1788
|
+
record: Record<string, T>,
|
|
1789
|
+
) =>
|
|
1790
|
+
Object.values(record)
|
|
1791
|
+
.map((entry) => ({
|
|
1792
|
+
styleId: entry.styleId,
|
|
1793
|
+
displayName: entry.displayName,
|
|
1794
|
+
kind: entry.kind,
|
|
1795
|
+
isDefault: entry.isDefault,
|
|
1796
|
+
...(entry.basedOn ? { basedOn: entry.basedOn } : {}),
|
|
1797
|
+
...(entry.nextStyle ? { nextStyle: entry.nextStyle } : {}),
|
|
1798
|
+
}))
|
|
1799
|
+
.sort((left, right) =>
|
|
1800
|
+
left.displayName.localeCompare(right.displayName) ||
|
|
1801
|
+
left.styleId.localeCompare(right.styleId),
|
|
1802
|
+
);
|
|
1803
|
+
|
|
1804
|
+
return {
|
|
1805
|
+
paragraphs: mapRecord(styles.paragraphs),
|
|
1806
|
+
characters: mapRecord(styles.characters),
|
|
1807
|
+
tables: mapRecord(styles.tables),
|
|
1808
|
+
fromPackage: styles.fromPackage === true,
|
|
1809
|
+
};
|
|
1263
1810
|
}
|
|
1264
1811
|
|
|
1265
|
-
function
|
|
1266
|
-
|
|
1267
|
-
|
|
1812
|
+
function applyRuntimeParagraphStyle(
|
|
1813
|
+
runtime: WordReviewEditorRuntime,
|
|
1814
|
+
styleId: string | null,
|
|
1815
|
+
): void {
|
|
1816
|
+
const context = getStoryMutationContext(runtime);
|
|
1817
|
+
if (!context) {
|
|
1268
1818
|
return;
|
|
1269
1819
|
}
|
|
1270
1820
|
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1821
|
+
const result = applyParagraphStyleToDocument(
|
|
1822
|
+
context.localDocument,
|
|
1823
|
+
context.localSnapshot,
|
|
1824
|
+
styleId,
|
|
1825
|
+
);
|
|
1826
|
+
dispatchStoryMutationResult(
|
|
1827
|
+
runtime,
|
|
1828
|
+
context,
|
|
1829
|
+
{
|
|
1830
|
+
...result,
|
|
1831
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1832
|
+
},
|
|
1833
|
+
context.timestamp,
|
|
1276
1834
|
);
|
|
1277
|
-
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1278
1835
|
}
|
|
1279
1836
|
|
|
1280
|
-
function
|
|
1837
|
+
function applyRuntimeTableStyle(
|
|
1281
1838
|
runtime: WordReviewEditorRuntime,
|
|
1282
|
-
|
|
1839
|
+
styleId: string | null,
|
|
1283
1840
|
): void {
|
|
1284
|
-
const
|
|
1285
|
-
if (!
|
|
1841
|
+
const context = getStoryMutationContext(runtime);
|
|
1842
|
+
if (!context) {
|
|
1286
1843
|
return;
|
|
1287
1844
|
}
|
|
1288
1845
|
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1846
|
+
const result = applyTableStyleToDocument(
|
|
1847
|
+
context.localDocument,
|
|
1848
|
+
context.localSnapshot,
|
|
1849
|
+
styleId,
|
|
1850
|
+
);
|
|
1851
|
+
dispatchStoryMutationResult(
|
|
1852
|
+
runtime,
|
|
1853
|
+
context,
|
|
1854
|
+
{
|
|
1855
|
+
...result,
|
|
1856
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1857
|
+
},
|
|
1858
|
+
context.timestamp,
|
|
1295
1859
|
);
|
|
1296
|
-
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1297
1860
|
}
|
|
1298
1861
|
|
|
1299
|
-
function
|
|
1862
|
+
function applyRuntimeParagraphIndentation(
|
|
1300
1863
|
runtime: WordReviewEditorRuntime,
|
|
1301
|
-
|
|
1864
|
+
indentation: {
|
|
1865
|
+
left?: number;
|
|
1866
|
+
right?: number;
|
|
1867
|
+
firstLine?: number;
|
|
1868
|
+
hanging?: number;
|
|
1869
|
+
},
|
|
1302
1870
|
): void {
|
|
1303
|
-
const
|
|
1304
|
-
if (!
|
|
1871
|
+
const context = getStoryMutationContext(runtime);
|
|
1872
|
+
if (!context) {
|
|
1305
1873
|
return;
|
|
1306
1874
|
}
|
|
1307
1875
|
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
changed: true,
|
|
1324
|
-
document: result.document,
|
|
1325
|
-
selection: result.selection,
|
|
1326
|
-
mapping: result.mapping,
|
|
1327
|
-
}, timestamp);
|
|
1328
|
-
} catch {
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1876
|
+
const result = setActiveParagraphIndentation(
|
|
1877
|
+
context.localDocument,
|
|
1878
|
+
context.localSnapshot,
|
|
1879
|
+
indentation,
|
|
1880
|
+
{ timestamp: context.timestamp },
|
|
1881
|
+
);
|
|
1882
|
+
dispatchStoryMutationResult(
|
|
1883
|
+
runtime,
|
|
1884
|
+
context,
|
|
1885
|
+
{
|
|
1886
|
+
...result,
|
|
1887
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1888
|
+
},
|
|
1889
|
+
context.timestamp,
|
|
1890
|
+
);
|
|
1331
1891
|
}
|
|
1332
1892
|
|
|
1333
|
-
function
|
|
1893
|
+
function applyRuntimeParagraphTabStops(
|
|
1334
1894
|
runtime: WordReviewEditorRuntime,
|
|
1335
|
-
|
|
1336
|
-
operation:
|
|
1337
|
-
| { type: "add-row-before" }
|
|
1338
|
-
| { type: "add-row-after" }
|
|
1339
|
-
| { type: "add-column-before" }
|
|
1340
|
-
| { type: "add-column-after" }
|
|
1341
|
-
| { type: "delete-row" }
|
|
1342
|
-
| { type: "delete-column" }
|
|
1343
|
-
| { type: "delete-table" }
|
|
1344
|
-
| { type: "merge-cells" }
|
|
1345
|
-
| { type: "split-cell" }
|
|
1346
|
-
| { type: "set-cell-background"; color: string },
|
|
1895
|
+
tabStops: Array<{ pos: number; val?: string; leader?: string }>,
|
|
1347
1896
|
): void {
|
|
1348
|
-
const
|
|
1349
|
-
if (!
|
|
1897
|
+
const context = getStoryMutationContext(runtime);
|
|
1898
|
+
if (!context) {
|
|
1350
1899
|
return;
|
|
1351
1900
|
}
|
|
1352
1901
|
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1902
|
+
const result = setActiveParagraphTabStops(
|
|
1903
|
+
context.localDocument,
|
|
1904
|
+
context.localSnapshot,
|
|
1905
|
+
tabStops,
|
|
1906
|
+
{ timestamp: context.timestamp },
|
|
1907
|
+
);
|
|
1908
|
+
dispatchStoryMutationResult(
|
|
1909
|
+
runtime,
|
|
1910
|
+
context,
|
|
1911
|
+
{
|
|
1912
|
+
...result,
|
|
1913
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1914
|
+
},
|
|
1915
|
+
context.timestamp,
|
|
1359
1916
|
);
|
|
1360
|
-
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
function applyRuntimeNumberingFlow(
|
|
1920
|
+
runtime: WordReviewEditorRuntime,
|
|
1921
|
+
operation: { type: "restart"; startAt?: number } | { type: "continue" },
|
|
1922
|
+
): void {
|
|
1923
|
+
const context = getStoryMutationContext(runtime);
|
|
1924
|
+
if (!context) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
|
|
1929
|
+
if (!paragraphContext?.paragraph.numbering) {
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
const result =
|
|
1934
|
+
operation.type === "restart"
|
|
1935
|
+
? restartListNumbering(
|
|
1936
|
+
context.localDocument,
|
|
1937
|
+
paragraphContext.paragraphIndex,
|
|
1938
|
+
{ timestamp: context.timestamp },
|
|
1939
|
+
operation.startAt,
|
|
1940
|
+
)
|
|
1941
|
+
: continueListNumbering(
|
|
1942
|
+
context.localDocument,
|
|
1943
|
+
paragraphContext.paragraphIndex,
|
|
1944
|
+
{ timestamp: context.timestamp },
|
|
1945
|
+
);
|
|
1946
|
+
|
|
1947
|
+
dispatchStoryMutationResult(
|
|
1948
|
+
runtime,
|
|
1949
|
+
context,
|
|
1950
|
+
{
|
|
1951
|
+
changed: result.affectedParagraphIndexes.length > 0,
|
|
1952
|
+
document: result.document,
|
|
1953
|
+
selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
|
|
1954
|
+
},
|
|
1955
|
+
context.timestamp,
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function applyRuntimeInsertSectionBreak(
|
|
1960
|
+
runtime: WordReviewEditorRuntime,
|
|
1961
|
+
breakType: SectionBreakType,
|
|
1962
|
+
options?: { afterSectionIndex?: number },
|
|
1963
|
+
): void {
|
|
1964
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1965
|
+
if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
const sessionState = runtime.getSessionState();
|
|
1970
|
+
const timestamp = new Date().toISOString();
|
|
1971
|
+
const result =
|
|
1972
|
+
typeof options?.afterSectionIndex === "number"
|
|
1973
|
+
? insertSectionBreakAfterSectionIndex(
|
|
1974
|
+
sessionState.canonicalDocument,
|
|
1975
|
+
options.afterSectionIndex,
|
|
1976
|
+
breakType,
|
|
1977
|
+
{ timestamp },
|
|
1978
|
+
)
|
|
1979
|
+
: insertSectionBreakAfterSectionIndex(
|
|
1980
|
+
sessionState.canonicalDocument,
|
|
1981
|
+
runtime.getDocumentNavigationSnapshot().activeSectionIndex,
|
|
1982
|
+
breakType,
|
|
1983
|
+
{ timestamp },
|
|
1984
|
+
);
|
|
1985
|
+
|
|
1986
|
+
dispatchRuntimeDocumentMutation(
|
|
1987
|
+
runtime,
|
|
1988
|
+
{
|
|
1989
|
+
changed: result.changed,
|
|
1990
|
+
document: result.document,
|
|
1991
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1992
|
+
},
|
|
1993
|
+
timestamp,
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function applyRuntimeDeleteSectionBreak(
|
|
1998
|
+
runtime: WordReviewEditorRuntime,
|
|
1999
|
+
sectionIndex: number,
|
|
2000
|
+
): void {
|
|
2001
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2002
|
+
if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
const sessionState = runtime.getSessionState();
|
|
2007
|
+
const timestamp = new Date().toISOString();
|
|
2008
|
+
const result = deleteSectionBreakAtSectionIndex(
|
|
2009
|
+
sessionState.canonicalDocument,
|
|
2010
|
+
sectionIndex,
|
|
2011
|
+
{ timestamp },
|
|
2012
|
+
);
|
|
2013
|
+
|
|
2014
|
+
dispatchRuntimeDocumentMutation(
|
|
2015
|
+
runtime,
|
|
2016
|
+
{
|
|
2017
|
+
changed: result.changed,
|
|
2018
|
+
document: result.document,
|
|
2019
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
2020
|
+
},
|
|
2021
|
+
timestamp,
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function applyRuntimeUpdateSectionLayout(
|
|
2026
|
+
runtime: WordReviewEditorRuntime,
|
|
2027
|
+
sectionIndex: number,
|
|
2028
|
+
patch: SectionLayoutPatch,
|
|
2029
|
+
): void {
|
|
2030
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2031
|
+
if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
const sessionState = runtime.getSessionState();
|
|
2036
|
+
const timestamp = new Date().toISOString();
|
|
2037
|
+
const result = updateSectionLayoutAtSectionIndex(
|
|
2038
|
+
sessionState.canonicalDocument,
|
|
2039
|
+
sectionIndex,
|
|
2040
|
+
{
|
|
2041
|
+
...(patch.pageSize ? { pageSize: patch.pageSize } : {}),
|
|
2042
|
+
...(patch.pageMargins ? { pageMargins: patch.pageMargins } : {}),
|
|
2043
|
+
...(patch.columns ? { columns: patch.columns } : {}),
|
|
2044
|
+
...(patch.titlePage !== undefined ? { titlePage: patch.titlePage } : {}),
|
|
2045
|
+
...(patch.sectionType ? { sectionType: patch.sectionType } : {}),
|
|
2046
|
+
},
|
|
2047
|
+
{ timestamp },
|
|
2048
|
+
);
|
|
2049
|
+
|
|
2050
|
+
dispatchRuntimeDocumentMutation(
|
|
2051
|
+
runtime,
|
|
2052
|
+
{
|
|
2053
|
+
changed: result.changed,
|
|
2054
|
+
document: result.document,
|
|
2055
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
2056
|
+
},
|
|
2057
|
+
timestamp,
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function applyRuntimeSetSectionPageNumbering(
|
|
2062
|
+
runtime: WordReviewEditorRuntime,
|
|
2063
|
+
sectionIndex: number,
|
|
2064
|
+
patch: SectionPageNumberingPatch | null,
|
|
2065
|
+
): void {
|
|
2066
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2067
|
+
if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const sessionState = runtime.getSessionState();
|
|
2072
|
+
const timestamp = new Date().toISOString();
|
|
2073
|
+
const normalizedPatch =
|
|
2074
|
+
patch === null
|
|
2075
|
+
? null
|
|
2076
|
+
: {
|
|
2077
|
+
...(patch.format !== undefined
|
|
2078
|
+
? { format: patch.format ?? undefined }
|
|
2079
|
+
: {}),
|
|
2080
|
+
...(patch.start !== undefined
|
|
2081
|
+
? { start: patch.start ?? undefined }
|
|
2082
|
+
: {}),
|
|
2083
|
+
...(patch.chapterStyle !== undefined
|
|
2084
|
+
? { chapStyle: patch.chapterStyle ?? undefined }
|
|
2085
|
+
: {}),
|
|
2086
|
+
...(patch.chapterSeparator !== undefined
|
|
2087
|
+
? { chapSep: patch.chapterSeparator ?? undefined }
|
|
2088
|
+
: {}),
|
|
2089
|
+
};
|
|
2090
|
+
const result = setSectionPageNumberingAtSectionIndex(
|
|
2091
|
+
sessionState.canonicalDocument,
|
|
2092
|
+
sectionIndex,
|
|
2093
|
+
normalizedPatch,
|
|
2094
|
+
{ timestamp },
|
|
2095
|
+
);
|
|
2096
|
+
|
|
2097
|
+
dispatchRuntimeDocumentMutation(
|
|
2098
|
+
runtime,
|
|
2099
|
+
{
|
|
2100
|
+
changed: result.changed,
|
|
2101
|
+
document: result.document,
|
|
2102
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
2103
|
+
},
|
|
2104
|
+
timestamp,
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function applyRuntimeSetHeaderFooterLink(
|
|
2109
|
+
runtime: WordReviewEditorRuntime,
|
|
2110
|
+
sectionIndex: number,
|
|
2111
|
+
patch: HeaderFooterLinkPatch,
|
|
2112
|
+
): void {
|
|
2113
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2114
|
+
if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const sessionState = runtime.getSessionState();
|
|
2119
|
+
const timestamp = new Date().toISOString();
|
|
2120
|
+
const result = setHeaderFooterLinkAtSectionIndex(
|
|
2121
|
+
sessionState.canonicalDocument,
|
|
2122
|
+
sectionIndex,
|
|
2123
|
+
patch,
|
|
2124
|
+
{ timestamp },
|
|
2125
|
+
);
|
|
2126
|
+
|
|
2127
|
+
dispatchRuntimeDocumentMutation(
|
|
2128
|
+
runtime,
|
|
2129
|
+
{
|
|
2130
|
+
changed: result.changed,
|
|
2131
|
+
document: result.document,
|
|
2132
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
2133
|
+
},
|
|
2134
|
+
timestamp,
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function applyRuntimeInsertPageBreak(runtime: WordReviewEditorRuntime): void {
|
|
2139
|
+
const context = getStoryMutationContext(runtime);
|
|
2140
|
+
if (!context) {
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
const result = insertPageBreakInDocument(
|
|
2145
|
+
context.localDocument,
|
|
2146
|
+
toRuntimeSelectionSnapshot(context.localSnapshot.selection),
|
|
2147
|
+
{ timestamp: context.timestamp },
|
|
2148
|
+
);
|
|
2149
|
+
dispatchStoryMutationResult(runtime, context, result, context.timestamp);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function applyRuntimeInsertTable(
|
|
2153
|
+
runtime: WordReviewEditorRuntime,
|
|
2154
|
+
options: InsertTableOptions,
|
|
2155
|
+
): void {
|
|
2156
|
+
const context = getStoryMutationContext(runtime);
|
|
2157
|
+
if (!context) {
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
const result = insertTableInDocument(
|
|
2162
|
+
context.localDocument,
|
|
2163
|
+
toRuntimeSelectionSnapshot(context.localSnapshot.selection),
|
|
2164
|
+
options,
|
|
2165
|
+
{ timestamp: context.timestamp },
|
|
2166
|
+
);
|
|
2167
|
+
dispatchStoryMutationResult(runtime, context, result, context.timestamp);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
function applyRuntimeInsertImage(
|
|
2171
|
+
runtime: WordReviewEditorRuntime,
|
|
2172
|
+
options: InsertImageOptions,
|
|
2173
|
+
): void {
|
|
2174
|
+
const context = getStoryMutationContext(runtime);
|
|
2175
|
+
if (!context) {
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
try {
|
|
2180
|
+
const result = insertImageInDocument(
|
|
2181
|
+
context.localDocument,
|
|
2182
|
+
toRuntimeSelectionSnapshot(context.localSnapshot.selection),
|
|
2183
|
+
options.data,
|
|
2184
|
+
options.mimeType,
|
|
2185
|
+
options.width,
|
|
2186
|
+
options.height,
|
|
2187
|
+
{
|
|
2188
|
+
timestamp: context.timestamp,
|
|
2189
|
+
altText: options.altText,
|
|
2190
|
+
},
|
|
2191
|
+
);
|
|
2192
|
+
dispatchStoryMutationResult(runtime, context, {
|
|
2193
|
+
changed: true,
|
|
2194
|
+
document: result.document,
|
|
2195
|
+
selection: result.selection,
|
|
2196
|
+
mapping: result.mapping,
|
|
2197
|
+
}, context.timestamp);
|
|
2198
|
+
} catch {
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function applyRuntimeImageResize(
|
|
2204
|
+
runtime: WordReviewEditorRuntime,
|
|
2205
|
+
mediaId: string,
|
|
2206
|
+
dimensions: { widthEmu: number; heightEmu: number },
|
|
2207
|
+
): void {
|
|
2208
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2209
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
try {
|
|
2214
|
+
const sessionState = runtime.getSessionState();
|
|
2215
|
+
const result = resizeImageInCatalog(
|
|
2216
|
+
sessionState.canonicalDocument,
|
|
2217
|
+
mediaId,
|
|
2218
|
+
dimensions,
|
|
2219
|
+
);
|
|
2220
|
+
runtime.dispatch({
|
|
2221
|
+
type: "document.replace",
|
|
2222
|
+
document: result.document,
|
|
2223
|
+
selection: toRuntimeSelectionSnapshot(snapshot.selection),
|
|
2224
|
+
origin: { source: "api", timestamp: new Date().toISOString() },
|
|
2225
|
+
});
|
|
2226
|
+
} catch {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function applyRuntimeImageReposition(
|
|
2232
|
+
runtime: WordReviewEditorRuntime,
|
|
2233
|
+
mediaId: string,
|
|
2234
|
+
offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
|
|
2235
|
+
): void {
|
|
2236
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2237
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
try {
|
|
2242
|
+
const sessionState = runtime.getSessionState();
|
|
2243
|
+
const result = repositionFloatingImageInDocument(
|
|
2244
|
+
sessionState.canonicalDocument,
|
|
2245
|
+
mediaId,
|
|
2246
|
+
offsets,
|
|
2247
|
+
);
|
|
2248
|
+
runtime.dispatch({
|
|
2249
|
+
type: "document.replace",
|
|
2250
|
+
document: result.document,
|
|
2251
|
+
selection: toRuntimeSelectionSnapshot(snapshot.selection),
|
|
2252
|
+
origin: { source: "api", timestamp: new Date().toISOString() },
|
|
2253
|
+
});
|
|
2254
|
+
} catch {
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// deriveViewState removed — the runtime's getViewState() is now the single
|
|
2260
|
+
// source of truth for EditorViewStateSnapshot, backed by view-state.ts.
|
|
2261
|
+
|
|
2262
|
+
function applyRuntimeTableStructureOperation(
|
|
2263
|
+
runtime: WordReviewEditorRuntime,
|
|
2264
|
+
mountedSurface: TwProseMirrorSurfaceRef | null | undefined,
|
|
2265
|
+
operation:
|
|
2266
|
+
| { type: "add-row-before" }
|
|
2267
|
+
| { type: "add-row-after" }
|
|
2268
|
+
| { type: "add-column-before" }
|
|
2269
|
+
| { type: "add-column-after" }
|
|
2270
|
+
| { type: "delete-row" }
|
|
2271
|
+
| { type: "delete-column" }
|
|
2272
|
+
| { type: "delete-table" }
|
|
2273
|
+
| { type: "merge-cells" }
|
|
2274
|
+
| { type: "split-cell" }
|
|
2275
|
+
| { type: "set-cell-background"; color: string },
|
|
2276
|
+
): void {
|
|
2277
|
+
const context = getStoryMutationContext(runtime);
|
|
2278
|
+
if (!context) {
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const result = applyTableStructureOperation(
|
|
2283
|
+
context.localDocument,
|
|
2284
|
+
context.localSnapshot,
|
|
2285
|
+
mountedSurface?.getTableSelection() ?? null,
|
|
2286
|
+
operation,
|
|
2287
|
+
);
|
|
2288
|
+
dispatchStoryMutationResult(runtime, context, result, context.timestamp);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function applyRuntimeTextCommand(
|
|
2292
|
+
runtime: WordReviewEditorRuntime,
|
|
2293
|
+
command:
|
|
2294
|
+
| { type: "insert-text"; text: string }
|
|
2295
|
+
| { type: "delete-backward" }
|
|
2296
|
+
| { type: "delete-forward" }
|
|
2297
|
+
| { type: "insert-tab" }
|
|
2298
|
+
| { type: "outdent-tab" }
|
|
2299
|
+
| { type: "insert-hard-break" }
|
|
2300
|
+
| { type: "split-paragraph" },
|
|
2301
|
+
): void {
|
|
2302
|
+
const context = getStoryMutationContext(runtime);
|
|
2303
|
+
if (!context) {
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
const listAwareResult = applyListAwareTextCommand(context, command);
|
|
2308
|
+
if (listAwareResult) {
|
|
2309
|
+
dispatchStoryMutationResult(runtime, context, listAwareResult, context.timestamp);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
if (context.activeStory.kind === "main") {
|
|
2314
|
+
switch (command.type) {
|
|
2315
|
+
case "insert-text":
|
|
2316
|
+
runtime.dispatch({ type: "text.insert", text: command.text });
|
|
2317
|
+
return;
|
|
2318
|
+
case "delete-backward":
|
|
2319
|
+
runtime.dispatch({ type: "text.delete-backward" });
|
|
2320
|
+
return;
|
|
2321
|
+
case "delete-forward":
|
|
2322
|
+
runtime.dispatch({ type: "text.delete-forward" });
|
|
2323
|
+
return;
|
|
2324
|
+
case "insert-tab":
|
|
2325
|
+
runtime.dispatch({ type: "text.insert-tab" });
|
|
2326
|
+
return;
|
|
2327
|
+
case "outdent-tab":
|
|
2328
|
+
return;
|
|
2329
|
+
case "insert-hard-break":
|
|
2330
|
+
runtime.dispatch({ type: "text.insert-hard-break" });
|
|
2331
|
+
return;
|
|
2332
|
+
case "split-paragraph":
|
|
2333
|
+
runtime.dispatch({ type: "paragraph.split" });
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
const selection = toRuntimeSelectionSnapshot(context.localSnapshot.selection);
|
|
2339
|
+
const localResult = (() => {
|
|
2340
|
+
switch (command.type) {
|
|
2341
|
+
case "insert-text":
|
|
2342
|
+
return insertTextInDocument(
|
|
2343
|
+
context.localDocument,
|
|
2344
|
+
selection,
|
|
2345
|
+
command.text,
|
|
2346
|
+
{ timestamp: context.timestamp },
|
|
2347
|
+
);
|
|
2348
|
+
case "delete-backward":
|
|
2349
|
+
return deleteSelectionOrBackward(
|
|
2350
|
+
context.localDocument,
|
|
2351
|
+
selection,
|
|
2352
|
+
{ timestamp: context.timestamp },
|
|
2353
|
+
);
|
|
2354
|
+
case "delete-forward":
|
|
2355
|
+
return deleteSelectionOrForward(
|
|
2356
|
+
context.localDocument,
|
|
2357
|
+
selection,
|
|
2358
|
+
{ timestamp: context.timestamp },
|
|
2359
|
+
);
|
|
2360
|
+
case "insert-tab":
|
|
2361
|
+
return insertTabInDocument(
|
|
2362
|
+
context.localDocument,
|
|
2363
|
+
selection,
|
|
2364
|
+
{ timestamp: context.timestamp },
|
|
2365
|
+
);
|
|
2366
|
+
case "outdent-tab":
|
|
2367
|
+
return {
|
|
2368
|
+
changed: false,
|
|
2369
|
+
document: context.localDocument,
|
|
2370
|
+
selection,
|
|
2371
|
+
};
|
|
2372
|
+
case "insert-hard-break":
|
|
2373
|
+
return insertHardBreakInDocument(
|
|
2374
|
+
context.localDocument,
|
|
2375
|
+
selection,
|
|
2376
|
+
{ timestamp: context.timestamp },
|
|
2377
|
+
);
|
|
2378
|
+
case "split-paragraph":
|
|
2379
|
+
return splitParagraphInDocument(
|
|
2380
|
+
context.localDocument,
|
|
2381
|
+
selection,
|
|
2382
|
+
{ timestamp: context.timestamp },
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
})();
|
|
2386
|
+
|
|
2387
|
+
dispatchStoryMutationResult(
|
|
2388
|
+
runtime,
|
|
2389
|
+
context,
|
|
2390
|
+
{
|
|
2391
|
+
changed: "changed" in localResult ? localResult.changed : true,
|
|
2392
|
+
document: localResult.document,
|
|
2393
|
+
selection: localResult.selection,
|
|
2394
|
+
mapping: "mapping" in localResult ? localResult.mapping : undefined,
|
|
2395
|
+
},
|
|
2396
|
+
context.timestamp,
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function applyListAwareTextCommand(
|
|
2401
|
+
context: NonNullable<ReturnType<typeof getStoryMutationContext>>,
|
|
2402
|
+
command:
|
|
2403
|
+
| { type: "insert-text"; text: string }
|
|
2404
|
+
| { type: "delete-backward" }
|
|
2405
|
+
| { type: "delete-forward" }
|
|
2406
|
+
| { type: "insert-tab" }
|
|
2407
|
+
| { type: "outdent-tab" }
|
|
2408
|
+
| { type: "insert-hard-break" }
|
|
2409
|
+
| { type: "split-paragraph" },
|
|
2410
|
+
): {
|
|
2411
|
+
changed: boolean;
|
|
2412
|
+
document: EditorSessionState["canonicalDocument"];
|
|
2413
|
+
selection: InternalSelectionSnapshot;
|
|
2414
|
+
} | null {
|
|
2415
|
+
const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
|
|
2416
|
+
if (!paragraphContext?.paragraph.numbering) {
|
|
2417
|
+
return null;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
switch (command.type) {
|
|
2421
|
+
case "insert-tab": {
|
|
2422
|
+
const result = indentListItems(
|
|
2423
|
+
context.localDocument,
|
|
2424
|
+
[paragraphContext.paragraphIndex],
|
|
2425
|
+
{ timestamp: context.timestamp },
|
|
2426
|
+
);
|
|
2427
|
+
return createListMutationResult(result, context.localSnapshot.selection);
|
|
2428
|
+
}
|
|
2429
|
+
case "outdent-tab": {
|
|
2430
|
+
const result = outdentListItems(
|
|
2431
|
+
context.localDocument,
|
|
2432
|
+
[paragraphContext.paragraphIndex],
|
|
2433
|
+
{ timestamp: context.timestamp },
|
|
2434
|
+
);
|
|
2435
|
+
return createListMutationResult(result, context.localSnapshot.selection);
|
|
2436
|
+
}
|
|
2437
|
+
case "delete-backward": {
|
|
2438
|
+
if (!paragraphContext.atParagraphStart || !context.localSnapshot.selection.isCollapsed) {
|
|
2439
|
+
return null;
|
|
2440
|
+
}
|
|
2441
|
+
const result = backspaceAtListStart(
|
|
2442
|
+
context.localDocument,
|
|
2443
|
+
paragraphContext.paragraphIndex,
|
|
2444
|
+
{ timestamp: context.timestamp },
|
|
2445
|
+
);
|
|
2446
|
+
return result.handled
|
|
2447
|
+
? createListMutationResult(result, context.localSnapshot.selection)
|
|
2448
|
+
: null;
|
|
2449
|
+
}
|
|
2450
|
+
case "split-paragraph": {
|
|
2451
|
+
if (!context.localSnapshot.selection.isCollapsed || !paragraphContext.isEmpty) {
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
const result = splitListParagraph(
|
|
2455
|
+
context.localDocument,
|
|
2456
|
+
paragraphContext.paragraphIndex,
|
|
2457
|
+
true,
|
|
2458
|
+
{ timestamp: context.timestamp },
|
|
2459
|
+
);
|
|
2460
|
+
return result.action === "split"
|
|
2461
|
+
? null
|
|
2462
|
+
: createListMutationResult(result, context.localSnapshot.selection);
|
|
2463
|
+
}
|
|
2464
|
+
default:
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function createListMutationResult(
|
|
2470
|
+
result: {
|
|
2471
|
+
document: EditorSessionState["canonicalDocument"];
|
|
2472
|
+
affectedParagraphIndexes: number[];
|
|
2473
|
+
},
|
|
2474
|
+
selection: RuntimeRenderSnapshot["selection"],
|
|
2475
|
+
): {
|
|
2476
|
+
changed: boolean;
|
|
2477
|
+
document: EditorSessionState["canonicalDocument"];
|
|
2478
|
+
selection: InternalSelectionSnapshot;
|
|
2479
|
+
} {
|
|
2480
|
+
return {
|
|
2481
|
+
changed: result.affectedParagraphIndexes.length > 0,
|
|
2482
|
+
document: result.document,
|
|
2483
|
+
selection: toRuntimeSelectionSnapshot(selection),
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
function resolveActiveParagraphContext(
|
|
2488
|
+
snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
|
|
2489
|
+
): {
|
|
2490
|
+
paragraphIndex: number;
|
|
2491
|
+
paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
|
|
2492
|
+
atParagraphStart: boolean;
|
|
2493
|
+
isEmpty: boolean;
|
|
2494
|
+
} | null {
|
|
2495
|
+
if (!snapshot.surface) {
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
const paragraphIndex = resolveActiveParagraphIndex(
|
|
2500
|
+
snapshot.surface.blocks,
|
|
2501
|
+
snapshot.selection,
|
|
2502
|
+
);
|
|
2503
|
+
if (paragraphIndex === null) {
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const selectionPosition =
|
|
2508
|
+
snapshot.selection.activeRange.kind === "node"
|
|
2509
|
+
? snapshot.selection.activeRange.at
|
|
2510
|
+
: snapshot.selection.head;
|
|
2511
|
+
const paragraph = findSurfaceParagraphAtPosition(snapshot.surface.blocks, selectionPosition);
|
|
2512
|
+
if (!paragraph) {
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
return {
|
|
2517
|
+
paragraphIndex,
|
|
2518
|
+
paragraph,
|
|
2519
|
+
atParagraphStart:
|
|
2520
|
+
snapshot.selection.isCollapsed &&
|
|
2521
|
+
snapshot.selection.activeRange.kind !== "node" &&
|
|
2522
|
+
snapshot.selection.anchor === snapshot.selection.head &&
|
|
2523
|
+
snapshot.selection.head === paragraph.from,
|
|
2524
|
+
isEmpty: isSurfaceParagraphEmpty(paragraph),
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function findSurfaceParagraphAtPosition(
|
|
2529
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
2530
|
+
position: number,
|
|
2531
|
+
): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null {
|
|
2532
|
+
for (const block of blocks) {
|
|
2533
|
+
if (position < block.from || position > block.to) {
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
if (block.kind === "paragraph") {
|
|
2537
|
+
return block;
|
|
2538
|
+
}
|
|
2539
|
+
if (block.kind === "table") {
|
|
2540
|
+
for (const row of block.rows) {
|
|
2541
|
+
for (const cell of row.cells) {
|
|
2542
|
+
const paragraph = findSurfaceParagraphAtPosition(cell.content, position);
|
|
2543
|
+
if (paragraph) {
|
|
2544
|
+
return paragraph;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
if (block.kind === "sdt_block") {
|
|
2551
|
+
const paragraph = findSurfaceParagraphAtPosition(block.children, position);
|
|
2552
|
+
if (paragraph) {
|
|
2553
|
+
return paragraph;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
return null;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
function isSurfaceParagraphEmpty(
|
|
2561
|
+
paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
2562
|
+
): boolean {
|
|
2563
|
+
if (paragraph.segments.length === 0) {
|
|
2564
|
+
return true;
|
|
2565
|
+
}
|
|
2566
|
+
return paragraph.segments.every((segment) => segment.kind === "text" && segment.text.length === 0);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function applyRuntimeSelection(
|
|
2570
|
+
runtime: WordReviewEditorRuntime,
|
|
2571
|
+
selection: PublicSelectionSnapshot,
|
|
2572
|
+
): void {
|
|
2573
|
+
const requestedStory = selection.storyTarget ?? { kind: "main" };
|
|
2574
|
+
if (requestedStory.kind === "main") {
|
|
2575
|
+
runtime.closeStory();
|
|
2576
|
+
} else if (!storyTargetsEqual(runtime.getActiveStory(), requestedStory)) {
|
|
2577
|
+
if (!runtime.openStory(requestedStory)) {
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
runtime.dispatch({
|
|
2583
|
+
type: "selection.set",
|
|
2584
|
+
selection: toRuntimeSelectionSnapshot(stripStoryTarget(selection)),
|
|
2585
|
+
});
|
|
1361
2586
|
}
|
|
1362
2587
|
|
|
1363
2588
|
function canApplyRuntimeMutation(snapshot: RuntimeRenderSnapshot): boolean {
|
|
1364
2589
|
return snapshot.isReady && !snapshot.readOnly && !snapshot.fatalError;
|
|
1365
2590
|
}
|
|
1366
2591
|
|
|
2592
|
+
function getStoryMutationContext(
|
|
2593
|
+
runtime: WordReviewEditorRuntime,
|
|
2594
|
+
): {
|
|
2595
|
+
timestamp: string;
|
|
2596
|
+
activeStory: EditorStoryTarget;
|
|
2597
|
+
persistedDocument: EditorSessionState["canonicalDocument"];
|
|
2598
|
+
localDocument: EditorSessionState["canonicalDocument"];
|
|
2599
|
+
localSnapshot: RuntimeRenderSnapshot;
|
|
2600
|
+
} | null {
|
|
2601
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2602
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
2603
|
+
return null;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
const persistedDocument = runtime.getSessionState().canonicalDocument;
|
|
2607
|
+
const activeStory = snapshot.activeStory;
|
|
2608
|
+
if (activeStory.kind === "main") {
|
|
2609
|
+
return {
|
|
2610
|
+
timestamp: new Date().toISOString(),
|
|
2611
|
+
activeStory,
|
|
2612
|
+
persistedDocument,
|
|
2613
|
+
localDocument: persistedDocument,
|
|
2614
|
+
localSnapshot: snapshot,
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
return {
|
|
2619
|
+
timestamp: new Date().toISOString(),
|
|
2620
|
+
activeStory,
|
|
2621
|
+
persistedDocument,
|
|
2622
|
+
localDocument: {
|
|
2623
|
+
...persistedDocument,
|
|
2624
|
+
content: {
|
|
2625
|
+
type: "doc",
|
|
2626
|
+
children: [...getStoryBlocks(persistedDocument, activeStory)],
|
|
2627
|
+
},
|
|
2628
|
+
},
|
|
2629
|
+
localSnapshot: {
|
|
2630
|
+
...snapshot,
|
|
2631
|
+
activeStory: { kind: "main" },
|
|
2632
|
+
selection: stripStoryTarget(snapshot.selection),
|
|
2633
|
+
},
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
function dispatchStoryMutationResult(
|
|
2638
|
+
runtime: WordReviewEditorRuntime,
|
|
2639
|
+
context: {
|
|
2640
|
+
activeStory: EditorStoryTarget;
|
|
2641
|
+
persistedDocument: EditorSessionState["canonicalDocument"];
|
|
2642
|
+
},
|
|
2643
|
+
result: {
|
|
2644
|
+
changed: boolean;
|
|
2645
|
+
document: EditorSessionState["canonicalDocument"];
|
|
2646
|
+
selection: InternalSelectionSnapshot;
|
|
2647
|
+
mapping?: TransactionMapping;
|
|
2648
|
+
},
|
|
2649
|
+
timestamp: string,
|
|
2650
|
+
): void {
|
|
2651
|
+
if (context.activeStory.kind === "main") {
|
|
2652
|
+
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
if (!result.changed) {
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
const nextDocument = replaceStoryBlocks(
|
|
2661
|
+
context.persistedDocument,
|
|
2662
|
+
context.activeStory,
|
|
2663
|
+
result.document.content.children,
|
|
2664
|
+
);
|
|
2665
|
+
dispatchRuntimeDocumentMutation(
|
|
2666
|
+
runtime,
|
|
2667
|
+
{
|
|
2668
|
+
changed: true,
|
|
2669
|
+
document: nextDocument,
|
|
2670
|
+
selection: result.selection,
|
|
2671
|
+
},
|
|
2672
|
+
timestamp,
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
1367
2676
|
function dispatchRuntimeDocumentMutation(
|
|
1368
2677
|
runtime: WordReviewEditorRuntime,
|
|
1369
2678
|
result: {
|
|
1370
2679
|
changed: boolean;
|
|
1371
|
-
document:
|
|
2680
|
+
document: EditorSessionState["canonicalDocument"];
|
|
1372
2681
|
selection: InternalSelectionSnapshot;
|
|
1373
2682
|
mapping?: TransactionMapping;
|
|
1374
2683
|
},
|
|
@@ -1393,6 +2702,13 @@ function dispatchRuntimeDocumentMutation(
|
|
|
1393
2702
|
});
|
|
1394
2703
|
}
|
|
1395
2704
|
|
|
2705
|
+
function stripStoryTarget(
|
|
2706
|
+
selection: PublicSelectionSnapshot,
|
|
2707
|
+
): PublicSelectionSnapshot {
|
|
2708
|
+
const { storyTarget: _storyTarget, ...rest } = selection;
|
|
2709
|
+
return rest;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
1396
2712
|
function applyRuntimeDeleteComment(
|
|
1397
2713
|
runtime: WordReviewEditorRuntime,
|
|
1398
2714
|
commentId: string,
|
|
@@ -1402,22 +2718,22 @@ function applyRuntimeDeleteComment(
|
|
|
1402
2718
|
return;
|
|
1403
2719
|
}
|
|
1404
2720
|
|
|
1405
|
-
const
|
|
1406
|
-
if (!
|
|
2721
|
+
const sessionState = runtime.getSessionState();
|
|
2722
|
+
if (!sessionState.canonicalDocument.review.comments[commentId]) {
|
|
1407
2723
|
return;
|
|
1408
2724
|
}
|
|
1409
2725
|
|
|
1410
2726
|
const nextComments = {
|
|
1411
|
-
...
|
|
2727
|
+
...sessionState.canonicalDocument.review.comments,
|
|
1412
2728
|
};
|
|
1413
2729
|
delete nextComments[commentId];
|
|
1414
2730
|
|
|
1415
2731
|
runtime.dispatch({
|
|
1416
2732
|
type: "document.replace",
|
|
1417
2733
|
document: {
|
|
1418
|
-
...
|
|
2734
|
+
...sessionState.canonicalDocument,
|
|
1419
2735
|
review: {
|
|
1420
|
-
...
|
|
2736
|
+
...sessionState.canonicalDocument.review,
|
|
1421
2737
|
comments: nextComments,
|
|
1422
2738
|
},
|
|
1423
2739
|
},
|
|
@@ -1433,10 +2749,19 @@ function normalizeRequestedSelection(
|
|
|
1433
2749
|
snapshot: RuntimeRenderSnapshot,
|
|
1434
2750
|
selection: PublicSelectionSnapshot | null,
|
|
1435
2751
|
): PublicSelectionSnapshot {
|
|
1436
|
-
return
|
|
2752
|
+
return (
|
|
2753
|
+
selection ??
|
|
2754
|
+
createCollapsedPublicSelection(
|
|
2755
|
+
snapshot.selection.head,
|
|
2756
|
+
snapshot.activeStory.kind === "main" ? undefined : snapshot.activeStory,
|
|
2757
|
+
)
|
|
2758
|
+
);
|
|
1437
2759
|
}
|
|
1438
2760
|
|
|
1439
|
-
function createCollapsedPublicSelection(
|
|
2761
|
+
function createCollapsedPublicSelection(
|
|
2762
|
+
position: number,
|
|
2763
|
+
storyTarget?: EditorStoryTarget,
|
|
2764
|
+
): PublicSelectionSnapshot {
|
|
1440
2765
|
return {
|
|
1441
2766
|
anchor: position,
|
|
1442
2767
|
head: position,
|
|
@@ -1450,6 +2775,7 @@ function createCollapsedPublicSelection(position: number): PublicSelectionSnapsh
|
|
|
1450
2775
|
end: 1,
|
|
1451
2776
|
},
|
|
1452
2777
|
},
|
|
2778
|
+
...(storyTarget ? { storyTarget } : {}),
|
|
1453
2779
|
};
|
|
1454
2780
|
}
|
|
1455
2781
|
|
|
@@ -1457,61 +2783,66 @@ function clonePublicValue<T>(value: T): T {
|
|
|
1457
2783
|
return structuredClone(value);
|
|
1458
2784
|
}
|
|
1459
2785
|
|
|
1460
|
-
function
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
2786
|
+
function openDefaultStoryVariant(
|
|
2787
|
+
runtime: WordReviewEditorRuntime,
|
|
2788
|
+
pageLayout: PageLayoutSnapshot | undefined,
|
|
2789
|
+
navigation: ReturnType<WordReviewEditorRuntime["getDocumentNavigationSnapshot"]> | undefined,
|
|
2790
|
+
kind: "header" | "footer",
|
|
2791
|
+
): void {
|
|
2792
|
+
const variants =
|
|
2793
|
+
kind === "header"
|
|
2794
|
+
? pageLayout?.headerVariants
|
|
2795
|
+
: pageLayout?.footerVariants;
|
|
2796
|
+
const activePage = navigation?.pages[navigation.activePageIndex];
|
|
2797
|
+
const isFirstPageInSection =
|
|
2798
|
+
activePage !== undefined &&
|
|
2799
|
+
activePage.sectionIndex === pageLayout?.sectionIndex &&
|
|
2800
|
+
activePage.pageInSection === 0;
|
|
2801
|
+
const isEvenDocumentPage = activePage !== undefined && (activePage.pageIndex + 1) % 2 === 0;
|
|
2802
|
+
|
|
2803
|
+
let variant =
|
|
2804
|
+
pageLayout?.differentFirstPage && isFirstPageInSection
|
|
2805
|
+
? variants?.find((entry) => entry.variant === "first")
|
|
2806
|
+
: undefined;
|
|
2807
|
+
|
|
2808
|
+
if (!variant && pageLayout?.differentOddEvenPages && isEvenDocumentPage) {
|
|
2809
|
+
variant = variants?.find((entry) => entry.variant === "even");
|
|
1468
2810
|
}
|
|
1469
2811
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
options,
|
|
1474
|
-
).slice(0, options.limit ?? Number.POSITIVE_INFINITY);
|
|
1475
|
-
const activeResultIndex = getActiveSearchResultIndex(rawResults, snapshot.selection);
|
|
2812
|
+
if (!variant) {
|
|
2813
|
+
variant = variants?.find((entry) => entry.variant === "default") ?? variants?.[0];
|
|
2814
|
+
}
|
|
1476
2815
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
},
|
|
1487
|
-
},
|
|
1488
|
-
excerpt: createSearchExcerpt(
|
|
1489
|
-
snapshot.surface?.plainText ?? "",
|
|
1490
|
-
result.from,
|
|
1491
|
-
result.to,
|
|
1492
|
-
),
|
|
1493
|
-
isActive: index === activeResultIndex,
|
|
1494
|
-
}));
|
|
2816
|
+
if (!variant) {
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
runtime.openStory({
|
|
2820
|
+
kind,
|
|
2821
|
+
relationshipId: variant.relationshipId,
|
|
2822
|
+
variant: variant.variant,
|
|
2823
|
+
sectionIndex: pageLayout?.sectionIndex,
|
|
2824
|
+
});
|
|
1495
2825
|
}
|
|
1496
2826
|
|
|
1497
|
-
function
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
2827
|
+
function searchRuntimeDocument(
|
|
2828
|
+
runtime: WordReviewEditorRuntime,
|
|
2829
|
+
mountedSurface: TwProseMirrorSurfaceRef | null,
|
|
2830
|
+
query: string,
|
|
2831
|
+
options: SearchOptions = {},
|
|
2832
|
+
): SearchResultSnapshot[] {
|
|
2833
|
+
if (mountedSurface) {
|
|
2834
|
+
return mountedSurface.search(query, options);
|
|
1503
2835
|
}
|
|
1504
2836
|
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return activeIndex >= 0 ? activeIndex : 0;
|
|
2837
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
2838
|
+
return searchDocument(
|
|
2839
|
+
runtime.getSessionState().canonicalDocument,
|
|
2840
|
+
snapshot.selection,
|
|
2841
|
+
snapshot.activeStory,
|
|
2842
|
+
runtime.getDocumentNavigationSnapshot(),
|
|
2843
|
+
query,
|
|
2844
|
+
options,
|
|
2845
|
+
);
|
|
1515
2846
|
}
|
|
1516
2847
|
|
|
1517
2848
|
function applyRegionAttributes(shell: HTMLElement): void {
|
|
@@ -1656,12 +2987,21 @@ function normalizeEditorError(error: unknown): EditorError {
|
|
|
1656
2987
|
|
|
1657
2988
|
function guessSourceLabel(
|
|
1658
2989
|
initialSourceLabel?: string,
|
|
2990
|
+
initialSessionState?: EditorSessionState,
|
|
1659
2991
|
initialSnapshot?: PersistedEditorSnapshot,
|
|
1660
2992
|
externalDocSource?: WordReviewEditorProps["externalDocSource"],
|
|
1661
2993
|
): string | undefined {
|
|
1662
2994
|
return (
|
|
1663
2995
|
externalDocSource?.sourceLabel ??
|
|
2996
|
+
(externalDocSource?.kind === "session"
|
|
2997
|
+
? externalDocSource.sessionState.sourcePackage?.sourceLabel
|
|
2998
|
+
: undefined) ??
|
|
2999
|
+
(externalDocSource?.kind === "snapshot"
|
|
3000
|
+
? externalDocSource.snapshot.sourcePackage?.sourceLabel
|
|
3001
|
+
: undefined) ??
|
|
1664
3002
|
initialSourceLabel ??
|
|
3003
|
+
initialSessionState?.sourcePackage?.sourceLabel ??
|
|
3004
|
+
initialSessionState?.editorBuild ??
|
|
1665
3005
|
initialSnapshot?.sourcePackage?.sourceLabel ??
|
|
1666
3006
|
initialSnapshot?.editorBuild ??
|
|
1667
3007
|
undefined
|
|
@@ -1682,6 +3022,7 @@ function createLoadingSnapshot(
|
|
|
1682
3022
|
isDirty: false,
|
|
1683
3023
|
readOnly,
|
|
1684
3024
|
selection: collapsedSelection(),
|
|
3025
|
+
activeStory: { kind: "main" },
|
|
1685
3026
|
documentStats: {
|
|
1686
3027
|
storyLength: 0,
|
|
1687
3028
|
commentCount: 0,
|
|
@@ -1721,6 +3062,16 @@ function createLoadingSnapshot(
|
|
|
1721
3062
|
};
|
|
1722
3063
|
}
|
|
1723
3064
|
|
|
3065
|
+
function deriveEditorViewMode(
|
|
3066
|
+
readOnly: boolean,
|
|
3067
|
+
reviewMode: WordReviewEditorProps["reviewMode"] = "review",
|
|
3068
|
+
): EditorViewMode {
|
|
3069
|
+
if (readOnly) {
|
|
3070
|
+
return "view";
|
|
3071
|
+
}
|
|
3072
|
+
return reviewMode === "editing" ? "editing" : "review";
|
|
3073
|
+
}
|
|
3074
|
+
|
|
1724
3075
|
function createErrorSnapshot(documentId: string, error: EditorError): RuntimeRenderSnapshot {
|
|
1725
3076
|
return {
|
|
1726
3077
|
...createLoadingSnapshot(documentId, true),
|
|
@@ -1739,6 +3090,7 @@ function createErrorSnapshot(documentId: string, error: EditorError): RuntimeRen
|
|
|
1739
3090
|
}
|
|
1740
3091
|
|
|
1741
3092
|
async function persistAndExport(input: {
|
|
3093
|
+
hostAdapter?: EditorHostAdapter;
|
|
1742
3094
|
datastore?: EditorDatastoreAdapter;
|
|
1743
3095
|
documentId: string;
|
|
1744
3096
|
runtime: WordReviewEditorRuntime;
|
|
@@ -1753,7 +3105,8 @@ async function persistAndExport(input: {
|
|
|
1753
3105
|
input.autosaveTimerRef.current = null;
|
|
1754
3106
|
}
|
|
1755
3107
|
|
|
1756
|
-
await
|
|
3108
|
+
await persistSession({
|
|
3109
|
+
hostAdapter: input.hostAdapter,
|
|
1757
3110
|
datastore: input.datastore,
|
|
1758
3111
|
documentId: input.documentId,
|
|
1759
3112
|
runtime: input.runtime,
|
|
@@ -1770,6 +3123,7 @@ async function persistAndExport(input: {
|
|
|
1770
3123
|
const normalized = normalizeExportError(error, input.documentId, input.options);
|
|
1771
3124
|
input.onError?.(normalized);
|
|
1772
3125
|
emitEditorEvent({
|
|
3126
|
+
hostAdapter: input.hostAdapter,
|
|
1773
3127
|
datastore: input.datastore,
|
|
1774
3128
|
onEvent: input.onEvent,
|
|
1775
3129
|
event: {
|
|
@@ -1781,24 +3135,43 @@ async function persistAndExport(input: {
|
|
|
1781
3135
|
throw normalized;
|
|
1782
3136
|
}
|
|
1783
3137
|
|
|
1784
|
-
|
|
3138
|
+
const saveExport = input.hostAdapter?.saveExport ?? input.datastore?.saveExport;
|
|
3139
|
+
const saveExportSource = input.hostAdapter?.saveExport ? "host" : "datastore";
|
|
3140
|
+
if (!saveExport) {
|
|
3141
|
+
result = downloadExportResult(result);
|
|
3142
|
+
emitEditorEvent({
|
|
3143
|
+
hostAdapter: input.hostAdapter,
|
|
3144
|
+
datastore: input.datastore,
|
|
3145
|
+
onEvent: input.onEvent,
|
|
3146
|
+
event: {
|
|
3147
|
+
type: "export_completed",
|
|
3148
|
+
documentId: input.documentId,
|
|
3149
|
+
result,
|
|
3150
|
+
},
|
|
3151
|
+
});
|
|
1785
3152
|
return result;
|
|
1786
3153
|
}
|
|
1787
3154
|
|
|
1788
3155
|
try {
|
|
1789
|
-
await
|
|
3156
|
+
const saveResult = await saveExport({
|
|
1790
3157
|
documentId: input.documentId,
|
|
1791
3158
|
result,
|
|
1792
3159
|
});
|
|
3160
|
+
result = withExportDelivery(result, {
|
|
3161
|
+
mode: "persisted-by-host",
|
|
3162
|
+
savedAt: saveResult.savedAt,
|
|
3163
|
+
});
|
|
1793
3164
|
} catch (error) {
|
|
1794
|
-
const normalized =
|
|
3165
|
+
const normalized = normalizeStorageError(error, {
|
|
1795
3166
|
message: "Export persisted bytes could not be stored.",
|
|
3167
|
+
source: saveExportSource,
|
|
1796
3168
|
details: {
|
|
1797
3169
|
operation: "saveExport",
|
|
1798
3170
|
},
|
|
1799
3171
|
});
|
|
1800
3172
|
input.onError?.(normalized);
|
|
1801
3173
|
emitEditorEvent({
|
|
3174
|
+
hostAdapter: input.hostAdapter,
|
|
1802
3175
|
datastore: input.datastore,
|
|
1803
3176
|
onEvent: input.onEvent,
|
|
1804
3177
|
event: {
|
|
@@ -1807,13 +3180,28 @@ async function persistAndExport(input: {
|
|
|
1807
3180
|
error: normalized,
|
|
1808
3181
|
},
|
|
1809
3182
|
});
|
|
3183
|
+
result = withExportDelivery(result, {
|
|
3184
|
+
mode: "exported-bytes-only",
|
|
3185
|
+
});
|
|
1810
3186
|
}
|
|
1811
3187
|
|
|
3188
|
+
emitEditorEvent({
|
|
3189
|
+
hostAdapter: input.hostAdapter,
|
|
3190
|
+
datastore: input.datastore,
|
|
3191
|
+
onEvent: input.onEvent,
|
|
3192
|
+
event: {
|
|
3193
|
+
type: "export_completed",
|
|
3194
|
+
documentId: input.documentId,
|
|
3195
|
+
result,
|
|
3196
|
+
},
|
|
3197
|
+
});
|
|
3198
|
+
|
|
1812
3199
|
return result;
|
|
1813
3200
|
}
|
|
1814
3201
|
|
|
1815
3202
|
function rejectExportWhileLoading(input: {
|
|
1816
3203
|
documentId: string;
|
|
3204
|
+
hostAdapter?: EditorHostAdapter;
|
|
1817
3205
|
datastore?: EditorDatastoreAdapter;
|
|
1818
3206
|
onError?: (error: EditorError) => void;
|
|
1819
3207
|
onEvent?: (event: WordReviewEditorEvent) => void;
|
|
@@ -1827,6 +3215,7 @@ function rejectExportWhileLoading(input: {
|
|
|
1827
3215
|
};
|
|
1828
3216
|
input.onError?.(error);
|
|
1829
3217
|
emitEditorEvent({
|
|
3218
|
+
hostAdapter: input.hostAdapter,
|
|
1830
3219
|
datastore: input.datastore,
|
|
1831
3220
|
onEvent: input.onEvent,
|
|
1832
3221
|
event: {
|
|
@@ -1838,7 +3227,8 @@ function rejectExportWhileLoading(input: {
|
|
|
1838
3227
|
return Promise.reject(error);
|
|
1839
3228
|
}
|
|
1840
3229
|
|
|
1841
|
-
async function
|
|
3230
|
+
async function persistSession(input: {
|
|
3231
|
+
hostAdapter?: EditorHostAdapter;
|
|
1842
3232
|
datastore?: EditorDatastoreAdapter;
|
|
1843
3233
|
documentId: string;
|
|
1844
3234
|
runtime: WordReviewEditorRuntime;
|
|
@@ -1847,15 +3237,19 @@ async function persistSnapshot(input: {
|
|
|
1847
3237
|
onEvent?: (event: WordReviewEditorEvent) => void;
|
|
1848
3238
|
lastSavedRevisionTokenRef: React.MutableRefObject<string | null>;
|
|
1849
3239
|
}): Promise<void> {
|
|
1850
|
-
|
|
3240
|
+
const saveSession = input.hostAdapter?.saveSession;
|
|
3241
|
+
const saveSnapshot = input.datastore?.saveSnapshot;
|
|
3242
|
+
if (!saveSession && !saveSnapshot) {
|
|
1851
3243
|
return;
|
|
1852
3244
|
}
|
|
1853
3245
|
|
|
3246
|
+
const sessionState = input.runtime.getSessionState();
|
|
1854
3247
|
const snapshot = input.runtime.getPersistedSnapshot();
|
|
1855
3248
|
const revisionToken = input.runtime.getRenderSnapshot().revisionToken;
|
|
1856
3249
|
|
|
1857
3250
|
if (input.isAutosave) {
|
|
1858
3251
|
emitEditorEvent({
|
|
3252
|
+
hostAdapter: input.hostAdapter,
|
|
1859
3253
|
datastore: input.datastore,
|
|
1860
3254
|
onEvent: input.onEvent,
|
|
1861
3255
|
event: {
|
|
@@ -1869,28 +3263,43 @@ async function persistSnapshot(input: {
|
|
|
1869
3263
|
}
|
|
1870
3264
|
|
|
1871
3265
|
try {
|
|
1872
|
-
const result =
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
3266
|
+
const result = saveSession
|
|
3267
|
+
? await saveSession({
|
|
3268
|
+
documentId: input.documentId,
|
|
3269
|
+
sessionState,
|
|
3270
|
+
isAutosave: input.isAutosave,
|
|
3271
|
+
})
|
|
3272
|
+
: await saveSnapshot!({
|
|
3273
|
+
documentId: input.documentId,
|
|
3274
|
+
snapshot,
|
|
3275
|
+
isAutosave: input.isAutosave,
|
|
3276
|
+
});
|
|
1881
3277
|
input.lastSavedRevisionTokenRef.current = revisionToken;
|
|
1882
3278
|
emitEditorEvent({
|
|
3279
|
+
hostAdapter: input.hostAdapter,
|
|
1883
3280
|
datastore: input.datastore,
|
|
1884
3281
|
onEvent: input.onEvent,
|
|
1885
|
-
event:
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
3282
|
+
event: saveSession
|
|
3283
|
+
? {
|
|
3284
|
+
type: "session_saved",
|
|
3285
|
+
documentId: input.documentId,
|
|
3286
|
+
sessionState: input.runtime.getSessionState(),
|
|
3287
|
+
savedAt: result.savedAt,
|
|
3288
|
+
isAutosave: input.isAutosave,
|
|
3289
|
+
}
|
|
3290
|
+
: {
|
|
3291
|
+
type: "snapshot_saved",
|
|
3292
|
+
documentId: input.documentId,
|
|
3293
|
+
snapshot: {
|
|
3294
|
+
...snapshot,
|
|
3295
|
+
savedAt: result.savedAt,
|
|
3296
|
+
},
|
|
3297
|
+
isAutosave: input.isAutosave,
|
|
3298
|
+
},
|
|
1891
3299
|
});
|
|
1892
3300
|
if (input.isAutosave) {
|
|
1893
3301
|
emitEditorEvent({
|
|
3302
|
+
hostAdapter: input.hostAdapter,
|
|
1894
3303
|
datastore: input.datastore,
|
|
1895
3304
|
onEvent: input.onEvent,
|
|
1896
3305
|
event: {
|
|
@@ -1904,17 +3313,23 @@ async function persistSnapshot(input: {
|
|
|
1904
3313
|
});
|
|
1905
3314
|
}
|
|
1906
3315
|
} catch (error) {
|
|
1907
|
-
const normalized =
|
|
3316
|
+
const normalized = normalizeStorageError(error, {
|
|
1908
3317
|
message: input.isAutosave
|
|
1909
|
-
?
|
|
1910
|
-
|
|
3318
|
+
? saveSession
|
|
3319
|
+
? "Autosave failed while storing the editor session."
|
|
3320
|
+
: "Autosave failed while storing the editor snapshot."
|
|
3321
|
+
: saveSession
|
|
3322
|
+
? "Session save failed while preparing the export checkpoint."
|
|
3323
|
+
: "Snapshot save failed while preparing the export checkpoint.",
|
|
3324
|
+
source: saveSession ? "host" : "datastore",
|
|
1911
3325
|
details: {
|
|
1912
|
-
operation: "saveSnapshot",
|
|
3326
|
+
operation: saveSession ? "saveSession" : "saveSnapshot",
|
|
1913
3327
|
isAutosave: input.isAutosave,
|
|
1914
3328
|
},
|
|
1915
3329
|
});
|
|
1916
3330
|
input.onError?.(normalized);
|
|
1917
3331
|
emitEditorEvent({
|
|
3332
|
+
hostAdapter: input.hostAdapter,
|
|
1918
3333
|
datastore: input.datastore,
|
|
1919
3334
|
onEvent: input.onEvent,
|
|
1920
3335
|
event: {
|
|
@@ -1925,6 +3340,7 @@ async function persistSnapshot(input: {
|
|
|
1925
3340
|
});
|
|
1926
3341
|
if (input.isAutosave) {
|
|
1927
3342
|
emitEditorEvent({
|
|
3343
|
+
hostAdapter: input.hostAdapter,
|
|
1928
3344
|
datastore: input.datastore,
|
|
1929
3345
|
onEvent: input.onEvent,
|
|
1930
3346
|
event: {
|
|
@@ -1944,12 +3360,14 @@ async function persistSnapshot(input: {
|
|
|
1944
3360
|
}
|
|
1945
3361
|
|
|
1946
3362
|
function emitEditorEvent(input: {
|
|
3363
|
+
hostAdapter?: EditorHostAdapter;
|
|
1947
3364
|
datastore?: EditorDatastoreAdapter;
|
|
1948
3365
|
onEvent?: (event: WordReviewEditorEvent) => void;
|
|
1949
3366
|
event: WordReviewEditorEvent;
|
|
1950
3367
|
}): void {
|
|
1951
3368
|
input.onEvent?.(input.event);
|
|
1952
|
-
input.datastore?.logEvent
|
|
3369
|
+
const logEvent = input.hostAdapter?.logEvent ?? input.datastore?.logEvent;
|
|
3370
|
+
logEvent?.({
|
|
1953
3371
|
type: input.event.type,
|
|
1954
3372
|
documentId: input.event.documentId,
|
|
1955
3373
|
detail: summarizeEventDetail(input.event),
|
|
@@ -1979,8 +3397,16 @@ function summarizeEventDetail(
|
|
|
1979
3397
|
return { status: event.state.status };
|
|
1980
3398
|
case "snapshot_saved":
|
|
1981
3399
|
return { isAutosave: event.isAutosave, savedAt: event.snapshot.savedAt };
|
|
3400
|
+
case "session_saved":
|
|
3401
|
+
return { isAutosave: event.isAutosave, savedAt: event.savedAt };
|
|
1982
3402
|
case "export_completed":
|
|
1983
|
-
return {
|
|
3403
|
+
return {
|
|
3404
|
+
fileName: event.result.fileName,
|
|
3405
|
+
deliveryMode: event.result.delivery?.mode,
|
|
3406
|
+
savedAt: event.result.delivery?.savedAt,
|
|
3407
|
+
};
|
|
3408
|
+
case "story_changed":
|
|
3409
|
+
return { activeStory: event.activeStory };
|
|
1984
3410
|
case "selection_changed":
|
|
1985
3411
|
return {
|
|
1986
3412
|
anchor: event.selection.anchor,
|
|
@@ -1996,7 +3422,7 @@ function summarizeEventDetail(
|
|
|
1996
3422
|
|
|
1997
3423
|
function createReadyEvent(
|
|
1998
3424
|
runtime: Pick<WordReviewEditorRuntime, "getCompatibilityReport" | "getRenderSnapshot">,
|
|
1999
|
-
source: "docx" | "
|
|
3425
|
+
source: "docx" | "session" | "snapshot",
|
|
2000
3426
|
): Extract<WordReviewEditorEvent, { type: "ready" }> {
|
|
2001
3427
|
const snapshot = runtime.getRenderSnapshot();
|
|
2002
3428
|
return {
|
|
@@ -2011,10 +3437,11 @@ function createReadyEvent(
|
|
|
2011
3437
|
};
|
|
2012
3438
|
}
|
|
2013
3439
|
|
|
2014
|
-
function
|
|
3440
|
+
function normalizeStorageError(
|
|
2015
3441
|
error: unknown,
|
|
2016
3442
|
fallback: {
|
|
2017
3443
|
message: string;
|
|
3444
|
+
source: "host" | "datastore";
|
|
2018
3445
|
details?: Record<string, unknown>;
|
|
2019
3446
|
},
|
|
2020
3447
|
): EditorError {
|
|
@@ -2029,18 +3456,26 @@ function normalizeDatastoreError(
|
|
|
2029
3456
|
}
|
|
2030
3457
|
|
|
2031
3458
|
return {
|
|
2032
|
-
errorId:
|
|
3459
|
+
errorId:
|
|
3460
|
+
fallback.source === "host"
|
|
3461
|
+
? "word-review-editor-host"
|
|
3462
|
+
: "word-review-editor-datastore",
|
|
2033
3463
|
code: "datastore_failed",
|
|
2034
3464
|
message: error instanceof Error ? error.message : fallback.message,
|
|
2035
3465
|
isFatal: false,
|
|
2036
|
-
source: "datastore",
|
|
3466
|
+
source: fallback.source === "host" ? "host" : "datastore",
|
|
2037
3467
|
details: fallback.details,
|
|
2038
3468
|
};
|
|
2039
3469
|
}
|
|
2040
3470
|
|
|
2041
3471
|
function createFallbackSnapshot(args: CreateRuntimeArgs): RuntimeRenderSnapshot {
|
|
2042
|
-
const
|
|
2043
|
-
|
|
3472
|
+
const initialSessionState =
|
|
3473
|
+
args.source.initialSessionState ??
|
|
3474
|
+
(args.source.initialSnapshot
|
|
3475
|
+
? editorSessionStateFromPersistedSnapshot(args.source.initialSnapshot)
|
|
3476
|
+
: undefined);
|
|
3477
|
+
const warnings = initialSessionState?.warningLog ?? [];
|
|
3478
|
+
const compatibility = initialSessionState?.compatibility ?? emptyCompatibilityReport();
|
|
2044
3479
|
|
|
2045
3480
|
return {
|
|
2046
3481
|
...createLoadingSnapshot(args.documentId, args.readOnly, args.source.sourceLabel),
|
|
@@ -2048,7 +3483,7 @@ function createFallbackSnapshot(args: CreateRuntimeArgs): RuntimeRenderSnapshot
|
|
|
2048
3483
|
revisionToken: `${args.documentId}:0`,
|
|
2049
3484
|
isReady: true,
|
|
2050
3485
|
documentStats: {
|
|
2051
|
-
storyLength: estimateStoryLength(args.source.initialSnapshot),
|
|
3486
|
+
storyLength: estimateStoryLength(initialSessionState ?? args.source.initialSnapshot),
|
|
2052
3487
|
commentCount: 0,
|
|
2053
3488
|
revisionCount: 0,
|
|
2054
3489
|
opaqueFragmentCount: 0,
|
|
@@ -2131,17 +3566,19 @@ function emptyCompatibilityReport(): CompatibilityReport {
|
|
|
2131
3566
|
};
|
|
2132
3567
|
}
|
|
2133
3568
|
|
|
2134
|
-
function
|
|
3569
|
+
function resolvePackageBackedExportSession(args: CreateRuntimeArgs): {
|
|
2135
3570
|
session?: PackageBackedDocxSession;
|
|
2136
3571
|
barrier?: SnapshotExportBarrier;
|
|
2137
3572
|
} {
|
|
2138
|
-
const sourcePackage =
|
|
3573
|
+
const sourcePackage =
|
|
3574
|
+
args.source.initialSessionState?.sourcePackage ??
|
|
3575
|
+
args.source.initialSnapshot?.sourcePackage;
|
|
2139
3576
|
if (!sourcePackage) {
|
|
2140
3577
|
return {
|
|
2141
3578
|
barrier: {
|
|
2142
3579
|
reason: "missing_source_package_provenance",
|
|
2143
3580
|
message:
|
|
2144
|
-
"DOCX export is blocked because this
|
|
3581
|
+
"DOCX export is blocked because this session was loaded without embedded source package provenance.",
|
|
2145
3582
|
},
|
|
2146
3583
|
};
|
|
2147
3584
|
}
|
|
@@ -2162,7 +3599,10 @@ function resolveSnapshotExportSession(args: CreateRuntimeArgs): {
|
|
|
2162
3599
|
documentId: args.documentId,
|
|
2163
3600
|
sourceLabel: sourcePackage.sourceLabel ?? args.source.sourceLabel,
|
|
2164
3601
|
bytes,
|
|
2165
|
-
editorBuild:
|
|
3602
|
+
editorBuild:
|
|
3603
|
+
args.source.initialSessionState?.editorBuild ??
|
|
3604
|
+
args.source.initialSnapshot?.editorBuild ??
|
|
3605
|
+
"dev",
|
|
2166
3606
|
});
|
|
2167
3607
|
if (session.readOnly || session.fatalError) {
|
|
2168
3608
|
return {
|
|
@@ -2186,17 +3626,17 @@ function resolveSnapshotExportSession(args: CreateRuntimeArgs): {
|
|
|
2186
3626
|
}
|
|
2187
3627
|
}
|
|
2188
3628
|
|
|
2189
|
-
function
|
|
2190
|
-
|
|
3629
|
+
function applySessionExportBarrier(
|
|
3630
|
+
sessionState: EditorSessionState,
|
|
2191
3631
|
barrier: SnapshotExportBarrier,
|
|
2192
|
-
):
|
|
3632
|
+
): EditorSessionState {
|
|
2193
3633
|
const featureEntryId = `feature:source-package-provenance:${barrier.reason}`;
|
|
2194
|
-
const featureEntries =
|
|
3634
|
+
const featureEntries = sessionState.compatibility.featureEntries.some(
|
|
2195
3635
|
(entry) => entry.featureEntryId === featureEntryId,
|
|
2196
3636
|
)
|
|
2197
|
-
?
|
|
3637
|
+
? sessionState.compatibility.featureEntries
|
|
2198
3638
|
: [
|
|
2199
|
-
...
|
|
3639
|
+
...sessionState.compatibility.featureEntries,
|
|
2200
3640
|
{
|
|
2201
3641
|
featureEntryId,
|
|
2202
3642
|
featureKey: "source-package-provenance",
|
|
@@ -2209,9 +3649,9 @@ function applySnapshotExportBarrier(
|
|
|
2209
3649
|
];
|
|
2210
3650
|
|
|
2211
3651
|
return {
|
|
2212
|
-
...
|
|
3652
|
+
...sessionState,
|
|
2213
3653
|
compatibility: {
|
|
2214
|
-
...
|
|
3654
|
+
...sessionState.compatibility,
|
|
2215
3655
|
blockExport: true,
|
|
2216
3656
|
featureEntries,
|
|
2217
3657
|
},
|
|
@@ -2311,9 +3751,17 @@ function createSelectionFromAnchor(
|
|
|
2311
3751
|
}
|
|
2312
3752
|
}
|
|
2313
3753
|
|
|
2314
|
-
function estimateStoryLength(
|
|
2315
|
-
|
|
2316
|
-
|
|
3754
|
+
function estimateStoryLength(
|
|
3755
|
+
sessionStateOrSnapshot?: EditorSessionState | PersistedEditorSnapshot,
|
|
3756
|
+
): number {
|
|
3757
|
+
if (!sessionStateOrSnapshot) {
|
|
3758
|
+
return 0;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
const content = "sessionVersion" in sessionStateOrSnapshot
|
|
3762
|
+
? sessionStateOrSnapshot.canonicalDocument.content
|
|
3763
|
+
: sessionStateOrSnapshot.canonicalDocument.content;
|
|
3764
|
+
return Array.isArray(content?.children) ? content.children.length : 0;
|
|
2317
3765
|
}
|
|
2318
3766
|
|
|
2319
3767
|
function collapsedSelection(): RuntimeRenderSnapshot["selection"] {
|
|
@@ -2358,3 +3806,145 @@ function summarizeSelectionPreview(snapshot: RuntimeRenderSnapshot): string | nu
|
|
|
2358
3806
|
|
|
2359
3807
|
return preview.length > 48 ? `${preview.slice(0, 45)}...` : preview;
|
|
2360
3808
|
}
|
|
3809
|
+
|
|
3810
|
+
function selectionToolbarAnchorsEqual(
|
|
3811
|
+
left: SelectionToolbarAnchor | null,
|
|
3812
|
+
right: SelectionToolbarAnchor | null,
|
|
3813
|
+
): boolean {
|
|
3814
|
+
if (left === right) {
|
|
3815
|
+
return true;
|
|
3816
|
+
}
|
|
3817
|
+
if (!left || !right) {
|
|
3818
|
+
return false;
|
|
3819
|
+
}
|
|
3820
|
+
return (
|
|
3821
|
+
left.left === right.left &&
|
|
3822
|
+
left.right === right.right &&
|
|
3823
|
+
left.top === right.top &&
|
|
3824
|
+
left.bottom === right.bottom
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
function createSelectionToolbarSelectionKey(
|
|
3829
|
+
selection: RuntimeRenderSnapshot["selection"],
|
|
3830
|
+
activeStory: EditorStoryTarget,
|
|
3831
|
+
): string | null {
|
|
3832
|
+
if (selection.isCollapsed || selection.activeRange.kind !== "range") {
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
return JSON.stringify({
|
|
3837
|
+
story: activeStory,
|
|
3838
|
+
from: selection.activeRange.from,
|
|
3839
|
+
to: selection.activeRange.to,
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
function buildSelectionToolbarModel(args: {
|
|
3844
|
+
snapshot: RuntimeRenderSnapshot;
|
|
3845
|
+
viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>;
|
|
3846
|
+
capabilities: ReturnType<typeof deriveCapabilities>;
|
|
3847
|
+
documentNavigation: ReturnType<WordReviewEditorRuntime["getDocumentNavigationSnapshot"]>;
|
|
3848
|
+
styleCatalog: StyleCatalogSnapshot;
|
|
3849
|
+
formattingState: FormattingStateSnapshot;
|
|
3850
|
+
addCommentDisabledReason?: string;
|
|
3851
|
+
}): SelectionToolbarModel | null {
|
|
3852
|
+
const {
|
|
3853
|
+
snapshot,
|
|
3854
|
+
viewState,
|
|
3855
|
+
capabilities,
|
|
3856
|
+
documentNavigation,
|
|
3857
|
+
styleCatalog,
|
|
3858
|
+
formattingState,
|
|
3859
|
+
addCommentDisabledReason,
|
|
3860
|
+
} = args;
|
|
3861
|
+
|
|
3862
|
+
if (
|
|
3863
|
+
!snapshot.surface ||
|
|
3864
|
+
snapshot.selection.isCollapsed ||
|
|
3865
|
+
snapshot.selection.activeRange.kind !== "range" ||
|
|
3866
|
+
!capabilities.canEdit ||
|
|
3867
|
+
viewState.viewMode === "view"
|
|
3868
|
+
) {
|
|
3869
|
+
return null;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
const previewText = summarizeSelectionPreview(snapshot);
|
|
3873
|
+
if (!previewText) {
|
|
3874
|
+
return null;
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
const badges = [
|
|
3878
|
+
createSelectionToolbarStoryBadge(viewState.activeStory),
|
|
3879
|
+
viewState.workspaceMode === "page" && documentNavigation.pageCount > 0
|
|
3880
|
+
? { label: `Page ${documentNavigation.activePageIndex + 1}` as const }
|
|
3881
|
+
: null,
|
|
3882
|
+
createSelectionToolbarStyleBadge(styleCatalog, formattingState),
|
|
3883
|
+
createSelectionToolbarListBadge(viewState),
|
|
3884
|
+
].filter((badge): badge is SelectionToolbarModel["badges"][number] => Boolean(badge));
|
|
3885
|
+
|
|
3886
|
+
return {
|
|
3887
|
+
previewText,
|
|
3888
|
+
badges,
|
|
3889
|
+
canToggleFormatting: true,
|
|
3890
|
+
boldActive: formattingState.bold,
|
|
3891
|
+
italicActive: formattingState.italic,
|
|
3892
|
+
underlineActive: formattingState.underline,
|
|
3893
|
+
canAddComment: capabilities.canAddComment,
|
|
3894
|
+
...(addCommentDisabledReason ? { disabledReason: addCommentDisabledReason } : {}),
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function createSelectionToolbarStoryBadge(
|
|
3899
|
+
target: EditorStoryTarget,
|
|
3900
|
+
): SelectionToolbarModel["badges"][number] | null {
|
|
3901
|
+
if (target.kind === "main") {
|
|
3902
|
+
return null;
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
return {
|
|
3906
|
+
label:
|
|
3907
|
+
target.kind === "header"
|
|
3908
|
+
? target.variant === "default"
|
|
3909
|
+
? "Header"
|
|
3910
|
+
: `Header ${target.variant}`
|
|
3911
|
+
: target.kind === "footer"
|
|
3912
|
+
? target.variant === "default"
|
|
3913
|
+
? "Footer"
|
|
3914
|
+
: `Footer ${target.variant}`
|
|
3915
|
+
: target.kind === "footnote"
|
|
3916
|
+
? "Footnote"
|
|
3917
|
+
: "Endnote",
|
|
3918
|
+
tone: "accent",
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
function createSelectionToolbarStyleBadge(
|
|
3923
|
+
styleCatalog: StyleCatalogSnapshot,
|
|
3924
|
+
formattingState: FormattingStateSnapshot,
|
|
3925
|
+
): SelectionToolbarModel["badges"][number] | null {
|
|
3926
|
+
if (!formattingState.paragraphStyleId) {
|
|
3927
|
+
return null;
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
const styleEntry = styleCatalog.paragraphs.find(
|
|
3931
|
+
(entry) => entry.styleId === formattingState.paragraphStyleId,
|
|
3932
|
+
);
|
|
3933
|
+
if (!styleEntry || styleEntry.isDefault) {
|
|
3934
|
+
return null;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
return { label: styleEntry.displayName };
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
function createSelectionToolbarListBadge(
|
|
3941
|
+
viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>,
|
|
3942
|
+
): SelectionToolbarModel["badges"][number] | null {
|
|
3943
|
+
if (!viewState.activeListContext) {
|
|
3944
|
+
return null;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
return {
|
|
3948
|
+
label: viewState.activeListContext.isOrdered ? "Numbered list" : "Bulleted list",
|
|
3949
|
+
};
|
|
3950
|
+
}
|