@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.23",
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": [
@@ -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
- // Create the revision with pre-mapping positions it refers to content
1325
- // that was just inserted, so its anchors are already correct in the new document
1326
- const revision = createAuthoredRevision(
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: revision.changeId, kind: "insertion" },
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 (!showTrackedChanges) continue;
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
- const pmFrom = positionMap.runtimeToPm(rev.from);
256
- const pmTo = positionMap.runtimeToPm(rev.to);
257
- if (pmFrom < pmTo) {
258
- decorations.push(
259
- Decoration.inline(pmFrom, pmTo, {
260
- class: cls,
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,