0

I'm using a div to format and display the text from a textarea of equal dimensions and I need them to be permanently in sync. However, I haven't been able to synchronize their respective scrollTops after the input text goes past the bottom of the textarea.

My process has been similar to the one described here, however I can't get his solution to work on my project.

Here's a demo and snippets of the minimum relevant code:

<section>
  <div class="input-text__container">
    <div id="input-text--mirror" class="input-text"></div>
      <textarea
        id="input-text--original"
        cols="30"
        rows="6"
        autofocus
        class="input-text"
        placeholder="Enter your text here"
        autocomplete="off"
        autocorrect="off"
        spellcheck="false"
      ></textarea>
  </div>
<section>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap');

html {
  font-size: 62.5%;
  box-sizing: border-box;
  scroll-behavior: smooth;
}

*,
*::after,
*::before {
  margin: 0;
  padding: 0;
  border: 0;
  box-sizing: inherit;
  vertical-align: baseline;
}

body {
  height: 100vh;
}

section {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  padding: 1rem;
}

.input-text__container {
  width: 100%;
  position: relative;
  flex: 1;
}

.input-text {
  width: 100%;
  height: 100%;
  position: absolute;
  font-size: 3.2rem;
  overflow-wrap: break-word;
  font-family: "Inter";
}

#input-text--mirror {
  background-color: #e9ecf8;
  color: #0a3871;
  overflow: hidden;
}

#input-text--original {
  background-color: transparent;
  -webkit-text-fill-color: transparent;
  resize: none;
  outline: none;
  -ms-overflow-style: none; /* IE and Edge */
  scrollbar-width: none; /* Firefox */
}

#input-text--original::placeholder {
  color: #e9ecf8;
  -webkit-text-fill-color: #052051;
}

#input-text--original::selection {
  -webkit-text-fill-color: #ffffff;
}

.invalid {
  font-weight: 400;
  color: #ff0000;
}

#input-text--original::-webkit-scrollbar {
  display: none;
}
let invalidInput = false;
const patterns = {
  valid: "a-z ",
  invalid: "[^a-z ]",
  linebreaks: "\r|\r\n|\n",
};
const textIn = document.querySelector("#input-text--original");
const mirror = document.querySelector("#input-text--mirror");

function validateInput(string, className) {
  let anyInvalidChar = false;

  // Generate regular expressions for validation
  const regExpInvalids = new RegExp(patterns.invalid, "g");
  const regExpLinebreaks = new RegExp(patterns.linebreaks);

  // Generate innerHTML for mirror
  const mirrorContent = string.replace(regExpInvalids, (match) => {
    if (regExpLinebreaks.test(match)) {
      return "<br/>";
    } else {
      anyInvalidChar = true;
      return `<span class=${className}>${match}</span>`;
    }
  });
  // Update mirror
  mirror.innerHTML = mirrorContent;

  return anyInvalidChar;
}

textIn.addEventListener("input", (e) => {
  const plain = textIn.value;
  const newInputValidity = validateInput(plain, "invalid");
  mirror.scrollTop = textIn.scrollTop;
});

textIn.addEventListener(
  "scroll",
  () => {
    mirror.scrollTop = textIn.scrollTop;
  },
  { passive: true }
);

On a desktop screen typing the first 8 natural numbers in a column should be enough to reproduce the issue.

The last thing I checked, but perhaps the most relevant so far was this. It seems to deal with the exact same issue on React, but I'm afraid I don't know how to adapt that solution to Vanilla JavaScript, since I'm just starting to learn React. Please, notice, I'm trying to find a solution that doesn't depend on libraries like jQuery or React.

Besides that, I tried the solution described in the aforementioned blog, by replacing return "<br/>"; with return "<br/>&nbsp;"; in my validateInput function but that didn't work. I also added a conditional to append a space to plain in const plain = textIn.value; in case the last char was a linebreak, but I had no luck.

I also included console.log commands before and after mirror.scrollTop = textIn.scrollTop; in the textIn scroll handler to track the values of each scrollTop and even when they were different, the mirror scrollTop wasn't updated. I read it might be because divs weren't scrollable by default, but adding "overflow: scroll" to its styles didn't fix the problem either.

I read about other properties related to scrollTop, like offsetTop and pageYOffset, but they're either read-only or not defined for divs.

I've reviewed the following posts/sites, too, but I've still haven't been able to fix this problem.

I no longer remember what else I've reviewed, but nothing has worked and I no longer know what else to do. Thank you for your attention and help.

1 Answers1

0

After trying to replicate the solution for a React app that I mentioned in the post, using vanilla JavaScript (demo here), I tried to apply that to my own project and all I had to do was adding a <br> tag to the mirror in the end of my validateInput function. That is: mirror.innerHTML = mirrorContent + "<br>";.

Besides that, updating the mirror's scrollTop every time the input event on the textarea was triggered was not needed. Neither was it to pass the { passive: true } argument to the scroll event.

The modified code is here:

function validateInput(string, className) {
  let anyInvalidChar = false;

  // Generate regular expressions for validation
  const regExpInvalids = new RegExp(patterns.invalid, "g");
  const regExpLinebreaks = new RegExp(patterns.linebreaks);

  // Generate innerHTML for mirror
  const mirrorContent = string.replace(regExpInvalids, (match) => {
    if (regExpLinebreaks.test(match)) {
      return "<br/>";
    } else {
      anyInvalidChar = true;
      return `<span class=${className}>${match}</span>`;
    }
  });
  // Update mirror
  mirror.innerHTML = mirrorContent + "<br>";

  return anyInvalidChar;
}

textIn.addEventListener("input", (e) => {
  const plain = textIn.value;
  const newInputValidity = validateInput(plain, "invalid");
});

textIn.addEventListener("scroll", () => mirror.scrollTop = textIn.scrollTop);