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