@beyondwork/docx-react-component 1.0.27 → 1.0.28

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.27",
4
+ "version": "1.0.28",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "packageManager": "pnpm@10.30.3",
7
7
  "type": "module",
@@ -95,6 +95,7 @@
95
95
  "test:repo": "node scripts/run-repo-tests.mjs core",
96
96
  "test:repo:all": "node scripts/run-repo-tests.mjs all",
97
97
  "test:repo:optional": "node scripts/run-repo-tests.mjs optional",
98
+ "test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
98
99
  "test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
99
100
  "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
100
101
  "lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
@@ -155,15 +156,7 @@
155
156
  "prosemirror-view": "^1.41.7",
156
157
  "react": "^19.2.0",
157
158
  "react-dom": "^19.2.0",
158
- "tailwindcss": "^4.2.2",
159
- "yjs": "^13.6.0",
160
- "y-prosemirror": "^1.2.0",
161
- "y-protocols": "^1.0.0"
162
- },
163
- "peerDependenciesMeta": {
164
- "yjs": { "optional": true },
165
- "y-prosemirror": { "optional": true },
166
- "y-protocols": { "optional": true }
159
+ "tailwindcss": "^4.2.2"
167
160
  },
168
161
  "devDependencies": {
169
162
  "@chllming/wave-orchestration": "^0.9.15",
@@ -181,10 +174,7 @@
181
174
  "react": "19.2.4",
182
175
  "react-dom": "19.2.4",
183
176
  "tsup": "^8.3.0",
184
- "tsx": "^4.21.0",
185
- "y-prosemirror": "^1.3.7",
186
- "y-protocols": "^1.0.7",
187
- "yjs": "^13.6.30"
177
+ "tsx": "^4.21.0"
188
178
  },
189
179
  "pnpm": {
190
180
  "onlyBuiltDependencies": [
@@ -1,8 +1,13 @@
1
1
  import type { PersistedEditorSnapshot as RuntimePersistedEditorSnapshot } from "../core/state/editor-state.ts";
2
+ import type {
3
+ FieldFamily as FieldFamilyType,
4
+ FieldRefreshStatus as FieldRefreshStatusType,
5
+ SupportedFieldFamily as SupportedFieldFamilyType,
6
+ } from "../model/canonical-document.ts";
2
7
 
3
- export type FieldFamily = import("../model/canonical-document.ts").FieldFamily;
4
- export type FieldRefreshStatus = import("../model/canonical-document.ts").FieldRefreshStatus;
5
- export type SupportedFieldFamily = import("../model/canonical-document.ts").SupportedFieldFamily;
8
+ export type FieldFamily = FieldFamilyType;
9
+ export type FieldRefreshStatus = FieldRefreshStatusType;
10
+ export type SupportedFieldFamily = SupportedFieldFamilyType;
6
11
 
7
12
  export type ExternalDocumentSource =
8
13
  | {
@@ -1383,8 +1388,6 @@ export interface WordReviewEditorRef {
1383
1388
  export interface WordReviewEditorProps {
1384
1389
  documentId: string;
1385
1390
  currentUser: EditorUser;
1386
- ydoc?: import('yjs').Doc;
1387
- awareness?: import("y-protocols/awareness").Awareness;
1388
1391
  initialDocx?: Uint8Array | ArrayBuffer;
1389
1392
  initialSessionState?: EditorSessionState;
1390
1393
  initialSnapshot?: PersistedEditorSnapshot;
@@ -169,7 +169,6 @@ import {
169
169
  } from "./browser-export";
170
170
  import { EditorShellView } from "./editor-shell-view.tsx";
171
171
  import { EditorSurfaceController } from "./editor-surface-controller.tsx";
172
- import { createCollabReviewSync } from "../runtime/collab-review-sync.ts";
173
172
 
174
173
  export {
175
174
  __createFallbackRuntime,
@@ -504,8 +503,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
504
503
  function WordReviewEditor(props, ref) {
505
504
  const {
506
505
  currentUser,
507
- ydoc,
508
- awareness,
509
506
  hostAdapter,
510
507
  datastore,
511
508
  documentId,
@@ -701,12 +698,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
701
698
  activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
702
699
  }, [activeRuntime, suggestionsEnabled]);
703
700
 
704
- useEffect(() => {
705
- if (!ydoc || !runtime) return;
706
- const handle = createCollabReviewSync(ydoc, runtime);
707
- return () => handle.destroy();
708
- }, [ydoc, runtime]);
709
-
710
701
  useEffect(() => {
711
702
  runtimeViewStateSeedRef.current = {
712
703
  workspaceMode: viewState.workspaceMode,
@@ -1714,8 +1705,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1714
1705
  <EditorSurfaceController
1715
1706
  ref={surfaceRef}
1716
1707
  currentUser={currentUser}
1717
- ydoc={ydoc}
1718
- awareness={awareness}
1719
1708
  snapshot={snapshot}
1720
1709
  canonicalDocument={canonicalDocument}
1721
1710
  documentNavigation={documentNavigation}
@@ -20,8 +20,6 @@ import type { MediaPreviewDescriptor } from "../ui-tailwind/editor-surface/pm-st
20
20
 
21
21
  export interface EditorSurfaceControllerProps {
22
22
  currentUser: EditorUser;
23
- ydoc?: import('yjs').Doc;
24
- awareness?: import("y-protocols/awareness").Awareness;
25
23
  snapshot: RuntimeRenderSnapshot;
26
24
  canonicalDocument: CanonicalDocumentEnvelope;
27
25
  documentNavigation: DocumentNavigationSnapshot;
@@ -9,13 +9,7 @@ import {
9
9
  } from "../../ui/headless/selection-helpers";
10
10
  import type { PositionMap } from "./pm-position-map";
11
11
 
12
- export interface SelectionSyncCallbacks {
13
- onSelectionChange: (selection: SelectionSnapshot) => void;
14
- getPositionMap: () => PositionMap | null;
15
- isSelectionSyncSuppressed?: () => boolean;
16
- }
17
-
18
- export interface CommandBridgeCallbacks extends SelectionSyncCallbacks {
12
+ export interface CommandBridgeCallbacks {
19
13
  onInsertText: (text: string) => void;
20
14
  onDeleteBackward: () => void;
21
15
  onDeleteForward: () => void;
@@ -26,20 +20,48 @@ export interface CommandBridgeCallbacks extends SelectionSyncCallbacks {
26
20
  onUndo: () => void;
27
21
  onRedo: () => void;
28
22
  onBlockedInput?: (command: "paste" | "drop", message: string) => void;
23
+ onSelectionChange: (selection: SelectionSnapshot) => void;
24
+ getPositionMap: () => PositionMap | null;
25
+ isSelectionSyncSuppressed?: () => boolean;
29
26
  }
30
27
 
31
28
  const bridgeKey = new PluginKey("command-bridge");
32
29
 
33
- export function createSelectionSyncPlugin(
34
- callbacks: SelectionSyncCallbacks,
35
- ): Plugin {
36
- return new Plugin({
30
+ /**
31
+ * Creates ProseMirror plugins that intercept user input and dispatch
32
+ * runtime commands instead of letting PM mutate its own state.
33
+ *
34
+ * All doc-changing transactions are blocked. Only the explicit input
35
+ * hooks below are allowed to trigger runtime commands.
36
+ */
37
+ export function createCommandBridgePlugins(
38
+ callbacks: CommandBridgeCallbacks,
39
+ ): Plugin[] {
40
+ let isComposing = false;
41
+
42
+ // Transaction filter: block ALL doc-changing transactions.
43
+ // The runtime is the sole authority for document mutations.
44
+ const filterPlugin = new Plugin({
45
+ key: bridgeKey,
46
+ filterTransaction(tr) {
47
+ // Allow selection-only and metadata-only transactions
48
+ if (!tr.docChanged) return true;
49
+ // Block doc changes — runtime handles mutations via callbacks
50
+ return false;
51
+ },
52
+ });
53
+
54
+ // Selection sync: when PM selection changes, notify the runtime.
55
+ const selectionPlugin = new Plugin({
37
56
  view() {
38
57
  return {
39
58
  update(view, prevState) {
40
59
  if (callbacks.isSelectionSyncSuppressed?.()) {
41
60
  return;
42
61
  }
62
+ if (isComposing) {
63
+ return;
64
+ }
43
65
  if (!view.state.selection.eq(prevState.selection)) {
44
66
  const posMap = callbacks.getPositionMap();
45
67
  if (!posMap) return;
@@ -60,22 +82,6 @@ export function createSelectionSyncPlugin(
60
82
  };
61
83
  },
62
84
  });
63
- }
64
-
65
- export function createCommandBridgePlugins(
66
- callbacks: CommandBridgeCallbacks,
67
- ): Plugin[] {
68
- let isComposing = false;
69
-
70
- const filterPlugin = new Plugin({
71
- key: bridgeKey,
72
- filterTransaction(tr) {
73
- if (!tr.docChanged) return true;
74
- return false;
75
- },
76
- });
77
-
78
- const selectionPlugin = createSelectionSyncPlugin(callbacks);
79
85
 
80
86
  // Text input hook: intercept typed characters.
81
87
  const inputPlugin = new Plugin({
@@ -39,8 +39,6 @@ import {
39
39
  createCommandBridgePlugins,
40
40
  type CommandBridgeCallbacks,
41
41
  } from "./pm-command-bridge";
42
- import { createCollabPlugins } from "./pm-collab-plugins";
43
- import { prosemirrorToYXmlFragment } from "y-prosemirror";
44
42
  import { buildDecorations } from "./pm-decorations";
45
43
  import { createContextualInteractionPlugin } from "./pm-contextual-ui";
46
44
  import {
@@ -70,8 +68,6 @@ import type { MediaPreviewDescriptor } from "./pm-state-from-snapshot";
70
68
  */
71
69
  export interface TwProseMirrorSurfaceProps {
72
70
  currentUser: EditorUser;
73
- ydoc?: import("yjs").Doc;
74
- awareness?: import("y-protocols/awareness").Awareness;
75
71
  snapshot: RuntimeRenderSnapshot;
76
72
  canonicalDocument: CanonicalDocumentEnvelope;
77
73
  documentNavigation: DocumentNavigationSnapshot;
@@ -240,46 +236,32 @@ export const TwProseMirrorSurface = forwardRef<
240
236
  ],
241
237
  );
242
238
 
243
- const isCollabMode = Boolean(props.ydoc);
244
-
245
239
  // Create PM plugins (stable across renders — callbacks accessed via ref)
246
240
  const plugins = useMemo(() => {
247
- const selectionCallbacks = {
248
- onSelectionChange: (sel: SelectionSnapshot) => callbacksRef.current?.onSelectionChange(sel),
249
- getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
250
- isSelectionSyncSuppressed: () =>
251
- callbacksRef.current?.isSelectionSyncSuppressed?.() ?? false,
252
- };
253
-
254
- const corePlugins = props.ydoc
255
- ? createCollabPlugins({
256
- ydoc: props.ydoc,
257
- awareness: props.awareness,
258
- selectionCallbacks,
259
- })
260
- : createCommandBridgePlugins({
261
- ...selectionCallbacks,
262
- onInsertText: (text) => callbacksRef.current?.onInsertText(text),
263
- onDeleteBackward: () => callbacksRef.current?.onDeleteBackward(),
264
- onDeleteForward: () => callbacksRef.current?.onDeleteForward(),
265
- onSplitParagraph: () => callbacksRef.current?.onSplitParagraph(),
266
- onInsertHardBreak: () => callbacksRef.current?.onInsertHardBreak(),
267
- onInsertTab: () => callbacksRef.current?.onInsertTab(),
268
- onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
269
- onUndo: () => callbacksRef.current?.onUndo(),
270
- onRedo: () => callbacksRef.current?.onRedo(),
271
- onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
272
- });
273
-
274
241
  return [
275
- ...corePlugins,
242
+ ...createCommandBridgePlugins({
243
+ onInsertText: (text) => callbacksRef.current?.onInsertText(text),
244
+ onDeleteBackward: () => callbacksRef.current?.onDeleteBackward(),
245
+ onDeleteForward: () => callbacksRef.current?.onDeleteForward(),
246
+ onSplitParagraph: () => callbacksRef.current?.onSplitParagraph(),
247
+ onInsertHardBreak: () => callbacksRef.current?.onInsertHardBreak(),
248
+ onInsertTab: () => callbacksRef.current?.onInsertTab(),
249
+ onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
250
+ onUndo: () => callbacksRef.current?.onUndo(),
251
+ onRedo: () => callbacksRef.current?.onRedo(),
252
+ onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
253
+ onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
254
+ getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
255
+ isSelectionSyncSuppressed: () =>
256
+ callbacksRef.current?.isSelectionSyncSuppressed?.() ?? false,
257
+ }),
276
258
  createContextualInteractionPlugin({
277
259
  onCommentActivated: (commentId) => props.onCommentActivated?.(commentId),
278
260
  onRevisionActivated: (revisionId) => props.onRevisionActivated?.(revisionId),
279
261
  }),
280
262
  createSearchPlugin(),
281
263
  ];
282
- }, [props.onCommentActivated, props.onRevisionActivated, props.ydoc, props.awareness]);
264
+ }, [props.onCommentActivated, props.onRevisionActivated]);
283
265
 
284
266
  const applyDecorationProps = useCallback(
285
267
  (view: EditorView, positionMap: PositionMap): void => {
@@ -321,14 +303,10 @@ export const TwProseMirrorSurface = forwardRef<
321
303
  ],
322
304
  );
323
305
 
306
+ // Create or update the PM document only when the structural key changes.
324
307
  useEffect(() => {
325
308
  if (!mountRef.current || !surface) return;
326
309
 
327
- // Collab mode: y-prosemirror owns the doc after initial mount
328
- if (isCollabMode && viewRef.current) {
329
- return;
330
- }
331
-
332
310
  if (viewRef.current && documentBuildKeyRef.current === documentBuildKey) {
333
311
  return;
334
312
  }
@@ -358,23 +336,19 @@ export const TwProseMirrorSurface = forwardRef<
358
336
  incrementInvalidationCounter("pm.laneA.rebuilds");
359
337
 
360
338
  if (!viewRef.current) {
339
+ // First time surface is available — create the EditorView
361
340
  const view = new EditorView(mountRef.current, {
362
341
  state,
363
342
  nodeViews: tableNodeViews,
364
343
  editable: () => canEdit,
365
344
  decorations: () => decorations,
345
+ dispatchTransaction(tr) {
346
+ const newState = view.state.apply(tr);
347
+ view.updateState(newState);
348
+ },
366
349
  });
367
350
  viewRef.current = view;
368
351
  recordPerfSample("pm.mount");
369
-
370
- if (isCollabMode && props.ydoc) {
371
- const yXmlFragment = props.ydoc.getXmlFragment("prosemirror");
372
- if (yXmlFragment.length === 0) {
373
- props.ydoc.transact(() => {
374
- prosemirrorToYXmlFragment(view.state.doc, yXmlFragment);
375
- });
376
- }
377
- }
378
352
  } else {
379
353
  suppressSelectionEchoRef.current = true;
380
354
  viewRef.current.updateState(state);
@@ -398,12 +372,10 @@ export const TwProseMirrorSurface = forwardRef<
398
372
  }, [
399
373
  applyDecorationProps,
400
374
  documentBuildKey,
401
- isCollabMode,
402
375
  surface,
403
376
  snapshot.selection,
404
377
  plugins,
405
378
  props.mediaPreviews,
406
- props.ydoc,
407
379
  ]);
408
380
 
409
381
  // Update decorations and editability without rebuilding the PM document.
@@ -436,7 +408,6 @@ export const TwProseMirrorSurface = forwardRef<
436
408
  ]);
437
409
 
438
410
  useEffect(() => {
439
- if (isCollabMode) return;
440
411
  const view = viewRef.current;
441
412
  const positionMap = positionMapRef.current;
442
413
  if (!view || !surface || !positionMap) {
@@ -458,7 +429,7 @@ export const TwProseMirrorSurface = forwardRef<
458
429
  queueMicrotask(() => {
459
430
  suppressSelectionEchoRef.current = false;
460
431
  });
461
- }, [isCollabMode, snapshot.selection, surface]);
432
+ }, [snapshot.selection, surface]);
462
433
 
463
434
  useEffect(() => {
464
435
  if (!pendingSelectionProbeRef.current) {
@@ -1,254 +0,0 @@
1
- import * as Y from "yjs";
2
-
3
- import type { DocumentRuntime, DocumentRuntimeEvent, Unsubscribe } from "./document-runtime.ts";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Serialised shapes stored inside the Y.Maps
7
- // ---------------------------------------------------------------------------
8
-
9
- interface YCommentThread {
10
- commentId: string;
11
- status: "open" | "resolved" | "detached";
12
- anchor: { kind: string; [key: string]: unknown };
13
- createdAt: string;
14
- createdBy: string;
15
- authorId: string;
16
- body: string;
17
- entries: Array<{
18
- entryId: string;
19
- authorId: string;
20
- body: string;
21
- createdAt: string;
22
- }>;
23
- resolvedAt?: string;
24
- resolvedBy?: string;
25
- warningIds: string[];
26
- sourceClientId: number;
27
- }
28
-
29
- interface YRevisionAction {
30
- changeId: string;
31
- action: "accept" | "reject";
32
- sourceClientId: number;
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // Public API
37
- // ---------------------------------------------------------------------------
38
-
39
- export interface CollabReviewSyncHandle {
40
- destroy(): void;
41
- }
42
-
43
- export function createCollabReviewSync(
44
- ydoc: Y.Doc,
45
- runtime: DocumentRuntime,
46
- ): CollabReviewSyncHandle {
47
- const yComments = ydoc.getMap<YCommentThread>("comments");
48
- const yRevisionActions = ydoc.getMap<YRevisionAction>("revisionActions");
49
- const clientId = ydoc.clientID;
50
-
51
- let suppressLocalEvents = false;
52
-
53
- // --- Local → Yjs ---------------------------------------------------------
54
-
55
- const unsubEvents: Unsubscribe = runtime.subscribeToEvents((event) => {
56
- if (suppressLocalEvents) return;
57
-
58
- switch (event.type) {
59
- case "comment_added":
60
- pushCommentToYjs(event.commentId);
61
- break;
62
- case "comment_resolved":
63
- syncCommentFieldToYjs(event.commentId);
64
- break;
65
- case "change_accepted":
66
- yRevisionActions.set(revisionActionKey(event.changeId, "accept"), {
67
- changeId: event.changeId,
68
- action: "accept",
69
- sourceClientId: clientId,
70
- });
71
- break;
72
- case "change_rejected":
73
- yRevisionActions.set(revisionActionKey(event.changeId, "reject"), {
74
- changeId: event.changeId,
75
- action: "reject",
76
- sourceClientId: clientId,
77
- });
78
- break;
79
- }
80
- });
81
-
82
- function pushCommentToYjs(commentId: string): void {
83
- const thread = runtime
84
- .getRenderSnapshot()
85
- .comments.threads.find((t) => t.commentId === commentId);
86
- if (!thread) return;
87
-
88
- yComments.set(commentId, {
89
- commentId: thread.commentId,
90
- status: thread.status,
91
- anchor: thread.anchor,
92
- createdAt: thread.createdAt,
93
- createdBy: thread.createdBy,
94
- authorId: thread.createdBy,
95
- body: thread.entries[0]?.body ?? "",
96
- entries: thread.entries.map((e) => ({
97
- entryId: e.entryId,
98
- authorId: e.authorId,
99
- body: e.body,
100
- createdAt: e.createdAt,
101
- })),
102
- resolvedAt: thread.resolvedAt,
103
- resolvedBy: thread.resolvedBy,
104
- warningIds: [],
105
- sourceClientId: clientId,
106
- });
107
- }
108
-
109
- function syncCommentFieldToYjs(commentId: string): void {
110
- const existing = yComments.get(commentId);
111
- const thread = runtime
112
- .getRenderSnapshot()
113
- .comments.threads.find((t) => t.commentId === commentId);
114
- if (!existing || !thread) return;
115
-
116
- yComments.set(commentId, {
117
- ...existing,
118
- status: thread.status,
119
- entries: thread.entries.map((e) => ({
120
- entryId: e.entryId,
121
- authorId: e.authorId,
122
- body: e.body,
123
- createdAt: e.createdAt,
124
- })),
125
- resolvedAt: thread.resolvedAt,
126
- resolvedBy: thread.resolvedBy,
127
- sourceClientId: clientId,
128
- });
129
- }
130
-
131
- // --- Yjs → Local ---------------------------------------------------------
132
-
133
- function onCommentMapChange(event: Y.YMapEvent<YCommentThread>): void {
134
- suppressLocalEvents = true;
135
- try {
136
- for (const [commentId, change] of event.changes.keys) {
137
- const entry = yComments.get(commentId);
138
- if (!entry || entry.sourceClientId === clientId) continue;
139
-
140
- const existing = runtime
141
- .getRenderSnapshot()
142
- .comments.threads.find((t) => t.commentId === commentId);
143
-
144
- if (change.action === "add" && !existing) {
145
- runtime.dispatch({
146
- type: "comment.add",
147
- comment: {
148
- commentId: entry.commentId,
149
- status: entry.status,
150
- anchor: entry.anchor as never,
151
- createdAt: entry.createdAt,
152
- createdBy: entry.createdBy,
153
- authorId: entry.authorId,
154
- body: entry.body,
155
- entries: entry.entries,
156
- warningIds: entry.warningIds,
157
- isResolved: entry.status === "resolved",
158
- metadata: { source: "runtime" },
159
- },
160
- });
161
- } else if (change.action === "update" && existing) {
162
- if (entry.status === "resolved" && existing.status !== "resolved") {
163
- runtime.dispatch({
164
- type: "comment.resolve",
165
- commentId,
166
- resolvedBy: entry.resolvedBy,
167
- });
168
- } else if (entry.status === "open" && existing.status === "resolved") {
169
- runtime.dispatch({ type: "comment.reopen", commentId });
170
- }
171
-
172
- const localEntryCount = existing.entries.length;
173
- if (entry.entries.length > localEntryCount) {
174
- for (const newEntry of entry.entries.slice(localEntryCount)) {
175
- runtime.dispatch({
176
- type: "comment.add-reply",
177
- commentId,
178
- body: newEntry.body,
179
- authorId: newEntry.authorId,
180
- });
181
- }
182
- }
183
- }
184
- }
185
- } finally {
186
- suppressLocalEvents = false;
187
- }
188
- }
189
-
190
- function onRevisionActionMapChange(event: Y.YMapEvent<YRevisionAction>): void {
191
- suppressLocalEvents = true;
192
- try {
193
- for (const [, change] of event.changes.keys) {
194
- if (change.action !== "add") continue;
195
- const entries = [...yRevisionActions.entries()];
196
- const latest = entries[entries.length - 1];
197
- if (!latest) continue;
198
- const entry = latest[1];
199
- if (entry.sourceClientId === clientId) continue;
200
-
201
- runtime.dispatch({
202
- type: entry.action === "accept" ? "change.accept" : "change.reject",
203
- changeId: entry.changeId,
204
- });
205
- }
206
- } finally {
207
- suppressLocalEvents = false;
208
- }
209
- }
210
-
211
- yComments.observe(onCommentMapChange);
212
- yRevisionActions.observe(onRevisionActionMapChange);
213
-
214
- // --- Initial sync: push existing comments to Yjs if first client ----------
215
-
216
- const snapshot = runtime.getRenderSnapshot();
217
- if (yComments.size === 0 && snapshot.comments.threads.length > 0) {
218
- ydoc.transact(() => {
219
- for (const thread of snapshot.comments.threads) {
220
- yComments.set(thread.commentId, {
221
- commentId: thread.commentId,
222
- status: thread.status,
223
- anchor: thread.anchor,
224
- createdAt: thread.createdAt,
225
- createdBy: thread.createdBy,
226
- authorId: thread.createdBy,
227
- body: thread.entries[0]?.body ?? "",
228
- entries: thread.entries.map((e) => ({
229
- entryId: e.entryId,
230
- authorId: e.authorId,
231
- body: e.body,
232
- createdAt: e.createdAt,
233
- })),
234
- resolvedAt: thread.resolvedAt,
235
- resolvedBy: thread.resolvedBy,
236
- warningIds: [],
237
- sourceClientId: clientId,
238
- });
239
- }
240
- });
241
- }
242
-
243
- return {
244
- destroy() {
245
- unsubEvents();
246
- yComments.unobserve(onCommentMapChange);
247
- yRevisionActions.unobserve(onRevisionActionMapChange);
248
- },
249
- };
250
- }
251
-
252
- function revisionActionKey(changeId: string, action: string): string {
253
- return `${changeId}:${action}`;
254
- }
@@ -1,40 +0,0 @@
1
- import { type Plugin } from "prosemirror-state";
2
- import { keymap } from "prosemirror-keymap";
3
- import { columnResizing, tableEditing } from "prosemirror-tables";
4
- import { yCursorPlugin, ySyncPlugin, yUndoPlugin, undo, redo } from "y-prosemirror";
5
- import type { Awareness } from "y-protocols/awareness";
6
- import type { Doc as YDoc } from "yjs";
7
-
8
- import {
9
- createSelectionSyncPlugin,
10
- type SelectionSyncCallbacks,
11
- } from "./pm-command-bridge";
12
-
13
- export interface CollabPluginOptions {
14
- ydoc: YDoc;
15
- awareness?: Awareness;
16
- selectionCallbacks: SelectionSyncCallbacks;
17
- }
18
-
19
- export function createCollabPlugins(options: CollabPluginOptions): Plugin[] {
20
- const yXmlFragment = options.ydoc.getXmlFragment("prosemirror");
21
-
22
- const plugins: Plugin[] = [
23
- ySyncPlugin(yXmlFragment),
24
- yUndoPlugin(),
25
- keymap({
26
- "Mod-z": undo,
27
- "Mod-y": redo,
28
- "Shift-Mod-z": redo,
29
- }),
30
- createSelectionSyncPlugin(options.selectionCallbacks),
31
- tableEditing(),
32
- columnResizing(),
33
- ];
34
-
35
- if (options.awareness) {
36
- plugins.splice(1, 0, yCursorPlugin(options.awareness));
37
- }
38
-
39
- return plugins;
40
- }