@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.cjs
CHANGED
|
@@ -1416,24 +1416,115 @@ const matchNodeName = (yElement, pNode) =>
|
|
|
1416
1416
|
*/
|
|
1417
1417
|
let viewsToUpdate = null;
|
|
1418
1418
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1419
|
+
class MetaEntry {
|
|
1420
|
+
/**
|
|
1421
|
+
* @param {EditorView} view
|
|
1422
|
+
* @param {any} key
|
|
1423
|
+
* @param {any} value
|
|
1424
|
+
*/
|
|
1425
|
+
constructor (view, key, value) {
|
|
1426
|
+
this.view = view;
|
|
1427
|
+
this.key = key;
|
|
1428
|
+
this.value = value;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
apply () {
|
|
1432
|
+
const syncState = ySyncPluginKey.getState(this.view.state);
|
|
1425
1433
|
if (syncState && syncState.binding && !syncState.binding.isDestroyed) {
|
|
1426
|
-
|
|
1427
|
-
|
|
1434
|
+
const tr = this.view.state.tr;
|
|
1435
|
+
tr.setMeta(this.key, this.value);
|
|
1436
|
+
this.view.dispatch(tr);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
class MetaEntriesQueue {
|
|
1442
|
+
/**
|
|
1443
|
+
* @param {Array<MetaEntry>} [entries=[]]
|
|
1444
|
+
*/
|
|
1445
|
+
constructor (entries = []) {
|
|
1446
|
+
this.entries = entries;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* @return {MetaEntry|undefined}
|
|
1451
|
+
*/
|
|
1452
|
+
getFirst () {
|
|
1453
|
+
return this.entries[0]
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* @return {MetaEntry|undefined}
|
|
1458
|
+
*/
|
|
1459
|
+
dequeueFirst () {
|
|
1460
|
+
return this.entries.shift()
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* @return {boolean}
|
|
1465
|
+
*/
|
|
1466
|
+
isEmpty () {
|
|
1467
|
+
return this.entries.length === 0
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
static fromViewsToUpdate () {
|
|
1471
|
+
const ups = /** @type {Map<EditorView, Map<any, any>>} */ (viewsToUpdate);
|
|
1472
|
+
viewsToUpdate = null;
|
|
1473
|
+
const entries = [];
|
|
1474
|
+
ups.forEach((metas, view) => {
|
|
1475
|
+
metas.forEach((value, key) => {
|
|
1476
|
+
entries.push(new MetaEntry(view, key, value));
|
|
1428
1477
|
});
|
|
1429
|
-
|
|
1478
|
+
});
|
|
1479
|
+
return new MetaEntriesQueue(entries)
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Dispatch queued plugin metadata in order, retrying only the remaining
|
|
1485
|
+
* entries if a transaction becomes stale while the async queue is flushing.
|
|
1486
|
+
*
|
|
1487
|
+
* Cursor awareness updates are decoration-only refreshes. If a real document
|
|
1488
|
+
* transaction lands before one of these queued meta transactions is applied,
|
|
1489
|
+
* ProseMirror can reject it with a RangeError. In that case we reschedule the
|
|
1490
|
+
* remaining entries on the next tick. If the first remaining entry fails again
|
|
1491
|
+
* on retry, we drop that entry and continue with the rest of the queue instead
|
|
1492
|
+
* of retrying forever or crashing the editor.
|
|
1493
|
+
*
|
|
1494
|
+
* @param {MetaEntriesQueue} [metaEntries=MetaEntriesQueue.fromViewsToUpdate()]
|
|
1495
|
+
* @param {boolean} [isRetry=false]
|
|
1496
|
+
*/
|
|
1497
|
+
const updateMetas = (metaEntries = MetaEntriesQueue.fromViewsToUpdate(), isRetry = false) => {
|
|
1498
|
+
let isFirst = true;
|
|
1499
|
+
while (!metaEntries.isEmpty()) {
|
|
1500
|
+
const metaEntry = metaEntries.getFirst();
|
|
1501
|
+
try {
|
|
1502
|
+
metaEntry.apply();
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
// ProseMirror throws a RangeError when this transaction was created from
|
|
1505
|
+
// an older state and another transaction changed the document before this
|
|
1506
|
+
// meta-only dispatch was applied ("Applying a mismatched transaction").
|
|
1507
|
+
if (err instanceof RangeError) {
|
|
1508
|
+
if (isRetry && isFirst) {
|
|
1509
|
+
// Drop the repeatedly stale entry so the queue can continue flushing.
|
|
1510
|
+
metaEntries.dequeueFirst();
|
|
1511
|
+
}
|
|
1512
|
+
if (!metaEntries.isEmpty()) {
|
|
1513
|
+
eventloop__namespace.timeout(0, () => updateMetas(metaEntries, true));
|
|
1514
|
+
}
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
throw err
|
|
1430
1518
|
}
|
|
1431
|
-
|
|
1519
|
+
isFirst = false;
|
|
1520
|
+
metaEntries.dequeueFirst();
|
|
1521
|
+
}
|
|
1432
1522
|
};
|
|
1433
1523
|
|
|
1434
1524
|
const setMeta = (view, key, value) => {
|
|
1435
1525
|
if (!viewsToUpdate) {
|
|
1436
1526
|
viewsToUpdate = new Map();
|
|
1527
|
+
// Awareness listeners can fire in bursts, so batch them into one tick.
|
|
1437
1528
|
eventloop__namespace.timeout(0, updateMetas);
|
|
1438
1529
|
}
|
|
1439
1530
|
map__namespace.setIfUndefined(viewsToUpdate, view, map__namespace.create).set(key, value);
|
|
@@ -1918,7 +2009,9 @@ const createDecorations = (
|
|
|
1918
2009
|
return
|
|
1919
2010
|
}
|
|
1920
2011
|
|
|
1921
|
-
|
|
2012
|
+
// `aw` can be null when a client disconnects, so we guard against it
|
|
2013
|
+
// before reading `cursor` to avoid a TypeError.
|
|
2014
|
+
if (aw && aw.cursor != null) {
|
|
1922
2015
|
const user = aw.user || {};
|
|
1923
2016
|
if (user.color == null) {
|
|
1924
2017
|
user.color = '#ffa500';
|
|
@@ -2033,6 +2126,9 @@ const yCursorPlugin = (
|
|
|
2033
2126
|
};
|
|
2034
2127
|
const updateCursorInfo = () => {
|
|
2035
2128
|
const ystate = ySyncPluginKey.getState(view.state);
|
|
2129
|
+
// `ystate` is undefined during sync-plugin init and Y.Doc transitions;
|
|
2130
|
+
// reading from it here would throw and break cursor tracking for the view.
|
|
2131
|
+
if (!ystate || !ystate.binding) return
|
|
2036
2132
|
// @note We make implicit checks when checking for the cursor property
|
|
2037
2133
|
const current = awareness.getLocalState() || {};
|
|
2038
2134
|
if (view.hasFocus()) {
|
|
@@ -2127,11 +2223,26 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2127
2223
|
init: (initargs, state) => {
|
|
2128
2224
|
// TODO: check if plugin order matches and fix
|
|
2129
2225
|
const ystate = ySyncPluginKey.getState(state);
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2226
|
+
let _undoManager = undoManager;
|
|
2227
|
+
if (!_undoManager) {
|
|
2228
|
+
// Y.UndoManager registers a `doc.on('destroy', …)` listener in its
|
|
2229
|
+
// constructor that UndoManager.destroy() never removes. When the doc
|
|
2230
|
+
// outlives the editor (e.g. several editors sharing one provider), that
|
|
2231
|
+
// listener keeps the UndoManager — and everything it references —
|
|
2232
|
+
// reachable from the doc, leaking memory on every editor destroy.
|
|
2233
|
+
// We only own the lifecycle of a manager we create here, so capture the
|
|
2234
|
+
// listener(s) it adds and remove them when the plugin view is destroyed.
|
|
2235
|
+
const doc = ystate.doc;
|
|
2236
|
+
const destroyListenersBefore = new Set(doc ? doc._observers.get('destroy') : []);
|
|
2237
|
+
_undoManager = new Y.UndoManager(ystate.type, {
|
|
2238
|
+
trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
|
|
2239
|
+
deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
|
|
2240
|
+
captureTransaction: tr => tr.meta.get('addToHistory') !== false
|
|
2241
|
+
});
|
|
2242
|
+
const destroyListenersAfter = doc ? doc._observers.get('destroy') : new Set();
|
|
2243
|
+
_undoManager._yTiptapDocDestroyListeners = Array.from(destroyListenersAfter || [])
|
|
2244
|
+
.filter(listener => !destroyListenersBefore.has(listener));
|
|
2245
|
+
}
|
|
2135
2246
|
return {
|
|
2136
2247
|
undoManager: _undoManager,
|
|
2137
2248
|
prevSel: null,
|
|
@@ -2184,6 +2295,13 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
|
|
|
2184
2295
|
return {
|
|
2185
2296
|
destroy: () => {
|
|
2186
2297
|
undoManager.destroy();
|
|
2298
|
+
// Remove the doc 'destroy' listener Y.UndoManager fails to clean up
|
|
2299
|
+
// (only for managers we created — see state.init above).
|
|
2300
|
+
const leakedDestroyListeners = undoManager._yTiptapDocDestroyListeners;
|
|
2301
|
+
if (leakedDestroyListeners && undoManager.doc) {
|
|
2302
|
+
leakedDestroyListeners.forEach(listener => undoManager.doc.off('destroy', listener));
|
|
2303
|
+
undoManager._yTiptapDocDestroyListeners = null;
|
|
2304
|
+
}
|
|
2187
2305
|
}
|
|
2188
2306
|
}
|
|
2189
2307
|
}
|