CM6 Scroll To Middle?

Is there a way to configure the “scrollIntoView” option in a transaction update to scroll the selection to the middle of the screen rather than the bottom?

Not currently. Would you want the cursor to always be kept in the middle, or only moved there when it hits one of the sides?

I think I would prefer it always to be in the middle if the option is enabled but I could see the argument for both.

I think the best way forward would be to write your own view plugin that does this. Something like…

const centerCursor = ViewPlugin.fromClass(class {
  update(update: ViewUpdate) {
    if (update.transactions.some(tr => tr.scrollIntoView)) {
      let view = update.view
      // (Sync with other DOM read/write phases for efficiency)
      view.requestMeasure({
        read() {
          return {
            cursor: view.coordsAtPos(view.state.selection.main.head),
            scroller: view.scrollDOM.getBoundingClientRect()
          }
        },
        write({cursor, scroller}) {
          if (cursor) {
            let curMid = (cursor.top + cursor.bottom) / 2
            let eltMid = (scroller.top + scroller.bottom) / 2
            if (Math.abs(curMid - eltMid) > 5)
              view.scrollDOM.scrollTop += curMid - eltMid
          }
        }
      })
    }
  }
})
3 Likes

Well that’s legit, thank you!

This doesn’t seem to be working for selections. If I wanted to make it so it always scrolled the top of the selection to the middle of the viewport, what would that look like?

The above snippet sometimes scrolls the selection to the bottom, top, or sometimes it puts the end of the selection at the top of the screen.

I’ve been trying to figure this out and this is what I have so far. It seems like view.scrollDOM.scrollTop += value doesn’t do anything, and then only way I’ve found to change the scroll position is by using view.scrollPosIntoView()

Something like this will make sure the top of the selection is always visible, which is closer to what I’m trying to do.

ViewPlugin.fromClass(class {
    update(update: ViewUpdate) {
        if (update.transactions.some(tr => tr.scrollIntoView)) {
            let view = update.view
            // (Sync with other DOM read/write phases for efficiency)
            view.requestMeasure({
                read() {
                    return {
                        selection: view.state.selection.main;
                    }
                },
                write({selection}) {
                    view.scrollPosIntoView(selection.from);
                }
            })
        }
    }

I tried to use the scroller, cursor and view.posAtCoords() to get it more toward the middle but view.posAtCoords() always returns null. Not sure if I’m on the right track or not.

ViewPlugin.fromClass(class {
    update(update: ViewUpdate) {
        if (update.transactions.some(tr => tr.scrollIntoView)) {
            let view = update.view
            // (Sync with other DOM read/write phases for efficiency)
            view.requestMeasure({
                read() {
                    let cursor = view.coordsAtPos(view.state.selection.main.anchor);
                    let scroller = view.scrollDOM.getBoundingClientRect();

                    let middle = cursor.top - ((scroller.top + scroller.bottom) / 2);

                    let middlePos = view.posAtCoords({x: cursor.left, y: middle });

                    return {
                        middlePos
                    }
                },
                write({middlePos}) {
                    // view.scrollDOM.scrollTo(null, curTop);
                    view.scrollPosIntoView(middlePos);
                }
            })
        }
    }
})

Anyone have any ideas about this?

Got this working. The math here was a bit off because coordsAtPos gets the scrolled position, not the absolute one, so you have to add the scroller’s scrollTop to it. Also I had to add a timeout of 1ms so that it doesn’t interfere with CM’s native scrolling.

export const scrollSelectionIntoView = ViewPlugin.fromClass(
  class {
    update(update: ViewUpdate) {
      if (update.transactions.some((tr) => tr.scrollIntoView)) {
        const view = update.view;
        view.requestMeasure({
          read() {
            const cursor = view.coordsAtPos(view.state.selection.main.anchor);
            const currentScroll = view.scrollDOM.scrollTop;
            const absoluteTop = (cursor?.top ?? 0) + currentScroll;
            const scroller = view.scrollDOM.getBoundingClientRect();
            const middleTop =
              absoluteTop - (scroller.top + scroller.bottom) * (1 / 3); // change to 1/2 if you want exactly the middle. I think 1/3 is nicer.

            return {
              selection: view.state.selection.main,
              middleTop,
            };
          },
          write({ middleTop }) {
            setTimeout(() => {
              view.scrollDOM.scrollTo({
                top: middleTop,
                behavior: "smooth",
              });
            }, 1);
          },
        });
      }
    }
  },
);

Using a transaction extender that adds a scroll effect to transactions that scroll the cursor into view will cause less DOM reflows (and allow you to use the existing centering code).