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;
        },
      },
    });
  },
});