@tiptap/y-tiptap 3.0.6 → 3.0.7
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.cjs +215 -54
- package/dist/y-tiptap.cjs.map +1 -1
- package/dist/y-tiptap.js +215 -54
- package/dist/y-tiptap.js.map +1 -1
- package/package.json +1 -1
package/dist/y-tiptap.js
CHANGED
|
@@ -281,6 +281,31 @@ const ySyncPlugin = (yXmlFragment, {
|
|
|
281
281
|
return plugin
|
|
282
282
|
};
|
|
283
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Resolves one text-selection endpoint after a remote update. Falls back to
|
|
286
|
+
* content-based matching when the Yjs resolution is missing or lands in the
|
|
287
|
+
* wrong block, and keeps the Yjs resolution when the fallback finds nothing.
|
|
288
|
+
*
|
|
289
|
+
* @param {import('prosemirror-model').Node} newDoc
|
|
290
|
+
* @param {import('prosemirror-model').Node} oldDoc
|
|
291
|
+
* @param {number|null|undefined} oldAbs
|
|
292
|
+
* @param {number|null} resolved
|
|
293
|
+
* @return {number|null}
|
|
294
|
+
*/
|
|
295
|
+
const recoverSelectionEndpoint = (newDoc, oldDoc, oldAbs, resolved) => {
|
|
296
|
+
if (oldAbs == null) {
|
|
297
|
+
return resolved
|
|
298
|
+
}
|
|
299
|
+
const misresolved = resolved === null ||
|
|
300
|
+
(oldAbs > 1 && resolved <= 1) ||
|
|
301
|
+
isMisresolvedAfterStructuralChange(oldDoc, newDoc, oldAbs, resolved);
|
|
302
|
+
if (!misresolved) {
|
|
303
|
+
return resolved
|
|
304
|
+
}
|
|
305
|
+
const recovered = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
|
|
306
|
+
return recovered !== null ? recovered : resolved
|
|
307
|
+
};
|
|
308
|
+
|
|
284
309
|
/**
|
|
285
310
|
* @param {import('prosemirror-state').Transaction} tr
|
|
286
311
|
* @param {ReturnType<typeof getRelativeSelection>} relSel
|
|
@@ -333,28 +358,17 @@ const restoreRelativeSelection = (tr, relSel, binding, oldDoc) => {
|
|
|
333
358
|
relSel.head,
|
|
334
359
|
binding.mapping
|
|
335
360
|
);
|
|
336
|
-
|
|
337
|
-
relSel.absAnchor
|
|
338
|
-
relSel.absHead
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
);
|
|
361
|
+
if (oldDoc != null) {
|
|
362
|
+
anchor = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absAnchor, anchor);
|
|
363
|
+
head = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absHead, head);
|
|
364
|
+
}
|
|
365
|
+
// Collapse to the surviving endpoint instead of dropping the selection;
|
|
366
|
+
// an unset selection maps through the full-doc replace to the doc start.
|
|
367
|
+
if (anchor === null) {
|
|
368
|
+
anchor = head;
|
|
369
|
+
}
|
|
370
|
+
if (head === null) {
|
|
371
|
+
head = anchor;
|
|
358
372
|
}
|
|
359
373
|
if (anchor !== null && head !== null) {
|
|
360
374
|
tr.setSelection(TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head)));
|
|
@@ -1770,6 +1784,73 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
|
|
|
1770
1784
|
return absPos
|
|
1771
1785
|
};
|
|
1772
1786
|
|
|
1787
|
+
/**
|
|
1788
|
+
* Shallow attrs comparison. Attr values are primitives in most schemas;
|
|
1789
|
+
* non-primitive values fail the check and callers fall back to text matching.
|
|
1790
|
+
*
|
|
1791
|
+
* @param {Object<string, any>} a
|
|
1792
|
+
* @param {Object<string, any>} b
|
|
1793
|
+
* @return {boolean}
|
|
1794
|
+
*/
|
|
1795
|
+
const attrsEqual = (a, b) => {
|
|
1796
|
+
if (a === b) {
|
|
1797
|
+
return true
|
|
1798
|
+
}
|
|
1799
|
+
const aKeys = Object.keys(a);
|
|
1800
|
+
return aKeys.length === Object.keys(b).length && aKeys.every((k) => a[k] === b[k])
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Returns true when any attr deviates from its spec default or has none.
|
|
1805
|
+
* Default-only attrs cannot tell same-type siblings apart.
|
|
1806
|
+
*
|
|
1807
|
+
* @param {import('prosemirror-model').Node} node
|
|
1808
|
+
* @return {boolean}
|
|
1809
|
+
*/
|
|
1810
|
+
const hasDistinctiveAttrs = (node) => {
|
|
1811
|
+
const specAttrs = node.type.spec.attrs || {};
|
|
1812
|
+
return Object.keys(node.attrs).some((key) => {
|
|
1813
|
+
const spec = specAttrs[key];
|
|
1814
|
+
return spec == null ||
|
|
1815
|
+
!Object.prototype.hasOwnProperty.call(spec, 'default') ||
|
|
1816
|
+
spec.default !== node.attrs[key]
|
|
1817
|
+
})
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
/**
|
|
1821
|
+
* Remaps a position into a matched block by walking the same child-index path
|
|
1822
|
+
* it had in the old block. A raw byte offset would overshoot into a sibling
|
|
1823
|
+
* inner textblock when the old block contains local keystrokes that are not
|
|
1824
|
+
* yet part of the rebuilt document.
|
|
1825
|
+
*
|
|
1826
|
+
* @param {import('prosemirror-model').ResolvedPos} $oldPos
|
|
1827
|
+
* @param {number} newBlockStart
|
|
1828
|
+
* @param {import('prosemirror-model').Node} newBlock
|
|
1829
|
+
* @return {number|null}
|
|
1830
|
+
*/
|
|
1831
|
+
const remapIntoBlock = ($oldPos, newBlockStart, newBlock) => {
|
|
1832
|
+
let pos = newBlockStart + 1;
|
|
1833
|
+
let node = newBlock;
|
|
1834
|
+
for (let depth = 1; depth < $oldPos.depth; depth++) {
|
|
1835
|
+
const idx = $oldPos.index(depth);
|
|
1836
|
+
if (idx >= node.childCount) {
|
|
1837
|
+
return null
|
|
1838
|
+
}
|
|
1839
|
+
for (let i = 0; i < idx; i++) {
|
|
1840
|
+
pos += node.child(i).nodeSize;
|
|
1841
|
+
}
|
|
1842
|
+
pos += 1;
|
|
1843
|
+
node = node.child(idx);
|
|
1844
|
+
if (node.type !== $oldPos.node(depth + 1).type) {
|
|
1845
|
+
return null
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (!node.isTextblock) {
|
|
1849
|
+
return null
|
|
1850
|
+
}
|
|
1851
|
+
return pos + Math.min($oldPos.parentOffset, node.content.size)
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1773
1854
|
/**
|
|
1774
1855
|
* @param {import('prosemirror-model').Node} oldDoc
|
|
1775
1856
|
* @param {import('prosemirror-model').Node} newDoc
|
|
@@ -1790,32 +1871,100 @@ const findAbsolutePositionAfterStructuralChange = (oldDoc, newDoc, absPos) => {
|
|
|
1790
1871
|
return null
|
|
1791
1872
|
}
|
|
1792
1873
|
const targetChild = oldDoc.child(targetIdx);
|
|
1793
|
-
const
|
|
1874
|
+
const $oldPos = oldDoc.resolve(absPos);
|
|
1794
1875
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1876
|
+
/**
|
|
1877
|
+
* @param {number} newBlockStart
|
|
1878
|
+
* @param {import('prosemirror-model').Node} newBlock
|
|
1879
|
+
* @return {number|null}
|
|
1880
|
+
*/
|
|
1881
|
+
const place = (newBlockStart, newBlock) => {
|
|
1882
|
+
// Positions between top-level blocks carry no inner path; clamp them just
|
|
1883
|
+
// inside the matched block like the previous raw-offset remap did.
|
|
1884
|
+
if ($oldPos.depth === 0) {
|
|
1885
|
+
const remapped = newBlockStart + (absPos - pos);
|
|
1886
|
+
const contentStart = newBlockStart + 1;
|
|
1887
|
+
const contentEnd = newBlockStart + newBlock.nodeSize - 1;
|
|
1888
|
+
return Math.max(contentStart, Math.min(remapped, contentEnd))
|
|
1800
1889
|
}
|
|
1801
|
-
|
|
1890
|
+
return remapIntoBlock($oldPos, newBlockStart, newBlock)
|
|
1891
|
+
};
|
|
1802
1892
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1893
|
+
/**
|
|
1894
|
+
* Finds the Nth block in newDoc matching `pred`, where N is the number of
|
|
1895
|
+
* matching blocks in oldDoc up to and including the target block.
|
|
1896
|
+
*
|
|
1897
|
+
* @param {function(import('prosemirror-model').Node): boolean} pred
|
|
1898
|
+
* @param {boolean} requireUnique
|
|
1899
|
+
* @return {number|null}
|
|
1900
|
+
*/
|
|
1901
|
+
const findByPredicate = (pred, requireUnique = false) => {
|
|
1902
|
+
let occurrence = 0;
|
|
1903
|
+
for (let i = 0; i <= targetIdx; i++) {
|
|
1904
|
+
if (pred(oldDoc.child(i))) {
|
|
1905
|
+
occurrence++;
|
|
1814
1906
|
}
|
|
1815
1907
|
}
|
|
1816
|
-
|
|
1908
|
+
let matchCount = 0;
|
|
1909
|
+
let matchStart = -1;
|
|
1910
|
+
let matchBlock = null;
|
|
1911
|
+
let newPos = 0;
|
|
1912
|
+
for (let i = 0; i < newDoc.childCount; i++) {
|
|
1913
|
+
const child = newDoc.child(i);
|
|
1914
|
+
if (pred(child)) {
|
|
1915
|
+
matchCount++;
|
|
1916
|
+
if (matchCount === occurrence) {
|
|
1917
|
+
matchStart = newPos;
|
|
1918
|
+
matchBlock = child;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
newPos += child.nodeSize;
|
|
1922
|
+
}
|
|
1923
|
+
if (matchBlock === null || (requireUnique && (occurrence !== 1 || matchCount !== 1))) {
|
|
1924
|
+
return null
|
|
1925
|
+
}
|
|
1926
|
+
return place(matchStart, matchBlock)
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
/**
|
|
1930
|
+
* @param {import('prosemirror-model').Node} child
|
|
1931
|
+
* @return {boolean}
|
|
1932
|
+
*/
|
|
1933
|
+
const sameTypeAndAttrs = (child) =>
|
|
1934
|
+
child.type === targetChild.type && attrsEqual(child.attrs, targetChild.attrs);
|
|
1935
|
+
const oldText = targetChild.textContent;
|
|
1936
|
+
|
|
1937
|
+
const byAll = findByPredicate((child) => sameTypeAndAttrs(child) && child.textContent === oldText);
|
|
1938
|
+
if (byAll !== null) {
|
|
1939
|
+
return byAll
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Text must be matched before attrs: after a remote attr-only edit, the
|
|
1943
|
+
// attrs pass would steer the cursor into a sibling that kept the old attrs.
|
|
1944
|
+
const byText = findByPredicate(
|
|
1945
|
+
(child) => child.type === targetChild.type && child.textContent === oldText
|
|
1946
|
+
);
|
|
1947
|
+
if (byText !== null) {
|
|
1948
|
+
return byText
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// In-flight local typing diverges the text between both docs. Distinctive
|
|
1952
|
+
// attrs still identify the block; default-only attrs match every sibling.
|
|
1953
|
+
if (hasDistinctiveAttrs(targetChild)) {
|
|
1954
|
+
const byAttrs = findByPredicate(sameTypeAndAttrs, true);
|
|
1955
|
+
if (byAttrs !== null) {
|
|
1956
|
+
return byAttrs
|
|
1957
|
+
}
|
|
1817
1958
|
}
|
|
1818
|
-
|
|
1959
|
+
|
|
1960
|
+
// Trailing in-flight keystrokes leave a prefix relation between old and new
|
|
1961
|
+
// text. Empty text is a prefix of everything and must never match.
|
|
1962
|
+
return findByPredicate(
|
|
1963
|
+
(child) => sameTypeAndAttrs(child) &&
|
|
1964
|
+
oldText !== '' && child.textContent !== '' &&
|
|
1965
|
+
(oldText.startsWith(child.textContent) || child.textContent.startsWith(oldText)),
|
|
1966
|
+
true
|
|
1967
|
+
)
|
|
1819
1968
|
};
|
|
1820
1969
|
|
|
1821
1970
|
/**
|
|
@@ -1876,16 +2025,27 @@ const isMisresolvedAfterStructuralChange = (oldDoc, newDoc, oldAbs, resolvedAbs)
|
|
|
1876
2025
|
}
|
|
1877
2026
|
const $old = oldDoc.resolve(oldAbs);
|
|
1878
2027
|
const $new = newDoc.resolve(resolvedAbs);
|
|
1879
|
-
if (!$old.parent.isTextblock
|
|
2028
|
+
if (!$old.parent.isTextblock) {
|
|
1880
2029
|
return false
|
|
1881
2030
|
}
|
|
2031
|
+
// A textblock cursor cannot legitimately resolve into a non-textblock;
|
|
2032
|
+
// a structural reorder replaced the block via delete + insert.
|
|
2033
|
+
if (!$new.parent.isTextblock) {
|
|
2034
|
+
return true
|
|
2035
|
+
}
|
|
1882
2036
|
if ($old.parent.textContent !== $new.parent.textContent) {
|
|
1883
2037
|
return true
|
|
1884
2038
|
}
|
|
1885
2039
|
if ($old.parentOffset !== 0 && $new.parentOffset === 0) {
|
|
1886
2040
|
return true
|
|
1887
2041
|
}
|
|
1888
|
-
|
|
2042
|
+
const bothAtStart = $old.parentOffset === 0 && $new.parentOffset === 0;
|
|
2043
|
+
// A changed offset, type or attrs hints at a same-text sibling. When all
|
|
2044
|
+
// of them agree there is no signal left and the Yjs resolution must win.
|
|
2045
|
+
const suspicious = $old.parentOffset !== $new.parentOffset ||
|
|
2046
|
+
$old.parent.type !== $new.parent.type ||
|
|
2047
|
+
!attrsEqual($old.parent.attrs, $new.parent.attrs);
|
|
2048
|
+
if (bothAtStart || suspicious) {
|
|
1889
2049
|
const expected = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
|
|
1890
2050
|
return expected !== null && expected !== resolvedAbs
|
|
1891
2051
|
}
|
|
@@ -2311,8 +2471,18 @@ const yCursorPlugin = (
|
|
|
2311
2471
|
apply (tr, prevState, oldState, newState) {
|
|
2312
2472
|
const ystate = ySyncPluginKey.getState(newState);
|
|
2313
2473
|
const yCursorState = tr.getMeta(yCursorPluginKey);
|
|
2474
|
+
const isRemoteChange = ystate && ystate.isChangeOrigin;
|
|
2314
2475
|
if (
|
|
2315
|
-
|
|
2476
|
+
tr.docChanged &&
|
|
2477
|
+
!isRemoteChange &&
|
|
2478
|
+
isStructuralTransaction(tr, oldState.doc)
|
|
2479
|
+
) {
|
|
2480
|
+
// The ProseMirror document leads the Yjs mapping during local moves.
|
|
2481
|
+
// Hide stale awareness until the collaborator publishes its new cursor.
|
|
2482
|
+
return DecorationSet.empty
|
|
2483
|
+
}
|
|
2484
|
+
if (
|
|
2485
|
+
isRemoteChange ||
|
|
2316
2486
|
(yCursorState && yCursorState.awarenessUpdated)
|
|
2317
2487
|
) {
|
|
2318
2488
|
return createDecorations(
|
|
@@ -2324,15 +2494,6 @@ const yCursorPlugin = (
|
|
|
2324
2494
|
)
|
|
2325
2495
|
}
|
|
2326
2496
|
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
2497
|
return prevState.map(tr.mapping, tr.doc)
|
|
2337
2498
|
}
|
|
2338
2499
|
return prevState
|