Skip to content Skip to sidebar Skip to footer

Preventing Textarea Scroll Behaviour In Chrome After Newline Added

Recently my version of chrome has been doing something strange (74.0.3729.131 on ubuntu 18.04) more and more often. I have a small editor script which has a textarea which displays

Solution 1:

It's almost the end of 2020, Chrome version 86 and this issue still exists? What's more, I am surprised I have not found more information (complaints) on this matter (this post is the only thing I've found which speaks of this issue specifically.) I have observed that this behavior occurs not only in typing, but pasting any text containing a newline. I also observed that if I execute an undo action after this occurs, another random scroll happens, taking me even farther up the page, and nowhere near where the caret is.

I experimented and examined this behavior at much length, and was not able to find any repeatable circumstances which might give a clue as to how to predict when this would occur. It truly just seems "random". Nonetheless, I've had to work around this issue for an NWJS editor app I'm creating (NWJS uses Chrome for UI.)

This is what seems to be working for me:

First all, let me start simple in order to introduce the principle. We attach an "input" listener and "scroll" listener to the textarea. This works because, from my observation anyway, the "input"[1] listener gets fired before the random scroll action occurs.

The scroll listener records each scrolling action and saves it in a global prevScrollPos. It also checks for a global flag scrollCorrection.

The "input" listener sets the scrollCorrection flag everytime text is input into the textarea. Remember, this has happened before the random scroll occurs.

So the next scroll to occur, which may be the nuisance random action, the scroll listener will clear scrollCorrection, then scroll the textarea to the previous scroll position, ie, scrolling it back to where it was before the "random" scroll. But the issue is unpredictable, what if there is no random scroll and the next scroll to occur is intentional? That is not a big deal. It just means that if the user scrolls manually, the first scroll event is basically nullified, but then after that (with scrollCorrection cleared) everything will scroll normally. Since during normal scrolling, events are spit out so rapidly, it is unlikely there will be any noticeable effect.

Here is the code:

let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;

function onScroll(evt) {
  if (scrollCorrection) {
    // Reset this right off so it doesn't get retriggered by the corrction.
    scrollCorrection = false;
    textarea.scrollTop = prevScrollPos;
  }
  prevScrollPos = textarea.scrollTop;
}

function onInput(evt) {
  scrollCorrection = true;
}

window.addEventListener("load", () => {
  textarea = document.getElementById("example_textarea");
  textarea.addEventListener("scroll", onScroll);
  textarea.addEventListener("input", onInput);
})

Now let's expand on it:

There is another consideration. What if the typing or pasting action puts the end of the typed or pasted text (and thus the caret) outside the view of the textarea viewport? When normal scrolling is in play, most browsers will scroll the page[2] so the caret will remain in view. However now that we've taken over scrolling action, we'll need to implement that ourselves.

In the psuedo-code below, on input to the textarea, besides setting scrollCorrection, we call a function which will:

  1. determine the xy position of caret relative to textarea viewport
  2. determine if it is scrolled out of view
  3. if so:
  4. determine the amount to scroll to bring it in view
  5. determine if the random scroll has already occurred by testing the state of scrollCorrection
  6. if it hasn't, set flag scrollCorrection2 containing the amount to scroll
  7. if it has, explicitly do the additional scrolling to bring it back into view

Finding the xy position of the caret in a textarea is not a trivial matter and is outside the scope of this answer, but there are plenty of methods to be found in searching the web. Most involve replicating the textarea contents in a non-form element, eg div block, with similar font, font-size, text wrapping etc, then using getBoundingClientRect on the resultant containing block and such. In my situation, I was already doing most of this for my editor, so it wasn't much of an additional expense. But I've included some psuedo-code to show how this can be implemented in the scroll correction mechanism. setCaretCorrection basically does steps 1 - 7 above.

let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;
let caretCorrection = 0;

function onScroll(evt) {
  if (scrollCorrection) {
    // Reset this right off so it doesn't get retriggered by the correction.
    scrollCorrection = false;
    textarea.scrollTop = prevScrollPos + caretCorrection;
    caretCorrection = 0;
  }

  prevScrollPos = textarea.scrollTop;
}

function onTextareaInput() {
  scrollCorrection = true;
  setCaretCorrection();
}

function setCaretCorrection(evt) {
  let caretPos = textarea.selectionStart;
  let scrollingNeeded;
  let amountToScroll;

  /* ... Some code to determine xy position of caret relative to
         textarea viewport, if it is scrolled out of view, and if
         so, how much to scroll to bring it in view. ... */

  if (scrollingNeeded) {
    if (scrollCorrection) {
      // scrollCorrection is true meaning random scroll has not occurred yet,
      // so flag the scroll listener to add additional correction. This method
      // won't cause a flicker which could happen if we scrollBy() explicitly.
      caretCorrection = amountToScroll;
    } else {
      // Random scroll has already occurred and been corrected, so we are
      // forced to do the additional "out of viewport" correction explicitly.
      // Note, in my situation I never saw this condition happen.
      textarea.scrollBy(0, amountToScroll);
    }
  }
}

One could go further and use the experimental event, "beforeinput"[3], to optimize this a little bit so fewer unnecessary calls to setCaretCorrection are made. If one examines event.data from "beforeinput" event, in certain cases it will report the data to be input. If it does not, then it outputs null. Unfortunately, when a newline is typed, event.data is null. However it will report newlines if they are pasted. So at least one can see if event.data contains a string, and if the string does not contain newlines, skip the whole correction action. (Also, see [1] below.)

[1] I also don't see any reason you couldn't do in the "beforeinput"[3] listener, what what we're doing in the "input" listener. That may also give more insurance that we set scrollCorrection before the random scroll occurs. Although note that "beforeinput" is experimental.

[2] I suspect it is broken implementation of this feature which is causing this issue.

[3] https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event (Available on Chrome and all major browsers except Firefox according to this link.)


Solution 2:

You can try avoiding the events on the textarea with css and js, then force the scroll to it's current position:

css:

textarea { 
    overflow:auto; 
    resize:none; 
    width:90%; 
    height:300px; 
} 

js: You'll need to insert the first answer from this question at A

function preventMoving(e) {
    var key = (e.keyCode ? e.keyCode : e.which);
    if(key == 13) {
        e.preventDefault();
        // A
    }
}

Then on your HTML:

<textarea onkeyup="preventMoving(event);"></textarea>

Post a Comment for "Preventing Textarea Scroll Behaviour In Chrome After Newline Added"