@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/src/lib.d.ts CHANGED
@@ -96,7 +96,11 @@ export function yDocToProsemirrorJSON(ydoc: Y.Doc, xmlFragment?: string): Record
96
96
  export function yXmlFragmentToProsemirrorJSON(xmlFragment: Y.XmlFragment): Record<string, any>;
97
97
  export function setMeta(view: any, key: any, value: any): void;
98
98
  export function absolutePositionToRelativePosition(pos: number, type: Y.XmlFragment, mapping: ProsemirrorMapping): any;
99
+ export function isMisresolvedTextPosition(y: Y.Doc, relPos: Y.RelativePosition, absPos: number | null): boolean;
99
100
  export function relativePositionToAbsolutePosition(y: Y.Doc, documentType: Y.XmlFragment, relPos: any, mapping: ProsemirrorMapping): null | number;
101
+ export function findAbsolutePositionAfterStructuralChange(oldDoc: import('prosemirror-model').Node, newDoc: import('prosemirror-model').Node, absPos: number): number | null;
102
+ export function isStructuralTransaction(tr: import('prosemirror-state').Transaction, oldDoc: import('prosemirror-model').Node): boolean;
103
+ export function isMisresolvedAfterStructuralChange(oldDoc: import('prosemirror-model').Node, newDoc: import('prosemirror-model').Node, oldAbs: number, resolvedAbs: number | null): boolean;
100
104
  export function yXmlFragmentToProseMirrorFragment(yXmlFragment: Y.XmlFragment, schema: Schema): Fragment;
101
105
  export function yXmlFragmentToProseMirrorRootNode(yXmlFragment: Y.XmlFragment, schema: Schema): Node;
102
106
  export function initProseMirrorDoc(yXmlFragment: Y.XmlFragment, schema: Schema): {
@@ -9,6 +9,8 @@ export function getRelativeSelection(pmbinding: ProsemirrorBinding, state: impor
9
9
  depth: any;
10
10
  anchor: any;
11
11
  head: any;
12
+ absAnchor: number;
13
+ absHead: number;
12
14
  };
13
15
  /**
14
16
  * Binding for prosemirror.
@@ -48,6 +50,8 @@ export class ProsemirrorBinding {
48
50
  depth: any;
49
51
  anchor: any;
50
52
  head: any;
53
+ absAnchor: number;
54
+ absHead: number;
51
55
  };
52
56
  beforeAllTransactions: () => void;
53
57
  afterAllTransactions: () => void;
@@ -2,4 +2,4 @@ export * from "./plugins/cursor-plugin.js";
2
2
  export * from "./plugins/undo-plugin.js";
3
3
  export * from "./plugins/keys.js";
4
4
  export { ySyncPlugin, isVisible, getRelativeSelection, ProsemirrorBinding, updateYFragment } from "./plugins/sync-plugin.js";
5
- export { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, setMeta, prosemirrorJSONToYDoc, yDocToProsemirrorJSON, yDocToProsemirror, prosemirrorToYDoc, prosemirrorJSONToYXmlFragment, yXmlFragmentToProsemirrorJSON, yXmlFragmentToProsemirror, prosemirrorToYXmlFragment, yXmlFragmentToProseMirrorRootNode, yXmlFragmentToProseMirrorFragment, initProseMirrorDoc } from "./lib.js";
5
+ export { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, setMeta, findAbsolutePositionAfterStructuralChange, isMisresolvedTextPosition, isMisresolvedAfterStructuralChange, isStructuralTransaction, prosemirrorJSONToYDoc, yDocToProsemirrorJSON, yDocToProsemirror, prosemirrorToYDoc, prosemirrorJSONToYXmlFragment, yXmlFragmentToProsemirrorJSON, yXmlFragmentToProsemirror, prosemirrorToYXmlFragment, yXmlFragmentToProseMirrorRootNode, yXmlFragmentToProseMirrorFragment, initProseMirrorDoc } from "./lib.js";
package/dist/y-tiptap.cjs CHANGED
@@ -20,6 +20,7 @@ var eventloop = require('lib0/eventloop');
20
20
  var map = require('lib0/map');
21
21
  var sha256 = require('lib0/hash/sha256');
22
22
  var buf = require('lib0/buffer');
23
+ var prosemirrorTransform = require('prosemirror-transform');
23
24
 
24
25
  function _interopNamespace(e) {
25
26
  if (e && e.__esModule) return e;
@@ -318,8 +319,9 @@ const ySyncPlugin = (yXmlFragment, {
318
319
  * @param {import('prosemirror-state').Transaction} tr
319
320
  * @param {ReturnType<typeof getRelativeSelection>} relSel
320
321
  * @param {ProsemirrorBinding} binding
322
+ * @param {import('prosemirror-model').Node} [oldDoc]
321
323
  */
322
- const restoreRelativeSelection = (tr, relSel, binding) => {
324
+ const restoreRelativeSelection = (tr, relSel, binding, oldDoc) => {
323
325
  if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
324
326
  if (relSel.type === 'all') {
325
327
  tr.setSelection(new prosemirrorState.AllSelection(tr.doc));
@@ -353,18 +355,41 @@ const restoreRelativeSelection = (tr, relSel, binding) => {
353
355
  tr.setSelection(selection);
354
356
  }
355
357
  } else {
356
- const anchor = relativePositionToAbsolutePosition(
358
+ let anchor = relativePositionToAbsolutePosition(
357
359
  binding.doc,
358
360
  binding.type,
359
361
  relSel.anchor,
360
362
  binding.mapping
361
363
  );
362
- const head = relativePositionToAbsolutePosition(
364
+ let head = relativePositionToAbsolutePosition(
363
365
  binding.doc,
364
366
  binding.type,
365
367
  relSel.head,
366
368
  binding.mapping
367
369
  );
370
+ const needsContentFallback = oldDoc != null &&
371
+ relSel.absAnchor != null &&
372
+ relSel.absHead != null &&
373
+ (
374
+ anchor === null ||
375
+ head === null ||
376
+ (relSel.absAnchor > 1 && anchor !== null && anchor <= 1) ||
377
+ (relSel.absHead > 1 && head !== null && head <= 1) ||
378
+ isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absAnchor, anchor) ||
379
+ isMisresolvedAfterStructuralChange(oldDoc, tr.doc, relSel.absHead, head)
380
+ );
381
+ if (needsContentFallback) {
382
+ anchor = findAbsolutePositionAfterStructuralChange(
383
+ oldDoc,
384
+ tr.doc,
385
+ relSel.absAnchor
386
+ );
387
+ head = findAbsolutePositionAfterStructuralChange(
388
+ oldDoc,
389
+ tr.doc,
390
+ relSel.absHead
391
+ );
392
+ }
368
393
  if (anchor !== null && head !== null) {
369
394
  tr.setSelection(prosemirrorState.TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head)));
370
395
  }
@@ -445,7 +470,9 @@ const getRelativeSelection = (pmbinding, state) => {
445
470
  state.selection.head,
446
471
  pmbinding.type,
447
472
  pmbinding.mapping
448
- )
473
+ ),
474
+ absAnchor: state.selection.anchor,
475
+ absHead: state.selection.head
449
476
  }
450
477
  };
451
478
 
@@ -770,6 +797,9 @@ class ProsemirrorBinding {
770
797
  );
771
798
  transaction.changed.forEach(delType);
772
799
  transaction.changedParentTypes.forEach(delType);
800
+ // Rebuild the full Y↔PM mapping so relative cursor positions resolve against
801
+ // current node sizes after structural changes (e.g. drag-and-drop block moves).
802
+ this.mapping.clear();
773
803
  const fragmentContent = this.type.toArray().map((t) =>
774
804
  createNodeIfNotExists(
775
805
  /** @type {Y.XmlElement | Y.XmlHook} */ (t),
@@ -777,13 +807,14 @@ class ProsemirrorBinding {
777
807
  this
778
808
  )
779
809
  ).filter((n) => n !== null);
810
+ const oldDoc = this.prosemirrorView.state.doc;
780
811
  // @ts-ignore
781
812
  let tr = this._tr.replace(
782
813
  0,
783
814
  this.prosemirrorView.state.doc.content.size,
784
815
  new PModel__namespace.Slice(PModel__namespace.Fragment.from(fragmentContent), 0, 0)
785
816
  );
786
- restoreRelativeSelection(tr, this.beforeTransactionSelection, this);
817
+ restoreRelativeSelection(tr, this.beforeTransactionSelection, this, oldDoc);
787
818
  tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, isUndoRedoOperation: transaction.origin instanceof Y__namespace.UndoManager });
788
819
  if (
789
820
  this.beforeTransactionSelection !== null && this._isLocalCursorInView()
@@ -1669,6 +1700,26 @@ const absolutePositionToRelativePosition = (pos, type, mapping) => {
1669
1700
  return Y__namespace.createRelativePositionFromTypeIndex(type, type._length, -1)
1670
1701
  };
1671
1702
 
1703
+ /**
1704
+ * Item-id based relative positions can misresolve to the document start after
1705
+ * block reorder during collaborative drag-and-drop.
1706
+ *
1707
+ * @param {Y.Doc} y
1708
+ * @param {Y.RelativePosition} relPos
1709
+ * @param {number|null} absPos
1710
+ * @return {boolean}
1711
+ */
1712
+ const isMisresolvedTextPosition = (y, relPos, absPos) => {
1713
+ if (absPos === null) {
1714
+ return false
1715
+ }
1716
+ const decoded = Y__namespace.createAbsolutePositionFromRelativePosition(relPos, y);
1717
+ return decoded !== null &&
1718
+ decoded.type instanceof Y__namespace.XmlText &&
1719
+ relPos.item !== null &&
1720
+ absPos <= 1
1721
+ };
1722
+
1672
1723
  const createRelativePosition = (type, item) => {
1673
1724
  let typeid = null;
1674
1725
  let tname = null;
@@ -1746,7 +1797,133 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
1746
1797
  }
1747
1798
  type = /** @type {Y.AbstractType} */ (parent);
1748
1799
  }
1749
- return pos - 1 // we don't count the most outer tag, because it is a fragment
1800
+ const absPos = pos - 1; // we don't count the most outer tag, because it is a fragment
1801
+ if (isMisresolvedTextPosition(y, relPos, absPos)) {
1802
+ return null
1803
+ }
1804
+ return absPos
1805
+ };
1806
+
1807
+ /**
1808
+ * @param {import('prosemirror-model').Node} oldDoc
1809
+ * @param {import('prosemirror-model').Node} newDoc
1810
+ * @param {number} absPos
1811
+ * @return {number|null}
1812
+ */
1813
+ const findAbsolutePositionAfterStructuralChange = (oldDoc, newDoc, absPos) => {
1814
+ let pos = 0;
1815
+ let targetIdx = 0;
1816
+ for (; targetIdx < oldDoc.childCount; targetIdx++) {
1817
+ const child = oldDoc.child(targetIdx);
1818
+ if (pos + child.nodeSize > absPos) {
1819
+ break
1820
+ }
1821
+ pos += child.nodeSize;
1822
+ }
1823
+ if (targetIdx >= oldDoc.childCount) {
1824
+ return null
1825
+ }
1826
+ const targetChild = oldDoc.child(targetIdx);
1827
+ const offsetInChild = absPos - pos;
1828
+
1829
+ let occurrence = 0;
1830
+ for (let i = 0; i <= targetIdx; i++) {
1831
+ const child = oldDoc.child(i);
1832
+ if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
1833
+ occurrence++;
1834
+ }
1835
+ }
1836
+
1837
+ let matchCount = 0;
1838
+ let newPos = 0;
1839
+ for (let i = 0; i < newDoc.childCount; i++) {
1840
+ const child = newDoc.child(i);
1841
+ if (child.type === targetChild.type && child.textContent === targetChild.textContent) {
1842
+ matchCount++;
1843
+ if (matchCount === occurrence) {
1844
+ const remapped = newPos + offsetInChild;
1845
+ const contentStart = newPos + 1;
1846
+ const contentEnd = newPos + child.nodeSize - 1;
1847
+ return Math.max(contentStart, Math.min(remapped, contentEnd))
1848
+ }
1849
+ }
1850
+ newPos += child.nodeSize;
1851
+ }
1852
+ return null
1853
+ };
1854
+
1855
+ /**
1856
+ * Returns true when a transaction changes block structure rather than only
1857
+ * editing inline content inside existing blocks.
1858
+ *
1859
+ * @param {import('prosemirror-state').Transaction} tr
1860
+ * @param {import('prosemirror-model').Node} oldDoc
1861
+ * @return {boolean}
1862
+ */
1863
+ const isStructuralTransaction = (tr, oldDoc) => {
1864
+ if (!tr.docChanged) {
1865
+ return false
1866
+ }
1867
+ if (tr.doc.childCount !== oldDoc.childCount) {
1868
+ return true
1869
+ }
1870
+ for (const step of tr.steps) {
1871
+ if (step instanceof prosemirrorTransform.ReplaceStep) {
1872
+ if (step.from === 0 && step.to === oldDoc.content.size) {
1873
+ return true
1874
+ }
1875
+ if (step.slice.content.size > 0) {
1876
+ let hasBlock = false;
1877
+ step.slice.content.forEach((node) => {
1878
+ if (node.isBlock) {
1879
+ hasBlock = true;
1880
+ }
1881
+ });
1882
+ if (hasBlock) {
1883
+ return true
1884
+ }
1885
+ } else if (step.to > step.from) {
1886
+ const $from = oldDoc.resolve(step.from);
1887
+ const $to = oldDoc.resolve(step.to);
1888
+ if ($from.depth === 0 && $to.depth === 0 && $from.index() !== $to.index()) {
1889
+ return true
1890
+ }
1891
+ }
1892
+ }
1893
+ }
1894
+ return false
1895
+ };
1896
+
1897
+ /**
1898
+ * Detect stale relative positions after structural changes that resolve to the
1899
+ * wrong text block or to the start of the correct block.
1900
+ *
1901
+ * @param {import('prosemirror-model').Node} oldDoc
1902
+ * @param {import('prosemirror-model').Node} newDoc
1903
+ * @param {number} oldAbs
1904
+ * @param {number|null} resolvedAbs
1905
+ * @return {boolean}
1906
+ */
1907
+ const isMisresolvedAfterStructuralChange = (oldDoc, newDoc, oldAbs, resolvedAbs) => {
1908
+ if (resolvedAbs === null) {
1909
+ return false
1910
+ }
1911
+ const $old = oldDoc.resolve(oldAbs);
1912
+ const $new = newDoc.resolve(resolvedAbs);
1913
+ if (!$old.parent.isTextblock || !$new.parent.isTextblock) {
1914
+ return false
1915
+ }
1916
+ if ($old.parent.textContent !== $new.parent.textContent) {
1917
+ return true
1918
+ }
1919
+ if ($old.parentOffset !== 0 && $new.parentOffset === 0) {
1920
+ return true
1921
+ }
1922
+ if ($old.parentOffset === 0 && $new.parentOffset === 0) {
1923
+ const expected = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
1924
+ return expected !== null && expected !== resolvedAbs
1925
+ }
1926
+ return false
1750
1927
  };
1751
1928
 
1752
1929
  /**
@@ -2165,7 +2342,7 @@ const yCursorPlugin = (
2165
2342
  selectionBuilder
2166
2343
  )
2167
2344
  },
2168
- apply (tr, prevState, _oldState, newState) {
2345
+ apply (tr, prevState, oldState, newState) {
2169
2346
  const ystate = ySyncPluginKey.getState(newState);
2170
2347
  const yCursorState = tr.getMeta(yCursorPluginKey);
2171
2348
  if (
@@ -2180,7 +2357,19 @@ const yCursorPlugin = (
2180
2357
  selectionBuilder
2181
2358
  )
2182
2359
  }
2183
- return prevState.map(tr.mapping, tr.doc)
2360
+ if (tr.docChanged) {
2361
+ if (isStructuralTransaction(tr, oldState.doc)) {
2362
+ return createDecorations(
2363
+ newState,
2364
+ awareness,
2365
+ awarenessStateFilter,
2366
+ cursorBuilder,
2367
+ selectionBuilder
2368
+ )
2369
+ }
2370
+ return prevState.map(tr.mapping, tr.doc)
2371
+ }
2372
+ return prevState
2184
2373
  }
2185
2374
  },
2186
2375
  props: {
@@ -2195,15 +2384,19 @@ const yCursorPlugin = (
2195
2384
  setMeta(view, yCursorPluginKey, { awarenessUpdated: true });
2196
2385
  }
2197
2386
  };
2198
- const updateCursorInfo = () => {
2199
- const ystate = ySyncPluginKey.getState(view.state);
2387
+ /**
2388
+ * @param {import('prosemirror-view').EditorView} editorView
2389
+ * @param {{ force?: boolean }} [opts]
2390
+ */
2391
+ const updateCursorInfo = (editorView, opts = {}) => {
2392
+ const ystate = ySyncPluginKey.getState(editorView.state);
2200
2393
  // `ystate` is undefined during sync-plugin init and Y.Doc transitions;
2201
2394
  // reading from it here would throw and break cursor tracking for the view.
2202
2395
  if (!ystate || !ystate.binding) return
2203
2396
  // @note We make implicit checks when checking for the cursor property
2204
2397
  const current = awareness.getLocalState() || {};
2205
- if (view.hasFocus()) {
2206
- const selection = getSelection(view.state);
2398
+ if (editorView.hasFocus()) {
2399
+ const selection = getSelection(editorView.state);
2207
2400
  /**
2208
2401
  * @type {Y.RelativePosition}
2209
2402
  */
@@ -2221,6 +2414,7 @@ const yCursorPlugin = (
2221
2414
  ystate.binding.mapping
2222
2415
  );
2223
2416
  if (
2417
+ opts.force ||
2224
2418
  current.cursor == null ||
2225
2419
  !Y__namespace.compareRelativePositions(
2226
2420
  Y__namespace.createRelativePositionFromJSON(current.cursor.anchor),
@@ -2249,14 +2443,23 @@ const yCursorPlugin = (
2249
2443
  awareness.setLocalStateField(cursorStateField, null);
2250
2444
  }
2251
2445
  };
2446
+ const onFocusChange = () => updateCursorInfo(view);
2252
2447
  awareness.on('change', awarenessListener);
2253
- view.dom.addEventListener('focusin', updateCursorInfo);
2254
- view.dom.addEventListener('focusout', updateCursorInfo);
2448
+ view.dom.addEventListener('focusin', onFocusChange);
2449
+ view.dom.addEventListener('focusout', onFocusChange);
2255
2450
  return {
2256
- update: updateCursorInfo,
2451
+ update: (editorView, prevState) => {
2452
+ const ystate = ySyncPluginKey.getState(editorView.state);
2453
+ const forceAwarenessUpdate = !!(
2454
+ ystate?.isChangeOrigin &&
2455
+ prevState?.doc &&
2456
+ !prevState.doc.eq(editorView.state.doc)
2457
+ );
2458
+ updateCursorInfo(editorView, { force: forceAwarenessUpdate });
2459
+ },
2257
2460
  destroy: () => {
2258
- view.dom.removeEventListener('focusin', updateCursorInfo);
2259
- view.dom.removeEventListener('focusout', updateCursorInfo);
2461
+ view.dom.removeEventListener('focusin', onFocusChange);
2462
+ view.dom.removeEventListener('focusout', onFocusChange);
2260
2463
  awareness.off('change', awarenessListener);
2261
2464
  awareness.setLocalStateField(cursorStateField, null);
2262
2465
  }
@@ -2386,8 +2589,12 @@ exports.defaultCursorBuilder = defaultCursorBuilder;
2386
2589
  exports.defaultDeleteFilter = defaultDeleteFilter;
2387
2590
  exports.defaultProtectedNodes = defaultProtectedNodes;
2388
2591
  exports.defaultSelectionBuilder = defaultSelectionBuilder;
2592
+ exports.findAbsolutePositionAfterStructuralChange = findAbsolutePositionAfterStructuralChange;
2389
2593
  exports.getRelativeSelection = getRelativeSelection;
2390
2594
  exports.initProseMirrorDoc = initProseMirrorDoc;
2595
+ exports.isMisresolvedAfterStructuralChange = isMisresolvedAfterStructuralChange;
2596
+ exports.isMisresolvedTextPosition = isMisresolvedTextPosition;
2597
+ exports.isStructuralTransaction = isStructuralTransaction;
2391
2598
  exports.isVisible = isVisible;
2392
2599
  exports.prosemirrorJSONToYDoc = prosemirrorJSONToYDoc;
2393
2600
  exports.prosemirrorJSONToYXmlFragment = prosemirrorJSONToYXmlFragment;