@tiptap/y-tiptap 3.0.2 → 3.0.4
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 +134 -16
- package/dist/y-tiptap.cjs.map +1 -1
- package/dist/y-tiptap.js +134 -16
- package/dist/y-tiptap.js.map +1 -1
- package/package.json +6 -2
package/dist/y-tiptap.js
CHANGED
|
@@ -1382,24 +1382,115 @@ const matchNodeName = (yElement, pNode) =>
|
|
|
1382
1382
|
*/
|
|
1383
1383
|
let viewsToUpdate = null;
|
|
1384
1384
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1385
|
+
class MetaEntry {
|
|
1386
|
+
/**
|
|
1387
|
+
* @param {EditorView} view
|
|
1388
|
+
* @param {any} key
|
|
1389
|
+
* @param {any} value
|
|
1390
|
+
*/
|
|
1391
|
+
constructor (view, key, value) {
|
|
1392
|
+
this.view = view;
|
|
1393
|
+
this.key = key;
|
|
1394
|
+
this.value = value;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
apply () {
|
|
1398
|
+
const syncState = ySyncPluginKey.getState(this.view.state);
|
|
1391
1399
|
if (syncState && syncState.binding && !syncState.binding.isDestroyed) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1400
|
+
const tr = this.view.state.tr;
|
|
1401
|
+
tr.setMeta(this.key, this.value);
|
|
1402
|
+
this.view.dispatch(tr);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
class MetaEntriesQueue {
|
|
1408
|
+
/**
|
|
1409
|
+
* @param {Array<MetaEntry>} [entries=[]]
|
|
1410
|
+
*/
|
|
1411
|
+
constructor (entries = []) {
|
|
1412
|
+
this.entries = entries;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* @return {MetaEntry|undefined}
|
|
1417
|
+
*/
|
|
1418
|
+
getFirst () {
|
|
1419
|
+
return this.entries[0]
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* @return {MetaEntry|undefined}
|
|
1424
|
+
*/
|
|
1425
|
+
dequeueFirst () {
|
|
1426
|
+
return this.entries.shift()
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* @return {boolean}
|
|
1431
|
+
*/
|
|
1432
|
+
isEmpty () {
|
|
1433
|
+
return this.entries.length === 0
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
static fromViewsToUpdate () {
|
|
1437
|
+
const ups = /** @type {Map<EditorView, Map<any, any>>} */ (viewsToUpdate);
|
|
1438
|
+
viewsToUpdate = null;
|
|
1439
|
+
const entries = [];
|
|
1440
|
+
ups.forEach((metas, view) => {
|
|
1441
|
+
metas.forEach((value, key) => {
|
|
1442
|
+
entries.push(new MetaEntry(view, key, value));
|
|
1394
1443
|
});
|
|
1395
|
-
|
|
1444
|
+
});
|
|
1445
|
+
return new MetaEntriesQueue(entries)
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Dispatch queued plugin metadata in order, retrying only the remaining
|
|
1451
|
+
* entries if a transaction becomes stale while the async queue is flushing.
|
|
1452
|
+
*
|
|
1453
|
+
* Cursor awareness updates are decoration-only refreshes. If a real document
|
|
1454
|
+
* transaction lands before one of these queued meta transactions is applied,
|
|
1455
|
+
* ProseMirror can reject it with a RangeError. In that case we reschedule the
|
|
1456
|
+
* remaining entries on the next tick. If the first remaining entry fails again
|
|
1457
|
+
* on retry, we drop that entry and continue with the rest of the queue instead
|
|
1458
|
+
* of retrying forever or crashing the editor.
|
|
1459
|
+
*
|
|
1460
|
+
* @param {MetaEntriesQueue} [metaEntries=MetaEntriesQueue.fromViewsToUpdate()]
|
|
1461
|
+
* @param {boolean} [isRetry=false]
|
|
1462
|
+
*/
|
|
1463
|
+
const updateMetas = (metaEntries = MetaEntriesQueue.fromViewsToUpdate(), isRetry = false) => {
|
|
1464
|
+
let isFirst = true;
|
|
1465
|
+
while (!metaEntries.isEmpty()) {
|
|
1466
|
+
const metaEntry = metaEntries.getFirst();
|
|
1467
|
+
try {
|
|
1468
|
+
metaEntry.apply();
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
// ProseMirror throws a RangeError when this transaction was created from
|
|
1471
|
+
// an older state and another transaction changed the document before this
|
|
1472
|
+
// meta-only dispatch was applied ("Applying a mismatched transaction").
|
|
1473
|
+
if (err instanceof RangeError) {
|
|
1474
|
+
if (isRetry && isFirst) {
|
|
1475
|
+
// Drop the repeatedly stale entry so the queue can continue flushing.
|
|
1476
|
+
metaEntries.dequeueFirst();
|
|
1477
|
+
}
|
|
1478
|
+
if (!metaEntries.isEmpty()) {
|
|
1479
|
+
eventloop.timeout(0, () => updateMetas(metaEntries, true));
|
|
1480
|
+
}
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
throw err
|
|
1396
1484
|
}
|
|
1397
|
-
|
|
1485
|
+
isFirst = false;
|
|
1486
|
+
metaEntries.dequeueFirst();
|
|
1487
|
+
}
|
|
1398
1488
|
};
|
|
1399
1489
|
|
|
1400
1490
|
const setMeta = (view, key, value) => {
|
|
1401
1491
|
if (!viewsToUpdate) {
|
|
1402
1492
|
viewsToUpdate = new Map();
|
|
1493
|
+
// Awareness listeners can fire in bursts, so batch them into one tick.
|
|
1403
1494
|
eventloop.timeout(0, updateMetas);
|
|
1404
1495
|
}
|
|
1405
1496
|
map.setIfUndefined(viewsToUpdate, view, map.create).set(key, value);
|
|
@@ -1884,7 +1975,9 @@ const createDecorations = (
|
|
|
1884
1975
|
return
|
|
1885
1976
|
}
|
|
1886
1977
|
|
|
1887
|
-
|
|
1978
|
+
// `aw` can be null when a client disconnects, so we guard against it
|
|
1979
|
+
// before reading `cursor` to avoid a TypeError.
|
|
1980
|
+
if (aw && aw.cursor != null) {
|
|
1888
1981
|
const user = aw.user || {};
|
|
1889
1982
|
if (user.color == null) {
|
|
1890
1983
|
user.color = '#ffa500';
|
|
@@ -1999,6 +2092,9 @@ const yCursorPlugin = (
|
|
|
1999
2092
|
};
|
|
2000
2093
|
const updateCursorInfo = () => {
|
|
2001
2094
|
const ystate = ySyncPluginKey.getState(view.state);
|
|
2095
|
+
// `ystate` is undefined during sync-plugin init and Y.Doc transitions;
|
|
2096
|
+
// reading from it here would throw and break cursor tracking for the view.
|
|
2097
|
+
if (!ystate || !ystate.binding) return
|
|
2002
2098
|
// @note We make implicit checks when checking for the cursor property
|
|
2003
2099
|
const current = awareness.getLocalState() || {};
|
|
2004
2100
|
if (view.hasFocus()) {
|
|
@@ -2093,11 +2189,26 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2093
2189
|
init: (initargs, state) => {
|
|
2094
2190
|
// TODO: check if plugin order matches and fix
|
|
2095
2191
|
const ystate = ySyncPluginKey.getState(state);
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2192
|
+
let _undoManager = undoManager;
|
|
2193
|
+
if (!_undoManager) {
|
|
2194
|
+
// Y.UndoManager registers a `doc.on('destroy', …)` listener in its
|
|
2195
|
+
// constructor that UndoManager.destroy() never removes. When the doc
|
|
2196
|
+
// outlives the editor (e.g. several editors sharing one provider), that
|
|
2197
|
+
// listener keeps the UndoManager — and everything it references —
|
|
2198
|
+
// reachable from the doc, leaking memory on every editor destroy.
|
|
2199
|
+
// We only own the lifecycle of a manager we create here, so capture the
|
|
2200
|
+
// listener(s) it adds and remove them when the plugin view is destroyed.
|
|
2201
|
+
const doc = ystate.doc;
|
|
2202
|
+
const destroyListenersBefore = new Set(doc ? doc._observers.get('destroy') : []);
|
|
2203
|
+
_undoManager = new UndoManager(ystate.type, {
|
|
2204
|
+
trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
|
|
2205
|
+
deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
|
|
2206
|
+
captureTransaction: tr => tr.meta.get('addToHistory') !== false
|
|
2207
|
+
});
|
|
2208
|
+
const destroyListenersAfter = doc ? doc._observers.get('destroy') : new Set();
|
|
2209
|
+
_undoManager._yTiptapDocDestroyListeners = Array.from(destroyListenersAfter || [])
|
|
2210
|
+
.filter(listener => !destroyListenersBefore.has(listener));
|
|
2211
|
+
}
|
|
2101
2212
|
return {
|
|
2102
2213
|
undoManager: _undoManager,
|
|
2103
2214
|
prevSel: null,
|
|
@@ -2150,6 +2261,13 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2150
2261
|
return {
|
|
2151
2262
|
destroy: () => {
|
|
2152
2263
|
undoManager.destroy();
|
|
2264
|
+
// Remove the doc 'destroy' listener Y.UndoManager fails to clean up
|
|
2265
|
+
// (only for managers we created — see state.init above).
|
|
2266
|
+
const leakedDestroyListeners = undoManager._yTiptapDocDestroyListeners;
|
|
2267
|
+
if (leakedDestroyListeners && undoManager.doc) {
|
|
2268
|
+
leakedDestroyListeners.forEach(listener => undoManager.doc.off('destroy', listener));
|
|
2269
|
+
undoManager._yTiptapDocDestroyListeners = null;
|
|
2270
|
+
}
|
|
2153
2271
|
}
|
|
2154
2272
|
}
|
|
2155
2273
|
}
|