@beyondwork/docx-react-component 1.0.12 → 1.0.14
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 +9 -1
- package/src/api/public-types.ts +72 -0
- package/src/core/commands/formatting-commands.ts +742 -0
- package/src/core/commands/image-commands.ts +84 -2
- package/src/core/commands/structural-helpers.ts +309 -0
- package/src/core/commands/table-structure-commands.ts +721 -0
- package/src/core/commands/text-commands.ts +166 -1
- package/src/core/selection/review-anchors.ts +89 -0
- package/src/formats/xlsx/io/parse-sheet.ts +177 -7
- package/src/formats/xlsx/io/parse-styles.ts +2 -0
- package/src/formats/xlsx/io/xlsx-session.ts +18 -12
- package/src/formats/xlsx/model/sheet.ts +81 -1
- package/src/formats/xlsx/model/workbook.ts +10 -6
- package/src/runtime/document-runtime.ts +13 -0
- package/src/runtime/session-capabilities.ts +22 -1
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/table-commands.ts +79 -0
- package/src/runtime/table-schema.ts +9 -0
- package/src/ui/WordReviewEditor.tsx +534 -8
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +8 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +7 -5
- package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +3 -0
|
@@ -14,9 +14,14 @@ import type {
|
|
|
14
14
|
CompatibilityReport,
|
|
15
15
|
EditorError,
|
|
16
16
|
EditorWarning,
|
|
17
|
+
FormattingAlignment,
|
|
17
18
|
ExportDocxOptions,
|
|
19
|
+
InsertImageOptions,
|
|
20
|
+
InsertTableOptions,
|
|
18
21
|
PersistedEditorSnapshot,
|
|
19
22
|
RuntimeRenderSnapshot,
|
|
23
|
+
SearchOptions,
|
|
24
|
+
SearchResultSnapshot,
|
|
20
25
|
SelectionSnapshot as PublicSelectionSnapshot,
|
|
21
26
|
ExportResult,
|
|
22
27
|
WordReviewEditorEvent,
|
|
@@ -27,8 +32,24 @@ import {
|
|
|
27
32
|
createDetachedAnchor,
|
|
28
33
|
createNodeAnchor,
|
|
29
34
|
createRangeAnchor,
|
|
35
|
+
type TransactionMapping,
|
|
30
36
|
} from "../core/selection/mapping.ts";
|
|
31
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
applyFormattingOperationToDocument,
|
|
39
|
+
getFormattingStateFromRenderSnapshot,
|
|
40
|
+
} from "../core/commands/formatting-commands.ts";
|
|
41
|
+
import { insertImage as insertImageInDocument } from "../core/commands/image-commands.ts";
|
|
42
|
+
import {
|
|
43
|
+
applyTableStructureOperation,
|
|
44
|
+
} from "../core/commands/table-structure-commands.ts";
|
|
45
|
+
import {
|
|
46
|
+
insertPageBreak as insertPageBreakInDocument,
|
|
47
|
+
insertTable as insertTableInDocument,
|
|
48
|
+
} from "../core/commands/text-commands.ts";
|
|
49
|
+
import {
|
|
50
|
+
createCanonicalDocumentId,
|
|
51
|
+
type SelectionSnapshot as InternalSelectionSnapshot,
|
|
52
|
+
} from "../core/state/editor-state.ts";
|
|
32
53
|
import {
|
|
33
54
|
createDocumentRuntime,
|
|
34
55
|
type DocumentRuntime,
|
|
@@ -39,7 +60,14 @@ import {
|
|
|
39
60
|
hasValidPersistedSourcePackageDigest,
|
|
40
61
|
} from "../io/source-package-provenance.ts";
|
|
41
62
|
import { deriveCapabilities } from "../runtime/session-capabilities";
|
|
42
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
createSearchExcerpt,
|
|
65
|
+
findSearchMatches,
|
|
66
|
+
} from "../ui-tailwind/editor-surface/search-plugin";
|
|
67
|
+
import {
|
|
68
|
+
TwProseMirrorSurface,
|
|
69
|
+
type TwProseMirrorSurfaceRef,
|
|
70
|
+
} from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
|
|
43
71
|
import { TwReviewWorkspace } from "../ui-tailwind/tw-review-workspace";
|
|
44
72
|
import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail";
|
|
45
73
|
import type { ViewMode } from "../ui-tailwind/toolbar/tw-toolbar";
|
|
@@ -103,6 +131,7 @@ type AccessibleRegionId = (typeof ACCESSIBLE_REGION_ORDER)[number];
|
|
|
103
131
|
|
|
104
132
|
export function __createWordReviewEditorRefBridge(
|
|
105
133
|
runtime: WordReviewEditorRuntime,
|
|
134
|
+
mountedSurface?: TwProseMirrorSurfaceRef | null,
|
|
106
135
|
): WordReviewEditorRef {
|
|
107
136
|
return {
|
|
108
137
|
focus: () => runtime.focus(),
|
|
@@ -125,6 +154,127 @@ export function __createWordReviewEditorRefBridge(
|
|
|
125
154
|
getWarnings: () => runtime.getWarnings(),
|
|
126
155
|
getComments: () => runtime.getRenderSnapshot().comments,
|
|
127
156
|
getTrackedChanges: () => runtime.getRenderSnapshot().trackedChanges,
|
|
157
|
+
getFormattingState: () => getFormattingStateFromRenderSnapshot(runtime.getRenderSnapshot()),
|
|
158
|
+
toggleBold: () => {
|
|
159
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "bold" });
|
|
160
|
+
},
|
|
161
|
+
toggleItalic: () => {
|
|
162
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "italic" });
|
|
163
|
+
},
|
|
164
|
+
toggleUnderline: () => {
|
|
165
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "underline" });
|
|
166
|
+
},
|
|
167
|
+
toggleStrikethrough: () => {
|
|
168
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
169
|
+
type: "toggle",
|
|
170
|
+
mark: "strikethrough",
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
toggleSuperscript: () => {
|
|
174
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
175
|
+
type: "toggle",
|
|
176
|
+
mark: "superscript",
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
toggleSubscript: () => {
|
|
180
|
+
applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "subscript" });
|
|
181
|
+
},
|
|
182
|
+
setFontFamily: (fontFamily) => {
|
|
183
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
184
|
+
type: "set-font-family",
|
|
185
|
+
fontFamily,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
setFontSize: (size) => {
|
|
189
|
+
applyRuntimeFormattingOperation(runtime, { type: "set-font-size", size });
|
|
190
|
+
},
|
|
191
|
+
setTextColor: (color) => {
|
|
192
|
+
applyRuntimeFormattingOperation(runtime, { type: "set-text-color", color });
|
|
193
|
+
},
|
|
194
|
+
setHighlightColor: (color) => {
|
|
195
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
196
|
+
type: "set-highlight-color",
|
|
197
|
+
color,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
setAlignment: (alignment) => {
|
|
201
|
+
applyRuntimeFormattingOperation(runtime, {
|
|
202
|
+
type: "set-alignment",
|
|
203
|
+
alignment,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
indent: () => {
|
|
207
|
+
applyRuntimeFormattingOperation(runtime, { type: "indent" });
|
|
208
|
+
},
|
|
209
|
+
outdent: () => {
|
|
210
|
+
applyRuntimeFormattingOperation(runtime, { type: "outdent" });
|
|
211
|
+
},
|
|
212
|
+
insertPageBreak: () => {
|
|
213
|
+
applyRuntimeInsertPageBreak(runtime);
|
|
214
|
+
},
|
|
215
|
+
insertTable: (options) => {
|
|
216
|
+
applyRuntimeInsertTable(runtime, options);
|
|
217
|
+
},
|
|
218
|
+
insertImage: (options) => {
|
|
219
|
+
applyRuntimeInsertImage(runtime, options);
|
|
220
|
+
},
|
|
221
|
+
addRowBefore: () => {
|
|
222
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
223
|
+
type: "add-row-before",
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
addRowAfter: () => {
|
|
227
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
228
|
+
type: "add-row-after",
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
addColumnBefore: () => {
|
|
232
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
233
|
+
type: "add-column-before",
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
addColumnAfter: () => {
|
|
237
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
238
|
+
type: "add-column-after",
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
deleteRow: () => {
|
|
242
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
243
|
+
type: "delete-row",
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
deleteColumn: () => {
|
|
247
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
248
|
+
type: "delete-column",
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
deleteTable: () => {
|
|
252
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
253
|
+
type: "delete-table",
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
mergeCells: () => {
|
|
257
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
258
|
+
type: "merge-cells",
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
splitCell: () => {
|
|
262
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
263
|
+
type: "split-cell",
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
setCellBackground: (color) => {
|
|
267
|
+
applyRuntimeTableStructureOperation(runtime, mountedSurface ?? null, {
|
|
268
|
+
type: "set-cell-background",
|
|
269
|
+
color,
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
search: (query, options) =>
|
|
273
|
+
mountedSurface?.search(query, options) ??
|
|
274
|
+
searchSnapshotSurface(runtime.getRenderSnapshot(), query, options),
|
|
275
|
+
clearSearch: () => {
|
|
276
|
+
mountedSurface?.clearSearch();
|
|
277
|
+
},
|
|
128
278
|
scrollToRevision: (revisionId: string) => {
|
|
129
279
|
const revision = runtime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
130
280
|
(r) => r.revisionId === revisionId,
|
|
@@ -312,6 +462,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
312
462
|
const [showTrackedChanges, setShowTrackedChanges] = useState(false);
|
|
313
463
|
const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
|
|
314
464
|
const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
|
|
465
|
+
const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
|
|
315
466
|
const shellRef = useRef<HTMLDivElement | null>(null);
|
|
316
467
|
const lastAnnouncedErrorIdRef = useRef<string | null>(null);
|
|
317
468
|
const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -547,6 +698,146 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
547
698
|
getWarnings: () => activeRuntime.getWarnings(),
|
|
548
699
|
getComments: () => activeRuntime.getRenderSnapshot().comments,
|
|
549
700
|
getTrackedChanges: () => activeRuntime.getRenderSnapshot().trackedChanges,
|
|
701
|
+
getFormattingState: () =>
|
|
702
|
+
getFormattingStateFromRenderSnapshot(activeRuntime.getRenderSnapshot()),
|
|
703
|
+
toggleBold: () => {
|
|
704
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
705
|
+
type: "toggle",
|
|
706
|
+
mark: "bold",
|
|
707
|
+
});
|
|
708
|
+
},
|
|
709
|
+
toggleItalic: () => {
|
|
710
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
711
|
+
type: "toggle",
|
|
712
|
+
mark: "italic",
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
toggleUnderline: () => {
|
|
716
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
717
|
+
type: "toggle",
|
|
718
|
+
mark: "underline",
|
|
719
|
+
});
|
|
720
|
+
},
|
|
721
|
+
toggleStrikethrough: () => {
|
|
722
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
723
|
+
type: "toggle",
|
|
724
|
+
mark: "strikethrough",
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
toggleSuperscript: () => {
|
|
728
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
729
|
+
type: "toggle",
|
|
730
|
+
mark: "superscript",
|
|
731
|
+
});
|
|
732
|
+
},
|
|
733
|
+
toggleSubscript: () => {
|
|
734
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
735
|
+
type: "toggle",
|
|
736
|
+
mark: "subscript",
|
|
737
|
+
});
|
|
738
|
+
},
|
|
739
|
+
setFontFamily: (fontFamily) => {
|
|
740
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
741
|
+
type: "set-font-family",
|
|
742
|
+
fontFamily,
|
|
743
|
+
});
|
|
744
|
+
},
|
|
745
|
+
setFontSize: (size) => {
|
|
746
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
747
|
+
type: "set-font-size",
|
|
748
|
+
size,
|
|
749
|
+
});
|
|
750
|
+
},
|
|
751
|
+
setTextColor: (color) => {
|
|
752
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
753
|
+
type: "set-text-color",
|
|
754
|
+
color,
|
|
755
|
+
});
|
|
756
|
+
},
|
|
757
|
+
setHighlightColor: (color) => {
|
|
758
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
759
|
+
type: "set-highlight-color",
|
|
760
|
+
color,
|
|
761
|
+
});
|
|
762
|
+
},
|
|
763
|
+
setAlignment: (alignment) => {
|
|
764
|
+
applyRuntimeFormattingOperation(activeRuntime, {
|
|
765
|
+
type: "set-alignment",
|
|
766
|
+
alignment,
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
indent: () => {
|
|
770
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "indent" });
|
|
771
|
+
},
|
|
772
|
+
outdent: () => {
|
|
773
|
+
applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" });
|
|
774
|
+
},
|
|
775
|
+
insertPageBreak: () => {
|
|
776
|
+
applyRuntimeInsertPageBreak(activeRuntime);
|
|
777
|
+
},
|
|
778
|
+
insertTable: (options) => {
|
|
779
|
+
applyRuntimeInsertTable(activeRuntime, options);
|
|
780
|
+
},
|
|
781
|
+
insertImage: (options) => {
|
|
782
|
+
applyRuntimeInsertImage(activeRuntime, options);
|
|
783
|
+
},
|
|
784
|
+
addRowBefore: () => {
|
|
785
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
786
|
+
type: "add-row-before",
|
|
787
|
+
});
|
|
788
|
+
},
|
|
789
|
+
addRowAfter: () => {
|
|
790
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
791
|
+
type: "add-row-after",
|
|
792
|
+
});
|
|
793
|
+
},
|
|
794
|
+
addColumnBefore: () => {
|
|
795
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
796
|
+
type: "add-column-before",
|
|
797
|
+
});
|
|
798
|
+
},
|
|
799
|
+
addColumnAfter: () => {
|
|
800
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
801
|
+
type: "add-column-after",
|
|
802
|
+
});
|
|
803
|
+
},
|
|
804
|
+
deleteRow: () => {
|
|
805
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
806
|
+
type: "delete-row",
|
|
807
|
+
});
|
|
808
|
+
},
|
|
809
|
+
deleteColumn: () => {
|
|
810
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
811
|
+
type: "delete-column",
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
deleteTable: () => {
|
|
815
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
816
|
+
type: "delete-table",
|
|
817
|
+
});
|
|
818
|
+
},
|
|
819
|
+
mergeCells: () => {
|
|
820
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
821
|
+
type: "merge-cells",
|
|
822
|
+
});
|
|
823
|
+
},
|
|
824
|
+
splitCell: () => {
|
|
825
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
826
|
+
type: "split-cell",
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
setCellBackground: (color) => {
|
|
830
|
+
applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
|
|
831
|
+
type: "set-cell-background",
|
|
832
|
+
color,
|
|
833
|
+
});
|
|
834
|
+
},
|
|
835
|
+
search: (query, options) =>
|
|
836
|
+
surfaceRef.current?.search(query, options) ??
|
|
837
|
+
searchSnapshotSurface(activeRuntime.getRenderSnapshot(), query, options),
|
|
838
|
+
clearSearch: () => {
|
|
839
|
+
surfaceRef.current?.clearSearch();
|
|
840
|
+
},
|
|
550
841
|
scrollToRevision: (revisionId: string) => {
|
|
551
842
|
const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
552
843
|
(r) => r.revisionId === revisionId,
|
|
@@ -667,12 +958,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
667
958
|
}
|
|
668
959
|
|
|
669
960
|
function addReviewComment(): void {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
961
|
+
try {
|
|
962
|
+
activeRuntime.addComment({
|
|
963
|
+
anchor: snapshot.selection.activeRange,
|
|
964
|
+
body: "New review comment",
|
|
965
|
+
authorId: currentUser.userId,
|
|
966
|
+
});
|
|
967
|
+
setActiveRailTab("comments");
|
|
968
|
+
} catch {
|
|
969
|
+
// Runtime already emitted a concrete export-safety error for invalid anchors.
|
|
970
|
+
}
|
|
676
971
|
}
|
|
677
972
|
|
|
678
973
|
function exportCurrentDocument(): void {
|
|
@@ -700,6 +995,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
700
995
|
? derivedCapabilities
|
|
701
996
|
: { ...derivedCapabilities, reviewRailVisible: false };
|
|
702
997
|
const diagnosticsModeMessage = getDiagnosticsModeMessage(loadError ?? snapshot.fatalError);
|
|
998
|
+
const addCommentDisabledReason =
|
|
999
|
+
!capabilities.canAddComment && !snapshot.selection.isCollapsed
|
|
1000
|
+
? "Select text within one paragraph to add a DOCX comment."
|
|
1001
|
+
: undefined;
|
|
703
1002
|
const accessibilityInstructionsId = `${documentId}-accessibility-instructions`;
|
|
704
1003
|
const accessibilityStatusId = `${documentId}-accessibility-status`;
|
|
705
1004
|
const accessibilityAlertId = `${documentId}-accessibility-alert`;
|
|
@@ -856,12 +1155,14 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
856
1155
|
activeRevisionId={activeRevisionId}
|
|
857
1156
|
showTrackedChanges={showTrackedChanges}
|
|
858
1157
|
selectionPreview={selectionPreview}
|
|
1158
|
+
addCommentDisabledReason={addCommentDisabledReason}
|
|
859
1159
|
onViewModeChange={setViewMode}
|
|
860
1160
|
onActiveRailTabChange={setActiveRailTab}
|
|
861
1161
|
onShowTrackedChangesChange={setShowTrackedChanges}
|
|
862
1162
|
{...reviewCallbacks}
|
|
863
1163
|
document={
|
|
864
1164
|
<TwProseMirrorSurface
|
|
1165
|
+
ref={surfaceRef}
|
|
865
1166
|
currentUser={currentUser}
|
|
866
1167
|
snapshot={snapshot}
|
|
867
1168
|
reviewMode={reviewMode}
|
|
@@ -885,6 +1186,231 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
885
1186
|
},
|
|
886
1187
|
);
|
|
887
1188
|
|
|
1189
|
+
function applyRuntimeFormattingOperation(
|
|
1190
|
+
runtime: WordReviewEditorRuntime,
|
|
1191
|
+
operation:
|
|
1192
|
+
| { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
|
|
1193
|
+
| { type: "set-font-family"; fontFamily: string | null }
|
|
1194
|
+
| { type: "set-font-size"; size: number | null }
|
|
1195
|
+
| { type: "set-text-color"; color: string | null }
|
|
1196
|
+
| { type: "set-highlight-color"; color: string | null }
|
|
1197
|
+
| { type: "set-alignment"; alignment: FormattingAlignment }
|
|
1198
|
+
| { type: "indent" }
|
|
1199
|
+
| { type: "outdent" },
|
|
1200
|
+
): void {
|
|
1201
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1202
|
+
if (!snapshot.isReady || snapshot.readOnly || snapshot.fatalError) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const result = applyFormattingOperationToDocument(
|
|
1207
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1208
|
+
snapshot,
|
|
1209
|
+
operation,
|
|
1210
|
+
);
|
|
1211
|
+
if (!result.changed) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
runtime.dispatch({
|
|
1216
|
+
type: "document.replace",
|
|
1217
|
+
document: result.document,
|
|
1218
|
+
selection: toRuntimeSelectionSnapshot(result.selection),
|
|
1219
|
+
origin: {
|
|
1220
|
+
source: "api",
|
|
1221
|
+
timestamp: new Date().toISOString(),
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function applyRuntimeInsertPageBreak(runtime: WordReviewEditorRuntime): void {
|
|
1227
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1228
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const timestamp = new Date().toISOString();
|
|
1233
|
+
const result = insertPageBreakInDocument(
|
|
1234
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1235
|
+
toRuntimeSelectionSnapshot(snapshot.selection),
|
|
1236
|
+
{ timestamp },
|
|
1237
|
+
);
|
|
1238
|
+
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function applyRuntimeInsertTable(
|
|
1242
|
+
runtime: WordReviewEditorRuntime,
|
|
1243
|
+
options: InsertTableOptions,
|
|
1244
|
+
): void {
|
|
1245
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1246
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const timestamp = new Date().toISOString();
|
|
1251
|
+
const result = insertTableInDocument(
|
|
1252
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1253
|
+
toRuntimeSelectionSnapshot(snapshot.selection),
|
|
1254
|
+
options,
|
|
1255
|
+
{ timestamp },
|
|
1256
|
+
);
|
|
1257
|
+
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function applyRuntimeInsertImage(
|
|
1261
|
+
runtime: WordReviewEditorRuntime,
|
|
1262
|
+
options: InsertImageOptions,
|
|
1263
|
+
): void {
|
|
1264
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1265
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const timestamp = new Date().toISOString();
|
|
1270
|
+
try {
|
|
1271
|
+
const result = insertImageInDocument(
|
|
1272
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1273
|
+
toRuntimeSelectionSnapshot(snapshot.selection),
|
|
1274
|
+
options.data,
|
|
1275
|
+
options.mimeType,
|
|
1276
|
+
options.width,
|
|
1277
|
+
options.height,
|
|
1278
|
+
{
|
|
1279
|
+
timestamp,
|
|
1280
|
+
altText: options.altText,
|
|
1281
|
+
},
|
|
1282
|
+
);
|
|
1283
|
+
dispatchRuntimeDocumentMutation(runtime, {
|
|
1284
|
+
changed: true,
|
|
1285
|
+
document: result.document,
|
|
1286
|
+
selection: result.selection,
|
|
1287
|
+
mapping: result.mapping,
|
|
1288
|
+
}, timestamp);
|
|
1289
|
+
} catch {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function applyRuntimeTableStructureOperation(
|
|
1295
|
+
runtime: WordReviewEditorRuntime,
|
|
1296
|
+
mountedSurface: TwProseMirrorSurfaceRef | null | undefined,
|
|
1297
|
+
operation:
|
|
1298
|
+
| { type: "add-row-before" }
|
|
1299
|
+
| { type: "add-row-after" }
|
|
1300
|
+
| { type: "add-column-before" }
|
|
1301
|
+
| { type: "add-column-after" }
|
|
1302
|
+
| { type: "delete-row" }
|
|
1303
|
+
| { type: "delete-column" }
|
|
1304
|
+
| { type: "delete-table" }
|
|
1305
|
+
| { type: "merge-cells" }
|
|
1306
|
+
| { type: "split-cell" }
|
|
1307
|
+
| { type: "set-cell-background"; color: string },
|
|
1308
|
+
): void {
|
|
1309
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
1310
|
+
if (!canApplyRuntimeMutation(snapshot)) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const timestamp = new Date().toISOString();
|
|
1315
|
+
const result = applyTableStructureOperation(
|
|
1316
|
+
runtime.getPersistedSnapshot().canonicalDocument,
|
|
1317
|
+
snapshot,
|
|
1318
|
+
mountedSurface?.getTableSelection() ?? null,
|
|
1319
|
+
operation,
|
|
1320
|
+
);
|
|
1321
|
+
dispatchRuntimeDocumentMutation(runtime, result, timestamp);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function canApplyRuntimeMutation(snapshot: RuntimeRenderSnapshot): boolean {
|
|
1325
|
+
return snapshot.isReady && !snapshot.readOnly && !snapshot.fatalError;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function dispatchRuntimeDocumentMutation(
|
|
1329
|
+
runtime: WordReviewEditorRuntime,
|
|
1330
|
+
result: {
|
|
1331
|
+
changed: boolean;
|
|
1332
|
+
document: PersistedEditorSnapshot["canonicalDocument"];
|
|
1333
|
+
selection: InternalSelectionSnapshot;
|
|
1334
|
+
mapping?: TransactionMapping;
|
|
1335
|
+
},
|
|
1336
|
+
timestamp: string,
|
|
1337
|
+
): void {
|
|
1338
|
+
if (!result.changed) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
runtime.dispatch({
|
|
1343
|
+
type: "document.replace",
|
|
1344
|
+
document: {
|
|
1345
|
+
...result.document,
|
|
1346
|
+
updatedAt: timestamp,
|
|
1347
|
+
},
|
|
1348
|
+
mapping: result.mapping,
|
|
1349
|
+
selection: result.selection,
|
|
1350
|
+
origin: {
|
|
1351
|
+
source: "api",
|
|
1352
|
+
timestamp,
|
|
1353
|
+
},
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function searchSnapshotSurface(
|
|
1358
|
+
snapshot: RuntimeRenderSnapshot,
|
|
1359
|
+
query: string,
|
|
1360
|
+
options: SearchOptions = {},
|
|
1361
|
+
): SearchResultSnapshot[] {
|
|
1362
|
+
const normalizedQuery = query.trim();
|
|
1363
|
+
if (!normalizedQuery || !snapshot.surface) {
|
|
1364
|
+
return [];
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const rawResults = findSearchMatches(
|
|
1368
|
+
snapshot.surface.plainText,
|
|
1369
|
+
normalizedQuery,
|
|
1370
|
+
options,
|
|
1371
|
+
).slice(0, options.limit ?? Number.POSITIVE_INFINITY);
|
|
1372
|
+
const activeResultIndex = getActiveSearchResultIndex(rawResults, snapshot.selection);
|
|
1373
|
+
|
|
1374
|
+
return rawResults.map((result, index) => ({
|
|
1375
|
+
resultId: `search-result-${index}`,
|
|
1376
|
+
anchor: {
|
|
1377
|
+
kind: "range",
|
|
1378
|
+
from: result.from,
|
|
1379
|
+
to: result.to,
|
|
1380
|
+
assoc: {
|
|
1381
|
+
start: -1,
|
|
1382
|
+
end: 1,
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
excerpt: createSearchExcerpt(
|
|
1386
|
+
snapshot.surface?.plainText ?? "",
|
|
1387
|
+
result.from,
|
|
1388
|
+
result.to,
|
|
1389
|
+
),
|
|
1390
|
+
isActive: index === activeResultIndex,
|
|
1391
|
+
}));
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function getActiveSearchResultIndex(
|
|
1395
|
+
results: Array<{ from: number; to: number }>,
|
|
1396
|
+
selection: PublicSelectionSnapshot,
|
|
1397
|
+
): number {
|
|
1398
|
+
if (results.length === 0) {
|
|
1399
|
+
return -1;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const selectionFrom = Math.min(selection.anchor, selection.head);
|
|
1403
|
+
const selectionTo = Math.max(selection.anchor, selection.head);
|
|
1404
|
+
const activeIndex = results.findIndex((result) => {
|
|
1405
|
+
if (selectionFrom === selectionTo) {
|
|
1406
|
+
return selectionFrom >= result.from && selectionFrom <= result.to;
|
|
1407
|
+
}
|
|
1408
|
+
return selectionFrom < result.to && selectionTo > result.from;
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
return activeIndex >= 0 ? activeIndex : 0;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
888
1414
|
function applyRegionAttributes(shell: HTMLElement): void {
|
|
889
1415
|
const toolbar = shell.querySelector<HTMLElement>("header");
|
|
890
1416
|
if (toolbar) {
|
|
@@ -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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, type Node as PMNode } from "prosemirror-model";
|
|
2
|
-
import { EditorState, type Plugin, TextSelection } from "prosemirror-state";
|
|
2
|
+
import { EditorState, type Plugin, Selection, TextSelection } from "prosemirror-state";
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
EditorSurfaceSnapshot,
|
|
@@ -43,12 +43,13 @@ export function createPMStateFromSnapshot(
|
|
|
43
43
|
positionMap.pmDocSize - 1,
|
|
44
44
|
);
|
|
45
45
|
|
|
46
|
-
let pmSelection:
|
|
46
|
+
let pmSelection: Selection;
|
|
47
47
|
try {
|
|
48
|
-
pmSelection = TextSelection.
|
|
48
|
+
pmSelection = TextSelection.between(doc.resolve(pmAnchor), doc.resolve(pmHead));
|
|
49
49
|
} catch {
|
|
50
|
-
// If the
|
|
51
|
-
|
|
50
|
+
// If the mapped runtime selection is invalid or lands in a non-text block,
|
|
51
|
+
// let ProseMirror choose the nearest valid starting selection.
|
|
52
|
+
pmSelection = Selection.atStart(doc);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const state = EditorState.create({
|
|
@@ -258,6 +259,7 @@ function buildTable(
|
|
|
258
259
|
rowspan: cell.rowspan,
|
|
259
260
|
gridSpan: cell.gridSpan,
|
|
260
261
|
verticalMerge: cell.verticalMerge,
|
|
262
|
+
backgroundColor: cell.backgroundColor ?? null,
|
|
261
263
|
},
|
|
262
264
|
Fragment.from(cellContent),
|
|
263
265
|
),
|