@beyondwork/docx-react-component 1.0.13 → 1.0.15
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/package.json +32 -35
- package/src/api/public-types.ts +6 -0
- package/src/compare/diff-engine.ts +84 -7
- package/src/compare/index.ts +25 -0
- package/src/compare/snapshot.ts +31 -0
- package/src/core/selection/review-anchors.ts +89 -0
- package/src/formats/xlsx/io/serialize-shared-strings.ts +72 -0
- package/src/formats/xlsx/io/serialize-sheet.ts +333 -0
- package/src/formats/xlsx/io/serialize-styles.ts +98 -0
- package/src/formats/xlsx/io/serialize-workbook.ts +429 -0
- package/src/formats/xlsx/runtime/cell-commands.ts +567 -0
- package/src/formats/xlsx/runtime/sheet-commands.ts +206 -0
- package/src/formats/xlsx/runtime/workbook-runtime.ts +177 -0
- package/src/formats/xlsx/runtime/workbook-transaction.ts +822 -0
- package/src/io/ooxml/parse-main-document.ts +6 -6
- package/src/io/ooxml/parse-revisions.ts +18 -24
- package/src/legal/bookmarks.ts +35 -0
- package/src/legal/index.ts +32 -0
- package/src/legal/signature-blocks.ts +259 -0
- package/src/runtime/document-runtime.ts +13 -0
- package/src/runtime/numbering-prefix.ts +195 -0
- package/src/runtime/session-capabilities.ts +22 -1
- package/src/runtime/surface-projection.ts +287 -8
- package/src/ui/WordReviewEditor.tsx +120 -10
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +8 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +148 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +15 -29
- package/src/ui-tailwind/tw-review-workspace.tsx +3 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { RuntimeRenderSnapshot } from "../api/public-types";
|
|
2
|
+
import {
|
|
3
|
+
createDetachedAnchor,
|
|
4
|
+
createNodeAnchor,
|
|
5
|
+
createRangeAnchor,
|
|
6
|
+
} from "../core/selection/mapping.ts";
|
|
7
|
+
import { canCreateDocxCommentAnchor } from "../core/selection/review-anchors";
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Session capabilities derived from the runtime snapshot.
|
|
@@ -80,7 +86,11 @@ export function deriveCapabilities(
|
|
|
80
86
|
const canEdit = isReady && !isReadOnly && !hasFatalError;
|
|
81
87
|
const canUndo = snapshot.commandState.canUndo && canEdit;
|
|
82
88
|
const canRedo = snapshot.commandState.canRedo && canEdit;
|
|
83
|
-
const canAddComment =
|
|
89
|
+
const canAddComment =
|
|
90
|
+
canEdit &&
|
|
91
|
+
!snapshot.selection.isCollapsed &&
|
|
92
|
+
Boolean(snapshot.surface) &&
|
|
93
|
+
canCreateDocxCommentAnchor(snapshot.surface, toRuntimeAnchor(snapshot.selection.activeRange));
|
|
84
94
|
const canExport = isReady && !exportBlocked && !hasFatalError;
|
|
85
95
|
|
|
86
96
|
// Revision capabilities
|
|
@@ -136,3 +146,14 @@ export function deriveCapabilities(
|
|
|
136
146
|
hasFatalError,
|
|
137
147
|
};
|
|
138
148
|
}
|
|
149
|
+
|
|
150
|
+
function toRuntimeAnchor(anchor: RuntimeRenderSnapshot["selection"]["activeRange"]) {
|
|
151
|
+
switch (anchor.kind) {
|
|
152
|
+
case "range":
|
|
153
|
+
return createRangeAnchor(anchor.from, anchor.to, anchor.assoc);
|
|
154
|
+
case "node":
|
|
155
|
+
return createNodeAnchor(anchor.at, anchor.assoc);
|
|
156
|
+
case "detached":
|
|
157
|
+
return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -29,6 +29,10 @@ import {
|
|
|
29
29
|
describeOpaqueFragment,
|
|
30
30
|
getOpaqueFragment,
|
|
31
31
|
} from "../preservation/store.ts";
|
|
32
|
+
import {
|
|
33
|
+
createNumberingPrefixResolver,
|
|
34
|
+
type NumberingPrefixResolver,
|
|
35
|
+
} from "./numbering-prefix.ts";
|
|
32
36
|
|
|
33
37
|
interface ParagraphAccumulator {
|
|
34
38
|
blockId: string;
|
|
@@ -37,6 +41,7 @@ interface ParagraphAccumulator {
|
|
|
37
41
|
to: number;
|
|
38
42
|
styleId?: string;
|
|
39
43
|
numbering?: ParagraphNode["numbering"];
|
|
44
|
+
numberingPrefix?: string;
|
|
40
45
|
segments: SurfaceInlineSegment[];
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -47,6 +52,7 @@ export function createEditorSurfaceSnapshot(
|
|
|
47
52
|
const root = normalizeDocumentRoot(document.content);
|
|
48
53
|
const blocks: SurfaceBlockSnapshot[] = [];
|
|
49
54
|
const lockedFragmentIds: string[] = [];
|
|
55
|
+
const numberingPrefixResolver = createNumberingPrefixResolver(document.numbering);
|
|
50
56
|
let cursor = 0;
|
|
51
57
|
const counters = {
|
|
52
58
|
paragraph: 0,
|
|
@@ -58,7 +64,13 @@ export function createEditorSurfaceSnapshot(
|
|
|
58
64
|
};
|
|
59
65
|
|
|
60
66
|
for (let index = 0; index < root.children.length; index += 1) {
|
|
61
|
-
const surfaceBlock = createSurfaceBlock(
|
|
67
|
+
const surfaceBlock = createSurfaceBlock(
|
|
68
|
+
root.children[index],
|
|
69
|
+
document,
|
|
70
|
+
cursor,
|
|
71
|
+
counters,
|
|
72
|
+
numberingPrefixResolver,
|
|
73
|
+
);
|
|
62
74
|
blocks.push(surfaceBlock.block);
|
|
63
75
|
lockedFragmentIds.push(...surfaceBlock.lockedFragmentIds);
|
|
64
76
|
cursor = surfaceBlock.nextCursor;
|
|
@@ -67,6 +79,8 @@ export function createEditorSurfaceSnapshot(
|
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
blocks.push(...createSecondaryStoryPreviewBlocks(document, cursor));
|
|
83
|
+
|
|
70
84
|
return {
|
|
71
85
|
storySize: cursor,
|
|
72
86
|
plainText: createPlainText(blocks),
|
|
@@ -87,6 +101,7 @@ function createSurfaceBlock(
|
|
|
87
101
|
customXml: number;
|
|
88
102
|
altChunk: number;
|
|
89
103
|
},
|
|
104
|
+
numberingPrefixResolver: NumberingPrefixResolver,
|
|
90
105
|
): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
|
|
91
106
|
if (block.type === "opaque_block") {
|
|
92
107
|
const fragment = getOpaqueFragment(document.preservation as never, block.fragmentId);
|
|
@@ -115,13 +130,27 @@ function createSurfaceBlock(
|
|
|
115
130
|
if (block.type === "table") {
|
|
116
131
|
const tableIndex = counters.table;
|
|
117
132
|
counters.table += 1;
|
|
118
|
-
return createTableBlock(
|
|
133
|
+
return createTableBlock(
|
|
134
|
+
tableIndex,
|
|
135
|
+
block,
|
|
136
|
+
document,
|
|
137
|
+
cursor,
|
|
138
|
+
counters,
|
|
139
|
+
numberingPrefixResolver,
|
|
140
|
+
);
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
if (block.type === "sdt") {
|
|
122
144
|
const sdtIndex = counters.sdt;
|
|
123
145
|
counters.sdt += 1;
|
|
124
|
-
return createSdtBlock(
|
|
146
|
+
return createSdtBlock(
|
|
147
|
+
sdtIndex,
|
|
148
|
+
block,
|
|
149
|
+
document,
|
|
150
|
+
cursor,
|
|
151
|
+
counters,
|
|
152
|
+
numberingPrefixResolver,
|
|
153
|
+
);
|
|
125
154
|
}
|
|
126
155
|
|
|
127
156
|
if (block.type === "custom_xml") {
|
|
@@ -189,7 +218,13 @@ function createSurfaceBlock(
|
|
|
189
218
|
|
|
190
219
|
const paragraphIndex = counters.paragraph;
|
|
191
220
|
counters.paragraph += 1;
|
|
192
|
-
return createParagraphBlock(
|
|
221
|
+
return createParagraphBlock(
|
|
222
|
+
paragraphIndex,
|
|
223
|
+
block,
|
|
224
|
+
document,
|
|
225
|
+
cursor,
|
|
226
|
+
numberingPrefixResolver,
|
|
227
|
+
);
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
function createTableBlock(
|
|
@@ -205,6 +240,7 @@ function createTableBlock(
|
|
|
205
240
|
customXml: number;
|
|
206
241
|
altChunk: number;
|
|
207
242
|
},
|
|
243
|
+
numberingPrefixResolver: NumberingPrefixResolver,
|
|
208
244
|
): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
|
|
209
245
|
const lockedFragmentIds: string[] = [];
|
|
210
246
|
let innerCursor = cursor;
|
|
@@ -216,7 +252,13 @@ function createTableBlock(
|
|
|
216
252
|
for (const [cellIndex, cell] of row.cells.entries()) {
|
|
217
253
|
const cellContent: SurfaceBlockSnapshot[] = [];
|
|
218
254
|
for (const child of cell.children) {
|
|
219
|
-
const result = createSurfaceBlock(
|
|
255
|
+
const result = createSurfaceBlock(
|
|
256
|
+
child,
|
|
257
|
+
document,
|
|
258
|
+
innerCursor,
|
|
259
|
+
counters,
|
|
260
|
+
numberingPrefixResolver,
|
|
261
|
+
);
|
|
220
262
|
cellContent.push(result.block);
|
|
221
263
|
lockedFragmentIds.push(...result.lockedFragmentIds);
|
|
222
264
|
innerCursor = result.nextCursor;
|
|
@@ -310,13 +352,20 @@ function createSdtBlock(
|
|
|
310
352
|
customXml: number;
|
|
311
353
|
altChunk: number;
|
|
312
354
|
},
|
|
355
|
+
numberingPrefixResolver: NumberingPrefixResolver,
|
|
313
356
|
): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
|
|
314
357
|
const children: SurfaceBlockSnapshot[] = [];
|
|
315
358
|
const lockedFragmentIds: string[] = [];
|
|
316
359
|
let innerCursor = cursor;
|
|
317
360
|
|
|
318
361
|
for (const child of block.children) {
|
|
319
|
-
const result = createSurfaceBlock(
|
|
362
|
+
const result = createSurfaceBlock(
|
|
363
|
+
child,
|
|
364
|
+
document,
|
|
365
|
+
innerCursor,
|
|
366
|
+
counters,
|
|
367
|
+
numberingPrefixResolver,
|
|
368
|
+
);
|
|
320
369
|
children.push(result.block);
|
|
321
370
|
lockedFragmentIds.push(...result.lockedFragmentIds);
|
|
322
371
|
innerCursor = result.nextCursor;
|
|
@@ -344,6 +393,7 @@ function createParagraphBlock(
|
|
|
344
393
|
paragraph: ParagraphNode,
|
|
345
394
|
document: CanonicalDocumentEnvelope,
|
|
346
395
|
start: number,
|
|
396
|
+
numberingPrefixResolver: NumberingPrefixResolver,
|
|
347
397
|
): {
|
|
348
398
|
block: SurfaceBlockSnapshot;
|
|
349
399
|
nextCursor: number;
|
|
@@ -356,12 +406,20 @@ function createParagraphBlock(
|
|
|
356
406
|
to: start,
|
|
357
407
|
...(paragraph.styleId ? { styleId: paragraph.styleId } : {}),
|
|
358
408
|
...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
|
|
409
|
+
...(paragraph.numbering
|
|
410
|
+
? {
|
|
411
|
+
numberingPrefix:
|
|
412
|
+
numberingPrefixResolver.resolve(paragraph.numbering) ?? undefined,
|
|
413
|
+
}
|
|
414
|
+
: {}),
|
|
359
415
|
...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
|
|
360
416
|
...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
|
|
361
417
|
...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
|
|
362
418
|
...(paragraph.borders ? { borders: paragraph.borders } : {}),
|
|
363
419
|
...(paragraph.shading ? { shading: paragraph.shading } : {}),
|
|
364
|
-
...(paragraph.tabStops && paragraph.tabStops.length > 0
|
|
420
|
+
...(paragraph.tabStops && paragraph.tabStops.length > 0
|
|
421
|
+
? { tabStops: paragraph.tabStops.map((tabStop) => toSurfaceTabStop(tabStop)) }
|
|
422
|
+
: {}),
|
|
365
423
|
...(paragraph.keepNext ? { keepNext: true } : {}),
|
|
366
424
|
...(paragraph.keepLines ? { keepLines: true } : {}),
|
|
367
425
|
...(paragraph.pageBreakBefore ? { pageBreakBefore: true } : {}),
|
|
@@ -459,6 +517,7 @@ function appendInlineSegments(
|
|
|
459
517
|
case "opaque_inline": {
|
|
460
518
|
const fragment = getOpaqueFragment(document.preservation as never, node.fragmentId);
|
|
461
519
|
const descriptor = fragment ? describeOpaqueFragment(fragment) : null;
|
|
520
|
+
const preview = fragment ? describePreservedInlinePreview(fragment.payloadReference) : null;
|
|
462
521
|
paragraph.segments.push({
|
|
463
522
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
464
523
|
kind: "opaque_inline",
|
|
@@ -466,8 +525,9 @@ function appendInlineSegments(
|
|
|
466
525
|
to: start + 1,
|
|
467
526
|
fragmentId: node.fragmentId,
|
|
468
527
|
warningId: node.warningId,
|
|
469
|
-
label: descriptor?.label ?? "Unsupported inline OOXML",
|
|
528
|
+
label: preview?.label ?? descriptor?.label ?? "Unsupported inline OOXML",
|
|
470
529
|
detail:
|
|
530
|
+
preview?.detail ??
|
|
471
531
|
descriptor?.detail ??
|
|
472
532
|
"Locked whole-unit to keep unsupported inline OOXML intact through export.",
|
|
473
533
|
state: "locked-preserve-only",
|
|
@@ -612,6 +672,9 @@ function createPlainText(
|
|
|
612
672
|
const text: string[] = [];
|
|
613
673
|
for (const block of blocks) {
|
|
614
674
|
if (block.kind === "opaque_block") {
|
|
675
|
+
if (block.fragmentId.startsWith("preview:")) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
615
678
|
text.push("\uFFFA");
|
|
616
679
|
continue;
|
|
617
680
|
}
|
|
@@ -650,6 +713,222 @@ function createPlainText(
|
|
|
650
713
|
return text.join("");
|
|
651
714
|
}
|
|
652
715
|
|
|
716
|
+
function createSecondaryStoryPreviewBlocks(
|
|
717
|
+
document: CanonicalDocumentEnvelope,
|
|
718
|
+
cursor: number,
|
|
719
|
+
): SurfaceBlockSnapshot[] {
|
|
720
|
+
const previews: SurfaceBlockSnapshot[] = [];
|
|
721
|
+
const subParts = document.subParts;
|
|
722
|
+
if (!subParts) {
|
|
723
|
+
return previews;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
for (const header of subParts.headers ?? []) {
|
|
727
|
+
previews.push(
|
|
728
|
+
createSecondaryStoryPreviewBlock(
|
|
729
|
+
`Header · ${header.variant}`,
|
|
730
|
+
`header:${header.relationshipId}`,
|
|
731
|
+
cursor,
|
|
732
|
+
createSecondaryStoryPreviewDetail(header.partPath, header.blocks),
|
|
733
|
+
),
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
for (const footer of subParts.footers ?? []) {
|
|
738
|
+
previews.push(
|
|
739
|
+
createSecondaryStoryPreviewBlock(
|
|
740
|
+
`Footer · ${footer.variant}`,
|
|
741
|
+
`footer:${footer.relationshipId}`,
|
|
742
|
+
cursor,
|
|
743
|
+
createSecondaryStoryPreviewDetail(footer.partPath, footer.blocks),
|
|
744
|
+
),
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const footnotes = Object.values(subParts.footnoteCollection?.footnotes ?? {}).sort(compareNoteIds);
|
|
749
|
+
for (const note of footnotes) {
|
|
750
|
+
previews.push(
|
|
751
|
+
createSecondaryStoryPreviewBlock(
|
|
752
|
+
`Footnote ${note.noteId}`,
|
|
753
|
+
`footnote:${note.noteId}`,
|
|
754
|
+
cursor,
|
|
755
|
+
createSecondaryStoryPreviewDetail(`/word/footnotes.xml#${note.noteId}`, note.blocks),
|
|
756
|
+
),
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const endnotes = Object.values(subParts.footnoteCollection?.endnotes ?? {}).sort(compareNoteIds);
|
|
761
|
+
for (const note of endnotes) {
|
|
762
|
+
previews.push(
|
|
763
|
+
createSecondaryStoryPreviewBlock(
|
|
764
|
+
`Endnote ${note.noteId}`,
|
|
765
|
+
`endnote:${note.noteId}`,
|
|
766
|
+
cursor,
|
|
767
|
+
createSecondaryStoryPreviewDetail(`/word/endnotes.xml#${note.noteId}`, note.blocks),
|
|
768
|
+
),
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return previews;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function createSecondaryStoryPreviewBlock(
|
|
776
|
+
label: string,
|
|
777
|
+
previewId: string,
|
|
778
|
+
cursor: number,
|
|
779
|
+
detail: string,
|
|
780
|
+
): Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }> {
|
|
781
|
+
return {
|
|
782
|
+
blockId: `secondary-story-${previewId.replace(/[^a-z0-9._:-]+/gi, "-")}`,
|
|
783
|
+
kind: "opaque_block",
|
|
784
|
+
from: cursor,
|
|
785
|
+
to: cursor,
|
|
786
|
+
fragmentId: `preview:${previewId}`,
|
|
787
|
+
warningId: `preview:${previewId}`,
|
|
788
|
+
label,
|
|
789
|
+
detail,
|
|
790
|
+
state: "locked-preserve-only",
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function createSecondaryStoryPreviewDetail(
|
|
795
|
+
sourceLabel: string,
|
|
796
|
+
blocks: readonly BlockNode[],
|
|
797
|
+
): string {
|
|
798
|
+
const previewLines = blocks
|
|
799
|
+
.map((block) => summarizePreviewBlock(block))
|
|
800
|
+
.filter((line) => line.trim().length > 0);
|
|
801
|
+
const previewText = previewLines.length > 0 ? previewLines.join("\n\n") : "Empty story.";
|
|
802
|
+
return `Read-only preview from ${sourceLabel}.\n${previewText}`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function summarizePreviewBlock(block: BlockNode): string {
|
|
806
|
+
switch (block.type) {
|
|
807
|
+
case "paragraph": {
|
|
808
|
+
const text = block.children.map((child) => summarizePreviewInline(child)).join("");
|
|
809
|
+
return text.trim().length > 0 ? text : "¶";
|
|
810
|
+
}
|
|
811
|
+
case "table":
|
|
812
|
+
return "[Table preserved in secondary story]";
|
|
813
|
+
case "opaque_block":
|
|
814
|
+
return "[Locked block preserved for export]";
|
|
815
|
+
case "sdt":
|
|
816
|
+
return "[Content control preview]";
|
|
817
|
+
case "custom_xml":
|
|
818
|
+
return "[Custom XML preview]";
|
|
819
|
+
case "alt_chunk":
|
|
820
|
+
return "[AltChunk preview]";
|
|
821
|
+
case "section_break":
|
|
822
|
+
return "[Section boundary]";
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function summarizePreviewInline(node: InlineNode): string {
|
|
827
|
+
switch (node.type) {
|
|
828
|
+
case "text":
|
|
829
|
+
return node.text;
|
|
830
|
+
case "tab":
|
|
831
|
+
return "\t";
|
|
832
|
+
case "hard_break":
|
|
833
|
+
return "\n";
|
|
834
|
+
case "hyperlink":
|
|
835
|
+
return node.children.map((child) => summarizePreviewInline(child)).join("");
|
|
836
|
+
case "footnote_ref":
|
|
837
|
+
return node.noteKind === "footnote" ? `[fn ${node.noteId}]` : `[en ${node.noteId}]`;
|
|
838
|
+
case "field": {
|
|
839
|
+
const instruction = node.instruction.trim();
|
|
840
|
+
return instruction.length > 0 ? `[Field: ${instruction}]` : "[Field]";
|
|
841
|
+
}
|
|
842
|
+
case "bookmark_start":
|
|
843
|
+
return node.name ? `[Bookmark: ${node.name}]` : "[Bookmark]";
|
|
844
|
+
case "bookmark_end":
|
|
845
|
+
return "";
|
|
846
|
+
case "image":
|
|
847
|
+
return node.altText ? `[Image: ${node.altText}]` : "[Image]";
|
|
848
|
+
case "opaque_inline":
|
|
849
|
+
return "[Locked inline content]";
|
|
850
|
+
case "symbol":
|
|
851
|
+
return node.char ? String.fromCodePoint(parseInt(node.char, 16)) : "\uFFFD";
|
|
852
|
+
case "column_break":
|
|
853
|
+
return "[Column break]";
|
|
854
|
+
case "chart_preview":
|
|
855
|
+
return "[Chart]";
|
|
856
|
+
case "smartart_preview":
|
|
857
|
+
return "[SmartArt]";
|
|
858
|
+
case "shape":
|
|
859
|
+
return node.text ? `[Shape: ${node.text}]` : "[Shape]";
|
|
860
|
+
case "wordart":
|
|
861
|
+
return node.text ? `[WordArt: ${node.text}]` : "[WordArt]";
|
|
862
|
+
case "vml_shape":
|
|
863
|
+
return node.text ? `[VML: ${node.text}]` : "[VML shape]";
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function compareNoteIds(
|
|
868
|
+
left: { noteId: string },
|
|
869
|
+
right: { noteId: string },
|
|
870
|
+
): number {
|
|
871
|
+
return Number.parseInt(left.noteId, 10) - Number.parseInt(right.noteId, 10);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function toSurfaceTabStop(
|
|
875
|
+
tabStop: NonNullable<ParagraphNode["tabStops"]>[number],
|
|
876
|
+
): { pos: number; val?: string; leader?: string } {
|
|
877
|
+
return {
|
|
878
|
+
pos: tabStop.position,
|
|
879
|
+
...(tabStop.align ? { val: tabStop.align } : {}),
|
|
880
|
+
...(tabStop.leader ? { leader: tabStop.leader } : {}),
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function describePreservedInlinePreview(
|
|
885
|
+
payloadReference: string,
|
|
886
|
+
): { label: string; detail: string } | null {
|
|
887
|
+
if (/\b(?:w:)?bookmarkStart\b/u.test(payloadReference)) {
|
|
888
|
+
const name = /\bw:name="([^"]+)"/u.exec(payloadReference)?.[1];
|
|
889
|
+
return {
|
|
890
|
+
label: name ? `Bookmark · ${name}` : "Bookmark",
|
|
891
|
+
detail: name
|
|
892
|
+
? `Bookmark anchor \"${name}\" is preserved as a read-only inline token for export safety.`
|
|
893
|
+
: "Bookmark anchor is preserved as a read-only inline token for export safety.",
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (/\b(?:w:)?bookmarkEnd\b/u.test(payloadReference)) {
|
|
898
|
+
return {
|
|
899
|
+
label: "Bookmark end",
|
|
900
|
+
detail: "Bookmark end marker is preserved as a read-only inline token for export safety.",
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (/\b(?:w:)?fldSimple\b|\b(?:w:)?fldChar\b|\b(?:w:)?instrText\b/u.test(payloadReference)) {
|
|
905
|
+
const simpleInstruction = /\bw:instr="([^"]*)"/u.exec(payloadReference)?.[1];
|
|
906
|
+
const complexInstruction = [...payloadReference.matchAll(/<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu)]
|
|
907
|
+
.map((match) => decodeXmlEntities(match[1] ?? ""))
|
|
908
|
+
.join("")
|
|
909
|
+
.trim();
|
|
910
|
+
const instruction = (simpleInstruction ?? complexInstruction ?? "").trim();
|
|
911
|
+
return {
|
|
912
|
+
label: "Field",
|
|
913
|
+
detail:
|
|
914
|
+
instruction.length > 0
|
|
915
|
+
? `Read-only field preserved for export safety. Instruction: ${instruction}.`
|
|
916
|
+
: "Read-only field preserved for export safety.",
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function decodeXmlEntities(text: string): string {
|
|
924
|
+
return text
|
|
925
|
+
.replace(/</g, "<")
|
|
926
|
+
.replace(/>/g, ">")
|
|
927
|
+
.replace(/"/g, "\"")
|
|
928
|
+
.replace(/'/g, "'")
|
|
929
|
+
.replace(/&/g, "&");
|
|
930
|
+
}
|
|
931
|
+
|
|
653
932
|
function cloneMarks(marks: TextMark[]): {
|
|
654
933
|
marks: SurfaceTextMark[];
|
|
655
934
|
markAttrs?: {
|
|
@@ -144,16 +144,26 @@ export function __createWordReviewEditorRefBridge(
|
|
|
144
144
|
reopenComment: (commentId) => runtime.reopenComment(commentId),
|
|
145
145
|
addCommentReply: (commentId, body) => runtime.addCommentReply(commentId, body),
|
|
146
146
|
editCommentBody: (commentId, body) => runtime.editCommentBody(commentId, body),
|
|
147
|
+
deleteComment: (commentId) => {
|
|
148
|
+
applyRuntimeDeleteComment(runtime, commentId);
|
|
149
|
+
},
|
|
147
150
|
acceptChange: (changeId) => runtime.acceptChange(changeId),
|
|
148
151
|
rejectChange: (changeId) => runtime.rejectChange(changeId),
|
|
149
152
|
acceptAllChanges: () => runtime.acceptAllChanges(),
|
|
150
153
|
rejectAllChanges: () => runtime.rejectAllChanges(),
|
|
151
154
|
exportDocx: (options) => runtime.exportDocx(options),
|
|
152
155
|
getSnapshot: () => runtime.getPersistedSnapshot(),
|
|
156
|
+
getRenderSnapshot: () => clonePublicValue(runtime.getRenderSnapshot()),
|
|
153
157
|
getCompatibilityReport: () => runtime.getCompatibilityReport(),
|
|
154
158
|
getWarnings: () => runtime.getWarnings(),
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
getCommentSidebarSnapshot: () =>
|
|
160
|
+
clonePublicValue(runtime.getRenderSnapshot().comments),
|
|
161
|
+
getTrackedChangesSnapshot: () =>
|
|
162
|
+
clonePublicValue(runtime.getRenderSnapshot().trackedChanges),
|
|
163
|
+
getComments: () => clonePublicValue(runtime.getRenderSnapshot().comments),
|
|
164
|
+
getTrackedChanges: () =>
|
|
165
|
+
clonePublicValue(runtime.getRenderSnapshot().trackedChanges),
|
|
166
|
+
isDirty: () => runtime.getRenderSnapshot().isDirty,
|
|
157
167
|
getFormattingState: () => getFormattingStateFromRenderSnapshot(runtime.getRenderSnapshot()),
|
|
158
168
|
toggleBold: () => {
|
|
159
169
|
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "bold" });
|
|
@@ -275,6 +285,14 @@ export function __createWordReviewEditorRefBridge(
|
|
|
275
285
|
clearSearch: () => {
|
|
276
286
|
mountedSurface?.clearSearch();
|
|
277
287
|
},
|
|
288
|
+
setSelection: (selection) => {
|
|
289
|
+
runtime.dispatch({
|
|
290
|
+
type: "selection.set",
|
|
291
|
+
selection: toRuntimeSelectionSnapshot(
|
|
292
|
+
normalizeRequestedSelection(runtime.getRenderSnapshot(), selection),
|
|
293
|
+
),
|
|
294
|
+
});
|
|
295
|
+
},
|
|
278
296
|
scrollToRevision: (revisionId: string) => {
|
|
279
297
|
const revision = runtime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
280
298
|
(r) => r.revisionId === revisionId,
|
|
@@ -671,6 +689,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
671
689
|
activeRuntime.addCommentReply(commentId, body, currentUser.userId),
|
|
672
690
|
editCommentBody: (commentId, body) =>
|
|
673
691
|
activeRuntime.editCommentBody(commentId, body),
|
|
692
|
+
deleteComment: (commentId) => {
|
|
693
|
+
applyRuntimeDeleteComment(activeRuntime, commentId);
|
|
694
|
+
},
|
|
674
695
|
acceptChange: (changeId) => activeRuntime.acceptChange(changeId),
|
|
675
696
|
rejectChange: (changeId) => activeRuntime.rejectChange(changeId),
|
|
676
697
|
acceptAllChanges: () => activeRuntime.acceptAllChanges(),
|
|
@@ -694,10 +715,18 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
694
715
|
onEvent: onEventRef.current,
|
|
695
716
|
}),
|
|
696
717
|
getSnapshot: () => activeRuntime.getPersistedSnapshot(),
|
|
718
|
+
getRenderSnapshot: () => clonePublicValue(activeRuntime.getRenderSnapshot()),
|
|
697
719
|
getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
|
|
698
720
|
getWarnings: () => activeRuntime.getWarnings(),
|
|
699
|
-
|
|
700
|
-
|
|
721
|
+
getCommentSidebarSnapshot: () =>
|
|
722
|
+
clonePublicValue(activeRuntime.getRenderSnapshot().comments),
|
|
723
|
+
getTrackedChangesSnapshot: () =>
|
|
724
|
+
clonePublicValue(activeRuntime.getRenderSnapshot().trackedChanges),
|
|
725
|
+
getComments: () =>
|
|
726
|
+
clonePublicValue(activeRuntime.getRenderSnapshot().comments),
|
|
727
|
+
getTrackedChanges: () =>
|
|
728
|
+
clonePublicValue(activeRuntime.getRenderSnapshot().trackedChanges),
|
|
729
|
+
isDirty: () => activeRuntime.getRenderSnapshot().isDirty,
|
|
701
730
|
getFormattingState: () =>
|
|
702
731
|
getFormattingStateFromRenderSnapshot(activeRuntime.getRenderSnapshot()),
|
|
703
732
|
toggleBold: () => {
|
|
@@ -838,6 +867,14 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
838
867
|
clearSearch: () => {
|
|
839
868
|
surfaceRef.current?.clearSearch();
|
|
840
869
|
},
|
|
870
|
+
setSelection: (selection) => {
|
|
871
|
+
activeRuntime.dispatch({
|
|
872
|
+
type: "selection.set",
|
|
873
|
+
selection: toRuntimeSelectionSnapshot(
|
|
874
|
+
normalizeRequestedSelection(activeRuntime.getRenderSnapshot(), selection),
|
|
875
|
+
),
|
|
876
|
+
});
|
|
877
|
+
},
|
|
841
878
|
scrollToRevision: (revisionId: string) => {
|
|
842
879
|
const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
843
880
|
(r) => r.revisionId === revisionId,
|
|
@@ -958,12 +995,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
958
995
|
}
|
|
959
996
|
|
|
960
997
|
function addReviewComment(): void {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
998
|
+
try {
|
|
999
|
+
activeRuntime.addComment({
|
|
1000
|
+
anchor: snapshot.selection.activeRange,
|
|
1001
|
+
body: "New review comment",
|
|
1002
|
+
authorId: currentUser.userId,
|
|
1003
|
+
});
|
|
1004
|
+
setActiveRailTab("comments");
|
|
1005
|
+
} catch {
|
|
1006
|
+
// Runtime already emitted a concrete export-safety error for invalid anchors.
|
|
1007
|
+
}
|
|
967
1008
|
}
|
|
968
1009
|
|
|
969
1010
|
function exportCurrentDocument(): void {
|
|
@@ -991,6 +1032,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
991
1032
|
? derivedCapabilities
|
|
992
1033
|
: { ...derivedCapabilities, reviewRailVisible: false };
|
|
993
1034
|
const diagnosticsModeMessage = getDiagnosticsModeMessage(loadError ?? snapshot.fatalError);
|
|
1035
|
+
const addCommentDisabledReason =
|
|
1036
|
+
!capabilities.canAddComment && !snapshot.selection.isCollapsed
|
|
1037
|
+
? "Select text within one paragraph to add a DOCX comment."
|
|
1038
|
+
: undefined;
|
|
994
1039
|
const accessibilityInstructionsId = `${documentId}-accessibility-instructions`;
|
|
995
1040
|
const accessibilityStatusId = `${documentId}-accessibility-status`;
|
|
996
1041
|
const accessibilityAlertId = `${documentId}-accessibility-alert`;
|
|
@@ -1147,6 +1192,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1147
1192
|
activeRevisionId={activeRevisionId}
|
|
1148
1193
|
showTrackedChanges={showTrackedChanges}
|
|
1149
1194
|
selectionPreview={selectionPreview}
|
|
1195
|
+
addCommentDisabledReason={addCommentDisabledReason}
|
|
1150
1196
|
onViewModeChange={setViewMode}
|
|
1151
1197
|
onActiveRailTabChange={setActiveRailTab}
|
|
1152
1198
|
onShowTrackedChangesChange={setShowTrackedChanges}
|
|
@@ -1345,6 +1391,70 @@ function dispatchRuntimeDocumentMutation(
|
|
|
1345
1391
|
});
|
|
1346
1392
|
}
|
|
1347
1393
|
|
|
1394
|
+
function applyRuntimeDeleteComment(
|
|
1395
|
+
runtime: WordReviewEditorRuntime,
|
|
1396
|
+
commentId: string,
|
|
1397
|
+
): void {
|
|
1398
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1399
|
+
if (!snapshot.isReady || snapshot.readOnly || snapshot.fatalError) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const persistedSnapshot = runtime.getPersistedSnapshot();
|
|
1404
|
+
if (!persistedSnapshot.canonicalDocument.review.comments[commentId]) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const nextComments = {
|
|
1409
|
+
...persistedSnapshot.canonicalDocument.review.comments,
|
|
1410
|
+
};
|
|
1411
|
+
delete nextComments[commentId];
|
|
1412
|
+
|
|
1413
|
+
runtime.dispatch({
|
|
1414
|
+
type: "document.replace",
|
|
1415
|
+
document: {
|
|
1416
|
+
...persistedSnapshot.canonicalDocument,
|
|
1417
|
+
review: {
|
|
1418
|
+
...persistedSnapshot.canonicalDocument.review,
|
|
1419
|
+
comments: nextComments,
|
|
1420
|
+
},
|
|
1421
|
+
},
|
|
1422
|
+
selection: toRuntimeSelectionSnapshot(snapshot.selection),
|
|
1423
|
+
origin: {
|
|
1424
|
+
source: "api",
|
|
1425
|
+
timestamp: new Date().toISOString(),
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function normalizeRequestedSelection(
|
|
1431
|
+
snapshot: RuntimeRenderSnapshot,
|
|
1432
|
+
selection: PublicSelectionSnapshot | null,
|
|
1433
|
+
): PublicSelectionSnapshot {
|
|
1434
|
+
return selection ?? createCollapsedPublicSelection(snapshot.selection.head);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function createCollapsedPublicSelection(position: number): PublicSelectionSnapshot {
|
|
1438
|
+
return {
|
|
1439
|
+
anchor: position,
|
|
1440
|
+
head: position,
|
|
1441
|
+
isCollapsed: true,
|
|
1442
|
+
activeRange: {
|
|
1443
|
+
kind: "range",
|
|
1444
|
+
from: position,
|
|
1445
|
+
to: position,
|
|
1446
|
+
assoc: {
|
|
1447
|
+
start: -1,
|
|
1448
|
+
end: 1,
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function clonePublicValue<T>(value: T): T {
|
|
1455
|
+
return structuredClone(value);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1348
1458
|
function searchSnapshotSurface(
|
|
1349
1459
|
snapshot: RuntimeRenderSnapshot,
|
|
1350
1460
|
query: string,
|
|
@@ -5,6 +5,8 @@ import { MessageSquare } from "lucide-react";
|
|
|
5
5
|
export interface TwSelectionToolbarProps {
|
|
6
6
|
selectionPreview: string;
|
|
7
7
|
readOnly: boolean;
|
|
8
|
+
canAddComment?: boolean;
|
|
9
|
+
disabledReason?: string;
|
|
8
10
|
onAddComment?: () => void;
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -12,6 +14,10 @@ const focusRingClass =
|
|
|
12
14
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
13
15
|
|
|
14
16
|
export function TwSelectionToolbar(props: TwSelectionToolbarProps) {
|
|
17
|
+
const addCommentDisabled = props.readOnly || props.canAddComment === false;
|
|
18
|
+
const tooltipLabel = addCommentDisabled
|
|
19
|
+
? props.disabledReason ?? "Select text within one paragraph to add a DOCX comment"
|
|
20
|
+
: "Add comment";
|
|
15
21
|
return (
|
|
16
22
|
<div className="mb-6 inline-flex items-center gap-1 rounded-lg bg-canvas shadow-lg ring-1 ring-border p-1">
|
|
17
23
|
<Tooltip.Root>
|
|
@@ -19,7 +25,7 @@ export function TwSelectionToolbar(props: TwSelectionToolbarProps) {
|
|
|
19
25
|
<button
|
|
20
26
|
type="button"
|
|
21
27
|
aria-label="Comment"
|
|
22
|
-
disabled={
|
|
28
|
+
disabled={addCommentDisabled}
|
|
23
29
|
onClick={props.onAddComment}
|
|
24
30
|
className={`inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors text-accent hover:bg-accent-soft disabled:opacity-30 disabled:cursor-not-allowed ${focusRingClass}`}
|
|
25
31
|
>
|
|
@@ -31,7 +37,7 @@ export function TwSelectionToolbar(props: TwSelectionToolbarProps) {
|
|
|
31
37
|
className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
|
|
32
38
|
sideOffset={6}
|
|
33
39
|
>
|
|
34
|
-
|
|
40
|
+
{tooltipLabel}
|
|
35
41
|
</Tooltip.Content>
|
|
36
42
|
</Tooltip.Portal>
|
|
37
43
|
</Tooltip.Root>
|