@tiptap/y-tiptap 3.0.5 → 3.0.6

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
@@ -18,6 +18,7 @@ import * as eventloop from 'lib0/eventloop';
18
18
  import * as map from 'lib0/map';
19
19
  import * as sha256 from 'lib0/hash/sha256';
20
20
  import * as buf from 'lib0/buffer';
21
+ import { ReplaceStep } from 'prosemirror-transform';
21
22
 
22
23
  /**
23
24
  * The unique prosemirror plugin key for syncPlugin
@@ -284,8 +285,9 @@ const ySyncPlugin = (yXmlFragment, {
284
285
  * @param {import('prosemirror-state').Transaction} tr
285
286
  * @param {ReturnType<typeof getRelativeSelection>} relSel
286
287
  * @param {ProsemirrorBinding} binding
288
+ * @param {import('prosemirror-model').Node} [oldDoc]
287
289
  */
288
- const restoreRelativeSelection = (tr, relSel, binding) => {
290
+ const restoreRelativeSelection = (tr, relSel, binding, oldDoc) => {
289
291
  if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
290
292
  if (relSel.type === 'all') {
291
293
  tr.setSelection(new AllSelection(tr.doc));
@@ -319,18 +321,41 @@ const restoreRelativeSelection = (tr, relSel, binding) => {
319
321
  tr.setSelection(selection);
320
322
  }
321
323
  } else {
322
- const anchor = relativePositionToAbsolutePosition(
324
+ let anchor = relativePositionToAbsolutePosition(
323
325
  binding.doc,
324
326
  binding.type,
325
327
  relSel.anchor,
326
328
  binding.mapping
327
329
  );
328
- const head = relativePositionToAbsolutePosition(
330
+ let head = relativePositionToAbsolutePosition(
329
331
  binding.doc,
330
332
  binding.type,
331
333
  relSel.head,
332
334
  binding.mapping
333
335
  );
336
+ const needsContentFallback = oldDoc != null &&
337
+ relSel.absAnchor != null &&
338
+ relSel.absHead != null &&
339
+ (
340
+ anchor === null ||
341
+ head === null ||
342
+ (relSel.absAnchor > 1 && anchor !== null && anchor <= 1) ||
343
+ (relSel.absHead > 1 && head !== null && head <= 1) ||
344
+ isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absAnchor, anchor) ||
345
+ isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absHead, head)
346
+ );
347
+ if (needsContentFallback) {
348
+ anchor = findAbsolutePositionAfterStructuralChange(
349
+ oldDoc,
350
+ tr.doc,
351
+ relSel.absAnchor
352
+ );
353
+ head = findAbsolutePositionAfterStructuralChange(
354
+ oldDoc,
355
+ tr.doc,
356
+ relSel.absHead
357
+ );
358
+ }
334
359
  if (anchor !== null && head !== null) {
335
360
  tr.setSelection(TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head)));
336
361
  }
@@ -411,7 +436,9 @@ const getRelativeSelection = (pmbinding, state) => {
411
436
  state.selection.head,
412
437
  pmbinding.type,
413
438
  pmbinding.mapping
414
- )
439
+ ),
440
+ absAnchor: state.selection.anchor,
441
+ absHead: state.selection.head
415
442
  }
416
443
  };
417
444
 
@@ -736,6 +763,9 @@ class ProsemirrorBinding {
736
763
  );
737
764
  transaction.changed.forEach(delType);
738
765
  transaction.changedParentTypes.forEach(delType);
766
+ // Rebuild the full Y↔PM mapping so relative cursor positions resolve against
767
+ // current node sizes after structural changes (e.g. drag-and-drop block moves).
768
+ this.mapping.clear();
739
769
  const fragmentContent = this.type.toArray().map((t) =>
740
770
  createNodeIfNotExists(
741
771
  /** @type {Y.XmlElement | Y.XmlHook} */ (t),
@@ -743,13 +773,14 @@ class ProsemirrorBinding {
743
773
  this
744
774
  )
745
775
  ).filter((n) => n !== null);
776
+ const oldDoc = this.prosemirrorView.state.doc;
746
777
  // @ts-ignore
747
778
  let tr = this._tr.replace(
748
779
  0,
749
780
  this.prosemirrorView.state.doc.content.size,
750
781
  new PModel.Slice(PModel.Fragment.from(fragmentContent), 0, 0)
751
782
  );
752
- restoreRelativeSelection(tr, this.beforeTransactionSelection, this);
783
+ restoreRelativeSelection(tr, this.beforeTransactionSelection, this, oldDoc);
753
784
  tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, isUndoRedoOperation: transaction.origin instanceof Y.UndoManager });
754
785
  if (
755
786
  this.beforeTransactionSelection !== null && this._isLocalCursorInView()
@@ -1635,6 +1666,26 @@ const absolutePositionToRelativePosition = (pos, type, mapping) => {
1635
1666
  return Y.createRelativePositionFromTypeIndex(type, type._length, -1)
1636
1667
  };
1637
1668
 
1669
+ /**
1670
+ * Item-id based relative positions can misresolve to the document start after
1671
+ * block reorder during collaborative drag-and-drop.
1672
+ *
1673
+ * @param {Y.Doc} y
1674
+ * @param {Y.RelativePosition} relPos
1675
+ * @param {number|null} absPos
1676
+ * @return {boolean}
1677
+ */
1678
+ const isMisresolvedTextPosition = (y, relPos, absPos) => {
1679
+ if (absPos === null) {
1680
+ return false
1681
+ }
1682
+ const decoded = Y.createAbsolutePositionFromRelativePosition(relPos, y);
1683
+ return decoded !== null &&
1684
+ decoded.type instanceof Y.XmlText &&
1685
+ relPos.item !== null &&
1686
+ absPos <= 1
1687
+ };
1688
+
1638
1689
  const createRelativePosition = (type, item) => {
1639
1690
  let typeid = null;
1640
1691
  let tname = null;
@@ -1712,7 +1763,133 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
1712
1763
  }
1713
1764
  type = /** @type {Y.AbstractType} */ (parent);
1714
1765
  }
1715
- return pos - 1 // we don't count the most outer tag, because it is a fragment
1766
+ const absPos = pos - 1; // we don't count the most outer tag, because it is a fragment
1767
+ if (isMisresolvedTextPosition(y, relPos, absPos)) {
1768
+ return null
1769
+ }
1770
+ return absPos
1771
+ };
1772
+
1773
+ /**
1774
+ * @param {import('prosemirror-model').Node} oldDoc
1775
+ * @param {import('prosemirror-model').Node} newDoc
1776
+ * @param {number} absPos
1777
+ * @return {number|null}
1778
+ */
1779
+ const findAbsolutePositionAfterStructuralChange = (oldDoc, newDoc, absPos) => {
1780
+ let pos = 0;
1781
+ let targetIdx = 0;
1782
+ for (; targetIdx < oldDoc.childCount; targetIdx++) {
1783
+ const child = oldDoc.child(targetIdx);
1784
+ if (pos + child.nodeSize > absPos) {
1785
+ break
1786
+ }
1787
+ pos += child.nodeSize;
1788
+ }
1789
+ if (targetIdx >= oldDoc.childCount) {
1790
+ return null
1791
+ }
1792
+ const targetChild = oldDoc.child(targetIdx);
1793
+ const offsetInChild = absPos - pos;
1794
+
1795
+ let occurrence = 0;
1796
+ for (let i = 0; i <= targetIdx; i++) {
1797
+ const child = oldDoc.child(i);
1798
+ if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
1799
+ occurrence++;
1800
+ }
1801
+ }
1802
+
1803
+ let matchCount = 0;
1804
+ let newPos = 0;
1805
+ for (let i = 0; i < newDoc.childCount; i++) {
1806
+ const child = newDoc.child(i);
1807
+ if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
1808
+ matchCount++;
1809
+ if (matchCount === occurrence) {
1810
+ const remapped = newPos + offsetInChild;
1811
+ const contentStart = newPos + 1;
1812
+ const contentEnd = newPos + child.nodeSize - 1;
1813
+ return Math.max(contentStart, Math.min(remapped, contentEnd))
1814
+ }
1815
+ }
1816
+ newPos += child.nodeSize;
1817
+ }
1818
+ return null
1819
+ };
1820
+
1821
+ /**
1822
+ * Returns true when a transaction changes block structure rather than only
1823
+ * editing inline content inside existing blocks.
1824
+ *
1825
+ * @param {import('prosemirror-state').Transaction} tr
1826
+ * @param {import('prosemirror-model').Node} oldDoc
1827
+ * @return {boolean}
1828
+ */
1829
+ const isStructuralTransaction = (tr, oldDoc) => {
1830
+ if (!tr.docChanged) {
1831
+ return false
1832
+ }
1833
+ if (tr.doc.childCount !== oldDoc.childCount) {
1834
+ return true
1835
+ }
1836
+ for (const step of tr.steps) {
1837
+ if (step instanceof ReplaceStep) {
1838
+ if (step.from === 0 && step.to === oldDoc.content.size) {
1839
+ return true
1840
+ }
1841
+ if (step.slice.content.size > 0) {
1842
+ let hasBlock = false;
1843
+ step.slice.content.forEach((node) => {
1844
+ if (node.isBlock) {
1845
+ hasBlock = true;
1846
+ }
1847
+ });
1848
+ if (hasBlock) {
1849
+ return true
1850
+ }
1851
+ } else if (step.to > step.from) {
1852
+ const $from = oldDoc.resolve(step.from);
1853
+ const $to = oldDoc.resolve(step.to);
1854
+ if ($from.depth === 0 && $to.depth === 0 && $from.index() !== $to.index()) {
1855
+ return true
1856
+ }
1857
+ }
1858
+ }
1859
+ }
1860
+ return false
1861
+ };
1862
+
1863
+ /**
1864
+ * Detect stale relative positions after structural changes that resolve to the
1865
+ * wrong text block or to the start of the correct block.
1866
+ *
1867
+ * @param {import('prosemirror-model').Node} oldDoc
1868
+ * @param {import('prosemirror-model').Node} newDoc
1869
+ * @param {number} oldAbs
1870
+ * @param {number|null} resolvedAbs
1871
+ * @return {boolean}
1872
+ */
1873
+ const isMisresolvedAfterStructuralChange = (oldDoc, newDoc, oldAbs, resolvedAbs) => {
1874
+ if (resolvedAbs === null) {
1875
+ return false
1876
+ }
1877
+ const $old = oldDoc.resolve(oldAbs);
1878
+ const $new = newDoc.resolve(resolvedAbs);
1879
+ if (!$old.parent.isTextblock || !$new.parent.isTextblock) {
1880
+ return false
1881
+ }
1882
+ if ($old.parent.textContent !== $new.parent.textContent) {
1883
+ return true
1884
+ }
1885
+ if ($old.parentOffset !== 0 && $new.parentOffset === 0) {
1886
+ return true
1887
+ }
1888
+ if ($old.parentOffset === 0 && $new.parentOffset === 0) {
1889
+ const expected = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
1890
+ return expected !== null && expected !== resolvedAbs
1891
+ }
1892
+ return false
1716
1893
  };
1717
1894
 
1718
1895
  /**
@@ -2131,7 +2308,7 @@ const yCursorPlugin = (
2131
2308
  selectionBuilder
2132
2309
  )
2133
2310
  },
2134
- apply (tr, prevState, _oldState, newState) {
2311
+ apply (tr, prevState, oldState, newState) {
2135
2312
  const ystate = ySyncPluginKey.getState(newState);
2136
2313
  const yCursorState = tr.getMeta(yCursorPluginKey);
2137
2314
  if (
@@ -2146,7 +2323,19 @@ const yCursorPlugin = (
2146
2323
  selectionBuilder
2147
2324
  )
2148
2325
  }
2149
- return prevState.map(tr.mapping, tr.doc)
2326
+ if (tr.docChanged) {
2327
+ if (isStructuralTransaction(tr, oldState.doc)) {
2328
+ return createDecorations(
2329
+ newState,
2330
+ awareness,
2331
+ awarenessStateFilter,
2332
+ cursorBuilder,
2333
+ selectionBuilder
2334
+ )
2335
+ }
2336
+ return prevState.map(tr.mapping, tr.doc)
2337
+ }
2338
+ return prevState
2150
2339
  }
2151
2340
  },
2152
2341
  props: {
@@ -2161,15 +2350,19 @@ const yCursorPlugin = (
2161
2350
  setMeta(view, yCursorPluginKey, { awarenessUpdated: true });
2162
2351
  }
2163
2352
  };
2164
- const updateCursorInfo = () => {
2165
- const ystate = ySyncPluginKey.getState(view.state);
2353
+ /**
2354
+ * @param {import('prosemirror-view').EditorView} editorView
2355
+ * @param {{ force?: boolean }} [opts]
2356
+ */
2357
+ const updateCursorInfo = (editorView, opts = {}) => {
2358
+ const ystate = ySyncPluginKey.getState(editorView.state);
2166
2359
  // `ystate` is undefined during sync-plugin init and Y.Doc transitions;
2167
2360
  // reading from it here would throw and break cursor tracking for the view.
2168
2361
  if (!ystate || !ystate.binding) return
2169
2362
  // @note We make implicit checks when checking for the cursor property
2170
2363
  const current = awareness.getLocalState() || {};
2171
- if (view.hasFocus()) {
2172
- const selection = getSelection(view.state);
2364
+ if (editorView.hasFocus()) {
2365
+ const selection = getSelection(editorView.state);
2173
2366
  /**
2174
2367
  * @type {Y.RelativePosition}
2175
2368
  */
@@ -2187,6 +2380,7 @@ const yCursorPlugin = (
2187
2380
  ystate.binding.mapping
2188
2381
  );
2189
2382
  if (
2383
+ opts.force ||
2190
2384
  current.cursor == null ||
2191
2385
  !Y.compareRelativePositions(
2192
2386
  Y.createRelativePositionFromJSON(current.cursor.anchor),
@@ -2215,14 +2409,23 @@ const yCursorPlugin = (
2215
2409
  awareness.setLocalStateField(cursorStateField, null);
2216
2410
  }
2217
2411
  };
2412
+ const onFocusChange = () => updateCursorInfo(view);
2218
2413
  awareness.on('change', awarenessListener);
2219
- view.dom.addEventListener('focusin', updateCursorInfo);
2220
- view.dom.addEventListener('focusout', updateCursorInfo);
2414
+ view.dom.addEventListener('focusin', onFocusChange);
2415
+ view.dom.addEventListener('focusout', onFocusChange);
2221
2416
  return {
2222
- update: updateCursorInfo,
2417
+ update: (editorView, prevState) => {
2418
+ const ystate = ySyncPluginKey.getState(editorView.state);
2419
+ const forceAwarenessUpdate = !!(
2420
+ ystate?.isChangeOrigin &&
2421
+ prevState?.doc &&
2422
+ !prevState.doc.eq(editorView.state.doc)
2423
+ );
2424
+ updateCursorInfo(editorView, { force: forceAwarenessUpdate });
2425
+ },
2223
2426
  destroy: () => {
2224
- view.dom.removeEventListener('focusin', updateCursorInfo);
2225
- view.dom.removeEventListener('focusout', updateCursorInfo);
2427
+ view.dom.removeEventListener('focusin', onFocusChange);
2428
+ view.dom.removeEventListener('focusout', onFocusChange);
2226
2429
  awareness.off('change', awarenessListener);
2227
2430
  awareness.setLocalStateField(cursorStateField, null);
2228
2431
  }
@@ -2344,5 +2547,5 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
2344
2547
  }
2345
2548
  });
2346
2549
 
2347
- 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 };
2550
+ export { ProsemirrorBinding, absolutePositionToRelativePosition, createDecorations, defaultAwarenessStateFilter, defaultCursorBuilder, defaultDeleteFilter, defaultProtectedNodes, defaultSelectionBuilder, findAbsolutePositionAfterStructuralChange, getRelativeSelection, initProseMirrorDoc, isMisresolvedAfterStructuralChange, isMisresolvedTextPosition, isStructuralTransaction, isVisible, prosemirrorJSONToYDoc, prosemirrorJSONToYXmlFragment, prosemirrorToYDoc, prosemirrorToYXmlFragment, redo, relativePositionToAbsolutePosition, setMeta, undo, updateYFragment, yCursorPlugin, yCursorPluginKey, yDocToProsemirror, yDocToProsemirrorJSON, ySyncPlugin, ySyncPluginKey, yUndoPlugin, yUndoPluginKey, yXmlFragmentToProseMirrorFragment, yXmlFragmentToProseMirrorRootNode, yXmlFragmentToProsemirror, yXmlFragmentToProsemirrorJSON };
2348
2551
  //# sourceMappingURL=y-tiptap.js.map