File size: 1,697 Bytes
76fc93a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { ySyncPluginKey } from "@tiptap/y-tiptap";
/**
* Prevents ProseMirror's scrollIntoView from firing after remote Yjs sync
* transactions. The y-tiptap binding calls tr.scrollIntoView() whenever
* _isLocalCursorInView() is true, but that check uses window dimensions
* instead of the actual scroll container. This fights the user's manual
* scroll, causing a "stuck then resume" feeling.
*
* Strategy: a filterTransaction hook sets a module-level flag when it
* sees a remote-origin sync transaction, and handleScrollToSelection
* suppresses the scroll while the flag is set. The flag is cleared
* asynchronously after the synchronous dispatch cycle completes.
*/
let _blockScroll = false;
export const ScrollGuard = Extension.create({
name: "scrollGuard",
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("scroll-guard"),
filterTransaction(tr) {
const meta = tr.getMeta(ySyncPluginKey);
if (meta?.isChangeOrigin) {
_blockScroll = true;
queueMicrotask(() => { _blockScroll = false; });
}
return true;
},
}),
];
},
addOptions() {
return {};
},
onBeforeCreate() {
const existing = this.editor.options.editorProps?.handleScrollToSelection;
this.editor.setOptions({
editorProps: {
...this.editor.options.editorProps,
handleScrollToSelection(view) {
if (_blockScroll) return true;
if (existing) return existing.call(this, view);
return false;
},
},
});
},
});
|