Editor starts partially scrolled down.

I’m not sure why sometimes the editor will start off initially scrolled downward by some amount. What are possible reasons this could happen?

For example, here’s the editor code that I have (defines a <code-mirror> custom element):

The whole connectedCallback method of the element is where all the logic to modify the EditorView instance exists, there’s no logic elsewhere. I’m not explicitly setting a scroll position anywhere there.

The <code-mirror> element is used in the implementation of the <live-code> element here,

The <live-code> element makes a live code editor with live preview, similar to CodePen, but embedded wherever you use the element.

An example of <live-code> follows, which also shows the issue. When you visit the example, it will start at some seemingly-unknown scroll position:

When I visit that example, the editor is scrolled to line 27 like so:

My guess is that maybe some CSS in the page (possibly inside the <live-code> element) is not in place before EditorView does some initial calculations. Not sure yet.

If that is indeed the issue, I wonder if we can fix this inside of EditorView such that if the user hasn’t scrolled, and the dev has not explicitly told EditorView where to scroll to, the initial scroll will always be at the top no matter what CSS styles currently affect layout.

Is this happening on refresh, or on a new navigation to the page? If the former, browsers try to restore the old scroll position on refresh, which can have effects like this.

It doesn’t matter if you refresh or navigate to the page, because either way it is generated from markdown at runtime using docsify.js.

What happens is, no matter how you visit that page, the markdown file for the page content is fetched, then the markdown gets converted to HTML, and inserted with innerHTML.

The HTML string contains the <live-code> element which gets instantiated and creates its ShadowRoot DOM, which in turn instantiates the <code-mirror> element and its ShadowRoot DOM and instantiates EditorView into that shadow DOM (you can see this structure in the browser element inspector).

Basically, the skeleton of the page exists, and the content in which EditorView will reside is generated, and finally EditorView is instantiated into that (regardless of how you arrive at the page).

You can see at the top of the site a blue progress bar. This represents the fetching and parsing of the markdown and eventual addition of the HTML string into the DOM. When the HTML is inserted, Docsify waits for any load events of any content that might load stuff (f.e. <img> elements), then finally reveals the page. When the page is revealed, that’s when the progress bar completes and disappears and we see the live code demo.

Could it be that the page is initially hidden when EditorView is instantiated, then when the page is revealed the new layout is not the same, and EditorView made some initial calculations based on the layout of the hidden DOM? The DOM not being visible could cause things like bounding client rects to be zero size, etc.

I am guessing that this might be the case.

I’m also thinking that we could update EditorView so that, no matter what, if these two conditions are true:

  • user has not scrolled the editor
  • code has not programmatically modified scroll position

then EditorView can ensure scroll position is zero and not rely on layout values from DOM in any way during that state, if that isn’t already the case.

The user will only scroll once the editor is visible in the DOM layout, so it should work well to prevent this from happening regardless what layout there is when the editor is created

I’m conjecturing though, not sure exactly what is happening yet.

Strange. If you can isolate it in a simpler setup I’d be interested in seeing if I can figure out what’s going on.

You can force the editor to scroll itself to the top on init with the scrollTo option to EditorView.

Just wanted to add here that I too was debugging a similar issue with code mirror initializing with content scrolled partially down (always by the same amount) inside a container with a fixed height. The amount scrolled down differed depending on the height of the container, but was the amount was consistently relative to the container height of the container. It also topped out at a certain amount, meaning it never scrolled past just ~7 lines of content.

I initially thought it was a CSS issue, but the EditorView config option scrollTo: EditorView.scrollIntoView(0) solved the issue.

It does seem like an unexpected behaviour though.

@marijn

I’m noticing an unexpected behaviour where scrollTo: EditorView.scrollIntoView(0) impacts the page scroll position on load

when I remove that line the page starts at the top,
when I add it, the page initializes somewhere near the bottom

const view = new EditorView({
  state: EditorState.create({
    doc: content,
    extensions,
  }),
  parent: container ?? undefined,
  // prevent issue where the editor initializes
  // with content partially scrolled down
  scrollTo: EditorView.scrollIntoView(0),
});

it doesn’t seem to have any effect on the page scroll position after the page has loaded tho (scratches head). Switching tabs and creating a new EditorView does not have any effect on the page scroll position.

Seems scrollTo: EditorView.scrollIntoView(0) will cause the page to scroll by way of scrollRectIntoView here.

I can only seem to create a snapshot AFTER the instantiation of an EditorView, and I can’t seem to pass in fake a snapshot because of the runtime type check.

Resetting the scroll position of the .cm-scroller container manually with setTimeout(resetScrollTop, 0) does resolve the page scrollTop, and content scrollTop issue but results in content flicker as it jumps from the initial scroll position back to the top.


Ok, I found a very hacky solution but it solves the issue and doesn’t produce a flicker

const scrollPosition: any = {
  isSnapshot: true,
  range: {
    head: 0,
  },
  yMargin: 0,
  xMargin: 0,
  map: () => scrollPosition,
};

const scrollSnapshot = {
  is: () => true,
  map: () => scrollSnapshot,
  value: {
    clip: () => scrollPosition,
  },
};

const view = new EditorView({
  state: EditorState.create({
    doc: content,
    extensions,
  }),
  parent: container ?? undefined,
  // prevent editor initializing
  // with content partially scrolled down,
  scrollTo: scrollSnapshot,
  // NOTE:
  // scrollTo: EditorView.scrollIntoView(0)
  // causes the page to scroll to the editor
  // https://discuss.codemirror.net/t/editor-starts-partially-scrolled-down/7567/6
});

Indeed, scrollIntoView will scroll the given position into view, which may also require moving the document scroll position or other scrollable parent elements.

Got an example? Would be nice for the API docs to have small example snippets. EDIT: nvm, I see @thomfoolery’s example above.

@thomfoolery what is scrollSnapshot in your example? I don’t see where it comes from. EDIT: Sorry, I do see it, but it references itself, not sure what it is doing.

@trusktr
scrollSnapshot returns a StateEffect that can be passed to scrollTo to set the initial scroll position. It’s handled differently than scrollIntoView and does not trigger the window/page to scroll the containing element into view.

The problem is scrollSnapshot is an instance method on EditorView, not a static method like scrollIntoView. So if you haven’t instantiated an EditorView yet there is no way of getting/creating a scrollSnapshot to pass in.

After reviewing the source code I was able to duck-type an object that masquerades as a StateEffect<ScrollPosition>. The result is that I am able to set the initial scrollTop of the cm-scroller element without triggering the page to scroll.

@marijn Is there a cleaner way to do this?
Why can’t we pass in a simple x or y scroll position to initialize too?

There does seem to be some thing funky happening when an EditorView with scrollable content is instantiated inside a hidden container, it appears to mess with the initial scroll position.

I’ll try and create an example in stackblitz I can share.

Would an additional field on the options to scrollIntoView that limits scrolling to the editor’s scroll DOM element provide what you are looking for here?

Would an additional field on the options to scrollIntoView that limits scrolling to the editor’s scroll DOM element provide what you are looking for here?

@marijn Apologies, I didn’t notice your response.
Yes, being able to isolate the scrollIntoView behavior to the editor’s scroll area would help my issue.

I’ll admit I’ve been struggling to re-create my issue in a simplified example in stackblitz, which is telling me that there is something our implementation that might is causing this issue for us. Once I can isolate the cause in our code base, I’ll hopefully be able to share public demo of the issue.

I hope I have more time next week to focus on this. Other priorities ate up my attention this week.