@tiptap/y-tiptap 3.0.5 → 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/src/lib.d.ts +4 -0
- package/dist/src/plugins/sync-plugin.d.ts +4 -0
- package/dist/src/y-tiptap.d.ts +1 -1
- package/dist/y-tiptap.cjs +386 -18
- package/dist/y-tiptap.cjs.map +1 -1
- package/dist/y-tiptap.js +383 -19
- package/dist/y-tiptap.js.map +1 -1
- package/package.json +1 -1
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;
|
package/dist/src/y-tiptap.d.ts
CHANGED
|
@@ -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;
|
|
@@ -314,12 +315,38 @@ const ySyncPlugin = (yXmlFragment, {
|
|
|
314
315
|
return plugin
|
|
315
316
|
};
|
|
316
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
|
+
|
|
317
343
|
/**
|
|
318
344
|
* @param {import('prosemirror-state').Transaction} tr
|
|
319
345
|
* @param {ReturnType<typeof getRelativeSelection>} relSel
|
|
320
346
|
* @param {ProsemirrorBinding} binding
|
|
347
|
+
* @param {import('prosemirror-model').Node} [oldDoc]
|
|
321
348
|
*/
|
|
322
|
-
const restoreRelativeSelection = (tr, relSel, binding) => {
|
|
349
|
+
const restoreRelativeSelection = (tr, relSel, binding, oldDoc) => {
|
|
323
350
|
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
|
|
324
351
|
if (relSel.type === 'all') {
|
|
325
352
|
tr.setSelection(new prosemirrorState.AllSelection(tr.doc));
|
|
@@ -353,18 +380,30 @@ const restoreRelativeSelection = (tr, relSel, binding) => {
|
|
|
353
380
|
tr.setSelection(selection);
|
|
354
381
|
}
|
|
355
382
|
} else {
|
|
356
|
-
|
|
383
|
+
let anchor = relativePositionToAbsolutePosition(
|
|
357
384
|
binding.doc,
|
|
358
385
|
binding.type,
|
|
359
386
|
relSel.anchor,
|
|
360
387
|
binding.mapping
|
|
361
388
|
);
|
|
362
|
-
|
|
389
|
+
let head = relativePositionToAbsolutePosition(
|
|
363
390
|
binding.doc,
|
|
364
391
|
binding.type,
|
|
365
392
|
relSel.head,
|
|
366
393
|
binding.mapping
|
|
367
394
|
);
|
|
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;
|
|
406
|
+
}
|
|
368
407
|
if (anchor !== null && head !== null) {
|
|
369
408
|
tr.setSelection(prosemirrorState.TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head)));
|
|
370
409
|
}
|
|
@@ -445,7 +484,9 @@ const getRelativeSelection = (pmbinding, state) => {
|
|
|
445
484
|
state.selection.head,
|
|
446
485
|
pmbinding.type,
|
|
447
486
|
pmbinding.mapping
|
|
448
|
-
)
|
|
487
|
+
),
|
|
488
|
+
absAnchor: state.selection.anchor,
|
|
489
|
+
absHead: state.selection.head
|
|
449
490
|
}
|
|
450
491
|
};
|
|
451
492
|
|
|
@@ -770,6 +811,9 @@ class ProsemirrorBinding {
|
|
|
770
811
|
);
|
|
771
812
|
transaction.changed.forEach(delType);
|
|
772
813
|
transaction.changedParentTypes.forEach(delType);
|
|
814
|
+
// Rebuild the full Y↔PM mapping so relative cursor positions resolve against
|
|
815
|
+
// current node sizes after structural changes (e.g. drag-and-drop block moves).
|
|
816
|
+
this.mapping.clear();
|
|
773
817
|
const fragmentContent = this.type.toArray().map((t) =>
|
|
774
818
|
createNodeIfNotExists(
|
|
775
819
|
/** @type {Y.XmlElement | Y.XmlHook} */ (t),
|
|
@@ -777,13 +821,14 @@ class ProsemirrorBinding {
|
|
|
777
821
|
this
|
|
778
822
|
)
|
|
779
823
|
).filter((n) => n !== null);
|
|
824
|
+
const oldDoc = this.prosemirrorView.state.doc;
|
|
780
825
|
// @ts-ignore
|
|
781
826
|
let tr = this._tr.replace(
|
|
782
827
|
0,
|
|
783
828
|
this.prosemirrorView.state.doc.content.size,
|
|
784
829
|
new PModel__namespace.Slice(PModel__namespace.Fragment.from(fragmentContent), 0, 0)
|
|
785
830
|
);
|
|
786
|
-
restoreRelativeSelection(tr, this.beforeTransactionSelection, this);
|
|
831
|
+
restoreRelativeSelection(tr, this.beforeTransactionSelection, this, oldDoc);
|
|
787
832
|
tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, isUndoRedoOperation: transaction.origin instanceof Y__namespace.UndoManager });
|
|
788
833
|
if (
|
|
789
834
|
this.beforeTransactionSelection !== null && this._isLocalCursorInView()
|
|
@@ -1669,6 +1714,26 @@ const absolutePositionToRelativePosition = (pos, type, mapping) => {
|
|
|
1669
1714
|
return Y__namespace.createRelativePositionFromTypeIndex(type, type._length, -1)
|
|
1670
1715
|
};
|
|
1671
1716
|
|
|
1717
|
+
/**
|
|
1718
|
+
* Item-id based relative positions can misresolve to the document start after
|
|
1719
|
+
* block reorder during collaborative drag-and-drop.
|
|
1720
|
+
*
|
|
1721
|
+
* @param {Y.Doc} y
|
|
1722
|
+
* @param {Y.RelativePosition} relPos
|
|
1723
|
+
* @param {number|null} absPos
|
|
1724
|
+
* @return {boolean}
|
|
1725
|
+
*/
|
|
1726
|
+
const isMisresolvedTextPosition = (y, relPos, absPos) => {
|
|
1727
|
+
if (absPos === null) {
|
|
1728
|
+
return false
|
|
1729
|
+
}
|
|
1730
|
+
const decoded = Y__namespace.createAbsolutePositionFromRelativePosition(relPos, y);
|
|
1731
|
+
return decoded !== null &&
|
|
1732
|
+
decoded.type instanceof Y__namespace.XmlText &&
|
|
1733
|
+
relPos.item !== null &&
|
|
1734
|
+
absPos <= 1
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1672
1737
|
const createRelativePosition = (type, item) => {
|
|
1673
1738
|
let typeid = null;
|
|
1674
1739
|
let tname = null;
|
|
@@ -1746,7 +1811,279 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
|
|
|
1746
1811
|
}
|
|
1747
1812
|
type = /** @type {Y.AbstractType} */ (parent);
|
|
1748
1813
|
}
|
|
1749
|
-
|
|
1814
|
+
const absPos = pos - 1; // we don't count the most outer tag, because it is a fragment
|
|
1815
|
+
if (isMisresolvedTextPosition(y, relPos, absPos)) {
|
|
1816
|
+
return null
|
|
1817
|
+
}
|
|
1818
|
+
return absPos
|
|
1819
|
+
};
|
|
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
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* @param {import('prosemirror-model').Node} oldDoc
|
|
1890
|
+
* @param {import('prosemirror-model').Node} newDoc
|
|
1891
|
+
* @param {number} absPos
|
|
1892
|
+
* @return {number|null}
|
|
1893
|
+
*/
|
|
1894
|
+
const findAbsolutePositionAfterStructuralChange = (oldDoc, newDoc, absPos) => {
|
|
1895
|
+
let pos = 0;
|
|
1896
|
+
let targetIdx = 0;
|
|
1897
|
+
for (; targetIdx < oldDoc.childCount; targetIdx++) {
|
|
1898
|
+
const child = oldDoc.child(targetIdx);
|
|
1899
|
+
if (pos + child.nodeSize > absPos) {
|
|
1900
|
+
break
|
|
1901
|
+
}
|
|
1902
|
+
pos += child.nodeSize;
|
|
1903
|
+
}
|
|
1904
|
+
if (targetIdx >= oldDoc.childCount) {
|
|
1905
|
+
return null
|
|
1906
|
+
}
|
|
1907
|
+
const targetChild = oldDoc.child(targetIdx);
|
|
1908
|
+
const $oldPos = oldDoc.resolve(absPos);
|
|
1909
|
+
|
|
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))
|
|
1923
|
+
}
|
|
1924
|
+
return remapIntoBlock($oldPos, newBlockStart, newBlock)
|
|
1925
|
+
};
|
|
1926
|
+
|
|
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++;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
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
|
+
}
|
|
1992
|
+
}
|
|
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
|
+
)
|
|
2002
|
+
};
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Returns true when a transaction changes block structure rather than only
|
|
2006
|
+
* editing inline content inside existing blocks.
|
|
2007
|
+
*
|
|
2008
|
+
* @param {import('prosemirror-state').Transaction} tr
|
|
2009
|
+
* @param {import('prosemirror-model').Node} oldDoc
|
|
2010
|
+
* @return {boolean}
|
|
2011
|
+
*/
|
|
2012
|
+
const isStructuralTransaction = (tr, oldDoc) => {
|
|
2013
|
+
if (!tr.docChanged) {
|
|
2014
|
+
return false
|
|
2015
|
+
}
|
|
2016
|
+
if (tr.doc.childCount !== oldDoc.childCount) {
|
|
2017
|
+
return true
|
|
2018
|
+
}
|
|
2019
|
+
for (const step of tr.steps) {
|
|
2020
|
+
if (step instanceof prosemirrorTransform.ReplaceStep) {
|
|
2021
|
+
if (step.from === 0 && step.to === oldDoc.content.size) {
|
|
2022
|
+
return true
|
|
2023
|
+
}
|
|
2024
|
+
if (step.slice.content.size > 0) {
|
|
2025
|
+
let hasBlock = false;
|
|
2026
|
+
step.slice.content.forEach((node) => {
|
|
2027
|
+
if (node.isBlock) {
|
|
2028
|
+
hasBlock = true;
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
if (hasBlock) {
|
|
2032
|
+
return true
|
|
2033
|
+
}
|
|
2034
|
+
} else if (step.to > step.from) {
|
|
2035
|
+
const $from = oldDoc.resolve(step.from);
|
|
2036
|
+
const $to = oldDoc.resolve(step.to);
|
|
2037
|
+
if ($from.depth === 0 && $to.depth === 0 && $from.index() !== $to.index()) {
|
|
2038
|
+
return true
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return false
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Detect stale relative positions after structural changes that resolve to the
|
|
2048
|
+
* wrong text block or to the start of the correct block.
|
|
2049
|
+
*
|
|
2050
|
+
* @param {import('prosemirror-model').Node} oldDoc
|
|
2051
|
+
* @param {import('prosemirror-model').Node} newDoc
|
|
2052
|
+
* @param {number} oldAbs
|
|
2053
|
+
* @param {number|null} resolvedAbs
|
|
2054
|
+
* @return {boolean}
|
|
2055
|
+
*/
|
|
2056
|
+
const isMisresolvedAfterStructuralChange = (oldDoc, newDoc, oldAbs, resolvedAbs) => {
|
|
2057
|
+
if (resolvedAbs === null) {
|
|
2058
|
+
return false
|
|
2059
|
+
}
|
|
2060
|
+
const $old = oldDoc.resolve(oldAbs);
|
|
2061
|
+
const $new = newDoc.resolve(resolvedAbs);
|
|
2062
|
+
if (!$old.parent.isTextblock) {
|
|
2063
|
+
return false
|
|
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
|
+
}
|
|
2070
|
+
if ($old.parent.textContent !== $new.parent.textContent) {
|
|
2071
|
+
return true
|
|
2072
|
+
}
|
|
2073
|
+
if ($old.parentOffset !== 0 && $new.parentOffset === 0) {
|
|
2074
|
+
return true
|
|
2075
|
+
}
|
|
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) {
|
|
2083
|
+
const expected = findAbsolutePositionAfterStructuralChange(oldDoc, newDoc, oldAbs);
|
|
2084
|
+
return expected !== null && expected !== resolvedAbs
|
|
2085
|
+
}
|
|
2086
|
+
return false
|
|
1750
2087
|
};
|
|
1751
2088
|
|
|
1752
2089
|
/**
|
|
@@ -2165,11 +2502,21 @@ const yCursorPlugin = (
|
|
|
2165
2502
|
selectionBuilder
|
|
2166
2503
|
)
|
|
2167
2504
|
},
|
|
2168
|
-
apply (tr, prevState,
|
|
2505
|
+
apply (tr, prevState, oldState, newState) {
|
|
2169
2506
|
const ystate = ySyncPluginKey.getState(newState);
|
|
2170
2507
|
const yCursorState = tr.getMeta(yCursorPluginKey);
|
|
2508
|
+
const isRemoteChange = ystate && ystate.isChangeOrigin;
|
|
2171
2509
|
if (
|
|
2172
|
-
|
|
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 ||
|
|
2173
2520
|
(yCursorState && yCursorState.awarenessUpdated)
|
|
2174
2521
|
) {
|
|
2175
2522
|
return createDecorations(
|
|
@@ -2180,7 +2527,10 @@ const yCursorPlugin = (
|
|
|
2180
2527
|
selectionBuilder
|
|
2181
2528
|
)
|
|
2182
2529
|
}
|
|
2183
|
-
|
|
2530
|
+
if (tr.docChanged) {
|
|
2531
|
+
return prevState.map(tr.mapping, tr.doc)
|
|
2532
|
+
}
|
|
2533
|
+
return prevState
|
|
2184
2534
|
}
|
|
2185
2535
|
},
|
|
2186
2536
|
props: {
|
|
@@ -2195,15 +2545,19 @@ const yCursorPlugin = (
|
|
|
2195
2545
|
setMeta(view, yCursorPluginKey, { awarenessUpdated: true });
|
|
2196
2546
|
}
|
|
2197
2547
|
};
|
|
2198
|
-
|
|
2199
|
-
|
|
2548
|
+
/**
|
|
2549
|
+
* @param {import('prosemirror-view').EditorView} editorView
|
|
2550
|
+
* @param {{ force?: boolean }} [opts]
|
|
2551
|
+
*/
|
|
2552
|
+
const updateCursorInfo = (editorView, opts = {}) => {
|
|
2553
|
+
const ystate = ySyncPluginKey.getState(editorView.state);
|
|
2200
2554
|
// `ystate` is undefined during sync-plugin init and Y.Doc transitions;
|
|
2201
2555
|
// reading from it here would throw and break cursor tracking for the view.
|
|
2202
2556
|
if (!ystate || !ystate.binding) return
|
|
2203
2557
|
// @note We make implicit checks when checking for the cursor property
|
|
2204
2558
|
const current = awareness.getLocalState() || {};
|
|
2205
|
-
if (
|
|
2206
|
-
const selection = getSelection(
|
|
2559
|
+
if (editorView.hasFocus()) {
|
|
2560
|
+
const selection = getSelection(editorView.state);
|
|
2207
2561
|
/**
|
|
2208
2562
|
* @type {Y.RelativePosition}
|
|
2209
2563
|
*/
|
|
@@ -2221,6 +2575,7 @@ const yCursorPlugin = (
|
|
|
2221
2575
|
ystate.binding.mapping
|
|
2222
2576
|
);
|
|
2223
2577
|
if (
|
|
2578
|
+
opts.force ||
|
|
2224
2579
|
current.cursor == null ||
|
|
2225
2580
|
!Y__namespace.compareRelativePositions(
|
|
2226
2581
|
Y__namespace.createRelativePositionFromJSON(current.cursor.anchor),
|
|
@@ -2249,14 +2604,23 @@ const yCursorPlugin = (
|
|
|
2249
2604
|
awareness.setLocalStateField(cursorStateField, null);
|
|
2250
2605
|
}
|
|
2251
2606
|
};
|
|
2607
|
+
const onFocusChange = () => updateCursorInfo(view);
|
|
2252
2608
|
awareness.on('change', awarenessListener);
|
|
2253
|
-
view.dom.addEventListener('focusin',
|
|
2254
|
-
view.dom.addEventListener('focusout',
|
|
2609
|
+
view.dom.addEventListener('focusin', onFocusChange);
|
|
2610
|
+
view.dom.addEventListener('focusout', onFocusChange);
|
|
2255
2611
|
return {
|
|
2256
|
-
update:
|
|
2612
|
+
update: (editorView, prevState) => {
|
|
2613
|
+
const ystate = ySyncPluginKey.getState(editorView.state);
|
|
2614
|
+
const forceAwarenessUpdate = !!(
|
|
2615
|
+
ystate?.isChangeOrigin &&
|
|
2616
|
+
prevState?.doc &&
|
|
2617
|
+
!prevState.doc.eq(editorView.state.doc)
|
|
2618
|
+
);
|
|
2619
|
+
updateCursorInfo(editorView, { force: forceAwarenessUpdate });
|
|
2620
|
+
},
|
|
2257
2621
|
destroy: () => {
|
|
2258
|
-
view.dom.removeEventListener('focusin',
|
|
2259
|
-
view.dom.removeEventListener('focusout',
|
|
2622
|
+
view.dom.removeEventListener('focusin', onFocusChange);
|
|
2623
|
+
view.dom.removeEventListener('focusout', onFocusChange);
|
|
2260
2624
|
awareness.off('change', awarenessListener);
|
|
2261
2625
|
awareness.setLocalStateField(cursorStateField, null);
|
|
2262
2626
|
}
|
|
@@ -2386,8 +2750,12 @@ exports.defaultCursorBuilder = defaultCursorBuilder;
|
|
|
2386
2750
|
exports.defaultDeleteFilter = defaultDeleteFilter;
|
|
2387
2751
|
exports.defaultProtectedNodes = defaultProtectedNodes;
|
|
2388
2752
|
exports.defaultSelectionBuilder = defaultSelectionBuilder;
|
|
2753
|
+
exports.findAbsolutePositionAfterStructuralChange = findAbsolutePositionAfterStructuralChange;
|
|
2389
2754
|
exports.getRelativeSelection = getRelativeSelection;
|
|
2390
2755
|
exports.initProseMirrorDoc = initProseMirrorDoc;
|
|
2756
|
+
exports.isMisresolvedAfterStructuralChange = isMisresolvedAfterStructuralChange;
|
|
2757
|
+
exports.isMisresolvedTextPosition = isMisresolvedTextPosition;
|
|
2758
|
+
exports.isStructuralTransaction = isStructuralTransaction;
|
|
2391
2759
|
exports.isVisible = isVisible;
|
|
2392
2760
|
exports.prosemirrorJSONToYDoc = prosemirrorJSONToYDoc;
|
|
2393
2761
|
exports.prosemirrorJSONToYXmlFragment = prosemirrorJSONToYXmlFragment;
|