@tiptap/y-tiptap 3.0.3 → 3.0.5
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/plugins/sync-plugin.d.ts +2 -0
- package/dist/y-tiptap.cjs +120 -22
- package/dist/y-tiptap.cjs.map +1 -1
- package/dist/y-tiptap.js +121 -23
- package/dist/y-tiptap.js.map +1 -1
- package/package.json +2 -2
|
@@ -6,6 +6,7 @@ export function isVisible(item: Y.Item, snapshot?: Y.Snapshot): boolean;
|
|
|
6
6
|
export function ySyncPlugin(yXmlFragment: Y.XmlFragment, { colors, colorMapping, permanentUserData, onFirstRender, mapping }?: YSyncOpts): any;
|
|
7
7
|
export function getRelativeSelection(pmbinding: ProsemirrorBinding, state: import('prosemirror-state').EditorState): {
|
|
8
8
|
type: any;
|
|
9
|
+
depth: any;
|
|
9
10
|
anchor: any;
|
|
10
11
|
head: any;
|
|
11
12
|
};
|
|
@@ -44,6 +45,7 @@ export class ProsemirrorBinding {
|
|
|
44
45
|
*/
|
|
45
46
|
beforeTransactionSelection: {
|
|
46
47
|
type: any;
|
|
48
|
+
depth: any;
|
|
47
49
|
anchor: any;
|
|
48
50
|
head: any;
|
|
49
51
|
};
|
package/dist/y-tiptap.cjs
CHANGED
|
@@ -330,7 +330,28 @@ const restoreRelativeSelection = (tr, relSel, binding) => {
|
|
|
330
330
|
relSel.anchor,
|
|
331
331
|
binding.mapping
|
|
332
332
|
);
|
|
333
|
-
|
|
333
|
+
// anchor is null when the referenced node was deleted or moved out of
|
|
334
|
+
// binding.type by a remote update; resolving null would throw.
|
|
335
|
+
if (anchor !== null) {
|
|
336
|
+
tr.setSelection(createSafeNodeSelection(tr, anchor));
|
|
337
|
+
}
|
|
338
|
+
} else if (relSel.type === 'nodeRange') {
|
|
339
|
+
const anchor = relativePositionToAbsolutePosition(
|
|
340
|
+
binding.doc,
|
|
341
|
+
binding.type,
|
|
342
|
+
relSel.anchor,
|
|
343
|
+
binding.mapping
|
|
344
|
+
);
|
|
345
|
+
const head = relativePositionToAbsolutePosition(
|
|
346
|
+
binding.doc,
|
|
347
|
+
binding.type,
|
|
348
|
+
relSel.head,
|
|
349
|
+
binding.mapping
|
|
350
|
+
);
|
|
351
|
+
const selection = createSafeNodeRangeSelection(tr, anchor, head, relSel.depth);
|
|
352
|
+
if (selection !== null) {
|
|
353
|
+
tr.setSelection(selection);
|
|
354
|
+
}
|
|
334
355
|
} else {
|
|
335
356
|
const anchor = relativePositionToAbsolutePosition(
|
|
336
357
|
binding.doc,
|
|
@@ -368,23 +389,65 @@ const createSafeNodeSelection = (tr, pos) => {
|
|
|
368
389
|
}
|
|
369
390
|
};
|
|
370
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Safely reconstructs a NodeRangeSelection from resolved absolute positions.
|
|
394
|
+
*
|
|
395
|
+
* @param {import('prosemirror-state').Transaction} tr - The transaction whose document provides resolved positions.
|
|
396
|
+
* @param {number|null} anchor - Absolute document position marking the start (boundary) of the node range.
|
|
397
|
+
* Use `relativePositionToAbsolutePosition` before calling this function.
|
|
398
|
+
* @param {number|null} head - Absolute document position marking the end (boundary) of the node range.
|
|
399
|
+
* Use `relativePositionToAbsolutePosition` before calling this function.
|
|
400
|
+
* @param {number|undefined} depth - The nesting depth at which the range operates (e.g. 0 for
|
|
401
|
+
* top-level blocks, 1 for blocks nested inside a wrapper). Passed through to
|
|
402
|
+
* `Selection.fromJSON`; ignored by `@tiptap/extension-node-range` < v2.29 but
|
|
403
|
+
* properly stored starting from that version.
|
|
404
|
+
* @returns {import('prosemirror-state').Selection|null} Reconstructed selection, or null if
|
|
405
|
+
* anchor/head could not be resolved.
|
|
406
|
+
*/
|
|
407
|
+
const createSafeNodeRangeSelection = (tr, anchor, head, depth) => {
|
|
408
|
+
if (anchor === null || head === null) {
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
const clampedAnchor = Math.min(Math.max(anchor, 0), tr.doc.content.size);
|
|
412
|
+
const clampedHead = Math.min(Math.max(head, 0), tr.doc.content.size);
|
|
413
|
+
try {
|
|
414
|
+
const selection = prosemirrorState.Selection.fromJSON(tr.doc, {
|
|
415
|
+
type: 'nodeRange',
|
|
416
|
+
anchor: clampedAnchor,
|
|
417
|
+
head: clampedHead,
|
|
418
|
+
depth
|
|
419
|
+
});
|
|
420
|
+
if (!selection.ranges.length) {
|
|
421
|
+
return prosemirrorState.TextSelection.near(tr.doc.resolve(clampedAnchor))
|
|
422
|
+
}
|
|
423
|
+
return selection
|
|
424
|
+
} catch (e) {
|
|
425
|
+
return prosemirrorState.TextSelection.near(tr.doc.resolve(clampedAnchor))
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
371
429
|
/**
|
|
372
430
|
* @param {ProsemirrorBinding} pmbinding
|
|
373
431
|
* @param {import('prosemirror-state').EditorState} state
|
|
374
432
|
*/
|
|
375
|
-
const getRelativeSelection = (pmbinding, state) =>
|
|
376
|
-
type
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
433
|
+
const getRelativeSelection = (pmbinding, state) => {
|
|
434
|
+
const type = /** @type {any} */ (state.selection).jsonID;
|
|
435
|
+
return {
|
|
436
|
+
type,
|
|
437
|
+
// `depth` is only meaningful for NodeRangeSelection; undefined for every other type.
|
|
438
|
+
depth: type === 'nodeRange' ? /** @type {any} */ (state.selection).depth : undefined,
|
|
439
|
+
anchor: absolutePositionToRelativePosition(
|
|
440
|
+
state.selection.anchor,
|
|
441
|
+
pmbinding.type,
|
|
442
|
+
pmbinding.mapping
|
|
443
|
+
),
|
|
444
|
+
head: absolutePositionToRelativePosition(
|
|
445
|
+
state.selection.head,
|
|
446
|
+
pmbinding.type,
|
|
447
|
+
pmbinding.mapping
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
};
|
|
388
451
|
|
|
389
452
|
/**
|
|
390
453
|
* Binding for prosemirror.
|
|
@@ -1643,7 +1706,11 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
|
|
|
1643
1706
|
if (t instanceof Y__namespace.XmlText) {
|
|
1644
1707
|
pos += t._length;
|
|
1645
1708
|
} else {
|
|
1646
|
-
|
|
1709
|
+
const mapped = mapping.get(t);
|
|
1710
|
+
if (mapped == null) {
|
|
1711
|
+
return null
|
|
1712
|
+
}
|
|
1713
|
+
pos += /** @type {any} */ (mapped).nodeSize;
|
|
1647
1714
|
}
|
|
1648
1715
|
}
|
|
1649
1716
|
n = /** @type {Y.Item} */ (n.right);
|
|
@@ -1667,7 +1734,11 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
|
|
|
1667
1734
|
if (contentType instanceof Y__namespace.XmlText) {
|
|
1668
1735
|
pos += contentType._length;
|
|
1669
1736
|
} else {
|
|
1670
|
-
|
|
1737
|
+
const mapped = mapping.get(contentType);
|
|
1738
|
+
if (mapped == null) {
|
|
1739
|
+
return null
|
|
1740
|
+
}
|
|
1741
|
+
pos += /** @type {any} */ (mapped).nodeSize;
|
|
1671
1742
|
}
|
|
1672
1743
|
}
|
|
1673
1744
|
n = n.right;
|
|
@@ -2009,7 +2080,9 @@ const createDecorations = (
|
|
|
2009
2080
|
return
|
|
2010
2081
|
}
|
|
2011
2082
|
|
|
2012
|
-
|
|
2083
|
+
// `aw` can be null when a client disconnects, so we guard against it
|
|
2084
|
+
// before reading `cursor` to avoid a TypeError.
|
|
2085
|
+
if (aw && aw.cursor != null) {
|
|
2013
2086
|
const user = aw.user || {};
|
|
2014
2087
|
if (user.color == null) {
|
|
2015
2088
|
user.color = '#ffa500';
|
|
@@ -2124,6 +2197,9 @@ const yCursorPlugin = (
|
|
|
2124
2197
|
};
|
|
2125
2198
|
const updateCursorInfo = () => {
|
|
2126
2199
|
const ystate = ySyncPluginKey.getState(view.state);
|
|
2200
|
+
// `ystate` is undefined during sync-plugin init and Y.Doc transitions;
|
|
2201
|
+
// reading from it here would throw and break cursor tracking for the view.
|
|
2202
|
+
if (!ystate || !ystate.binding) return
|
|
2127
2203
|
// @note We make implicit checks when checking for the cursor property
|
|
2128
2204
|
const current = awareness.getLocalState() || {};
|
|
2129
2205
|
if (view.hasFocus()) {
|
|
@@ -2218,11 +2294,26 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2218
2294
|
init: (initargs, state) => {
|
|
2219
2295
|
// TODO: check if plugin order matches and fix
|
|
2220
2296
|
const ystate = ySyncPluginKey.getState(state);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2297
|
+
let _undoManager = undoManager;
|
|
2298
|
+
if (!_undoManager) {
|
|
2299
|
+
// Y.UndoManager registers a `doc.on('destroy', …)` listener in its
|
|
2300
|
+
// constructor that UndoManager.destroy() never removes. When the doc
|
|
2301
|
+
// outlives the editor (e.g. several editors sharing one provider), that
|
|
2302
|
+
// listener keeps the UndoManager — and everything it references —
|
|
2303
|
+
// reachable from the doc, leaking memory on every editor destroy.
|
|
2304
|
+
// We only own the lifecycle of a manager we create here, so capture the
|
|
2305
|
+
// listener(s) it adds and remove them when the plugin view is destroyed.
|
|
2306
|
+
const doc = ystate.doc;
|
|
2307
|
+
const destroyListenersBefore = new Set(doc ? doc._observers.get('destroy') : []);
|
|
2308
|
+
_undoManager = new Y.UndoManager(ystate.type, {
|
|
2309
|
+
trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
|
|
2310
|
+
deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
|
|
2311
|
+
captureTransaction: tr => tr.meta.get('addToHistory') !== false
|
|
2312
|
+
});
|
|
2313
|
+
const destroyListenersAfter = doc ? doc._observers.get('destroy') : new Set();
|
|
2314
|
+
_undoManager._yTiptapDocDestroyListeners = Array.from(destroyListenersAfter || [])
|
|
2315
|
+
.filter(listener => !destroyListenersBefore.has(listener));
|
|
2316
|
+
}
|
|
2226
2317
|
return {
|
|
2227
2318
|
undoManager: _undoManager,
|
|
2228
2319
|
prevSel: null,
|
|
@@ -2275,6 +2366,13 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2275
2366
|
return {
|
|
2276
2367
|
destroy: () => {
|
|
2277
2368
|
undoManager.destroy();
|
|
2369
|
+
// Remove the doc 'destroy' listener Y.UndoManager fails to clean up
|
|
2370
|
+
// (only for managers we created — see state.init above).
|
|
2371
|
+
const leakedDestroyListeners = undoManager._yTiptapDocDestroyListeners;
|
|
2372
|
+
if (leakedDestroyListeners && undoManager.doc) {
|
|
2373
|
+
leakedDestroyListeners.forEach(listener => undoManager.doc.off('destroy', listener));
|
|
2374
|
+
undoManager._yTiptapDocDestroyListeners = null;
|
|
2375
|
+
}
|
|
2278
2376
|
}
|
|
2279
2377
|
}
|
|
2280
2378
|
}
|