@tiptap/y-tiptap 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/y-tiptap.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as Y from 'yjs';
2
2
  import { Item, ContentType, Text, XmlElement, UndoManager } from 'yjs';
3
3
  import { DecorationSet, Decoration } from 'prosemirror-view';
4
- import { PluginKey, Plugin, TextSelection, AllSelection, NodeSelection } from 'prosemirror-state';
4
+ import { PluginKey, Plugin, TextSelection, AllSelection, NodeSelection, Selection } from 'prosemirror-state';
5
5
  import 'y-protocols/awareness';
6
6
  import { createMutex } from 'lib0/mutex';
7
7
  import * as PModel from 'prosemirror-model';
@@ -296,7 +296,28 @@ const restoreRelativeSelection = (tr, relSel, binding) => {
296
296
  relSel.anchor,
297
297
  binding.mapping
298
298
  );
299
- tr.setSelection(createSafeNodeSelection(tr, anchor));
299
+ // anchor is null when the referenced node was deleted or moved out of
300
+ // binding.type by a remote update; resolving null would throw.
301
+ if (anchor !== null) {
302
+ tr.setSelection(createSafeNodeSelection(tr, anchor));
303
+ }
304
+ } else if (relSel.type === 'nodeRange') {
305
+ const anchor = relativePositionToAbsolutePosition(
306
+ binding.doc,
307
+ binding.type,
308
+ relSel.anchor,
309
+ binding.mapping
310
+ );
311
+ const head = relativePositionToAbsolutePosition(
312
+ binding.doc,
313
+ binding.type,
314
+ relSel.head,
315
+ binding.mapping
316
+ );
317
+ const selection = createSafeNodeRangeSelection(tr, anchor, head, relSel.depth);
318
+ if (selection !== null) {
319
+ tr.setSelection(selection);
320
+ }
300
321
  } else {
301
322
  const anchor = relativePositionToAbsolutePosition(
302
323
  binding.doc,
@@ -334,23 +355,65 @@ const createSafeNodeSelection = (tr, pos) => {
334
355
  }
335
356
  };
336
357
 
358
+ /**
359
+ * Safely reconstructs a NodeRangeSelection from resolved absolute positions.
360
+ *
361
+ * @param {import('prosemirror-state').Transaction} tr - The transaction whose document provides resolved positions.
362
+ * @param {number|null} anchor - Absolute document position marking the start (boundary) of the node range.
363
+ * Use `relativePositionToAbsolutePosition` before calling this function.
364
+ * @param {number|null} head - Absolute document position marking the end (boundary) of the node range.
365
+ * Use `relativePositionToAbsolutePosition` before calling this function.
366
+ * @param {number|undefined} depth - The nesting depth at which the range operates (e.g. 0 for
367
+ * top-level blocks, 1 for blocks nested inside a wrapper). Passed through to
368
+ * `Selection.fromJSON`; ignored by `@tiptap/extension-node-range` < v2.29 but
369
+ * properly stored starting from that version.
370
+ * @returns {import('prosemirror-state').Selection|null} Reconstructed selection, or null if
371
+ * anchor/head could not be resolved.
372
+ */
373
+ const createSafeNodeRangeSelection = (tr, anchor, head, depth) => {
374
+ if (anchor === null || head === null) {
375
+ return null
376
+ }
377
+ const clampedAnchor = Math.min(Math.max(anchor, 0), tr.doc.content.size);
378
+ const clampedHead = Math.min(Math.max(head, 0), tr.doc.content.size);
379
+ try {
380
+ const selection = Selection.fromJSON(tr.doc, {
381
+ type: 'nodeRange',
382
+ anchor: clampedAnchor,
383
+ head: clampedHead,
384
+ depth
385
+ });
386
+ if (!selection.ranges.length) {
387
+ return TextSelection.near(tr.doc.resolve(clampedAnchor))
388
+ }
389
+ return selection
390
+ } catch (e) {
391
+ return TextSelection.near(tr.doc.resolve(clampedAnchor))
392
+ }
393
+ };
394
+
337
395
  /**
338
396
  * @param {ProsemirrorBinding} pmbinding
339
397
  * @param {import('prosemirror-state').EditorState} state
340
398
  */
341
- const getRelativeSelection = (pmbinding, state) => ({
342
- type: /** @type {any} */ (state.selection).jsonID,
343
- anchor: absolutePositionToRelativePosition(
344
- state.selection.anchor,
345
- pmbinding.type,
346
- pmbinding.mapping
347
- ),
348
- head: absolutePositionToRelativePosition(
349
- state.selection.head,
350
- pmbinding.type,
351
- pmbinding.mapping
352
- )
353
- });
399
+ const getRelativeSelection = (pmbinding, state) => {
400
+ const type = /** @type {any} */ (state.selection).jsonID;
401
+ return {
402
+ type,
403
+ // `depth` is only meaningful for NodeRangeSelection; undefined for every other type.
404
+ depth: type === 'nodeRange' ? /** @type {any} */ (state.selection).depth : undefined,
405
+ anchor: absolutePositionToRelativePosition(
406
+ state.selection.anchor,
407
+ pmbinding.type,
408
+ pmbinding.mapping
409
+ ),
410
+ head: absolutePositionToRelativePosition(
411
+ state.selection.head,
412
+ pmbinding.type,
413
+ pmbinding.mapping
414
+ )
415
+ }
416
+ };
354
417
 
355
418
  /**
356
419
  * Binding for prosemirror.
@@ -1609,7 +1672,11 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
1609
1672
  if (t instanceof Y.XmlText) {
1610
1673
  pos += t._length;
1611
1674
  } else {
1612
- pos += /** @type {any} */ (mapping.get(t)).nodeSize;
1675
+ const mapped = mapping.get(t);
1676
+ if (mapped == null) {
1677
+ return null
1678
+ }
1679
+ pos += /** @type {any} */ (mapped).nodeSize;
1613
1680
  }
1614
1681
  }
1615
1682
  n = /** @type {Y.Item} */ (n.right);
@@ -1633,7 +1700,11 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
1633
1700
  if (contentType instanceof Y.XmlText) {
1634
1701
  pos += contentType._length;
1635
1702
  } else {
1636
- pos += /** @type {any} */ (mapping.get(contentType)).nodeSize;
1703
+ const mapped = mapping.get(contentType);
1704
+ if (mapped == null) {
1705
+ return null
1706
+ }
1707
+ pos += /** @type {any} */ (mapped).nodeSize;
1637
1708
  }
1638
1709
  }
1639
1710
  n = n.right;