@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
package/dist/y-tiptap.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
2
|
import { Item, ContentType, Text, XmlElement, UndoManager } from 'yjs';
|
|
3
3
|
import { DecorationSet, Decoration } from 'prosemirror-view';
|
|
4
|
-
import { PluginKey, Plugin, TextSelection, AllSelection, NodeSelection } from 'prosemirror-state';
|
|
4
|
+
import { PluginKey, Plugin, TextSelection, AllSelection, NodeSelection, Selection } from 'prosemirror-state';
|
|
5
5
|
import 'y-protocols/awareness';
|
|
6
6
|
import { createMutex } from 'lib0/mutex';
|
|
7
7
|
import * as PModel from 'prosemirror-model';
|
|
@@ -296,7 +296,28 @@ const restoreRelativeSelection = (tr, relSel, binding) => {
|
|
|
296
296
|
relSel.anchor,
|
|
297
297
|
binding.mapping
|
|
298
298
|
);
|
|
299
|
-
|
|
299
|
+
// anchor is null when the referenced node was deleted or moved out of
|
|
300
|
+
// binding.type by a remote update; resolving null would throw.
|
|
301
|
+
if (anchor !== null) {
|
|
302
|
+
tr.setSelection(createSafeNodeSelection(tr, anchor));
|
|
303
|
+
}
|
|
304
|
+
} else if (relSel.type === 'nodeRange') {
|
|
305
|
+
const anchor = relativePositionToAbsolutePosition(
|
|
306
|
+
binding.doc,
|
|
307
|
+
binding.type,
|
|
308
|
+
relSel.anchor,
|
|
309
|
+
binding.mapping
|
|
310
|
+
);
|
|
311
|
+
const head = relativePositionToAbsolutePosition(
|
|
312
|
+
binding.doc,
|
|
313
|
+
binding.type,
|
|
314
|
+
relSel.head,
|
|
315
|
+
binding.mapping
|
|
316
|
+
);
|
|
317
|
+
const selection = createSafeNodeRangeSelection(tr, anchor, head, relSel.depth);
|
|
318
|
+
if (selection !== null) {
|
|
319
|
+
tr.setSelection(selection);
|
|
320
|
+
}
|
|
300
321
|
} else {
|
|
301
322
|
const anchor = relativePositionToAbsolutePosition(
|
|
302
323
|
binding.doc,
|
|
@@ -334,23 +355,65 @@ const createSafeNodeSelection = (tr, pos) => {
|
|
|
334
355
|
}
|
|
335
356
|
};
|
|
336
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Safely reconstructs a NodeRangeSelection from resolved absolute positions.
|
|
360
|
+
*
|
|
361
|
+
* @param {import('prosemirror-state').Transaction} tr - The transaction whose document provides resolved positions.
|
|
362
|
+
* @param {number|null} anchor - Absolute document position marking the start (boundary) of the node range.
|
|
363
|
+
* Use `relativePositionToAbsolutePosition` before calling this function.
|
|
364
|
+
* @param {number|null} head - Absolute document position marking the end (boundary) of the node range.
|
|
365
|
+
* Use `relativePositionToAbsolutePosition` before calling this function.
|
|
366
|
+
* @param {number|undefined} depth - The nesting depth at which the range operates (e.g. 0 for
|
|
367
|
+
* top-level blocks, 1 for blocks nested inside a wrapper). Passed through to
|
|
368
|
+
* `Selection.fromJSON`; ignored by `@tiptap/extension-node-range` < v2.29 but
|
|
369
|
+
* properly stored starting from that version.
|
|
370
|
+
* @returns {import('prosemirror-state').Selection|null} Reconstructed selection, or null if
|
|
371
|
+
* anchor/head could not be resolved.
|
|
372
|
+
*/
|
|
373
|
+
const createSafeNodeRangeSelection = (tr, anchor, head, depth) => {
|
|
374
|
+
if (anchor === null || head === null) {
|
|
375
|
+
return null
|
|
376
|
+
}
|
|
377
|
+
const clampedAnchor = Math.min(Math.max(anchor, 0), tr.doc.content.size);
|
|
378
|
+
const clampedHead = Math.min(Math.max(head, 0), tr.doc.content.size);
|
|
379
|
+
try {
|
|
380
|
+
const selection = Selection.fromJSON(tr.doc, {
|
|
381
|
+
type: 'nodeRange',
|
|
382
|
+
anchor: clampedAnchor,
|
|
383
|
+
head: clampedHead,
|
|
384
|
+
depth
|
|
385
|
+
});
|
|
386
|
+
if (!selection.ranges.length) {
|
|
387
|
+
return TextSelection.near(tr.doc.resolve(clampedAnchor))
|
|
388
|
+
}
|
|
389
|
+
return selection
|
|
390
|
+
} catch (e) {
|
|
391
|
+
return TextSelection.near(tr.doc.resolve(clampedAnchor))
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
337
395
|
/**
|
|
338
396
|
* @param {ProsemirrorBinding} pmbinding
|
|
339
397
|
* @param {import('prosemirror-state').EditorState} state
|
|
340
398
|
*/
|
|
341
|
-
const getRelativeSelection = (pmbinding, state) =>
|
|
342
|
-
type
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
399
|
+
const getRelativeSelection = (pmbinding, state) => {
|
|
400
|
+
const type = /** @type {any} */ (state.selection).jsonID;
|
|
401
|
+
return {
|
|
402
|
+
type,
|
|
403
|
+
// `depth` is only meaningful for NodeRangeSelection; undefined for every other type.
|
|
404
|
+
depth: type === 'nodeRange' ? /** @type {any} */ (state.selection).depth : undefined,
|
|
405
|
+
anchor: absolutePositionToRelativePosition(
|
|
406
|
+
state.selection.anchor,
|
|
407
|
+
pmbinding.type,
|
|
408
|
+
pmbinding.mapping
|
|
409
|
+
),
|
|
410
|
+
head: absolutePositionToRelativePosition(
|
|
411
|
+
state.selection.head,
|
|
412
|
+
pmbinding.type,
|
|
413
|
+
pmbinding.mapping
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
};
|
|
354
417
|
|
|
355
418
|
/**
|
|
356
419
|
* Binding for prosemirror.
|
|
@@ -1609,7 +1672,11 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
|
|
|
1609
1672
|
if (t instanceof Y.XmlText) {
|
|
1610
1673
|
pos += t._length;
|
|
1611
1674
|
} else {
|
|
1612
|
-
|
|
1675
|
+
const mapped = mapping.get(t);
|
|
1676
|
+
if (mapped == null) {
|
|
1677
|
+
return null
|
|
1678
|
+
}
|
|
1679
|
+
pos += /** @type {any} */ (mapped).nodeSize;
|
|
1613
1680
|
}
|
|
1614
1681
|
}
|
|
1615
1682
|
n = /** @type {Y.Item} */ (n.right);
|
|
@@ -1633,7 +1700,11 @@ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) =>
|
|
|
1633
1700
|
if (contentType instanceof Y.XmlText) {
|
|
1634
1701
|
pos += contentType._length;
|
|
1635
1702
|
} else {
|
|
1636
|
-
|
|
1703
|
+
const mapped = mapping.get(contentType);
|
|
1704
|
+
if (mapped == null) {
|
|
1705
|
+
return null
|
|
1706
|
+
}
|
|
1707
|
+
pos += /** @type {any} */ (mapped).nodeSize;
|
|
1637
1708
|
}
|
|
1638
1709
|
}
|
|
1639
1710
|
n = n.right;
|
|
@@ -1975,7 +2046,9 @@ const createDecorations = (
|
|
|
1975
2046
|
return
|
|
1976
2047
|
}
|
|
1977
2048
|
|
|
1978
|
-
|
|
2049
|
+
// `aw` can be null when a client disconnects, so we guard against it
|
|
2050
|
+
// before reading `cursor` to avoid a TypeError.
|
|
2051
|
+
if (aw && aw.cursor != null) {
|
|
1979
2052
|
const user = aw.user || {};
|
|
1980
2053
|
if (user.color == null) {
|
|
1981
2054
|
user.color = '#ffa500';
|
|
@@ -2090,6 +2163,9 @@ const yCursorPlugin = (
|
|
|
2090
2163
|
};
|
|
2091
2164
|
const updateCursorInfo = () => {
|
|
2092
2165
|
const ystate = ySyncPluginKey.getState(view.state);
|
|
2166
|
+
// `ystate` is undefined during sync-plugin init and Y.Doc transitions;
|
|
2167
|
+
// reading from it here would throw and break cursor tracking for the view.
|
|
2168
|
+
if (!ystate || !ystate.binding) return
|
|
2093
2169
|
// @note We make implicit checks when checking for the cursor property
|
|
2094
2170
|
const current = awareness.getLocalState() || {};
|
|
2095
2171
|
if (view.hasFocus()) {
|
|
@@ -2184,11 +2260,26 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2184
2260
|
init: (initargs, state) => {
|
|
2185
2261
|
// TODO: check if plugin order matches and fix
|
|
2186
2262
|
const ystate = ySyncPluginKey.getState(state);
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2263
|
+
let _undoManager = undoManager;
|
|
2264
|
+
if (!_undoManager) {
|
|
2265
|
+
// Y.UndoManager registers a `doc.on('destroy', …)` listener in its
|
|
2266
|
+
// constructor that UndoManager.destroy() never removes. When the doc
|
|
2267
|
+
// outlives the editor (e.g. several editors sharing one provider), that
|
|
2268
|
+
// listener keeps the UndoManager — and everything it references —
|
|
2269
|
+
// reachable from the doc, leaking memory on every editor destroy.
|
|
2270
|
+
// We only own the lifecycle of a manager we create here, so capture the
|
|
2271
|
+
// listener(s) it adds and remove them when the plugin view is destroyed.
|
|
2272
|
+
const doc = ystate.doc;
|
|
2273
|
+
const destroyListenersBefore = new Set(doc ? doc._observers.get('destroy') : []);
|
|
2274
|
+
_undoManager = new UndoManager(ystate.type, {
|
|
2275
|
+
trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
|
|
2276
|
+
deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
|
|
2277
|
+
captureTransaction: tr => tr.meta.get('addToHistory') !== false
|
|
2278
|
+
});
|
|
2279
|
+
const destroyListenersAfter = doc ? doc._observers.get('destroy') : new Set();
|
|
2280
|
+
_undoManager._yTiptapDocDestroyListeners = Array.from(destroyListenersAfter || [])
|
|
2281
|
+
.filter(listener => !destroyListenersBefore.has(listener));
|
|
2282
|
+
}
|
|
2192
2283
|
return {
|
|
2193
2284
|
undoManager: _undoManager,
|
|
2194
2285
|
prevSel: null,
|
|
@@ -2241,6 +2332,13 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2241
2332
|
return {
|
|
2242
2333
|
destroy: () => {
|
|
2243
2334
|
undoManager.destroy();
|
|
2335
|
+
// Remove the doc 'destroy' listener Y.UndoManager fails to clean up
|
|
2336
|
+
// (only for managers we created — see state.init above).
|
|
2337
|
+
const leakedDestroyListeners = undoManager._yTiptapDocDestroyListeners;
|
|
2338
|
+
if (leakedDestroyListeners && undoManager.doc) {
|
|
2339
|
+
leakedDestroyListeners.forEach(listener => undoManager.doc.off('destroy', listener));
|
|
2340
|
+
undoManager._yTiptapDocDestroyListeners = null;
|
|
2341
|
+
}
|
|
2244
2342
|
}
|
|
2245
2343
|
}
|
|
2246
2344
|
}
|