@kerebron/extension-yjs 0.5.2 → 0.5.4

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.
Files changed (67) hide show
  1. package/README.md +3 -89
  2. package/esm/ExtensionYjs.d.ts +10 -1
  3. package/esm/ExtensionYjs.d.ts.map +1 -1
  4. package/esm/ExtensionYjs.js +47 -6
  5. package/esm/ExtensionYjs.js.map +1 -1
  6. package/esm/MarkYChange.d.ts +7 -0
  7. package/esm/MarkYChange.d.ts.map +1 -0
  8. package/esm/MarkYChange.js +21 -0
  9. package/esm/MarkYChange.js.map +1 -0
  10. package/esm/ProsemirrorBinding.d.ts +60 -0
  11. package/esm/ProsemirrorBinding.d.ts.map +1 -0
  12. package/esm/ProsemirrorBinding.js +405 -0
  13. package/esm/ProsemirrorBinding.js.map +1 -0
  14. package/esm/createNodeFromYElement.d.ts +10 -0
  15. package/esm/createNodeFromYElement.d.ts.map +1 -0
  16. package/esm/createNodeFromYElement.js +123 -0
  17. package/esm/createNodeFromYElement.js.map +1 -0
  18. package/esm/debug.d.ts +13 -0
  19. package/esm/debug.d.ts.map +1 -0
  20. package/esm/debug.js +147 -0
  21. package/esm/debug.js.map +1 -0
  22. package/esm/keys.d.ts +5 -8
  23. package/esm/keys.d.ts.map +1 -1
  24. package/esm/keys.js +1 -6
  25. package/esm/keys.js.map +1 -1
  26. package/esm/lib.d.ts +1 -2
  27. package/esm/lib.d.ts.map +1 -1
  28. package/esm/lib.js +12 -2
  29. package/esm/lib.js.map +1 -1
  30. package/esm/updateYFragment.d.ts +17 -0
  31. package/esm/updateYFragment.d.ts.map +1 -0
  32. package/esm/updateYFragment.js +333 -0
  33. package/esm/updateYFragment.js.map +1 -0
  34. package/esm/utils.d.ts +2 -0
  35. package/esm/utils.d.ts.map +1 -1
  36. package/esm/utils.js +4 -0
  37. package/esm/utils.js.map +1 -1
  38. package/esm/yPositionPlugin.d.ts +12 -4
  39. package/esm/yPositionPlugin.d.ts.map +1 -1
  40. package/esm/yPositionPlugin.js +114 -61
  41. package/esm/yPositionPlugin.js.map +1 -1
  42. package/esm/ySyncPlugin.d.ts +16 -78
  43. package/esm/ySyncPlugin.d.ts.map +1 -1
  44. package/esm/ySyncPlugin.js +81 -848
  45. package/esm/ySyncPlugin.js.map +1 -1
  46. package/esm/yUndoPlugin.d.ts +1 -1
  47. package/esm/yUndoPlugin.d.ts.map +1 -1
  48. package/esm/yUndoPlugin.js +1 -1
  49. package/esm/yUndoPlugin.js.map +1 -1
  50. package/package.json +9 -3
  51. package/src/ExtensionYjs.ts +65 -9
  52. package/src/MarkYChange.ts +23 -0
  53. package/src/ProsemirrorBinding.ts +607 -0
  54. package/src/createNodeFromYElement.ts +175 -0
  55. package/src/debug.ts +218 -0
  56. package/src/keys.ts +9 -9
  57. package/src/lib.ts +11 -3
  58. package/src/updateYFragment.ts +439 -0
  59. package/src/utils.ts +6 -0
  60. package/src/yPositionPlugin.ts +167 -92
  61. package/src/ySyncPlugin.ts +135 -1193
  62. package/src/yUndoPlugin.ts +1 -1
  63. package/esm/convertUtils.d.ts +0 -59
  64. package/esm/convertUtils.d.ts.map +0 -1
  65. package/esm/convertUtils.js +0 -89
  66. package/esm/convertUtils.js.map +0 -1
  67. package/src/convertUtils.ts +0 -143
@@ -0,0 +1,607 @@
1
+ // deno-lint-ignore-file no-window
2
+ import * as dom from 'lib0/dom';
3
+ import * as environment from 'lib0/environment';
4
+ import * as error from 'lib0/error';
5
+ import * as eventloop from 'lib0/eventloop';
6
+ import * as math from 'lib0/math';
7
+ import { createMutex, mutex } from 'lib0/mutex';
8
+ import * as random from 'lib0/random';
9
+ import * as set from 'lib0/set';
10
+ import * as PModel from 'prosemirror-model';
11
+ import { Node } from 'prosemirror-model';
12
+ import {
13
+ AllSelection,
14
+ EditorState,
15
+ NodeSelection,
16
+ Selection,
17
+ TextSelection,
18
+ Transaction,
19
+ } from 'prosemirror-state';
20
+ import * as Y from 'yjs';
21
+
22
+ import { remoteSelectionPluginKey } from '@kerebron/extension-basic-editor/ExtensionRemoteSelection';
23
+
24
+ import { ySyncPluginKey } from './keys.js';
25
+ import {
26
+ absolutePositionToRelativePosition,
27
+ ProsemirrorMapping,
28
+ relativePositionToAbsolutePosition,
29
+ } from './lib.js';
30
+ import { updateYFragment } from './updateYFragment.js';
31
+ import {
32
+ createNodeFromYElement,
33
+ createNodeIfNotExists,
34
+ } from './createNodeFromYElement.js';
35
+ import { type ColorDef, type YSyncPluginState } from './ySyncPlugin.js';
36
+ import { isVisible } from './utils.js';
37
+
38
+ export const defaultColors: Array<ColorDef> = [{
39
+ light: '#ecd44433',
40
+ dark: '#ecd444',
41
+ }];
42
+
43
+ const getUserColor = (
44
+ colorMapping: Map<string, ColorDef>,
45
+ colors: Array<ColorDef>,
46
+ user: string,
47
+ ): ColorDef => {
48
+ // @todo do not hit the same color twice if possible
49
+ if (!colorMapping.has(user)) {
50
+ if (colorMapping.size < colors.length) {
51
+ const usedColors = set.create();
52
+ colorMapping.forEach((color) => usedColors.add(color));
53
+ colors = colors.filter((color) => !usedColors.has(color));
54
+ }
55
+ colorMapping.set(user, random.oneOf(colors));
56
+ }
57
+ return colorMapping.get(user) || defaultColors[0];
58
+ };
59
+
60
+ export interface BindingMetadata {
61
+ mapping: ProsemirrorMapping;
62
+ isOMark: Map<string, boolean>;
63
+ }
64
+
65
+ interface TransactionSelection {
66
+ type: string;
67
+ anchor: Y.RelativePosition;
68
+ head: Y.RelativePosition;
69
+ }
70
+
71
+ function getSelectionType(selection: Selection) {
72
+ if (selection instanceof TextSelection) {
73
+ return 'text';
74
+ }
75
+ if (selection instanceof AllSelection) {
76
+ return 'all';
77
+ }
78
+ if (selection instanceof NodeSelection) {
79
+ return 'node';
80
+ }
81
+ return 'other_selection';
82
+ }
83
+
84
+ const restoreRelativeSelection = (
85
+ tr: Transaction,
86
+ relSel: ReturnType<typeof getRelativeSelection>,
87
+ binding: ProsemirrorBinding,
88
+ ) => {
89
+ if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
90
+ if (relSel.type === 'all') {
91
+ tr.setSelection(new AllSelection(tr.doc));
92
+ } else if (relSel.type === 'node') {
93
+ const anchor = relativePositionToAbsolutePosition(
94
+ binding.ydoc,
95
+ binding.type,
96
+ relSel.anchor,
97
+ binding.mapping,
98
+ );
99
+ if (anchor !== null) {
100
+ tr.setSelection(NodeSelection.create(tr.doc, anchor));
101
+ }
102
+ } else {
103
+ const anchor = relativePositionToAbsolutePosition(
104
+ binding.ydoc,
105
+ binding.type,
106
+ relSel.anchor,
107
+ binding.mapping,
108
+ );
109
+ const head = relativePositionToAbsolutePosition(
110
+ binding.ydoc,
111
+ binding.type,
112
+ relSel.head,
113
+ binding.mapping,
114
+ );
115
+ if (anchor !== null && head !== null) {
116
+ const sel = TextSelection.between(
117
+ tr.doc.resolve(anchor),
118
+ tr.doc.resolve(head),
119
+ );
120
+ tr.setSelection(sel);
121
+ }
122
+ }
123
+ }
124
+ };
125
+
126
+ export const getRelativeSelection = (
127
+ pmbinding: ProsemirrorBinding,
128
+ state: EditorState,
129
+ ): TransactionSelection => ({
130
+ type: getSelectionType(state.selection),
131
+ anchor: absolutePositionToRelativePosition(
132
+ state.selection.anchor,
133
+ pmbinding.type,
134
+ pmbinding.mapping,
135
+ ),
136
+ head: absolutePositionToRelativePosition(
137
+ state.selection.head,
138
+ pmbinding.type,
139
+ pmbinding.mapping,
140
+ ),
141
+ });
142
+
143
+ interface IEditorView {
144
+ state: EditorState;
145
+ dispatch(tr: Transaction): void;
146
+ fake?: boolean;
147
+ }
148
+
149
+ /**
150
+ * Binding for prosemirror.
151
+ *
152
+ * @protected
153
+ */
154
+ export class ProsemirrorBinding implements BindingMetadata {
155
+ public ydoc: Y.Doc;
156
+ public isOMark: Map<string, boolean>;
157
+ public type: Y.XmlFragment;
158
+ public readonly mux: mutex;
159
+ public prosemirrorView?: IEditorView;
160
+
161
+ private _beforeTransactionSelection: TransactionSelection | null = null;
162
+ private readonly beforeAllTransactions: () => void;
163
+ private readonly afterAllTransactions: () => void;
164
+
165
+ private _observeFunction: (event: any, transaction: any) => void;
166
+ private _domSelectionInView: boolean = false;
167
+ get beforeTransactionSelection(): TransactionSelection | null {
168
+ return this._beforeTransactionSelection;
169
+ }
170
+ set beforeTransactionSelection(value: TransactionSelection) {
171
+ this._beforeTransactionSelection = value;
172
+ }
173
+
174
+ constructor(
175
+ yXmlFragment: Y.XmlFragment,
176
+ public readonly mapping: ProsemirrorMapping = new Map(),
177
+ ) {
178
+ this.type = yXmlFragment;
179
+ this.prosemirrorView = undefined;
180
+ // {
181
+ // state: new EditorState(),
182
+ // dispatch: () => false,
183
+ // fake: true
184
+ // };
185
+ this.mux = createMutex();
186
+ /**
187
+ * Is overlapping mark - i.e. mark does not exclude itself.
188
+ */
189
+ this.isOMark = new Map();
190
+ this._observeFunction = (event, transaction) => {
191
+ this.yXmlChanged(event, transaction);
192
+ };
193
+ this.ydoc = yXmlFragment.doc!;
194
+ /**
195
+ * current selection as relative positions in the Yjs model
196
+ */
197
+ this.beforeAllTransactions = () => {
198
+ if (
199
+ !this._beforeTransactionSelection &&
200
+ this.prosemirrorView
201
+ ) {
202
+ this._beforeTransactionSelection = getRelativeSelection(
203
+ this,
204
+ this.prosemirrorView.state,
205
+ );
206
+ }
207
+ };
208
+ this.afterAllTransactions = () => {
209
+ this._beforeTransactionSelection = null;
210
+ };
211
+ this._domSelectionInView = false;
212
+ }
213
+
214
+ changeRoom(yXmlFragment: Y.XmlFragment) {
215
+ const view = this.prosemirrorView;
216
+ this.destroy();
217
+
218
+ this.type = yXmlFragment;
219
+ this.ydoc = yXmlFragment.doc!;
220
+ this.initView(view);
221
+ // this._forceRerender();
222
+ }
223
+
224
+ debug(msg = 'ydoc.prosemirror') {
225
+ console.log(msg, this.type.toString());
226
+ }
227
+
228
+ _isLocalCursorInView(): boolean {
229
+ if (!this.prosemirrorView?.hasFocus()) return false;
230
+ if (environment.isBrowser && this._domSelectionInView === false) {
231
+ // Calculate the domSelectionInView and clear by next tick after all events are finished
232
+ eventloop.timeout(0, () => {
233
+ this._domSelectionInView = false;
234
+ });
235
+ this._domSelectionInView = this._isDomSelectionInView();
236
+ }
237
+ return this._domSelectionInView;
238
+ }
239
+
240
+ _isDomSelectionInView(): boolean {
241
+ const selection = this.prosemirrorView?.root?.getSelection(); // https://stackoverflow.com/questions/62054839/shadowroot-getselection
242
+
243
+ if (!selection || selection.anchorNode == null) return false;
244
+
245
+ const range = dom.doc.createRange(); // https://github.com/yjs/y-prosemirror/pull/193
246
+ range.setStart(selection.anchorNode, selection.anchorOffset);
247
+ range.setEnd(selection.focusNode, selection.focusOffset);
248
+
249
+ // This is a workaround for an edgecase where getBoundingClientRect will
250
+ // return zero values if the selection is collapsed at the start of a newline
251
+ // see reference here: https://stackoverflow.com/a/59780954
252
+ const rects = range.getClientRects();
253
+ if (rects.length === 0) {
254
+ // probably buggy newline behavior, explicitly select the node contents
255
+ if (range.startContainer && range.collapsed) {
256
+ range.selectNodeContents(range.startContainer);
257
+ }
258
+ }
259
+
260
+ const bounding = range.getBoundingClientRect();
261
+ const documentElement = dom.doc.documentElement;
262
+
263
+ return bounding.bottom >= 0 && bounding.right >= 0 &&
264
+ bounding.left <=
265
+ (globalThis.innerWidth || documentElement.clientWidth || 0) &&
266
+ bounding.top <= (globalThis.innerHeight || documentElement.clientHeight || 0);
267
+ }
268
+
269
+ renderSnapshot(
270
+ snapshot: Y.Snapshot | undefined,
271
+ prevSnapshot: Y.Snapshot | undefined,
272
+ ) {
273
+ if (!prevSnapshot) {
274
+ prevSnapshot = Y.createSnapshot(Y.createDeleteSet(), new Map());
275
+ }
276
+
277
+ if (!this.prosemirrorView) {
278
+ return;
279
+ }
280
+ const state = this.prosemirrorView.state;
281
+
282
+ const tr = state.tr.setMeta('addToHistory', false)
283
+ .setMeta(ySyncPluginKey, { snapshot, prevSnapshot });
284
+ this.prosemirrorView.dispatch(tr);
285
+ }
286
+
287
+ unrenderSnapshot() {
288
+ this.mapping.clear();
289
+ this.mux(() => {
290
+ if (!this.prosemirrorView) {
291
+ return;
292
+ }
293
+ const state = this.prosemirrorView.state;
294
+ const children = this.type.toArray();
295
+ const fragmentContent = children.map((t) =>
296
+ createNodeFromYElement(
297
+ t as Y.XmlElement,
298
+ state.schema,
299
+ this,
300
+ )
301
+ ).filter((n) => n !== null);
302
+ const _tr = state.tr.setMeta('addToHistory', false);
303
+ const tr = _tr.replace(
304
+ 0,
305
+ state.doc.content.size,
306
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
307
+ );
308
+ tr.setMeta(ySyncPluginKey, {
309
+ snapshot: undefined,
310
+ prevSnapshot: undefined,
311
+ });
312
+ if (this.prosemirrorView) {
313
+ this.prosemirrorView.dispatch(tr);
314
+ }
315
+ });
316
+ }
317
+
318
+ _forceRerender() {
319
+ this.mapping.clear();
320
+ this.mux(() => {
321
+ if (!this.prosemirrorView) {
322
+ return;
323
+ }
324
+ const state = this.prosemirrorView.state;
325
+ // If this is a forced rerender, this might neither happen as a pm change nor within a Yjs
326
+ // transaction. Then the "before selection" doesn't exist. In this case, we need to create a
327
+ // relative position before replacing content. Fixes #126
328
+ const sel = this._beforeTransactionSelection !== null
329
+ ? null
330
+ : state.selection;
331
+ const fragmentContent = this.type.toArray().map((t) =>
332
+ createNodeFromYElement(
333
+ t as Y.XmlElement,
334
+ state.schema,
335
+ this,
336
+ )
337
+ ).filter((n) => n !== null);
338
+ const _tr = state.tr.setMeta('addToHistory', false);
339
+ const tr = _tr.replace(
340
+ 0,
341
+ state.doc.content.size,
342
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
343
+ );
344
+ if (sel) {
345
+ /**
346
+ * If the Prosemirror document we just created from this.type is
347
+ * smaller than the previous document, the selection might be
348
+ * out of bound, which would make Prosemirror throw an error.
349
+ */
350
+ const clampedAnchor = math.min(
351
+ math.max(sel.anchor, 0),
352
+ tr.doc.content.size,
353
+ );
354
+ const clampedHead = math.min(
355
+ math.max(sel.head, 0),
356
+ tr.doc.content.size,
357
+ );
358
+
359
+ tr.setSelection(
360
+ TextSelection.create(tr.doc, clampedAnchor, clampedHead),
361
+ );
362
+ }
363
+ if (this.prosemirrorView) {
364
+ this.prosemirrorView.dispatch(
365
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this })
366
+ .setMeta(remoteSelectionPluginKey, { isChangeOrigin: true }),
367
+ );
368
+ }
369
+ });
370
+ }
371
+
372
+ _renderSnapshot(
373
+ snapshot: Y.Snapshot | Uint8Array | undefined,
374
+ prevSnapshot: Y.Snapshot | Uint8Array,
375
+ pluginState: YSyncPluginState,
376
+ ) {
377
+ if (!snapshot) {
378
+ snapshot = Y.snapshot(this.ydoc);
379
+ }
380
+
381
+ /**
382
+ * The document that contains the full history of this document.
383
+ */
384
+ let historyDoc = this.ydoc;
385
+ let historyType: Y.AbstractType<any> = this.type;
386
+
387
+ if (snapshot instanceof Uint8Array || prevSnapshot instanceof Uint8Array) {
388
+ if (
389
+ !(snapshot instanceof Uint8Array) ||
390
+ !(prevSnapshot instanceof Uint8Array)
391
+ ) {
392
+ // expected both snapshots to be v2 updates
393
+ error.unexpectedCase();
394
+ }
395
+ historyDoc = new Y.Doc({ gc: false });
396
+ Y.applyUpdateV2(historyDoc, prevSnapshot);
397
+ prevSnapshot = Y.snapshot(historyDoc);
398
+ Y.applyUpdateV2(historyDoc, snapshot);
399
+ snapshot = Y.snapshot(historyDoc);
400
+ if (historyType._item === null) {
401
+ /**
402
+ * If is a root type, we need to find the root key in the initial document
403
+ * and use it to get the history type.
404
+ */
405
+ const share: Map<string, Y.AbstractType<Y.YEvent<any>>> =
406
+ this.ydoc.share;
407
+ const rootKey = Array.from(share.keys()).find(
408
+ (key) => share.get(key) === this.type as Y.AbstractType<any>,
409
+ );
410
+ historyType = historyDoc.getXmlFragment(rootKey);
411
+ } else {
412
+ /**
413
+ * If it is a sub type, we use the item id to find the history type.
414
+ */
415
+ const historyStructs =
416
+ historyDoc.store.clients.get(historyType._item.id.client) ?? [];
417
+ const itemIndex = Y.findIndexSS(
418
+ historyStructs,
419
+ historyType._item.id.clock,
420
+ );
421
+ if (historyStructs[itemIndex] instanceof Y.GC) {
422
+ throw new Error('Incorrect type Y.GC');
423
+ }
424
+ const item: Y.Item = historyStructs[itemIndex];
425
+ const content: Y.ContentType = item.content;
426
+ historyType = content.type;
427
+ }
428
+ }
429
+ // clear mapping because we are going to rerender
430
+ this.mapping.clear();
431
+
432
+ this.mux(() => {
433
+ historyDoc.transact((transaction) => {
434
+ if (!this.prosemirrorView) {
435
+ return;
436
+ }
437
+ const state = this.prosemirrorView.state;
438
+
439
+ // before rendering, we are going to sanitize ops and split deleted ops
440
+ // if they were deleted by seperate users.
441
+ const pud: Y.PermanentUserData | undefined =
442
+ pluginState.permanentUserData;
443
+ if (pud) {
444
+ pud.dss.forEach((ds) => {
445
+ Y.iterateDeletedStructs(transaction, ds, (_item) => {});
446
+ });
447
+ }
448
+ const computeYChange = (type: 'removed' | 'added', id: Y.ID) => {
449
+ const user = pud
450
+ ? (type === 'added'
451
+ ? pud.getUserByClientId(id.client)
452
+ : pud.getUserByDeletedId(id))
453
+ : undefined;
454
+ return {
455
+ user,
456
+ type,
457
+ color: getUserColor(
458
+ pluginState.colorMapping,
459
+ pluginState.colors,
460
+ user,
461
+ ),
462
+ };
463
+ };
464
+
465
+ // Create document fragment and render
466
+ const fragmentContent = Y.typeListToArraySnapshot(
467
+ historyType,
468
+ new Y.Snapshot(prevSnapshot.ds, snapshot.sv),
469
+ ).map((t) => {
470
+ if (
471
+ !t._item.deleted || isVisible(t._item, snapshot) ||
472
+ isVisible(t._item, prevSnapshot)
473
+ ) {
474
+ return createNodeFromYElement(
475
+ t,
476
+ state.schema,
477
+ { mapping: new Map(), isOMark: new Map() },
478
+ snapshot,
479
+ prevSnapshot,
480
+ computeYChange,
481
+ );
482
+ } else {
483
+ // No need to render elements that are not visible by either snapshot.
484
+ // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
485
+ return null;
486
+ }
487
+ }).filter((n) => n !== null);
488
+
489
+ const tr = state.tr.setMeta('addToHistory', false);
490
+ tr.replace(
491
+ 0,
492
+ state.doc.content.size,
493
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
494
+ );
495
+
496
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true }),
497
+ this.prosemirrorView.dispatch(tr);
498
+ }, ySyncPluginKey);
499
+ });
500
+ }
501
+
502
+ yXmlChanged(events: Array<Y.YEvent<any>>, transaction: Y.Transaction) {
503
+ if (!this.prosemirrorView) {
504
+ return;
505
+ }
506
+
507
+ const syncState = ySyncPluginKey.getState(this.prosemirrorView.state)!;
508
+ if (
509
+ events.length === 0 || syncState.snapshot ||
510
+ syncState.prevSnapshot
511
+ ) {
512
+ // drop out if snapshot is active
513
+ this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot);
514
+ return;
515
+ }
516
+
517
+ this.mux(() => {
518
+ if (!this.prosemirrorView) {
519
+ return;
520
+ }
521
+ const state = this.prosemirrorView.state;
522
+
523
+ const delType = (_: any, type: Y.AbstractType<any>) =>
524
+ this.mapping.delete(type);
525
+ Y.iterateDeletedStructs(
526
+ transaction,
527
+ transaction.deleteSet,
528
+ (struct) => {
529
+ if (struct.constructor === Y.Item) {
530
+ const type = (struct as Y.Item).content.type;
531
+ type && this.mapping.delete(type);
532
+ }
533
+ },
534
+ );
535
+ transaction.changed.forEach(delType);
536
+ transaction.changedParentTypes.forEach(delType);
537
+
538
+ const fragmentContent = this.type.toArray().map((t) =>
539
+ createNodeIfNotExists(
540
+ t as Y.XmlElement,
541
+ state.schema,
542
+ this,
543
+ )
544
+ ).filter((n) => n !== null);
545
+
546
+ const tr = state.tr.replace(
547
+ 0,
548
+ this.prosemirrorView.state.doc.content.size,
549
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0),
550
+ );
551
+ if (this._beforeTransactionSelection) {
552
+ restoreRelativeSelection(tr, this._beforeTransactionSelection, this);
553
+ }
554
+ tr.setMeta(ySyncPluginKey, {
555
+ isChangeOrigin: true,
556
+ isUndoRedoOperation: transaction.origin instanceof Y.UndoManager,
557
+ });
558
+ if (
559
+ this.beforeTransactionSelection !== null && this._isLocalCursorInView()
560
+ ) {
561
+ tr.scrollIntoView();
562
+ }
563
+ this.prosemirrorView.dispatch(tr);
564
+ });
565
+ }
566
+
567
+ prosemirrorChanged(doc: Node) {
568
+ this.ydoc.transact(() => {
569
+ if (!this.prosemirrorView) {
570
+ throw new Error('Prosemirror changed without view?!');
571
+ }
572
+ updateYFragment(this.ydoc, this.type, doc, this);
573
+ this._beforeTransactionSelection = getRelativeSelection(
574
+ this,
575
+ this.prosemirrorView.state,
576
+ );
577
+ }, ySyncPluginKey);
578
+ }
579
+
580
+ /**
581
+ * View is ready to listen to changes. Register observers.
582
+ */
583
+ initView(prosemirrorView?: IEditorView) {
584
+ if (this.prosemirrorView) {
585
+ this.destroy();
586
+ }
587
+ this.prosemirrorView = prosemirrorView;
588
+ this.ydoc.on('beforeAllTransactions', this.beforeAllTransactions);
589
+ this.ydoc.on('afterAllTransactions', this.afterAllTransactions);
590
+ this.type.observeDeep(this._observeFunction);
591
+ }
592
+
593
+ destroy() {
594
+ this.type.unobserveDeep(this._observeFunction);
595
+ this.ydoc.off('beforeAllTransactions', this.beforeAllTransactions);
596
+ this.ydoc.off('afterAllTransactions', this.afterAllTransactions);
597
+ if (!this.prosemirrorView) {
598
+ return;
599
+ }
600
+ this.prosemirrorView = undefined;
601
+ // {
602
+ // state: new EditorState(),
603
+ // dispatch: () => false,
604
+ // fake: true
605
+ // };
606
+ }
607
+ }