@tiptap/y-tiptap 1.0.0

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.
@@ -0,0 +1,2099 @@
1
+ import * as Y from 'yjs';
2
+ import { Item, ContentType, Text, XmlElement, UndoManager } from 'yjs';
3
+ import { DecorationSet, Decoration } from '@tiptap/pm/view';
4
+ import { PluginKey, Plugin, TextSelection } from '@tiptap/pm/state';
5
+ import 'y-protocols/awareness';
6
+ import { createMutex } from 'lib0/mutex';
7
+ import * as PModel from '@tiptap/pm/model';
8
+ import { Fragment, Node } from '@tiptap/pm/model';
9
+ import * as math from 'lib0/math';
10
+ import * as set from 'lib0/set';
11
+ import { simpleDiff } from 'lib0/diff';
12
+ import * as error from 'lib0/error';
13
+ import * as random from 'lib0/random';
14
+ import * as environment from 'lib0/environment';
15
+ import * as dom from 'lib0/dom';
16
+ import * as eventloop from 'lib0/eventloop';
17
+ import * as f from 'lib0/function';
18
+ import * as map from 'lib0/map';
19
+
20
+ /**
21
+ * The unique prosemirror plugin key for syncPlugin
22
+ *
23
+ * @public
24
+ */
25
+ const ySyncPluginKey = new PluginKey('y-sync');
26
+
27
+ /**
28
+ * The unique prosemirror plugin key for undoPlugin
29
+ *
30
+ * @public
31
+ */
32
+ const yUndoPluginKey = new PluginKey('y-undo');
33
+
34
+ /**
35
+ * The unique prosemirror plugin key for cursorPlugin
36
+ *
37
+ * @public
38
+ */
39
+ const yCursorPluginKey = new PluginKey('yjs-cursor');
40
+
41
+ /**
42
+ * @module bindings/prosemirror
43
+ */
44
+
45
+ const MarkPrefix = '_mark_';
46
+
47
+ /**
48
+ * @param {Y.Item} item
49
+ * @param {Y.Snapshot} [snapshot]
50
+ */
51
+ const isVisible = (item, snapshot) =>
52
+ snapshot === undefined
53
+ ? !item.deleted
54
+ : (snapshot.sv.has(item.id.client) && /** @type {number} */
55
+ (snapshot.sv.get(item.id.client)) > item.id.clock &&
56
+ !Y.isDeleted(snapshot.ds, item.id));
57
+
58
+ /**
59
+ * Either a node if type is YXmlElement or an Array of text nodes if YXmlText
60
+ * @typedef {Map<Y.AbstractType<any>, PModel.Node | Array<PModel.Node>>} ProsemirrorMapping
61
+ */
62
+
63
+ /**
64
+ * @typedef {Object} ColorDef
65
+ * @property {string} ColorDef.light
66
+ * @property {string} ColorDef.dark
67
+ */
68
+
69
+ /**
70
+ * @typedef {Object} YSyncOpts
71
+ * @property {Array<ColorDef>} [YSyncOpts.colors]
72
+ * @property {Map<string,ColorDef>} [YSyncOpts.colorMapping]
73
+ * @property {Y.PermanentUserData|null} [YSyncOpts.permanentUserData]
74
+ * @property {ProsemirrorMapping} [YSyncOpts.mapping]
75
+ * @property {function} [YSyncOpts.onFirstRender] Fired when the content from Yjs is initially rendered to ProseMirror
76
+ */
77
+
78
+ /**
79
+ * @type {Array<ColorDef>}
80
+ */
81
+ const defaultColors = [{ light: '#ecd44433', dark: '#ecd444' }];
82
+
83
+ /**
84
+ * @param {Map<string,ColorDef>} colorMapping
85
+ * @param {Array<ColorDef>} colors
86
+ * @param {string} user
87
+ * @return {ColorDef}
88
+ */
89
+ const getUserColor = (colorMapping, colors, user) => {
90
+ // @todo do not hit the same color twice if possible
91
+ if (!colorMapping.has(user)) {
92
+ if (colorMapping.size < colors.length) {
93
+ const usedColors = set.create();
94
+ colorMapping.forEach((color) => usedColors.add(color));
95
+ colors = colors.filter((color) => !usedColors.has(color));
96
+ }
97
+ colorMapping.set(user, random.oneOf(colors));
98
+ }
99
+ return /** @type {ColorDef} */ (colorMapping.get(user))
100
+ };
101
+
102
+ /**
103
+ * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
104
+ *
105
+ * This plugin also keeps references to the type and the shared document so other plugins can access it.
106
+ * @param {Y.XmlFragment} yXmlFragment
107
+ * @param {YSyncOpts} opts
108
+ * @return {any} Returns a prosemirror plugin that binds to this type
109
+ */
110
+ const ySyncPlugin = (yXmlFragment, {
111
+ colors = defaultColors,
112
+ colorMapping = new Map(),
113
+ permanentUserData = null,
114
+ onFirstRender = () => {},
115
+ mapping
116
+ } = {}) => {
117
+ let initialContentChanged = false;
118
+ const binding = new ProsemirrorBinding(yXmlFragment, mapping);
119
+ const plugin = new Plugin({
120
+ props: {
121
+ editable: (state) => {
122
+ const syncState = ySyncPluginKey.getState(state);
123
+ return syncState.snapshot == null && syncState.prevSnapshot == null
124
+ }
125
+ },
126
+ key: ySyncPluginKey,
127
+ state: {
128
+ /**
129
+ * @returns {any}
130
+ */
131
+ init: (_initargs, _state) => {
132
+ return {
133
+ type: yXmlFragment,
134
+ doc: yXmlFragment.doc,
135
+ binding,
136
+ snapshot: null,
137
+ prevSnapshot: null,
138
+ isChangeOrigin: false,
139
+ isUndoRedoOperation: false,
140
+ addToHistory: true,
141
+ colors,
142
+ colorMapping,
143
+ permanentUserData
144
+ }
145
+ },
146
+ apply: (tr, pluginState) => {
147
+ const change = tr.getMeta(ySyncPluginKey);
148
+ if (change !== undefined) {
149
+ pluginState = Object.assign({}, pluginState);
150
+ for (const key in change) {
151
+ pluginState[key] = change[key];
152
+ }
153
+ }
154
+ pluginState.addToHistory = tr.getMeta('addToHistory') !== false;
155
+ // always set isChangeOrigin. If undefined, this is not change origin.
156
+ pluginState.isChangeOrigin = change !== undefined &&
157
+ !!change.isChangeOrigin;
158
+ pluginState.isUndoRedoOperation = change !== undefined && !!change.isChangeOrigin && !!change.isUndoRedoOperation;
159
+ if (binding.prosemirrorView !== null) {
160
+ if (
161
+ change !== undefined &&
162
+ (change.snapshot != null || change.prevSnapshot != null)
163
+ ) {
164
+ // snapshot changed, rerender next
165
+ eventloop.timeout(0, () => {
166
+ if (binding.prosemirrorView == null) {
167
+ return
168
+ }
169
+ if (change.restore == null) {
170
+ binding._renderSnapshot(
171
+ change.snapshot,
172
+ change.prevSnapshot,
173
+ pluginState
174
+ );
175
+ } else {
176
+ binding._renderSnapshot(
177
+ change.snapshot,
178
+ change.snapshot,
179
+ pluginState
180
+ );
181
+ // reset to current prosemirror state
182
+ delete pluginState.restore;
183
+ delete pluginState.snapshot;
184
+ delete pluginState.prevSnapshot;
185
+ binding.mux(() => {
186
+ binding._prosemirrorChanged(
187
+ binding.prosemirrorView.state.doc
188
+ );
189
+ });
190
+ }
191
+ });
192
+ }
193
+ }
194
+ return pluginState
195
+ }
196
+ },
197
+ view: (view) => {
198
+ binding.initView(view);
199
+ if (mapping == null) {
200
+ // force rerender to update the bindings mapping
201
+ binding._forceRerender();
202
+ }
203
+ onFirstRender();
204
+ return {
205
+ update: () => {
206
+ const pluginState = plugin.getState(view.state);
207
+ if (
208
+ pluginState.snapshot == null && pluginState.prevSnapshot == null
209
+ ) {
210
+ if (
211
+ // If the content doesn't change initially, we don't render anything to Yjs
212
+ // If the content was cleared by a user action, we want to catch the change and
213
+ // represent it in Yjs
214
+ initialContentChanged ||
215
+ view.state.doc.content.findDiffStart(
216
+ view.state.doc.type.createAndFill().content
217
+ ) !== null
218
+ ) {
219
+ initialContentChanged = true;
220
+ if (
221
+ pluginState.addToHistory === false &&
222
+ !pluginState.isChangeOrigin
223
+ ) {
224
+ const yUndoPluginState = yUndoPluginKey.getState(view.state);
225
+ /**
226
+ * @type {Y.UndoManager}
227
+ */
228
+ const um = yUndoPluginState && yUndoPluginState.undoManager;
229
+ if (um) {
230
+ um.stopCapturing();
231
+ }
232
+ }
233
+ binding.mux(() => {
234
+ /** @type {Y.Doc} */ (pluginState.doc).transact((tr) => {
235
+ tr.meta.set('addToHistory', pluginState.addToHistory);
236
+ binding._prosemirrorChanged(view.state.doc);
237
+ }, ySyncPluginKey);
238
+ });
239
+ }
240
+ }
241
+ },
242
+ destroy: () => {
243
+ binding.destroy();
244
+ }
245
+ }
246
+ }
247
+ });
248
+ return plugin
249
+ };
250
+
251
+ /**
252
+ * @param {any} tr
253
+ * @param {any} relSel
254
+ * @param {ProsemirrorBinding} binding
255
+ */
256
+ const restoreRelativeSelection = (tr, relSel, binding) => {
257
+ if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
258
+ const anchor = relativePositionToAbsolutePosition(
259
+ binding.doc,
260
+ binding.type,
261
+ relSel.anchor,
262
+ binding.mapping
263
+ );
264
+ const head = relativePositionToAbsolutePosition(
265
+ binding.doc,
266
+ binding.type,
267
+ relSel.head,
268
+ binding.mapping
269
+ );
270
+ if (anchor !== null && head !== null) {
271
+ tr = tr.setSelection(TextSelection.create(tr.doc, anchor, head));
272
+ }
273
+ }
274
+ };
275
+
276
+ const getRelativeSelection = (pmbinding, state) => ({
277
+ anchor: absolutePositionToRelativePosition(
278
+ state.selection.anchor,
279
+ pmbinding.type,
280
+ pmbinding.mapping
281
+ ),
282
+ head: absolutePositionToRelativePosition(
283
+ state.selection.head,
284
+ pmbinding.type,
285
+ pmbinding.mapping
286
+ )
287
+ });
288
+
289
+ /**
290
+ * Binding for prosemirror.
291
+ *
292
+ * @protected
293
+ */
294
+ class ProsemirrorBinding {
295
+ /**
296
+ * @param {Y.XmlFragment} yXmlFragment The bind source
297
+ * @param {ProsemirrorMapping} mapping
298
+ */
299
+ constructor (yXmlFragment, mapping = new Map()) {
300
+ this.type = yXmlFragment;
301
+ /**
302
+ * this will be set once the view is created
303
+ * @type {any}
304
+ */
305
+ this.prosemirrorView = null;
306
+ this.mux = createMutex();
307
+ this.mapping = mapping;
308
+ this._observeFunction = this._typeChanged.bind(this);
309
+ /**
310
+ * @type {Y.Doc}
311
+ */
312
+ // @ts-ignore
313
+ this.doc = yXmlFragment.doc;
314
+ /**
315
+ * current selection as relative positions in the Yjs model
316
+ */
317
+ this.beforeTransactionSelection = null;
318
+ this.beforeAllTransactions = () => {
319
+ if (this.beforeTransactionSelection === null && this.prosemirrorView != null) {
320
+ this.beforeTransactionSelection = getRelativeSelection(
321
+ this,
322
+ this.prosemirrorView.state
323
+ );
324
+ }
325
+ };
326
+ this.afterAllTransactions = () => {
327
+ this.beforeTransactionSelection = null;
328
+ };
329
+ this._domSelectionInView = null;
330
+ }
331
+
332
+ /**
333
+ * Create a transaction for changing the prosemirror state.
334
+ *
335
+ * @returns
336
+ */
337
+ get _tr () {
338
+ return this.prosemirrorView.state.tr.setMeta('addToHistory', false)
339
+ }
340
+
341
+ _isLocalCursorInView () {
342
+ if (!this.prosemirrorView.hasFocus()) return false
343
+ if (environment.isBrowser && this._domSelectionInView === null) {
344
+ // Calculate the domSelectionInView and clear by next tick after all events are finished
345
+ eventloop.timeout(0, () => {
346
+ this._domSelectionInView = null;
347
+ });
348
+ this._domSelectionInView = this._isDomSelectionInView();
349
+ }
350
+ return this._domSelectionInView
351
+ }
352
+
353
+ _isDomSelectionInView () {
354
+ const selection = this.prosemirrorView._root.getSelection();
355
+
356
+ if (selection == null || selection.anchorNode == null) return false
357
+
358
+ const range = this.prosemirrorView._root.createRange();
359
+ range.setStart(selection.anchorNode, selection.anchorOffset);
360
+ range.setEnd(selection.focusNode, selection.focusOffset);
361
+
362
+ // This is a workaround for an edgecase where getBoundingClientRect will
363
+ // return zero values if the selection is collapsed at the start of a newline
364
+ // see reference here: https://stackoverflow.com/a/59780954
365
+ const rects = range.getClientRects();
366
+ if (rects.length === 0) {
367
+ // probably buggy newline behavior, explicitly select the node contents
368
+ if (range.startContainer && range.collapsed) {
369
+ range.selectNodeContents(range.startContainer);
370
+ }
371
+ }
372
+
373
+ const bounding = range.getBoundingClientRect();
374
+ const documentElement = dom.doc.documentElement;
375
+
376
+ return bounding.bottom >= 0 && bounding.right >= 0 &&
377
+ bounding.left <=
378
+ (window.innerWidth || documentElement.clientWidth || 0) &&
379
+ bounding.top <= (window.innerHeight || documentElement.clientHeight || 0)
380
+ }
381
+
382
+ /**
383
+ * @param {Y.Snapshot} snapshot
384
+ * @param {Y.Snapshot} prevSnapshot
385
+ */
386
+ renderSnapshot (snapshot, prevSnapshot) {
387
+ if (!prevSnapshot) {
388
+ prevSnapshot = Y.createSnapshot(Y.createDeleteSet(), new Map());
389
+ }
390
+ this.prosemirrorView.dispatch(
391
+ this._tr.setMeta(ySyncPluginKey, { snapshot, prevSnapshot })
392
+ );
393
+ }
394
+
395
+ unrenderSnapshot () {
396
+ this.mapping.clear();
397
+ this.mux(() => {
398
+ const fragmentContent = this.type.toArray().map((t) =>
399
+ createNodeFromYElement(
400
+ /** @type {Y.XmlElement} */ (t),
401
+ this.prosemirrorView.state.schema,
402
+ this.mapping
403
+ )
404
+ ).filter((n) => n !== null);
405
+ // @ts-ignore
406
+ const tr = this._tr.replace(
407
+ 0,
408
+ this.prosemirrorView.state.doc.content.size,
409
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
410
+ );
411
+ tr.setMeta(ySyncPluginKey, { snapshot: null, prevSnapshot: null });
412
+ this.prosemirrorView.dispatch(tr);
413
+ });
414
+ }
415
+
416
+ _forceRerender () {
417
+ this.mapping.clear();
418
+ this.mux(() => {
419
+ // If this is a forced rerender, this might neither happen as a pm change nor within a Yjs
420
+ // transaction. Then the "before selection" doesn't exist. In this case, we need to create a
421
+ // relative position before replacing content. Fixes #126
422
+ const sel = this.beforeTransactionSelection !== null ? null : this.prosemirrorView.state.selection;
423
+ const fragmentContent = this.type.toArray().map((t) =>
424
+ createNodeFromYElement(
425
+ /** @type {Y.XmlElement} */ (t),
426
+ this.prosemirrorView.state.schema,
427
+ this.mapping
428
+ )
429
+ ).filter((n) => n !== null);
430
+ // @ts-ignore
431
+ const tr = this._tr.replace(
432
+ 0,
433
+ this.prosemirrorView.state.doc.content.size,
434
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
435
+ );
436
+ if (sel) {
437
+ /**
438
+ * If the Prosemirror document we just created from this.type is
439
+ * smaller than the previous document, the selection might be
440
+ * out of bound, which would make Prosemirror throw an error.
441
+ */
442
+ const clampedAnchor = math.min(math.max(sel.anchor, 0), tr.doc.content.size);
443
+ const clampedHead = math.min(math.max(sel.head, 0), tr.doc.content.size);
444
+
445
+ tr.setSelection(TextSelection.create(tr.doc, clampedAnchor, clampedHead));
446
+ }
447
+ this.prosemirrorView.dispatch(
448
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this })
449
+ );
450
+ });
451
+ }
452
+
453
+ /**
454
+ * @param {Y.Snapshot|Uint8Array} snapshot
455
+ * @param {Y.Snapshot|Uint8Array} prevSnapshot
456
+ * @param {Object} pluginState
457
+ */
458
+ _renderSnapshot (snapshot, prevSnapshot, pluginState) {
459
+ /**
460
+ * The document that contains the full history of this document.
461
+ * @type {Y.Doc}
462
+ */
463
+ let historyDoc = this.doc;
464
+ if (!snapshot) {
465
+ snapshot = Y.snapshot(this.doc);
466
+ }
467
+ if (snapshot instanceof Uint8Array || prevSnapshot instanceof Uint8Array) {
468
+ if (!(snapshot instanceof Uint8Array) || !(prevSnapshot instanceof Uint8Array)) {
469
+ // expected both snapshots to be v2 updates
470
+ error.unexpectedCase();
471
+ }
472
+ historyDoc = new Y.Doc({ gc: false });
473
+ Y.applyUpdateV2(historyDoc, prevSnapshot);
474
+ prevSnapshot = Y.snapshot(historyDoc);
475
+ Y.applyUpdateV2(historyDoc, snapshot);
476
+ snapshot = Y.snapshot(historyDoc);
477
+ }
478
+ // clear mapping because we are going to rerender
479
+ this.mapping.clear();
480
+ this.mux(() => {
481
+ historyDoc.transact((transaction) => {
482
+ // before rendering, we are going to sanitize ops and split deleted ops
483
+ // if they were deleted by seperate users.
484
+ const pud = pluginState.permanentUserData;
485
+ if (pud) {
486
+ pud.dss.forEach((ds) => {
487
+ Y.iterateDeletedStructs(transaction, ds, (_item) => {});
488
+ });
489
+ }
490
+ /**
491
+ * @param {'removed'|'added'} type
492
+ * @param {Y.ID} id
493
+ */
494
+ const computeYChange = (type, id) => {
495
+ const user = type === 'added'
496
+ ? pud.getUserByClientId(id.client)
497
+ : pud.getUserByDeletedId(id);
498
+ return {
499
+ user,
500
+ type,
501
+ color: getUserColor(
502
+ pluginState.colorMapping,
503
+ pluginState.colors,
504
+ user
505
+ )
506
+ }
507
+ };
508
+ // Create document fragment and render
509
+ const fragmentContent = Y.typeListToArraySnapshot(
510
+ this.type,
511
+ new Y.Snapshot(prevSnapshot.ds, snapshot.sv)
512
+ ).map((t) => {
513
+ if (
514
+ !t._item.deleted || isVisible(t._item, snapshot) ||
515
+ isVisible(t._item, prevSnapshot)
516
+ ) {
517
+ return createNodeFromYElement(
518
+ t,
519
+ this.prosemirrorView.state.schema,
520
+ new Map(),
521
+ snapshot,
522
+ prevSnapshot,
523
+ computeYChange
524
+ )
525
+ } else {
526
+ // No need to render elements that are not visible by either snapshot.
527
+ // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
528
+ return null
529
+ }
530
+ }).filter((n) => n !== null);
531
+ // @ts-ignore
532
+ const tr = this._tr.replace(
533
+ 0,
534
+ this.prosemirrorView.state.doc.content.size,
535
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
536
+ );
537
+ this.prosemirrorView.dispatch(
538
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true })
539
+ );
540
+ }, ySyncPluginKey);
541
+ });
542
+ }
543
+
544
+ /**
545
+ * @param {Array<Y.YEvent<any>>} events
546
+ * @param {Y.Transaction} transaction
547
+ */
548
+ _typeChanged (events, transaction) {
549
+ if (this.prosemirrorView == null) return
550
+ const syncState = ySyncPluginKey.getState(this.prosemirrorView.state);
551
+ if (
552
+ events.length === 0 || syncState.snapshot != null ||
553
+ syncState.prevSnapshot != null
554
+ ) {
555
+ // drop out if snapshot is active
556
+ this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot);
557
+ return
558
+ }
559
+ this.mux(() => {
560
+ /**
561
+ * @param {any} _
562
+ * @param {Y.AbstractType<any>} type
563
+ */
564
+ const delType = (_, type) => this.mapping.delete(type);
565
+ Y.iterateDeletedStructs(
566
+ transaction,
567
+ transaction.deleteSet,
568
+ (struct) => {
569
+ if (struct.constructor === Y.Item) {
570
+ const type = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type;
571
+ type && this.mapping.delete(type);
572
+ }
573
+ }
574
+ );
575
+ transaction.changed.forEach(delType);
576
+ transaction.changedParentTypes.forEach(delType);
577
+ const fragmentContent = this.type.toArray().map((t) =>
578
+ createNodeIfNotExists(
579
+ /** @type {Y.XmlElement | Y.XmlHook} */ (t),
580
+ this.prosemirrorView.state.schema,
581
+ this.mapping
582
+ )
583
+ ).filter((n) => n !== null);
584
+ // @ts-ignore
585
+ let tr = this._tr.replace(
586
+ 0,
587
+ this.prosemirrorView.state.doc.content.size,
588
+ new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
589
+ );
590
+ restoreRelativeSelection(tr, this.beforeTransactionSelection, this);
591
+ tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, isUndoRedoOperation: transaction.origin instanceof Y.UndoManager });
592
+ if (
593
+ this.beforeTransactionSelection !== null && this._isLocalCursorInView()
594
+ ) {
595
+ tr.scrollIntoView();
596
+ }
597
+ this.prosemirrorView.dispatch(tr);
598
+ });
599
+ }
600
+
601
+ _prosemirrorChanged (doc) {
602
+ this.doc.transact(() => {
603
+ updateYFragment(this.doc, this.type, doc, this.mapping);
604
+ this.beforeTransactionSelection = getRelativeSelection(
605
+ this,
606
+ this.prosemirrorView.state
607
+ );
608
+ }, ySyncPluginKey);
609
+ }
610
+
611
+ /**
612
+ * View is ready to listen to changes. Register observers.
613
+ * @param {any} prosemirrorView
614
+ */
615
+ initView (prosemirrorView) {
616
+ if (this.prosemirrorView != null) this.destroy();
617
+ this.prosemirrorView = prosemirrorView;
618
+ this.doc.on('beforeAllTransactions', this.beforeAllTransactions);
619
+ this.doc.on('afterAllTransactions', this.afterAllTransactions);
620
+ this.type.observeDeep(this._observeFunction);
621
+ }
622
+
623
+ destroy () {
624
+ if (this.prosemirrorView == null) return
625
+ this.prosemirrorView = null;
626
+ this.type.unobserveDeep(this._observeFunction);
627
+ this.doc.off('beforeAllTransactions', this.beforeAllTransactions);
628
+ this.doc.off('afterAllTransactions', this.afterAllTransactions);
629
+ }
630
+ }
631
+
632
+ /**
633
+ * @private
634
+ * @param {Y.XmlElement | Y.XmlHook} el
635
+ * @param {PModel.Schema} schema
636
+ * @param {ProsemirrorMapping} mapping
637
+ * @param {Y.Snapshot} [snapshot]
638
+ * @param {Y.Snapshot} [prevSnapshot]
639
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
640
+ * @return {PModel.Node | null}
641
+ */
642
+ const createNodeIfNotExists = (
643
+ el,
644
+ schema,
645
+ mapping,
646
+ snapshot,
647
+ prevSnapshot,
648
+ computeYChange
649
+ ) => {
650
+ const node = /** @type {PModel.Node} */ (mapping.get(el));
651
+ if (node === undefined) {
652
+ if (el instanceof Y.XmlElement) {
653
+ return createNodeFromYElement(
654
+ el,
655
+ schema,
656
+ mapping,
657
+ snapshot,
658
+ prevSnapshot,
659
+ computeYChange
660
+ )
661
+ } else {
662
+ throw error.methodUnimplemented() // we are currently not handling hooks
663
+ }
664
+ }
665
+ return node
666
+ };
667
+
668
+ /**
669
+ * @private
670
+ * @param {Y.XmlElement} el
671
+ * @param {any} schema
672
+ * @param {ProsemirrorMapping} mapping
673
+ * @param {Y.Snapshot} [snapshot]
674
+ * @param {Y.Snapshot} [prevSnapshot]
675
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
676
+ * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
677
+ */
678
+ const createNodeFromYElement = (
679
+ el,
680
+ schema,
681
+ mapping,
682
+ snapshot,
683
+ prevSnapshot,
684
+ computeYChange
685
+ ) => {
686
+ const children = [];
687
+ const createChildren = (type) => {
688
+ if (type.constructor === Y.XmlElement) {
689
+ const n = createNodeIfNotExists(
690
+ type,
691
+ schema,
692
+ mapping,
693
+ snapshot,
694
+ prevSnapshot,
695
+ computeYChange
696
+ );
697
+ if (n !== null) {
698
+ children.push(n);
699
+ }
700
+ } else {
701
+ // If the next ytext exists and was created by us, move the content to the current ytext.
702
+ // This is a fix for #160 -- duplication of characters when two Y.Text exist next to each
703
+ // other.
704
+ const nextytext = type._item.right?.content.type;
705
+ if (nextytext instanceof Y.Text && !nextytext._item.deleted && nextytext._item.id.client === nextytext.doc.clientID) {
706
+ type.applyDelta([
707
+ { retain: type.length },
708
+ ...nextytext.toDelta()
709
+ ]);
710
+ nextytext.doc.transact(tr => {
711
+ nextytext._item.delete(tr);
712
+ });
713
+ }
714
+ // now create the prosemirror text nodes
715
+ const ns = createTextNodesFromYText(
716
+ type,
717
+ schema,
718
+ mapping,
719
+ snapshot,
720
+ prevSnapshot,
721
+ computeYChange
722
+ );
723
+ if (ns !== null) {
724
+ ns.forEach((textchild) => {
725
+ if (textchild !== null) {
726
+ children.push(textchild);
727
+ }
728
+ });
729
+ }
730
+ }
731
+ };
732
+ if (snapshot === undefined || prevSnapshot === undefined) {
733
+ el.toArray().forEach(createChildren);
734
+ } else {
735
+ Y.typeListToArraySnapshot(el, new Y.Snapshot(prevSnapshot.ds, snapshot.sv))
736
+ .forEach(createChildren);
737
+ }
738
+ try {
739
+ const attrs = el.getAttributes(snapshot);
740
+ if (snapshot !== undefined) {
741
+ if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) {
742
+ attrs.ychange = computeYChange
743
+ ? computeYChange('removed', /** @type {Y.Item} */ (el._item).id)
744
+ : { type: 'removed' };
745
+ } else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) {
746
+ attrs.ychange = computeYChange
747
+ ? computeYChange('added', /** @type {Y.Item} */ (el._item).id)
748
+ : { type: 'added' };
749
+ }
750
+ }
751
+ const nodeAttrs = {};
752
+ const nodeMarks = [];
753
+
754
+ for (const key in attrs) {
755
+ if (key.startsWith(MarkPrefix)) {
756
+ const markName = key.replace(MarkPrefix, '');
757
+ const markValue = attrs[key];
758
+ if (isObject(markValue)) {
759
+ nodeMarks.push(schema.mark(markName, /** @type {Object} */ (markValue).attrs));
760
+ } else if (Array.isArray(markValue)) {
761
+ nodeMarks.push(...markValue.map(attrs => schema.mark(markName, attrs)));
762
+ }
763
+ } else {
764
+ nodeAttrs[key] = attrs[key];
765
+ }
766
+ }
767
+
768
+ const node = schema.node(el.nodeName, nodeAttrs, children, nodeMarks);
769
+ mapping.set(el, node);
770
+ return node
771
+ } catch (e) {
772
+ // an error occured while creating the node. This is probably a result of a concurrent action.
773
+ /** @type {Y.Doc} */ (el.doc).transact((transaction) => {
774
+ /** @type {Y.Item} */ (el._item).delete(transaction);
775
+ }, ySyncPluginKey);
776
+ mapping.delete(el);
777
+ return null
778
+ }
779
+ };
780
+
781
+ /**
782
+ * @private
783
+ * @param {Y.XmlText} text
784
+ * @param {any} schema
785
+ * @param {ProsemirrorMapping} _mapping
786
+ * @param {Y.Snapshot} [snapshot]
787
+ * @param {Y.Snapshot} [prevSnapshot]
788
+ * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
789
+ * @return {Array<PModel.Node>|null}
790
+ */
791
+ const createTextNodesFromYText = (
792
+ text,
793
+ schema,
794
+ _mapping,
795
+ snapshot,
796
+ prevSnapshot,
797
+ computeYChange
798
+ ) => {
799
+ const nodes = [];
800
+ const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange);
801
+ try {
802
+ for (let i = 0; i < deltas.length; i++) {
803
+ const delta = deltas[i];
804
+ const marks = [];
805
+ for (const markName in delta.attributes) {
806
+ if (Array.isArray(delta.attributes[markName])) {
807
+ // multiple marks of same type
808
+ delta.attributes[markName].forEach(attrs => {
809
+ marks.push(schema.mark(markName, attrs));
810
+ });
811
+ } else {
812
+ // single mark
813
+ marks.push(schema.mark(markName, delta.attributes[markName]));
814
+ }
815
+ }
816
+ nodes.push(schema.text(delta.insert, marks));
817
+ }
818
+ } catch (e) {
819
+ // an error occured while creating the node. This is probably a result of a concurrent action.
820
+ /** @type {Y.Doc} */ (text.doc).transact((transaction) => {
821
+ /** @type {Y.Item} */ (text._item).delete(transaction);
822
+ }, ySyncPluginKey);
823
+ return null
824
+ }
825
+ // @ts-ignore
826
+ return nodes
827
+ };
828
+
829
+ /**
830
+ * @private
831
+ * @param {Array<any>} nodes prosemirror node
832
+ * @param {ProsemirrorMapping} mapping
833
+ * @return {Y.XmlText}
834
+ */
835
+ const createTypeFromTextNodes = (nodes, mapping) => {
836
+ const type = new Y.XmlText();
837
+ const delta = nodes.map((node) => ({
838
+ // @ts-ignore
839
+ insert: node.text,
840
+ attributes: marksToAttributes(node.marks)
841
+ }));
842
+ type.applyDelta(delta);
843
+ mapping.set(type, nodes);
844
+ return type
845
+ };
846
+
847
+ /**
848
+ * @private
849
+ * @param {any} node prosemirror node
850
+ * @param {ProsemirrorMapping} mapping
851
+ * @return {Y.XmlElement}
852
+ */
853
+ const createTypeFromElementNode = (node, mapping) => {
854
+ const type = new Y.XmlElement(node.type.name);
855
+ const nodeMarksAttr = nodeMarksToAttributes(node.marks);
856
+ for (const key in node.attrs) {
857
+ const val = node.attrs[key];
858
+ if (val !== null && key !== 'ychange') {
859
+ type.setAttribute(key, val);
860
+ }
861
+ }
862
+ for (const key in nodeMarksAttr) {
863
+ type.setAttribute(key, nodeMarksAttr[key]);
864
+ }
865
+ type.insert(
866
+ 0,
867
+ normalizePNodeContent(node).map((n) =>
868
+ createTypeFromTextOrElementNode(n, mapping)
869
+ )
870
+ );
871
+ mapping.set(type, node);
872
+ return type
873
+ };
874
+
875
+ /**
876
+ * @private
877
+ * @param {PModel.Node|Array<PModel.Node>} node prosemirror text node
878
+ * @param {ProsemirrorMapping} mapping
879
+ * @return {Y.XmlElement|Y.XmlText}
880
+ */
881
+ const createTypeFromTextOrElementNode = (node, mapping) =>
882
+ node instanceof Array
883
+ ? createTypeFromTextNodes(node, mapping)
884
+ : createTypeFromElementNode(node, mapping);
885
+
886
+ const isObject = (val) => typeof val === 'object' && val !== null;
887
+
888
+ const equalAttrs = (pattrs, yattrs) => {
889
+ const keys = Object.keys(pattrs).filter((key) => pattrs[key] !== null);
890
+ let eq =
891
+ keys.length ===
892
+ Object.keys(yattrs).filter((key) => yattrs[key] !== null && !key.startsWith(MarkPrefix)).length;
893
+ for (let i = 0; i < keys.length && eq; i++) {
894
+ const key = keys[i];
895
+ const l = pattrs[key];
896
+ const r = yattrs[key];
897
+ eq = key === 'ychange' || l === r ||
898
+ (isObject(l) && isObject(r) && equalAttrs(l, r));
899
+ }
900
+ return eq
901
+ };
902
+
903
+ const equalMarks = (pmarks, yattrs) => {
904
+ const keys = Object.keys(yattrs).filter((key) => key.startsWith(MarkPrefix));
905
+ let eq =
906
+ keys.length === pmarks.length;
907
+ const pMarkAttr = nodeMarksToAttributes(pmarks);
908
+ for (let i = 0; i < keys.length && eq; i++) {
909
+ const key = keys[i];
910
+ const l = pMarkAttr[key];
911
+ const r = yattrs[key];
912
+ eq = key === 'ychange' || f.equalityDeep(l, r);
913
+ }
914
+ return eq
915
+ };
916
+
917
+ /**
918
+ * @typedef {Array<Array<PModel.Node>|PModel.Node>} NormalizedPNodeContent
919
+ */
920
+
921
+ /**
922
+ * @param {any} pnode
923
+ * @return {NormalizedPNodeContent}
924
+ */
925
+ const normalizePNodeContent = (pnode) => {
926
+ const c = pnode.content.content;
927
+ const res = [];
928
+ for (let i = 0; i < c.length; i++) {
929
+ const n = c[i];
930
+ if (n.isText) {
931
+ const textNodes = [];
932
+ for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) {
933
+ textNodes.push(tnode);
934
+ }
935
+ i--;
936
+ res.push(textNodes);
937
+ } else {
938
+ res.push(n);
939
+ }
940
+ }
941
+ return res
942
+ };
943
+
944
+ /**
945
+ * @param {Y.XmlText} ytext
946
+ * @param {Array<any>} ptexts
947
+ */
948
+ const equalYTextPText = (ytext, ptexts) => {
949
+ const delta = ytext.toDelta();
950
+ return delta.length === ptexts.length &&
951
+ delta.every((d, i) =>
952
+ d.insert === /** @type {any} */ (ptexts[i]).text &&
953
+ Object.keys(d.attributes || {}).reduce((sum, val) => sum + (Array.isArray(val) ? val.length : 1), 0) === ptexts[i].marks.length &&
954
+ ptexts[i].marks.every((mark) => {
955
+ const yattrs = d.attributes || {};
956
+ if (Array.isArray(yattrs)) {
957
+ return yattrs.some((yattr) => equalAttrs(mark.attrs, yattr))
958
+ }
959
+ return equalAttrs(mark.attrs, yattrs)
960
+ }
961
+ )
962
+ )
963
+ };
964
+
965
+ /**
966
+ * @param {Y.XmlElement|Y.XmlText|Y.XmlHook} ytype
967
+ * @param {any|Array<any>} pnode
968
+ */
969
+ const equalYTypePNode = (ytype, pnode) => {
970
+ if (
971
+ ytype instanceof Y.XmlElement && !(pnode instanceof Array) &&
972
+ matchNodeName(ytype, pnode)
973
+ ) {
974
+ const normalizedContent = normalizePNodeContent(pnode);
975
+ return ytype._length === normalizedContent.length &&
976
+ equalAttrs(pnode.attrs, ytype.getAttributes()) &&
977
+ equalMarks(pnode.marks, ytype.getAttributes()) &&
978
+ ytype.toArray().every((ychild, i) =>
979
+ equalYTypePNode(ychild, normalizedContent[i])
980
+ )
981
+ }
982
+ return ytype instanceof Y.XmlText && pnode instanceof Array &&
983
+ equalYTextPText(ytype, pnode)
984
+ };
985
+
986
+ /**
987
+ * @param {PModel.Node | Array<PModel.Node> | undefined} mapped
988
+ * @param {PModel.Node | Array<PModel.Node>} pcontent
989
+ */
990
+ const mappedIdentity = (mapped, pcontent) =>
991
+ mapped === pcontent ||
992
+ (mapped instanceof Array && pcontent instanceof Array &&
993
+ mapped.length === pcontent.length && mapped.every((a, i) =>
994
+ pcontent[i] === a
995
+ ));
996
+
997
+ /**
998
+ * @param {Y.XmlElement} ytype
999
+ * @param {PModel.Node} pnode
1000
+ * @param {ProsemirrorMapping} mapping
1001
+ * @return {{ foundMappedChild: boolean, equalityFactor: number }}
1002
+ */
1003
+ const computeChildEqualityFactor = (ytype, pnode, mapping) => {
1004
+ const yChildren = ytype.toArray();
1005
+ const pChildren = normalizePNodeContent(pnode);
1006
+ const pChildCnt = pChildren.length;
1007
+ const yChildCnt = yChildren.length;
1008
+ const minCnt = math.min(yChildCnt, pChildCnt);
1009
+ let left = 0;
1010
+ let right = 0;
1011
+ let foundMappedChild = false;
1012
+ for (; left < minCnt; left++) {
1013
+ const leftY = yChildren[left];
1014
+ const leftP = pChildren[left];
1015
+ if (mappedIdentity(mapping.get(leftY), leftP)) {
1016
+ foundMappedChild = true; // definite (good) match!
1017
+ } else if (!equalYTypePNode(leftY, leftP)) {
1018
+ break
1019
+ }
1020
+ }
1021
+ for (; left + right < minCnt; right++) {
1022
+ const rightY = yChildren[yChildCnt - right - 1];
1023
+ const rightP = pChildren[pChildCnt - right - 1];
1024
+ if (mappedIdentity(mapping.get(rightY), rightP)) {
1025
+ foundMappedChild = true;
1026
+ } else if (!equalYTypePNode(rightY, rightP)) {
1027
+ break
1028
+ }
1029
+ }
1030
+ return {
1031
+ equalityFactor: left + right,
1032
+ foundMappedChild
1033
+ }
1034
+ };
1035
+
1036
+ const ytextTrans = (ytext) => {
1037
+ let str = '';
1038
+ /**
1039
+ * @type {Y.Item|null}
1040
+ */
1041
+ let n = ytext._start;
1042
+ const nAttrs = {};
1043
+ while (n !== null) {
1044
+ if (!n.deleted) {
1045
+ if (n.countable && n.content instanceof Y.ContentString) {
1046
+ str += n.content.str;
1047
+ } else if (n.content instanceof Y.ContentFormat) {
1048
+ nAttrs[n.content.key] = null;
1049
+ }
1050
+ }
1051
+ n = n.right;
1052
+ }
1053
+ return {
1054
+ str,
1055
+ nAttrs
1056
+ }
1057
+ };
1058
+
1059
+ /**
1060
+ * @todo test this more
1061
+ *
1062
+ * @param {Y.Text} ytext
1063
+ * @param {Array<any>} ptexts
1064
+ * @param {ProsemirrorMapping} mapping
1065
+ */
1066
+ const updateYText = (ytext, ptexts, mapping) => {
1067
+ mapping.set(ytext, ptexts);
1068
+ const { nAttrs, str } = ytextTrans(ytext);
1069
+ const content = ptexts.map((p) => ({
1070
+ insert: /** @type {any} */ (p).text,
1071
+ attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks))
1072
+ }));
1073
+ const { insert, remove, index } = simpleDiff(
1074
+ str,
1075
+ content.map((c) => c.insert).join('')
1076
+ );
1077
+ ytext.delete(index, remove);
1078
+ ytext.insert(index, insert);
1079
+ ytext.applyDelta(
1080
+ content.map((c) => ({ retain: c.insert.length, attributes: c.attributes }))
1081
+ );
1082
+ };
1083
+
1084
+ const marksToAttributes = (marks) => {
1085
+ const pattrs = {};
1086
+ marks.forEach((mark) => {
1087
+ if (mark.type.name !== 'ychange') {
1088
+ if (pattrs[mark.type.name] && Array.isArray(pattrs[mark.type.name])) {
1089
+ // already has multiple marks of same type
1090
+ pattrs[mark.type.name].push(mark.attrs);
1091
+ } else if (pattrs[mark.type.name]) {
1092
+ // already has mark of same type, change to array
1093
+ pattrs[mark.type.name] = [pattrs[mark.type.name], mark.attrs];
1094
+ } else {
1095
+ // first mark of this type
1096
+ pattrs[mark.type.name] = mark.attrs;
1097
+ }
1098
+ }
1099
+ });
1100
+ return pattrs
1101
+ };
1102
+
1103
+ const nodeMarksToAttributes = (marks) => {
1104
+ const pattrs = {};
1105
+ marks.forEach((mark) => {
1106
+ if (mark.type.name !== 'ychange') {
1107
+ pattrs[`${MarkPrefix}${mark.type.name}`] = mark.toJSON();
1108
+ }
1109
+ });
1110
+ return pattrs
1111
+ };
1112
+
1113
+ /**
1114
+ * Update a yDom node by syncing the current content of the prosemirror node.
1115
+ *
1116
+ * This is a y-tiptap internal feature that you can use at your own risk.
1117
+ *
1118
+ * @private
1119
+ * @unstable
1120
+ *
1121
+ * @param {{transact: Function}} y
1122
+ * @param {Y.XmlFragment} yDomFragment
1123
+ * @param {any} pNode
1124
+ * @param {ProsemirrorMapping} mapping
1125
+ */
1126
+ const updateYFragment = (y, yDomFragment, pNode, mapping) => {
1127
+ if (
1128
+ yDomFragment instanceof Y.XmlElement &&
1129
+ yDomFragment.nodeName !== pNode.type.name
1130
+ ) {
1131
+ throw new Error('node name mismatch!')
1132
+ }
1133
+ mapping.set(yDomFragment, pNode);
1134
+ // update attributes
1135
+ if (yDomFragment instanceof Y.XmlElement) {
1136
+ const yDomAttrs = yDomFragment.getAttributes();
1137
+ const pAttrs = pNode.attrs;
1138
+ const pNodeMarksAttr = nodeMarksToAttributes(pNode.marks);
1139
+ const attrs = { ...pAttrs, ...pNodeMarksAttr };
1140
+
1141
+ for (const key in attrs) {
1142
+ if (attrs[key] !== null) {
1143
+ if (yDomAttrs[key] !== attrs[key] && key !== 'ychange') {
1144
+ yDomFragment.setAttribute(key, attrs[key]);
1145
+ }
1146
+ } else {
1147
+ yDomFragment.removeAttribute(key);
1148
+ }
1149
+ }
1150
+ // remove all keys that are no longer in pAttrs
1151
+ for (const key in yDomAttrs) {
1152
+ if (attrs[key] === undefined) {
1153
+ yDomFragment.removeAttribute(key);
1154
+ }
1155
+ }
1156
+ }
1157
+ // update children
1158
+ const pChildren = normalizePNodeContent(pNode);
1159
+ const pChildCnt = pChildren.length;
1160
+ const yChildren = yDomFragment.toArray();
1161
+ const yChildCnt = yChildren.length;
1162
+ const minCnt = math.min(pChildCnt, yChildCnt);
1163
+ let left = 0;
1164
+ let right = 0;
1165
+ // find number of matching elements from left
1166
+ for (; left < minCnt; left++) {
1167
+ const leftY = yChildren[left];
1168
+ const leftP = pChildren[left];
1169
+ if (!mappedIdentity(mapping.get(leftY), leftP)) {
1170
+ if (equalYTypePNode(leftY, leftP)) {
1171
+ // update mapping
1172
+ mapping.set(leftY, leftP);
1173
+ } else {
1174
+ break
1175
+ }
1176
+ }
1177
+ }
1178
+ // find number of matching elements from right
1179
+ for (; right + left + 1 < minCnt; right++) {
1180
+ const rightY = yChildren[yChildCnt - right - 1];
1181
+ const rightP = pChildren[pChildCnt - right - 1];
1182
+ if (!mappedIdentity(mapping.get(rightY), rightP)) {
1183
+ if (equalYTypePNode(rightY, rightP)) {
1184
+ // update mapping
1185
+ mapping.set(rightY, rightP);
1186
+ } else {
1187
+ break
1188
+ }
1189
+ }
1190
+ }
1191
+ y.transact(() => {
1192
+ // try to compare and update
1193
+ while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
1194
+ const leftY = yChildren[left];
1195
+ const leftP = pChildren[left];
1196
+ const rightY = yChildren[yChildCnt - right - 1];
1197
+ const rightP = pChildren[pChildCnt - right - 1];
1198
+ if (leftY instanceof Y.XmlText && leftP instanceof Array) {
1199
+ if (!equalYTextPText(leftY, leftP)) {
1200
+ updateYText(leftY, leftP, mapping);
1201
+ }
1202
+ left += 1;
1203
+ } else {
1204
+ let updateLeft = leftY instanceof Y.XmlElement &&
1205
+ matchNodeName(leftY, leftP);
1206
+ let updateRight = rightY instanceof Y.XmlElement &&
1207
+ matchNodeName(rightY, rightP);
1208
+ if (updateLeft && updateRight) {
1209
+ // decide which which element to update
1210
+ const equalityLeft = computeChildEqualityFactor(
1211
+ /** @type {Y.XmlElement} */ (leftY),
1212
+ /** @type {PModel.Node} */ (leftP),
1213
+ mapping
1214
+ );
1215
+ const equalityRight = computeChildEqualityFactor(
1216
+ /** @type {Y.XmlElement} */ (rightY),
1217
+ /** @type {PModel.Node} */ (rightP),
1218
+ mapping
1219
+ );
1220
+ if (
1221
+ equalityLeft.foundMappedChild && !equalityRight.foundMappedChild
1222
+ ) {
1223
+ updateRight = false;
1224
+ } else if (
1225
+ !equalityLeft.foundMappedChild && equalityRight.foundMappedChild
1226
+ ) {
1227
+ updateLeft = false;
1228
+ } else if (
1229
+ equalityLeft.equalityFactor < equalityRight.equalityFactor
1230
+ ) {
1231
+ updateLeft = false;
1232
+ } else {
1233
+ updateRight = false;
1234
+ }
1235
+ }
1236
+ if (updateLeft) {
1237
+ updateYFragment(
1238
+ y,
1239
+ /** @type {Y.XmlFragment} */ (leftY),
1240
+ /** @type {PModel.Node} */ (leftP),
1241
+ mapping
1242
+ );
1243
+ left += 1;
1244
+ } else if (updateRight) {
1245
+ updateYFragment(
1246
+ y,
1247
+ /** @type {Y.XmlFragment} */ (rightY),
1248
+ /** @type {PModel.Node} */ (rightP),
1249
+ mapping
1250
+ );
1251
+ right += 1;
1252
+ } else {
1253
+ mapping.delete(yDomFragment.get(left));
1254
+ yDomFragment.delete(left, 1);
1255
+ yDomFragment.insert(left, [
1256
+ createTypeFromTextOrElementNode(leftP, mapping)
1257
+ ]);
1258
+ left += 1;
1259
+ }
1260
+ }
1261
+ }
1262
+ const yDelLen = yChildCnt - left - right;
1263
+ if (
1264
+ yChildCnt === 1 && pChildCnt === 0 && yChildren[0] instanceof Y.XmlText
1265
+ ) {
1266
+ mapping.delete(yChildren[0]);
1267
+ // Edge case handling https://github.com/yjs/y-tiptap/issues/108
1268
+ // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
1269
+ yChildren[0].delete(0, yChildren[0].length);
1270
+ } else if (yDelLen > 0) {
1271
+ yDomFragment.slice(left, left + yDelLen).forEach(type => mapping.delete(type));
1272
+ yDomFragment.delete(left, yDelLen);
1273
+ }
1274
+ if (left + right < pChildCnt) {
1275
+ const ins = [];
1276
+ for (let i = left; i < pChildCnt - right; i++) {
1277
+ ins.push(createTypeFromTextOrElementNode(pChildren[i], mapping));
1278
+ }
1279
+ yDomFragment.insert(left, ins);
1280
+ }
1281
+ }, ySyncPluginKey);
1282
+ };
1283
+
1284
+ /**
1285
+ * @function
1286
+ * @param {Y.XmlElement} yElement
1287
+ * @param {any} pNode Prosemirror Node
1288
+ */
1289
+ const matchNodeName = (yElement, pNode) =>
1290
+ !(pNode instanceof Array) && yElement.nodeName === pNode.type.name;
1291
+
1292
+ /**
1293
+ * Either a node if type is YXmlElement or an Array of text nodes if YXmlText
1294
+ * @typedef {Map<Y.AbstractType, Node | Array<Node>>} ProsemirrorMapping
1295
+ */
1296
+
1297
+ /**
1298
+ * Is null if no timeout is in progress.
1299
+ * Is defined if a timeout is in progress.
1300
+ * Maps from view
1301
+ * @type {Map<EditorView, Map<any, any>>|null}
1302
+ */
1303
+ let viewsToUpdate = null;
1304
+
1305
+ const updateMetas = () => {
1306
+ const ups = /** @type {Map<EditorView, Map<any, any>>} */ (viewsToUpdate);
1307
+ viewsToUpdate = null;
1308
+ ups.forEach((metas, view) => {
1309
+ const tr = view.state.tr;
1310
+ const syncState = ySyncPluginKey.getState(view.state);
1311
+ if (syncState && syncState.binding && !syncState.binding.isDestroyed) {
1312
+ metas.forEach((val, key) => {
1313
+ tr.setMeta(key, val);
1314
+ });
1315
+ view.dispatch(tr);
1316
+ }
1317
+ });
1318
+ };
1319
+
1320
+ const setMeta = (view, key, value) => {
1321
+ if (!viewsToUpdate) {
1322
+ viewsToUpdate = new Map();
1323
+ eventloop.timeout(0, updateMetas);
1324
+ }
1325
+ map.setIfUndefined(viewsToUpdate, view, map.create).set(key, value);
1326
+ };
1327
+
1328
+ /**
1329
+ * Transforms a Prosemirror based absolute position to a Yjs Cursor (relative position in the Yjs model).
1330
+ *
1331
+ * @param {number} pos
1332
+ * @param {Y.XmlFragment} type
1333
+ * @param {ProsemirrorMapping} mapping
1334
+ * @return {any} relative position
1335
+ */
1336
+ const absolutePositionToRelativePosition = (pos, type, mapping) => {
1337
+ if (pos === 0) {
1338
+ return Y.createRelativePositionFromTypeIndex(type, 0, -1)
1339
+ }
1340
+ /**
1341
+ * @type {any}
1342
+ */
1343
+ let n = type._first === null ? null : /** @type {Y.ContentType} */ (type._first.content).type;
1344
+ while (n !== null && type !== n) {
1345
+ if (n instanceof Y.XmlText) {
1346
+ if (n._length >= pos) {
1347
+ return Y.createRelativePositionFromTypeIndex(n, pos, -1)
1348
+ } else {
1349
+ pos -= n._length;
1350
+ }
1351
+ if (n._item !== null && n._item.next !== null) {
1352
+ n = /** @type {Y.ContentType} */ (n._item.next.content).type;
1353
+ } else {
1354
+ do {
1355
+ n = n._item === null ? null : n._item.parent;
1356
+ pos--;
1357
+ } while (n !== type && n !== null && n._item !== null && n._item.next === null)
1358
+ if (n !== null && n !== type) {
1359
+ // @ts-gnore we know that n.next !== null because of above loop conditition
1360
+ n = n._item === null ? null : /** @type {Y.ContentType} */ (/** @type Y.Item */ (n._item.next).content).type;
1361
+ }
1362
+ }
1363
+ } else {
1364
+ const pNodeSize = /** @type {any} */ (mapping.get(n) || { nodeSize: 0 }).nodeSize;
1365
+ if (n._first !== null && pos < pNodeSize) {
1366
+ n = /** @type {Y.ContentType} */ (n._first.content).type;
1367
+ pos--;
1368
+ } else {
1369
+ if (pos === 1 && n._length === 0 && pNodeSize > 1) {
1370
+ // edge case, should end in this paragraph
1371
+ return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null)
1372
+ }
1373
+ pos -= pNodeSize;
1374
+ if (n._item !== null && n._item.next !== null) {
1375
+ n = /** @type {Y.ContentType} */ (n._item.next.content).type;
1376
+ } else {
1377
+ if (pos === 0) {
1378
+ // set to end of n.parent
1379
+ n = n._item === null ? n : n._item.parent;
1380
+ return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null)
1381
+ }
1382
+ do {
1383
+ n = /** @type {Y.Item} */ (n._item).parent;
1384
+ pos--;
1385
+ } while (n !== type && /** @type {Y.Item} */ (n._item).next === null)
1386
+ // if n is null at this point, we have an unexpected case
1387
+ if (n !== type) {
1388
+ // We know that n._item.next is defined because of above loop condition
1389
+ n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type;
1390
+ }
1391
+ }
1392
+ }
1393
+ }
1394
+ if (n === null) {
1395
+ throw error.unexpectedCase()
1396
+ }
1397
+ if (pos === 0 && n.constructor !== Y.XmlText && n !== type) { // TODO: set to <= 0
1398
+ return createRelativePosition(n._item.parent, n._item)
1399
+ }
1400
+ }
1401
+ return Y.createRelativePositionFromTypeIndex(type, type._length, -1)
1402
+ };
1403
+
1404
+ const createRelativePosition = (type, item) => {
1405
+ let typeid = null;
1406
+ let tname = null;
1407
+ if (type._item === null) {
1408
+ tname = Y.findRootTypeKey(type);
1409
+ } else {
1410
+ typeid = Y.createID(type._item.id.client, type._item.id.clock);
1411
+ }
1412
+ return new Y.RelativePosition(typeid, tname, item.id)
1413
+ };
1414
+
1415
+ /**
1416
+ * @param {Y.Doc} y
1417
+ * @param {Y.XmlFragment} documentType Top level type that is bound to pView
1418
+ * @param {any} relPos Encoded Yjs based relative position
1419
+ * @param {ProsemirrorMapping} mapping
1420
+ * @return {null|number}
1421
+ */
1422
+ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) => {
1423
+ const decodedPos = Y.createAbsolutePositionFromRelativePosition(relPos, y);
1424
+ if (decodedPos === null || (decodedPos.type !== documentType && !Y.isParentOf(documentType, decodedPos.type._item))) {
1425
+ return null
1426
+ }
1427
+ let type = decodedPos.type;
1428
+ let pos = 0;
1429
+ if (type.constructor === Y.XmlText) {
1430
+ pos = decodedPos.index;
1431
+ } else if (type._item === null || !type._item.deleted) {
1432
+ let n = type._first;
1433
+ let i = 0;
1434
+ while (i < type._length && i < decodedPos.index && n !== null) {
1435
+ if (!n.deleted) {
1436
+ const t = /** @type {Y.ContentType} */ (n.content).type;
1437
+ i++;
1438
+ if (t instanceof Y.XmlText) {
1439
+ pos += t._length;
1440
+ } else {
1441
+ pos += /** @type {any} */ (mapping.get(t)).nodeSize;
1442
+ }
1443
+ }
1444
+ n = /** @type {Y.Item} */ (n.right);
1445
+ }
1446
+ pos += 1; // increase because we go out of n
1447
+ }
1448
+ while (type !== documentType && type._item !== null) {
1449
+ // @ts-ignore
1450
+ const parent = type._item.parent;
1451
+ // @ts-ignore
1452
+ if (parent._item === null || !parent._item.deleted) {
1453
+ pos += 1; // the start tag
1454
+ let n = /** @type {Y.AbstractType} */ (parent)._first;
1455
+ // now iterate until we found type
1456
+ while (n !== null) {
1457
+ const contentType = /** @type {Y.ContentType} */ (n.content).type;
1458
+ if (contentType === type) {
1459
+ break
1460
+ }
1461
+ if (!n.deleted) {
1462
+ if (contentType instanceof Y.XmlText) {
1463
+ pos += contentType._length;
1464
+ } else {
1465
+ pos += /** @type {any} */ (mapping.get(contentType)).nodeSize;
1466
+ }
1467
+ }
1468
+ n = n.right;
1469
+ }
1470
+ }
1471
+ type = /** @type {Y.AbstractType} */ (parent);
1472
+ }
1473
+ return pos - 1 // we don't count the most outer tag, because it is a fragment
1474
+ };
1475
+
1476
+ /**
1477
+ * Utility function for converting an Y.Fragment to a ProseMirror fragment.
1478
+ *
1479
+ * @param {Y.XmlFragment} yXmlFragment
1480
+ * @param {Schema} schema
1481
+ */
1482
+ const yXmlFragmentToProseMirrorFragment = (yXmlFragment, schema) => {
1483
+ const fragmentContent = yXmlFragment.toArray().map((t) =>
1484
+ createNodeFromYElement(
1485
+ /** @type {Y.XmlElement} */ (t),
1486
+ schema,
1487
+ new Map()
1488
+ )
1489
+ ).filter((n) => n !== null);
1490
+ return Fragment.fromArray(fragmentContent)
1491
+ };
1492
+
1493
+ /**
1494
+ * Utility function for converting an Y.Fragment to a ProseMirror node.
1495
+ *
1496
+ * @param {Y.XmlFragment} yXmlFragment
1497
+ * @param {Schema} schema
1498
+ */
1499
+ const yXmlFragmentToProseMirrorRootNode = (yXmlFragment, schema) =>
1500
+ schema.topNodeType.create(null, yXmlFragmentToProseMirrorFragment(yXmlFragment, schema));
1501
+
1502
+ /**
1503
+ * The initial ProseMirror content should be supplied by Yjs. This function transforms a Y.Fragment
1504
+ * to a ProseMirror Doc node and creates a mapping that is used by the sync plugin.
1505
+ *
1506
+ * @param {Y.XmlFragment} yXmlFragment
1507
+ * @param {Schema} schema
1508
+ */
1509
+ const initProseMirrorDoc = (yXmlFragment, schema) => {
1510
+ /**
1511
+ * @type {ProsemirrorMapping}
1512
+ */
1513
+ const mapping = new Map();
1514
+ const fragmentContent = yXmlFragment.toArray().map((t) =>
1515
+ createNodeFromYElement(
1516
+ /** @type {Y.XmlElement} */ (t),
1517
+ schema,
1518
+ mapping
1519
+ )
1520
+ ).filter((n) => n !== null);
1521
+ const doc = schema.topNodeType.create(null, Fragment.fromArray(fragmentContent));
1522
+ return { doc, mapping }
1523
+ };
1524
+
1525
+ /**
1526
+ * Utility method to convert a Prosemirror Doc Node into a Y.Doc.
1527
+ *
1528
+ * This can be used when importing existing content to Y.Doc for the first time,
1529
+ * note that this should not be used to rehydrate a Y.Doc from a database once
1530
+ * collaboration has begun as all history will be lost
1531
+ *
1532
+ * @param {Node} doc
1533
+ * @param {string} xmlFragment
1534
+ * @return {Y.Doc}
1535
+ */
1536
+ function prosemirrorToYDoc (doc, xmlFragment = 'prosemirror') {
1537
+ const ydoc = new Y.Doc();
1538
+ const type = /** @type {Y.XmlFragment} */ (ydoc.get(xmlFragment, Y.XmlFragment));
1539
+ if (!type.doc) {
1540
+ return ydoc
1541
+ }
1542
+
1543
+ prosemirrorToYXmlFragment(doc, type);
1544
+ return type.doc
1545
+ }
1546
+
1547
+ /**
1548
+ * Utility method to update an empty Y.XmlFragment with content from a Prosemirror Doc Node.
1549
+ *
1550
+ * This can be used when importing existing content to Y.Doc for the first time,
1551
+ * note that this should not be used to rehydrate a Y.Doc from a database once
1552
+ * collaboration has begun as all history will be lost
1553
+ *
1554
+ * Note: The Y.XmlFragment does not need to be part of a Y.Doc document at the time that this
1555
+ * method is called, but it must be added before any other operations are performed on it.
1556
+ *
1557
+ * @param {Node} doc prosemirror document.
1558
+ * @param {Y.XmlFragment} [xmlFragment] If supplied, an xml fragment to be
1559
+ * populated from the prosemirror state; otherwise a new XmlFragment will be created.
1560
+ * @return {Y.XmlFragment}
1561
+ */
1562
+ function prosemirrorToYXmlFragment (doc, xmlFragment) {
1563
+ const type = xmlFragment || new Y.XmlFragment();
1564
+ const ydoc = type.doc ? type.doc : { transact: (transaction) => transaction(undefined) };
1565
+ updateYFragment(ydoc, type, doc, new Map());
1566
+ return type
1567
+ }
1568
+
1569
+ /**
1570
+ * Utility method to convert Prosemirror compatible JSON into a Y.Doc.
1571
+ *
1572
+ * This can be used when importing existing content to Y.Doc for the first time,
1573
+ * note that this should not be used to rehydrate a Y.Doc from a database once
1574
+ * collaboration has begun as all history will be lost
1575
+ *
1576
+ * @param {Schema} schema
1577
+ * @param {any} state
1578
+ * @param {string} xmlFragment
1579
+ * @return {Y.Doc}
1580
+ */
1581
+ function prosemirrorJSONToYDoc (schema, state, xmlFragment = 'prosemirror') {
1582
+ const doc = Node.fromJSON(schema, state);
1583
+ return prosemirrorToYDoc(doc, xmlFragment)
1584
+ }
1585
+
1586
+ /**
1587
+ * Utility method to convert Prosemirror compatible JSON to a Y.XmlFragment
1588
+ *
1589
+ * This can be used when importing existing content to Y.Doc for the first time,
1590
+ * note that this should not be used to rehydrate a Y.Doc from a database once
1591
+ * collaboration has begun as all history will be lost
1592
+ *
1593
+ * @param {Schema} schema
1594
+ * @param {any} state
1595
+ * @param {Y.XmlFragment} [xmlFragment] If supplied, an xml fragment to be
1596
+ * populated from the prosemirror state; otherwise a new XmlFragment will be created.
1597
+ * @return {Y.XmlFragment}
1598
+ */
1599
+ function prosemirrorJSONToYXmlFragment (schema, state, xmlFragment) {
1600
+ const doc = Node.fromJSON(schema, state);
1601
+ return prosemirrorToYXmlFragment(doc, xmlFragment)
1602
+ }
1603
+
1604
+ /**
1605
+ * @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
1606
+ *
1607
+ * Utility method to convert a Y.Doc to a Prosemirror Doc node.
1608
+ *
1609
+ * @param {Schema} schema
1610
+ * @param {Y.Doc} ydoc
1611
+ * @return {Node}
1612
+ */
1613
+ function yDocToProsemirror (schema, ydoc) {
1614
+ const state = yDocToProsemirrorJSON(ydoc);
1615
+ return Node.fromJSON(schema, state)
1616
+ }
1617
+
1618
+ /**
1619
+ *
1620
+ * @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
1621
+ *
1622
+ * Utility method to convert a Y.XmlFragment to a Prosemirror Doc node.
1623
+ *
1624
+ * @param {Schema} schema
1625
+ * @param {Y.XmlFragment} xmlFragment
1626
+ * @return {Node}
1627
+ */
1628
+ function yXmlFragmentToProsemirror (schema, xmlFragment) {
1629
+ const state = yXmlFragmentToProsemirrorJSON(xmlFragment);
1630
+ return Node.fromJSON(schema, state)
1631
+ }
1632
+
1633
+ /**
1634
+ *
1635
+ * @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
1636
+ *
1637
+ * Utility method to convert a Y.Doc to Prosemirror compatible JSON.
1638
+ *
1639
+ * @param {Y.Doc} ydoc
1640
+ * @param {string} xmlFragment
1641
+ * @return {Record<string, any>}
1642
+ */
1643
+ function yDocToProsemirrorJSON (
1644
+ ydoc,
1645
+ xmlFragment = 'prosemirror'
1646
+ ) {
1647
+ return yXmlFragmentToProsemirrorJSON(ydoc.getXmlFragment(xmlFragment))
1648
+ }
1649
+
1650
+ /**
1651
+ * @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
1652
+ *
1653
+ * Utility method to convert a Y.Doc to Prosemirror compatible JSON.
1654
+ *
1655
+ * @param {Y.XmlFragment} xmlFragment The fragment, which must be part of a Y.Doc.
1656
+ * @return {Record<string, any>}
1657
+ */
1658
+ function yXmlFragmentToProsemirrorJSON (xmlFragment) {
1659
+ const items = xmlFragment.toArray();
1660
+
1661
+ function serialize (item) {
1662
+ /**
1663
+ * @type {Object} NodeObject
1664
+ * @property {string} NodeObject.type
1665
+ * @property {Record<string, string>=} NodeObject.attrs
1666
+ * @property {Array<NodeObject>=} NodeObject.content
1667
+ */
1668
+ let response;
1669
+
1670
+ // TODO: Must be a better way to detect text nodes than this
1671
+ if (!item.nodeName) {
1672
+ const delta = item.toDelta();
1673
+ response = delta.map((d) => {
1674
+ const text = {
1675
+ type: 'text',
1676
+ text: d.insert
1677
+ };
1678
+
1679
+ if (d.attributes) {
1680
+ const marks = [];
1681
+
1682
+ Object.keys(d.attributes).forEach((type) => {
1683
+ const attrs = d.attributes[type];
1684
+ if (Array.isArray(attrs)) {
1685
+ // multiple marks of same type
1686
+ attrs.forEach(singleAttrs => {
1687
+ const mark = {
1688
+ type
1689
+ };
1690
+
1691
+ if (Object.keys(singleAttrs)) {
1692
+ mark.attrs = singleAttrs;
1693
+ }
1694
+
1695
+ marks.push(mark);
1696
+ });
1697
+ } else {
1698
+ const mark = {
1699
+ type
1700
+ };
1701
+
1702
+ if (Object.keys(attrs)) {
1703
+ mark.attrs = attrs;
1704
+ }
1705
+
1706
+ marks.push(mark);
1707
+ }
1708
+ });
1709
+
1710
+ text.marks = marks;
1711
+ }
1712
+ return text
1713
+ });
1714
+ } else {
1715
+ response = {
1716
+ type: item.nodeName
1717
+ };
1718
+
1719
+ const attrs = item.getAttributes();
1720
+
1721
+ // Add all non-mark attributes to the element
1722
+ for (const key of Object.keys(attrs).filter((key) => !key.startsWith(MarkPrefix))) {
1723
+ if (!response.attrs) response.attrs = {};
1724
+ response.attrs[key] = attrs[key];
1725
+ }
1726
+
1727
+ // Check whether the attrs contains marks, if so, add them to the response
1728
+ if (Object.keys(attrs).some((key) => key.startsWith(MarkPrefix))) {
1729
+ response.marks = Object.keys(attrs)
1730
+ .filter((key) => key.startsWith(MarkPrefix))
1731
+ .map((key) => {
1732
+ return attrs[key]
1733
+ });
1734
+ }
1735
+
1736
+ const children = item.toArray();
1737
+ if (children.length) {
1738
+ response.content = children.map(serialize).flat();
1739
+ }
1740
+ }
1741
+
1742
+ return response
1743
+ }
1744
+
1745
+ return {
1746
+ type: 'doc',
1747
+ content: items.map(serialize)
1748
+ }
1749
+ }
1750
+
1751
+ /**
1752
+ * Default awareness state filter
1753
+ *
1754
+ * @param {number} currentClientId current client id
1755
+ * @param {number} userClientId user client id
1756
+ * @param {any} _user user data
1757
+ * @return {boolean}
1758
+ */
1759
+ const defaultAwarenessStateFilter = (currentClientId, userClientId, _user) => currentClientId !== userClientId;
1760
+
1761
+ /**
1762
+ * Default generator for a cursor element
1763
+ *
1764
+ * @param {any} user user data
1765
+ * @return {HTMLElement}
1766
+ */
1767
+ const defaultCursorBuilder = (user) => {
1768
+ const cursor = document.createElement('span');
1769
+ cursor.classList.add('ProseMirror-yjs-cursor');
1770
+ cursor.setAttribute('style', `border-color: ${user.color}`);
1771
+ const userDiv = document.createElement('div');
1772
+ userDiv.setAttribute('style', `background-color: ${user.color}`);
1773
+ userDiv.insertBefore(document.createTextNode(user.name), null);
1774
+ const nonbreakingSpace1 = document.createTextNode('\u2060');
1775
+ const nonbreakingSpace2 = document.createTextNode('\u2060');
1776
+ cursor.insertBefore(nonbreakingSpace1, null);
1777
+ cursor.insertBefore(userDiv, null);
1778
+ cursor.insertBefore(nonbreakingSpace2, null);
1779
+ return cursor
1780
+ };
1781
+
1782
+ /**
1783
+ * Default generator for the selection attributes
1784
+ *
1785
+ * @param {any} user user data
1786
+ * @return {import('@tiptap/pm/view').DecorationAttrs}
1787
+ */
1788
+ const defaultSelectionBuilder = (user) => {
1789
+ return {
1790
+ style: `background-color: ${user.color}70`,
1791
+ class: 'ProseMirror-yjs-selection'
1792
+ }
1793
+ };
1794
+
1795
+ const rxValidColor = /^#[0-9a-fA-F]{6}$/;
1796
+
1797
+ /**
1798
+ * @param {any} state
1799
+ * @param {Awareness} awareness
1800
+ * @param {function(number, number, any):boolean} awarenessFilter
1801
+ * @param {function({ name: string, color: string }):Element} createCursor
1802
+ * @param {function({ name: string, color: string }):import('@tiptap/pm/view').DecorationAttrs} createSelection
1803
+ * @return {any} DecorationSet
1804
+ */
1805
+ const createDecorations = (
1806
+ state,
1807
+ awareness,
1808
+ awarenessFilter,
1809
+ createCursor,
1810
+ createSelection
1811
+ ) => {
1812
+ const ystate = ySyncPluginKey.getState(state);
1813
+ const y = ystate.doc;
1814
+ const decorations = [];
1815
+ if (
1816
+ ystate.snapshot != null || ystate.prevSnapshot != null ||
1817
+ ystate.binding.mapping.size === 0
1818
+ ) {
1819
+ // do not render cursors while snapshot is active
1820
+ return DecorationSet.create(state.doc, [])
1821
+ }
1822
+ awareness.getStates().forEach((aw, clientId) => {
1823
+ if (!awarenessFilter(y.clientID, clientId, aw)) {
1824
+ return
1825
+ }
1826
+
1827
+ if (aw.cursor != null) {
1828
+ const user = aw.user || {};
1829
+ if (user.color == null) {
1830
+ user.color = '#ffa500';
1831
+ } else if (!rxValidColor.test(user.color)) {
1832
+ // We only support 6-digit RGB colors in y-tiptap
1833
+ console.warn('A user uses an unsupported color format', user);
1834
+ }
1835
+ if (user.name == null) {
1836
+ user.name = `User: ${clientId}`;
1837
+ }
1838
+ let anchor = relativePositionToAbsolutePosition(
1839
+ y,
1840
+ ystate.type,
1841
+ Y.createRelativePositionFromJSON(aw.cursor.anchor),
1842
+ ystate.binding.mapping
1843
+ );
1844
+ let head = relativePositionToAbsolutePosition(
1845
+ y,
1846
+ ystate.type,
1847
+ Y.createRelativePositionFromJSON(aw.cursor.head),
1848
+ ystate.binding.mapping
1849
+ );
1850
+ if (anchor !== null && head !== null) {
1851
+ const maxsize = math.max(state.doc.content.size - 1, 0);
1852
+ anchor = math.min(anchor, maxsize);
1853
+ head = math.min(head, maxsize);
1854
+ decorations.push(
1855
+ Decoration.widget(head, () => createCursor(user), {
1856
+ key: clientId + '',
1857
+ side: 10
1858
+ })
1859
+ );
1860
+ const from = math.min(anchor, head);
1861
+ const to = math.max(anchor, head);
1862
+ decorations.push(
1863
+ Decoration.inline(from, to, createSelection(user), {
1864
+ inclusiveEnd: true,
1865
+ inclusiveStart: false
1866
+ })
1867
+ );
1868
+ }
1869
+ }
1870
+ });
1871
+ return DecorationSet.create(state.doc, decorations)
1872
+ };
1873
+
1874
+ /**
1875
+ * A prosemirror plugin that listens to awareness information on Yjs.
1876
+ * This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
1877
+ *
1878
+ * @public
1879
+ * @param {Awareness} awareness
1880
+ * @param {object} opts
1881
+ * @param {function(any, any, any):boolean} [opts.awarenessStateFilter]
1882
+ * @param {function(any):HTMLElement} [opts.cursorBuilder]
1883
+ * @param {function(any):import('@tiptap/pm/view').DecorationAttrs} [opts.selectionBuilder]
1884
+ * @param {function(any):any} [opts.getSelection]
1885
+ * @param {string} [cursorStateField] By default all editor bindings use the awareness 'cursor' field to propagate cursor information.
1886
+ * @return {any}
1887
+ */
1888
+ const yCursorPlugin = (
1889
+ awareness,
1890
+ {
1891
+ awarenessStateFilter = defaultAwarenessStateFilter,
1892
+ cursorBuilder = defaultCursorBuilder,
1893
+ selectionBuilder = defaultSelectionBuilder,
1894
+ getSelection = (state) => state.selection
1895
+ } = {},
1896
+ cursorStateField = 'cursor'
1897
+ ) =>
1898
+ new Plugin({
1899
+ key: yCursorPluginKey,
1900
+ state: {
1901
+ init (_, state) {
1902
+ return createDecorations(
1903
+ state,
1904
+ awareness,
1905
+ awarenessStateFilter,
1906
+ cursorBuilder,
1907
+ selectionBuilder
1908
+ )
1909
+ },
1910
+ apply (tr, prevState, _oldState, newState) {
1911
+ const ystate = ySyncPluginKey.getState(newState);
1912
+ const yCursorState = tr.getMeta(yCursorPluginKey);
1913
+ if (
1914
+ (ystate && ystate.isChangeOrigin) ||
1915
+ (yCursorState && yCursorState.awarenessUpdated)
1916
+ ) {
1917
+ return createDecorations(
1918
+ newState,
1919
+ awareness,
1920
+ awarenessStateFilter,
1921
+ cursorBuilder,
1922
+ selectionBuilder
1923
+ )
1924
+ }
1925
+ return prevState.map(tr.mapping, tr.doc)
1926
+ }
1927
+ },
1928
+ props: {
1929
+ decorations: (state) => {
1930
+ return yCursorPluginKey.getState(state)
1931
+ }
1932
+ },
1933
+ view: (view) => {
1934
+ const awarenessListener = () => {
1935
+ // @ts-ignore
1936
+ if (view.docView) {
1937
+ setMeta(view, yCursorPluginKey, { awarenessUpdated: true });
1938
+ }
1939
+ };
1940
+ const updateCursorInfo = () => {
1941
+ const ystate = ySyncPluginKey.getState(view.state);
1942
+ // @note We make implicit checks when checking for the cursor property
1943
+ const current = awareness.getLocalState() || {};
1944
+ if (view.hasFocus()) {
1945
+ const selection = getSelection(view.state);
1946
+ /**
1947
+ * @type {Y.RelativePosition}
1948
+ */
1949
+ const anchor = absolutePositionToRelativePosition(
1950
+ selection.anchor,
1951
+ ystate.type,
1952
+ ystate.binding.mapping
1953
+ );
1954
+ /**
1955
+ * @type {Y.RelativePosition}
1956
+ */
1957
+ const head = absolutePositionToRelativePosition(
1958
+ selection.head,
1959
+ ystate.type,
1960
+ ystate.binding.mapping
1961
+ );
1962
+ if (
1963
+ current.cursor == null ||
1964
+ !Y.compareRelativePositions(
1965
+ Y.createRelativePositionFromJSON(current.cursor.anchor),
1966
+ anchor
1967
+ ) ||
1968
+ !Y.compareRelativePositions(
1969
+ Y.createRelativePositionFromJSON(current.cursor.head),
1970
+ head
1971
+ )
1972
+ ) {
1973
+ awareness.setLocalStateField(cursorStateField, {
1974
+ anchor,
1975
+ head
1976
+ });
1977
+ }
1978
+ } else if (
1979
+ current.cursor != null &&
1980
+ relativePositionToAbsolutePosition(
1981
+ ystate.doc,
1982
+ ystate.type,
1983
+ Y.createRelativePositionFromJSON(current.cursor.anchor),
1984
+ ystate.binding.mapping
1985
+ ) !== null
1986
+ ) {
1987
+ // delete cursor information if current cursor information is owned by this editor binding
1988
+ awareness.setLocalStateField(cursorStateField, null);
1989
+ }
1990
+ };
1991
+ awareness.on('change', awarenessListener);
1992
+ view.dom.addEventListener('focusin', updateCursorInfo);
1993
+ view.dom.addEventListener('focusout', updateCursorInfo);
1994
+ return {
1995
+ update: updateCursorInfo,
1996
+ destroy: () => {
1997
+ view.dom.removeEventListener('focusin', updateCursorInfo);
1998
+ view.dom.removeEventListener('focusout', updateCursorInfo);
1999
+ awareness.off('change', awarenessListener);
2000
+ awareness.setLocalStateField(cursorStateField, null);
2001
+ }
2002
+ }
2003
+ }
2004
+ });
2005
+
2006
+ const undo = state => {
2007
+ const undoManager = yUndoPluginKey.getState(state).undoManager;
2008
+ if (undoManager != null) {
2009
+ undoManager.undo();
2010
+ return true
2011
+ }
2012
+ };
2013
+
2014
+ const redo = state => {
2015
+ const undoManager = yUndoPluginKey.getState(state).undoManager;
2016
+ if (undoManager != null) {
2017
+ undoManager.redo();
2018
+ return true
2019
+ }
2020
+ };
2021
+
2022
+ const defaultProtectedNodes = new Set(['paragraph']);
2023
+
2024
+ const defaultDeleteFilter = (item, protectedNodes) => !(item instanceof Item) ||
2025
+ !(item.content instanceof ContentType) ||
2026
+ !(item.content.type instanceof Text ||
2027
+ (item.content.type instanceof XmlElement && protectedNodes.has(item.content.type.nodeName))) ||
2028
+ item.content.type._length === 0;
2029
+
2030
+ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins = [], undoManager = null } = {}) => new Plugin({
2031
+ key: yUndoPluginKey,
2032
+ state: {
2033
+ init: (initargs, state) => {
2034
+ // TODO: check if plugin order matches and fix
2035
+ const ystate = ySyncPluginKey.getState(state);
2036
+ const _undoManager = undoManager || new UndoManager(ystate.type, {
2037
+ trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
2038
+ deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
2039
+ captureTransaction: tr => tr.meta.get('addToHistory') !== false
2040
+ });
2041
+ return {
2042
+ undoManager: _undoManager,
2043
+ prevSel: null,
2044
+ hasUndoOps: _undoManager.undoStack.length > 0,
2045
+ hasRedoOps: _undoManager.redoStack.length > 0
2046
+ }
2047
+ },
2048
+ /**
2049
+ * @returns {any}
2050
+ */
2051
+ apply: (tr, val, oldState, state) => {
2052
+ const binding = ySyncPluginKey.getState(state).binding;
2053
+ const undoManager = val.undoManager;
2054
+ const hasUndoOps = undoManager.undoStack.length > 0;
2055
+ const hasRedoOps = undoManager.redoStack.length > 0;
2056
+ if (binding) {
2057
+ return {
2058
+ undoManager,
2059
+ prevSel: getRelativeSelection(binding, oldState),
2060
+ hasUndoOps,
2061
+ hasRedoOps
2062
+ }
2063
+ } else {
2064
+ if (hasUndoOps !== val.hasUndoOps || hasRedoOps !== val.hasRedoOps) {
2065
+ return Object.assign({}, val, {
2066
+ hasUndoOps: undoManager.undoStack.length > 0,
2067
+ hasRedoOps: undoManager.redoStack.length > 0
2068
+ })
2069
+ } else { // nothing changed
2070
+ return val
2071
+ }
2072
+ }
2073
+ }
2074
+ },
2075
+ view: view => {
2076
+ const ystate = ySyncPluginKey.getState(view.state);
2077
+ const undoManager = yUndoPluginKey.getState(view.state).undoManager;
2078
+ undoManager.on('stack-item-added', ({ stackItem }) => {
2079
+ const binding = ystate.binding;
2080
+ if (binding) {
2081
+ stackItem.meta.set(binding, yUndoPluginKey.getState(view.state).prevSel);
2082
+ }
2083
+ });
2084
+ undoManager.on('stack-item-popped', ({ stackItem }) => {
2085
+ const binding = ystate.binding;
2086
+ if (binding) {
2087
+ binding.beforeTransactionSelection = stackItem.meta.get(binding) || binding.beforeTransactionSelection;
2088
+ }
2089
+ });
2090
+ return {
2091
+ destroy: () => {
2092
+ undoManager.destroy();
2093
+ }
2094
+ }
2095
+ }
2096
+ });
2097
+
2098
+ export { ProsemirrorBinding, absolutePositionToRelativePosition, createDecorations, defaultAwarenessStateFilter, defaultCursorBuilder, defaultDeleteFilter, defaultProtectedNodes, defaultSelectionBuilder, getRelativeSelection, initProseMirrorDoc, isVisible, prosemirrorJSONToYDoc, prosemirrorJSONToYXmlFragment, prosemirrorToYDoc, prosemirrorToYXmlFragment, redo, relativePositionToAbsolutePosition, setMeta, undo, updateYFragment, yCursorPlugin, yCursorPluginKey, yDocToProsemirror, yDocToProsemirrorJSON, ySyncPlugin, ySyncPluginKey, yUndoPlugin, yUndoPluginKey, yXmlFragmentToProseMirrorFragment, yXmlFragmentToProseMirrorRootNode, yXmlFragmentToProsemirror, yXmlFragmentToProsemirrorJSON };
2099
+ //# sourceMappingURL=y-tiptap.js.map