Smooth scroll line into view

I’m very new to CM6 and probably missing some obvious pieces but please help me.

I want to scroll editor to a specific line with fractional precision. My use case is I’m showing a preview of the document being edited. I have a mapping between vertical positions in the preview and line numbers. I want to achieve smooth scrolling of the editor when the preview is scrolled.

I think I’ve found how to get line a number from editor position but I can’t find how to get a position from line number. I also found how to scroll a position into view but I believe it’s granularity is whole lines and it’s instantaneous without an option to animate it.

Please advice.

To get a line’s vertical position, you could use visualLineAtHeight. Smooth scrolling isn’t something the library will do for you—you’ll have to set up your own timed process that incrementally updates the scroll position of editor.scrollDOM.

Two question.

  1. Is it safe to rely on editor.scrollDOM position for the editor scroll position? Can I also put an event handler on it to monitor editor scroll position?

  2. visualLineAtHeight seem to do the trick but it also seems a bit off.

Consider this state of the editor:

visualLine = view.visualLineAtHeight(0)
line = view.state.doc.lineAt(visualLine.from)
line.number // => 3368
line.text //  => "xero_tenant_id = 'YOUR_XERO_TENANT_ID' # String | Xero identifier for Tenant"

The line text matches the line text:

But I’d expect a different line. 3369 in the first screenshot. Is this an issue with my setup of is this some sort of a bug?

Registering a "scroll" handler through the editor’s DOM event system will notify you whenever the editor, or any of its parent elements, scrolls.

I don’t understand your visualLineAtHeight example. Why would 3369 sit at height 0?

Line 3369 is partially in view. The line I get (3368) is completely out of view. Isn’t height 0 the top of the view?

Ah, I was looking at the wrong screenshot (where 3368 was in view). Anyway, if you don’t specify a 2nd argument, your first argument is interpreted in terms of window coordinates, so if the editor isn’t right at the top of the window, you’ll get the line that would sit there there, even if scrolled out of view.

That second argument is a bit confusing.

Docs say:

Heights are interpreted relative to the given editorTop position.

My interpretation is that for each line editorTop has to increase. That is, for example, with lines of 20px, view.visualLineAtHeight(0, 10) would give me the first line view.visualLineAtHeight(0, 30) would give me the second and so on.

However, after experimentation I’ve found the following snippet that seem to give me the line number at the top edge of the editor:

view.state.doc.lineAt(
  view.visualLineAtHeight(
    0,
    view.contentDOM.getBoundingClientRect().top - editorContainer.getBoundingClientRect().top
  ).from
).number

That top difference comes out negative (or 0 when scrolled all the way up) and I don’t know how to interpret it or how to reconcile it with the docs.

Is this code correct?

I agree that is not very obvious. What is happening is that the height you pass is interpreted relative to some ‘top of the content’ value. If you don’t pass a second argument, it will use the pixel position of the top of the content on (or off) screen. That’s the usual case, used when interpreting something like mouseEvent.clientY. You can also pass 0 to have the first argument interpreted as a plain in-document vertical offset.

In your case, you’d want to pass view.scrollDOM.getBoundingClientRect().top as first argument, and omit the second argument. (What you’re doing now will work for most cases but break down when the content is so tall that non-rendered content is scaled down to avoid DOM size limitations.)

I’ve changed the argument name to docTop and modified the doc comments a bit in this patch to hopefully make this a bit more obvious.

contentDOM is what holds the lines, isn’t it? So with relation to it’s bounding rect I would always get the first line. I tried and it appears to be the case: regardless of the scroll position, I always get the first line.

Other options I tried is dom and scrollDOM. These two give me the topmost visible line. Which one is better to use?

And with regards of the documentation I wonder if it needs to be updated again or if I have done something not quite right again.

Recap:

I want the number of the topmost visible line in the editor. This code gives me it:

editor.state.doc.lineAt(
  editor.visualLineAtHeight(
    editor.scrollDOM.getBoundingClientRect().top
  ).from
).number

Sorry, I meant scrollDOM, not contentDOM, in my example. I’ve edited my post to fix that.

2 Likes

(For anyone following this thread, please note that visualLineAtHeight appears to be deprecated: Release 0.20.0. The new way to handle this is: CodeMirror Reference Manual)

1 Like