@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.cjs
CHANGED
|
@@ -315,6 +315,31 @@ const ySyncPlugin = (yXmlFragment, {
|
|
|
315
315
|
return plugin
|
|
316
316
|
};
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Resolves one text-selection endpoint after a remote update. Falls back to
|
|
320
|
+
* content-based matching when the Yjs resolution is missing or lands in the
|
|
321
|
+
* wrong block, and keeps the Yjs resolution when the fallback finds nothing.
|
|
322
|
+
*
|
|
323
|
+
* @param {import('prosemirror-model').Node} newDoc
|
|
324
|
+
* @param {import('prosemirror-model').Node} oldDoc
|
|
325
|
+
* @param {number|null|undefined} oldAbs
|
|
326
|
+
* @param {number|null} resolved
|
|
327
|
+
* @return {number|null}
|
|
328
|
+
*/
|
|
329
|
+
const recoverSelectionEndpoint = (newDoc, oldDoc, oldAbs, resolved) => {
|
|
330
|
+
if (oldAbs == null) {
|
|
331
|
+
return resolved
|
|
332
|
+
}
|
|
333
|
+
const misresolved = resolved === null ||
|
|
334
|
+
(oldAbs > 1 && resolved <= 1) ||
|
|
335
|
+
isMisresolvedAfterStructuralChange(oldDoc, newDoc, oldAbs, resolved);
|
|
336
|
+
if (!misresolved) {
|
|
337
|
+
return resolved
|
|
338
|
+
}
|
|
339
|
+
const recovered = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
|
|
340
|
+
return recovered !== null ? recovered : resolved
|
|
341
|
+
};
|
|
342
|
+
|
|
318
343
|
/**
|
|
319
344
|
* @param {import('prosemirror-state').Transaction} tr
|
|
320
345
|
* @param {ReturnType<typeof getRelativeSelection>} relSel
|
|
@@ -367,28 +392,17 @@ const restoreRelativeSelection = (tr, relSel, binding, oldDoc) => {
|
|
|
367
392
|
relSel.head,
|
|
368
393
|
binding.mapping
|
|
369
394
|
);
|
|
370
|
-
|
|
371
|
-
relSel.absAnchor
|
|
372
|
-
relSel.absHead
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
);
|
|
395
|
+
if (oldDoc != null) {
|
|
396
|
+
anchor = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absAnchor, anchor);
|
|
397
|
+
head = recoverSelectionEndpoint(tr.doc, oldDoc, relSel.absHead, head);
|
|
398
|
+
}
|
|
399
|
+
// Collapse to the surviving endpoint instead of dropping the selection;
|
|
400
|
+
// an unset selection maps through the full-doc replace to the doc start.
|
|
401
|
+
if (anchor === null) {
|
|
402
|
+
anchor = head;
|
|
403
|
+
}
|
|
404
|
+
if (head === null) {
|
|
405
|
+
head = anchor;
|
|
392
406
|
}
|
|
393
407
|
if (anchor !== null && head !== null) {
|
|
394
408
|
tr.setSelection(prosemirrorState.TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head)));
|
|
@@ -1804,6 +1818,73 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
|
|
|
1804
1818
|
return absPos
|
|
1805
1819
|
};
|
|
1806
1820
|
|
|
1821
|
+
/**
|
|
1822
|
+
* Shallow attrs comparison. Attr values are primitives in most schemas;
|
|
1823
|
+
* non-primitive values fail the check and callers fall back to text matching.
|
|
1824
|
+
*
|
|
1825
|
+
* @param {Object<string, any>} a
|
|
1826
|
+
* @param {Object<string, any>} b
|
|
1827
|
+
* @return {boolean}
|
|
1828
|
+
*/
|
|
1829
|
+
const attrsEqual = (a, b) => {
|
|
1830
|
+
if (a === b) {
|
|
1831
|
+
return true
|
|
1832
|
+
}
|
|
1833
|
+
const aKeys = Object.keys(a);
|
|
1834
|
+
return aKeys.length === Object.keys(b).length && aKeys.every((k) => a[k] === b[k])
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* Returns true when any attr deviates from its spec default or has none.
|
|
1839
|
+
* Default-only attrs cannot tell same-type siblings apart.
|
|
1840
|
+
*
|
|
1841
|
+
* @param {import('prosemirror-model').Node} node
|
|
1842
|
+
* @return {boolean}
|
|
1843
|
+
*/
|
|
1844
|
+
const hasDistinctiveAttrs = (node) => {
|
|
1845
|
+
const specAttrs = node.type.spec.attrs || {};
|
|
1846
|
+
return Object.keys(node.attrs).some((key) => {
|
|
1847
|
+
const spec = specAttrs[key];
|
|
1848
|
+
return spec == null ||
|
|
1849
|
+
!Object.prototype.hasOwnProperty.call(spec, 'default') ||
|
|
1850
|
+
spec.default !== node.attrs[key]
|
|
1851
|
+
})
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Remaps a position into a matched block by walking the same child-index path
|
|
1856
|
+
* it had in the old block. A raw byte offset would overshoot into a sibling
|
|
1857
|
+
* inner textblock when the old block contains local keystrokes that are not
|
|
1858
|
+
* yet part of the rebuilt document.
|
|
1859
|
+
*
|
|
1860
|
+
* @param {import('prosemirror-model').ResolvedPos} $oldPos
|
|
1861
|
+
* @param {number} newBlockStart
|
|
1862
|
+
* @param {import('prosemirror-model').Node} newBlock
|
|
1863
|
+
* @return {number|null}
|
|
1864
|
+
*/
|
|
1865
|
+
const remapIntoBlock = ($oldPos, newBlockStart, newBlock) => {
|
|
1866
|
+
let pos = newBlockStart + 1;
|
|
1867
|
+
let node = newBlock;
|
|
1868
|
+
for (let depth = 1; depth < $oldPos.depth; depth++) {
|
|
1869
|
+
const idx = $oldPos.index(depth);
|
|
1870
|
+
if (idx >= node.childCount) {
|
|
1871
|
+
return null
|
|
1872
|
+
}
|
|
1873
|
+
for (let i = 0; i < idx; i++) {
|
|
1874
|
+
pos += node.child(i).nodeSize;
|
|
1875
|
+
}
|
|
1876
|
+
pos += 1;
|
|
1877
|
+
node = node.child(idx);
|
|
1878
|
+
if (node.type !== $oldPos.node(depth + 1).type) {
|
|
1879
|
+
return null
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (!node.isTextblock) {
|
|
1883
|
+
return null
|
|
1884
|
+
}
|
|
1885
|
+
return pos + Math.min($oldPos.parentOffset, node.content.size)
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1807
1888
|
/**
|
|
1808
1889
|
* @param {import('prosemirror-model').Node} oldDoc
|
|
1809
1890
|
* @param {import('prosemirror-model').Node} newDoc
|
|
@@ -1824,32 +1905,100 @@ const findAbsolutePositionAfterStructuralChange = (oldDoc, newDoc, absPos) => {
|
|
|
1824
1905
|
return null
|
|
1825
1906
|
}
|
|
1826
1907
|
const targetChild = oldDoc.child(targetIdx);
|
|
1827
|
-
const
|
|
1908
|
+
const $oldPos = oldDoc.resolve(absPos);
|
|
1828
1909
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1910
|
+
/**
|
|
1911
|
+
* @param {number} newBlockStart
|
|
1912
|
+
* @param {import('prosemirror-model').Node} newBlock
|
|
1913
|
+
* @return {number|null}
|
|
1914
|
+
*/
|
|
1915
|
+
const place = (newBlockStart, newBlock) => {
|
|
1916
|
+
// Positions between top-level blocks carry no inner path; clamp them just
|
|
1917
|
+
// inside the matched block like the previous raw-offset remap did.
|
|
1918
|
+
if ($oldPos.depth === 0) {
|
|
1919
|
+
const remapped = newBlockStart + (absPos - pos);
|
|
1920
|
+
const contentStart = newBlockStart + 1;
|
|
1921
|
+
const contentEnd = newBlockStart + newBlock.nodeSize - 1;
|
|
1922
|
+
return Math.max(contentStart, Math.min(remapped, contentEnd))
|
|
1834
1923
|
}
|
|
1835
|
-
|
|
1924
|
+
return remapIntoBlock($oldPos, newBlockStart, newBlock)
|
|
1925
|
+
};
|
|
1836
1926
|
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1927
|
+
/**
|
|
1928
|
+
* Finds the Nth block in newDoc matching `pred`, where N is the number of
|
|
1929
|
+
* matching blocks in oldDoc up to and including the target block.
|
|
1930
|
+
*
|
|
1931
|
+
* @param {function(import('prosemirror-model').Node): boolean} pred
|
|
1932
|
+
* @param {boolean} requireUnique
|
|
1933
|
+
* @return {number|null}
|
|
1934
|
+
*/
|
|
1935
|
+
const findByPredicate = (pred, requireUnique = false) => {
|
|
1936
|
+
let occurrence = 0;
|
|
1937
|
+
for (let i = 0; i <= targetIdx; i++) {
|
|
1938
|
+
if (pred(oldDoc.child(i))) {
|
|
1939
|
+
occurrence++;
|
|
1848
1940
|
}
|
|
1849
1941
|
}
|
|
1850
|
-
|
|
1942
|
+
let matchCount = 0;
|
|
1943
|
+
let matchStart = -1;
|
|
1944
|
+
let matchBlock = null;
|
|
1945
|
+
let newPos = 0;
|
|
1946
|
+
for (let i = 0; i < newDoc.childCount; i++) {
|
|
1947
|
+
const child = newDoc.child(i);
|
|
1948
|
+
if (pred(child)) {
|
|
1949
|
+
matchCount++;
|
|
1950
|
+
if (matchCount === occurrence) {
|
|
1951
|
+
matchStart = newPos;
|
|
1952
|
+
matchBlock = child;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
newPos += child.nodeSize;
|
|
1956
|
+
}
|
|
1957
|
+
if (matchBlock === null || (requireUnique && (occurrence !== 1 || matchCount !== 1))) {
|
|
1958
|
+
return null
|
|
1959
|
+
}
|
|
1960
|
+
return place(matchStart, matchBlock)
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* @param {import('prosemirror-model').Node} child
|
|
1965
|
+
* @return {boolean}
|
|
1966
|
+
*/
|
|
1967
|
+
const sameTypeAndAttrs = (child) =>
|
|
1968
|
+
child.type === targetChild.type && attrsEqual(child.attrs, targetChild.attrs);
|
|
1969
|
+
const oldText = targetChild.textContent;
|
|
1970
|
+
|
|
1971
|
+
const byAll = findByPredicate((child) => sameTypeAndAttrs(child) && child.textContent === oldText);
|
|
1972
|
+
if (byAll !== null) {
|
|
1973
|
+
return byAll
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// Text must be matched before attrs: after a remote attr-only edit, the
|
|
1977
|
+
// attrs pass would steer the cursor into a sibling that kept the old attrs.
|
|
1978
|
+
const byText = findByPredicate(
|
|
1979
|
+
(child) => child.type === targetChild.type && child.textContent === oldText
|
|
1980
|
+
);
|
|
1981
|
+
if (byText !== null) {
|
|
1982
|
+
return byText
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// In-flight local typing diverges the text between both docs. Distinctive
|
|
1986
|
+
// attrs still identify the block; default-only attrs match every sibling.
|
|
1987
|
+
if (hasDistinctiveAttrs(targetChild)) {
|
|
1988
|
+
const byAttrs = findByPredicate(sameTypeAndAttrs, true);
|
|
1989
|
+
if (byAttrs !== null) {
|
|
1990
|
+
return byAttrs
|
|
1991
|
+
}
|
|
1851
1992
|
}
|
|
1852
|
-
|
|
1993
|
+
|
|
1994
|
+
// Trailing in-flight keystrokes leave a prefix relation between old and new
|
|
1995
|
+
// text. Empty text is a prefix of everything and must never match.
|
|
1996
|
+
return findByPredicate(
|
|
1997
|
+
(child) => sameTypeAndAttrs(child) &&
|
|
1998
|
+
oldText !== '' && child.textContent !== '' &&
|
|
1999
|
+
(oldText.startsWith(child.textContent) || child.textContent.startsWith(oldText)),
|
|
2000
|
+
true
|
|
2001
|
+
)
|
|
1853
2002
|
};
|
|
1854
2003
|
|
|
1855
2004
|
/**
|
|
@@ -1910,16 +2059,27 @@ const isMisresolvedAfterStructuralChange = (oldDoc, newDoc, oldAbs, resolvedAbs)
|
|
|
1910
2059
|
}
|
|
1911
2060
|
const $old = oldDoc.resolve(oldAbs);
|
|
1912
2061
|
const $new = newDoc.resolve(resolvedAbs);
|
|
1913
|
-
if (!$old.parent.isTextblock
|
|
2062
|
+
if (!$old.parent.isTextblock) {
|
|
1914
2063
|
return false
|
|
1915
2064
|
}
|
|
2065
|
+
// A textblock cursor cannot legitimately resolve into a non-textblock;
|
|
2066
|
+
// a structural reorder replaced the block via delete + insert.
|
|
2067
|
+
if (!$new.parent.isTextblock) {
|
|
2068
|
+
return true
|
|
2069
|
+
}
|
|
1916
2070
|
if ($old.parent.textContent !== $new.parent.textContent) {
|
|
1917
2071
|
return true
|
|
1918
2072
|
}
|
|
1919
2073
|
if ($old.parentOffset !== 0 && $new.parentOffset === 0) {
|
|
1920
2074
|
return true
|
|
1921
2075
|
}
|
|
1922
|
-
|
|
2076
|
+
const bothAtStart = $old.parentOffset === 0 && $new.parentOffset === 0;
|
|
2077
|
+
// A changed offset, type or attrs hints at a same-text sibling. When all
|
|
2078
|
+
// of them agree there is no signal left and the Yjs resolution must win.
|
|
2079
|
+
const suspicious = $old.parentOffset !== $new.parentOffset ||
|
|
2080
|
+
$old.parent.type !== $new.parent.type ||
|
|
2081
|
+
!attrsEqual($old.parent.attrs, $new.parent.attrs);
|
|
2082
|
+
if (bothAtStart || suspicious) {
|
|
1923
2083
|
const expected = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
|
|
1924
2084
|
return expected !== null && expected !== resolvedAbs
|
|
1925
2085
|
}
|
|
@@ -2345,8 +2505,18 @@ const yCursorPlugin = (
|
|
|
2345
2505
|
apply (tr, prevState, oldState, newState) {
|
|
2346
2506
|
const ystate = ySyncPluginKey.getState(newState);
|
|
2347
2507
|
const yCursorState = tr.getMeta(yCursorPluginKey);
|
|
2508
|
+
const isRemoteChange = ystate && ystate.isChangeOrigin;
|
|
2348
2509
|
if (
|
|
2349
|
-
|
|
2510
|
+
tr.docChanged &&
|
|
2511
|
+
!isRemoteChange &&
|
|
2512
|
+
isStructuralTransaction(tr, oldState.doc)
|
|
2513
|
+
) {
|
|
2514
|
+
// The ProseMirror document leads the Yjs mapping during local moves.
|
|
2515
|
+
// Hide stale awareness until the collaborator publishes its new cursor.
|
|
2516
|
+
return prosemirrorView.DecorationSet.empty
|
|
2517
|
+
}
|
|
2518
|
+
if (
|
|
2519
|
+
isRemoteChange ||
|
|
2350
2520
|
(yCursorState && yCursorState.awarenessUpdated)
|
|
2351
2521
|
) {
|
|
2352
2522
|
return createDecorations(
|
|
@@ -2358,15 +2528,6 @@ const yCursorPlugin = (
|
|
|
2358
2528
|
)
|
|
2359
2529
|
}
|
|
2360
2530
|
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
2531
|
return prevState.map(tr.mapping, tr.doc)
|
|
2371
2532
|
}
|
|
2372
2533
|
return prevState
|