@beyondwork/docx-react-component 1.0.23 → 1.0.24-rc
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 +1 -1
- package/src/api/public-types.ts +1 -0
- package/src/core/commands/index.ts +64 -13
- package/src/ui/WordReviewEditor.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +52 -12
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +5 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.24rc",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -1398,6 +1398,7 @@ export interface WordReviewEditorProps {
|
|
|
1398
1398
|
loadSourcePolicy?: LoadSourcePolicy;
|
|
1399
1399
|
readOnly?: boolean;
|
|
1400
1400
|
reviewMode?: "editing" | "review";
|
|
1401
|
+
suggestionsEnabled?: boolean;
|
|
1401
1402
|
markupDisplay?: "clean" | "simple" | "all";
|
|
1402
1403
|
showReviewPanel?: boolean;
|
|
1403
1404
|
chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
|
|
@@ -1309,7 +1309,7 @@ function applySuggestingInsert(
|
|
|
1309
1309
|
}
|
|
1310
1310
|
|
|
1311
1311
|
if (isCollapsed) {
|
|
1312
|
-
// Pure insertion at cursor: apply normally, then create insertion revision
|
|
1312
|
+
// Pure insertion at cursor: apply normally, then create or extend an insertion revision.
|
|
1313
1313
|
const result = insertText(state.document, selection, text, { timestamp: context.timestamp });
|
|
1314
1314
|
const insertedFrom = from;
|
|
1315
1315
|
const insertedTo = from + Array.from(text).length;
|
|
@@ -1321,25 +1321,53 @@ function applySuggestingInsert(
|
|
|
1321
1321
|
result.mapping,
|
|
1322
1322
|
);
|
|
1323
1323
|
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
|
|
1324
|
+
// If there is an authored insertion revision that ends exactly where this
|
|
1325
|
+
// text was inserted (i.e. the cursor was at the end of a prior insertion),
|
|
1326
|
+
// extend that revision to cover the new text rather than creating a new one.
|
|
1327
|
+
// This groups consecutive keystrokes into a single tracked change.
|
|
1328
|
+
const adjacentInsertion = findAdjacentAuthoredInsertion(
|
|
1327
1329
|
reviewState.document.review.revisions,
|
|
1328
|
-
"insertion",
|
|
1329
1330
|
insertedFrom,
|
|
1330
|
-
insertedTo,
|
|
1331
|
-
authorId,
|
|
1332
|
-
context.timestamp,
|
|
1333
1331
|
);
|
|
1334
1332
|
|
|
1333
|
+
let finalRevisionId: string;
|
|
1334
|
+
let finalRevisions: Record<string, CanonicalRevisionRecord>;
|
|
1335
|
+
|
|
1336
|
+
if (adjacentInsertion && adjacentInsertion.anchor.kind === "range") {
|
|
1337
|
+
const extended: CanonicalRevisionRecord = {
|
|
1338
|
+
...adjacentInsertion,
|
|
1339
|
+
anchor: {
|
|
1340
|
+
kind: "range",
|
|
1341
|
+
range: { from: adjacentInsertion.anchor.range.from, to: insertedTo },
|
|
1342
|
+
assoc: { start: 1, end: -1 },
|
|
1343
|
+
},
|
|
1344
|
+
};
|
|
1345
|
+
finalRevisionId = extended.changeId;
|
|
1346
|
+
finalRevisions = {
|
|
1347
|
+
...reviewState.document.review.revisions,
|
|
1348
|
+
[extended.changeId]: extended,
|
|
1349
|
+
};
|
|
1350
|
+
} else {
|
|
1351
|
+
const revision = createAuthoredRevision(
|
|
1352
|
+
reviewState.document.review.revisions,
|
|
1353
|
+
"insertion",
|
|
1354
|
+
insertedFrom,
|
|
1355
|
+
insertedTo,
|
|
1356
|
+
authorId,
|
|
1357
|
+
context.timestamp,
|
|
1358
|
+
);
|
|
1359
|
+
finalRevisionId = revision.changeId;
|
|
1360
|
+
finalRevisions = {
|
|
1361
|
+
...reviewState.document.review.revisions,
|
|
1362
|
+
[revision.changeId]: revision,
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1335
1366
|
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1336
1367
|
...reviewState.document,
|
|
1337
1368
|
review: {
|
|
1338
1369
|
...reviewState.document.review,
|
|
1339
|
-
revisions:
|
|
1340
|
-
...reviewState.document.review.revisions,
|
|
1341
|
-
[revision.changeId]: revision,
|
|
1342
|
-
},
|
|
1370
|
+
revisions: finalRevisions,
|
|
1343
1371
|
},
|
|
1344
1372
|
};
|
|
1345
1373
|
|
|
@@ -1360,7 +1388,7 @@ function applySuggestingInsert(
|
|
|
1360
1388
|
mapping: result.mapping,
|
|
1361
1389
|
effects: {
|
|
1362
1390
|
...reviewState.effects,
|
|
1363
|
-
revisionAuthored: { changeId:
|
|
1391
|
+
revisionAuthored: { changeId: finalRevisionId, kind: "insertion" },
|
|
1364
1392
|
},
|
|
1365
1393
|
},
|
|
1366
1394
|
);
|
|
@@ -1704,3 +1732,26 @@ function findOverlappingAuthoredDeletion(
|
|
|
1704
1732
|
}
|
|
1705
1733
|
return undefined;
|
|
1706
1734
|
}
|
|
1735
|
+
|
|
1736
|
+
/**
|
|
1737
|
+
* Find an open authored insertion revision whose end position equals `cursorAt`.
|
|
1738
|
+
* Used to extend an existing insertion when the user keeps typing at the same
|
|
1739
|
+
* position rather than creating a new revision for every character.
|
|
1740
|
+
*/
|
|
1741
|
+
function findAdjacentAuthoredInsertion(
|
|
1742
|
+
revisions: Record<string, CanonicalRevisionRecord>,
|
|
1743
|
+
cursorAt: number,
|
|
1744
|
+
): CanonicalRevisionRecord | undefined {
|
|
1745
|
+
for (const revision of Object.values(revisions)) {
|
|
1746
|
+
if (
|
|
1747
|
+
revision.kind === "insertion" &&
|
|
1748
|
+
revision.status === "open" &&
|
|
1749
|
+
revision.metadata?.source === "runtime" &&
|
|
1750
|
+
revision.anchor.kind === "range" &&
|
|
1751
|
+
revision.anchor.range.to === cursorAt
|
|
1752
|
+
) {
|
|
1753
|
+
return revision;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return undefined;
|
|
1757
|
+
}
|
|
@@ -520,6 +520,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
520
520
|
onWarning,
|
|
521
521
|
readOnly = false,
|
|
522
522
|
reviewMode = "review",
|
|
523
|
+
suggestionsEnabled = false,
|
|
523
524
|
showReviewPanel = true,
|
|
524
525
|
chromeVisibility,
|
|
525
526
|
} = props;
|
|
@@ -693,6 +694,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
693
694
|
activeRuntime.setViewMode(effectiveViewMode);
|
|
694
695
|
}, [activeRuntime, effectiveViewMode]);
|
|
695
696
|
|
|
697
|
+
useEffect(() => {
|
|
698
|
+
activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
|
|
699
|
+
}, [activeRuntime, suggestionsEnabled]);
|
|
700
|
+
|
|
696
701
|
useEffect(() => {
|
|
697
702
|
runtimeViewStateSeedRef.current = {
|
|
698
703
|
workspaceMode: viewState.workspaceMode,
|
|
@@ -1707,6 +1712,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1707
1712
|
markupDisplay={liveMarkupDisplay}
|
|
1708
1713
|
activeRevisionId={activeRevisionId}
|
|
1709
1714
|
showTrackedChanges={showTrackedChanges}
|
|
1715
|
+
suggestionsEnabled={suggestionsEnabled}
|
|
1710
1716
|
mediaPreviews={mediaPreviews}
|
|
1711
1717
|
isPageWorkspace={isPageWorkspace}
|
|
1712
1718
|
workflowScopes={workflowScopeSnapshot?.scopes}
|
|
@@ -27,6 +27,7 @@ export interface EditorSurfaceControllerProps {
|
|
|
27
27
|
markupDisplay: MarkupDisplay;
|
|
28
28
|
activeRevisionId?: string;
|
|
29
29
|
showTrackedChanges?: boolean;
|
|
30
|
+
suggestionsEnabled?: boolean;
|
|
30
31
|
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
31
32
|
isPageWorkspace?: boolean;
|
|
32
33
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
@@ -190,10 +190,13 @@ export function buildDecorations(
|
|
|
190
190
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
|
|
191
191
|
activeWorkflowWorkItemId?: string | null,
|
|
192
192
|
activeWorkflowScopeIds?: readonly string[],
|
|
193
|
+
suggestionsEnabled = false,
|
|
193
194
|
): DecorationSet {
|
|
194
195
|
const decorations: Decoration[] = [];
|
|
195
196
|
const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
|
|
196
197
|
const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
|
|
198
|
+
// In suggestions mode, tracked changes are always shown regardless of the toggle.
|
|
199
|
+
const effectiveShowTracked = suggestionsEnabled ? true : showTrackedChanges;
|
|
197
200
|
|
|
198
201
|
// Walk comment threads and create inline decorations
|
|
199
202
|
if (commentModel) {
|
|
@@ -227,7 +230,7 @@ export function buildDecorations(
|
|
|
227
230
|
// Always hide deletions in clean mode (final-text semantics).
|
|
228
231
|
// This is the critical behavior: "hide tracked changes" must show
|
|
229
232
|
// the document as if accepted, not show deleted text as kept text.
|
|
230
|
-
if (markupDisplay === "clean" && rev.kind === "deletion") {
|
|
233
|
+
if (markupDisplay === "clean" && rev.kind === "deletion" && !suggestionsEnabled) {
|
|
231
234
|
const pmFrom = positionMap.runtimeToPm(rev.from);
|
|
232
235
|
const pmTo = positionMap.runtimeToPm(rev.to);
|
|
233
236
|
if (pmFrom < pmTo) {
|
|
@@ -242,7 +245,48 @@ export function buildDecorations(
|
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
// Skip visual styling when tracked changes display is off
|
|
245
|
-
if (!
|
|
248
|
+
if (!effectiveShowTracked) continue;
|
|
249
|
+
|
|
250
|
+
const pmFrom = positionMap.runtimeToPm(rev.from);
|
|
251
|
+
const pmTo = positionMap.runtimeToPm(rev.to);
|
|
252
|
+
if (pmFrom >= pmTo) continue;
|
|
253
|
+
|
|
254
|
+
if (suggestionsEnabled) {
|
|
255
|
+
if (rev.kind === "insertion") {
|
|
256
|
+
decorations.push(
|
|
257
|
+
Decoration.inline(pmFrom, pmTo, {
|
|
258
|
+
class: "text-insert",
|
|
259
|
+
"data-revision-id": rev.revisionId,
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
decorations.push(
|
|
263
|
+
Decoration.widget(pmFrom, () => {
|
|
264
|
+
const el = document.createElement("span");
|
|
265
|
+
el.textContent = "[";
|
|
266
|
+
el.className = "text-insert";
|
|
267
|
+
el.setAttribute("contenteditable", "false");
|
|
268
|
+
return el;
|
|
269
|
+
}, { side: -1, key: `${rev.revisionId}-open` }),
|
|
270
|
+
);
|
|
271
|
+
decorations.push(
|
|
272
|
+
Decoration.widget(pmTo, () => {
|
|
273
|
+
const el = document.createElement("span");
|
|
274
|
+
el.textContent = "]";
|
|
275
|
+
el.className = "text-insert";
|
|
276
|
+
el.setAttribute("contenteditable", "false");
|
|
277
|
+
return el;
|
|
278
|
+
}, { side: 1, key: `${rev.revisionId}-close` }),
|
|
279
|
+
);
|
|
280
|
+
} else if (rev.kind === "deletion") {
|
|
281
|
+
decorations.push(
|
|
282
|
+
Decoration.inline(pmFrom, pmTo, {
|
|
283
|
+
class: "text-danger line-through decoration-danger/80 decoration-1",
|
|
284
|
+
"data-revision-id": rev.revisionId,
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
246
290
|
|
|
247
291
|
const cls = getRevisionHighlightClass(
|
|
248
292
|
revisionModel,
|
|
@@ -252,16 +296,12 @@ export function buildDecorations(
|
|
|
252
296
|
);
|
|
253
297
|
if (!cls) continue;
|
|
254
298
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"data-revision-id": rev.revisionId,
|
|
262
|
-
}),
|
|
263
|
-
);
|
|
264
|
-
}
|
|
299
|
+
decorations.push(
|
|
300
|
+
Decoration.inline(pmFrom, pmTo, {
|
|
301
|
+
class: cls,
|
|
302
|
+
"data-revision-id": rev.revisionId,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
265
305
|
}
|
|
266
306
|
}
|
|
267
307
|
|
|
@@ -43,6 +43,7 @@ export function createSurfaceDecorationKey(input: {
|
|
|
43
43
|
workflowBlockedSignature?: string;
|
|
44
44
|
activeWorkflowWorkItemId?: string | null;
|
|
45
45
|
activeWorkflowScopeIds?: readonly string[];
|
|
46
|
+
suggestionsEnabled?: boolean;
|
|
46
47
|
}): string {
|
|
47
48
|
return JSON.stringify({
|
|
48
49
|
markupDisplay: input.markupDisplay,
|
|
@@ -55,5 +56,6 @@ export function createSurfaceDecorationKey(input: {
|
|
|
55
56
|
workflowBlockedSignature: input.workflowBlockedSignature ?? null,
|
|
56
57
|
activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
|
|
57
58
|
activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
|
|
59
|
+
suggestionsEnabled: input.suggestionsEnabled ?? false,
|
|
58
60
|
});
|
|
59
61
|
}
|
|
@@ -75,6 +75,7 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
75
75
|
markupDisplay: MarkupDisplay;
|
|
76
76
|
activeRevisionId?: string;
|
|
77
77
|
showTrackedChanges?: boolean;
|
|
78
|
+
suggestionsEnabled?: boolean;
|
|
78
79
|
/** When true, the surface renders inside the page workspace (vs canvas). */
|
|
79
80
|
isPageWorkspace?: boolean;
|
|
80
81
|
onFocus: FocusEventHandler<HTMLDivElement>;
|
|
@@ -204,6 +205,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
204
205
|
}),
|
|
205
206
|
[mediaPreviewKey, snapshot.activeStory, surface],
|
|
206
207
|
);
|
|
208
|
+
const suggestionsEnabled = props.suggestionsEnabled === true;
|
|
207
209
|
const decorationBuildKey = useMemo(
|
|
208
210
|
() =>
|
|
209
211
|
createSurfaceDecorationKey({
|
|
@@ -217,6 +219,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
217
219
|
workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
|
|
218
220
|
activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
|
|
219
221
|
activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
|
|
222
|
+
suggestionsEnabled,
|
|
220
223
|
}),
|
|
221
224
|
[
|
|
222
225
|
canEdit,
|
|
@@ -229,6 +232,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
229
232
|
props.workflowScopes,
|
|
230
233
|
showTrackedChanges,
|
|
231
234
|
snapshot.comments.activeCommentId,
|
|
235
|
+
suggestionsEnabled,
|
|
232
236
|
],
|
|
233
237
|
);
|
|
234
238
|
|
|
@@ -274,6 +278,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
274
278
|
props.workflowBlockedReasons,
|
|
275
279
|
props.activeWorkflowWorkItemId,
|
|
276
280
|
props.activeWorkflowScopeIds,
|
|
281
|
+
suggestionsEnabled,
|
|
277
282
|
);
|
|
278
283
|
view.setProps({
|
|
279
284
|
editable: () => canEdit,
|