How to vertically navigate through a textarea using caret position

I’m working on a set of vanilla js functions to navigate an html textarea when the appropriate arrow button is clicked.

for example,

var text = document.getElementById('text');
function larr(){
  text.focus();
  var pos = text.selectionStart;
  pos--
  text.setSelectionRange(pos, pos);
}
<textarea id='text'></textarea>
<button onclick="larr()">&larr;</button>

The left and right functions are simple enough, but I would also like to include up and down arrows. Since each newline can have a different amount of characters, I don’t think it is as simple as setting the position forward or back one max-line-length.

I would settle for the arrows taking you to the next or previous line break. I was thinking about splitting the textarea value at the caret position and looping through characters in that direction until a n is reached, but I can’t wrap my head around it.

Does anyone have any suggestions? Thanks!

*IMPORTANT NOTEthis would be for mobile with the native keyboard hidden, so no help from the os! (unless maybe jquery trigger() or execCommand?)

Answer

Caveat: it is recommended if you use this method to disable the ability to resize your textarea. This way you can control the adjustor needed to account for the difference of varying sizes of charactrer widths and how selection of characters varies in terms of actual width with textarea wordwrap.

NOTE: Did not test this on MOBILE VERSION.

I was able to get this to work using a couple of functions I found online here: How to get number of rows in textarea using JavaScript?. Basically the poster used the height, line-height, overflow and scroll-height to determine the height of the textarea and, in short, get the amount of lines within the textareas content. I included the posters original comments to help you understand the logic. I adjusted their second function to divide the amount of lines with the amount of characters within the textarea, this gives us a rudimentary idea of how wide each line is, although it is not precise! See Caveat… Depending on the characters within each line, which can change depending on the width of the textarea, the rounded amount of characters in the line will vary. So when we go up a line or down a line it will jump right or left depending on that variation.

How we get the line up/down…

    let style = (window.getComputedStyle) ?
    window.getComputedStyle(text) : text.currentStyle,

    // This will get the line-height if it is set in the css
    textLineHeight = parseInt(style.lineHeight, 10),
    // Get the scroll height of the textarea
    textHeight = calculateContentHeight(text, textLineHeight),
    // calculate the number of lines by dividing
    // the scroll height by the line-height
    numberOfLines = Math.ceil(textHeight / textLineHeight),
    // get the amount of characters in the textarea
    numOfChars = text.value.length,
    // this following number will vary depending on how the width of your 
    // lines character count is calculated and rounded in terms 
    // of what the actual width in character count actually is
    // you will have to adjust this number accordingly
    adjustor = 14,
    // divide the number of characters by the amount of lines
    percentage = numOfChars / numberOfLines + adjustor;

    return percentage;

Again, this is not as precise with the up and down but it works, moving the cursor up or down on button press it shifts lightly left or right depending on the amount the rounded count of characters is against the amount on that particular line.

EDIT: I have combined your movement functions into one function that runs the button class through a forEach loop and uses the event target to check the ID of each element and move the cursor accordingly.

let text = document.getElementById('text');
text.value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Mauris in aliquam sem fringilla ut morbi tincidunt augue. Scelerisque in dictum non consectetur a erat nam. Consectetur adipiscing elit ut aliquam purus sit amet luctus venenatis. ";
// lets just set a cursor middle of the road for testing...
text.focus();
text.setSelectionRange(181, 181);

let calculateContentHeight = (text, scanAmount) => {
  let origHeight = text.style.height,
    height = text.offsetHeight,
    scrollHeight = text.scrollHeight,
    overflow = text.style.overflow;

  /// only bother if the ta is bigger than content
  if (height >= scrollHeight) {

    /// check that our browser supports changing dimension
    /// calculations mid-way through a function call...
    text.style.height = `${(height + scanAmount)}px`;

    /// because the scrollbar can cause calculation problems
    text.style.overflow = 'hidden';

    /// by checking that scrollHeight has updated
    if (scrollHeight < text.scrollHeight) {

      /// now try and scan the ta's height downwards
      /// until scrollHeight becomes larger than height
      while (text.offsetHeight >= text.scrollHeight) {
        text.style.height = `${(height -= scanAmount)}px`;
      }

      /// be more specific to get the exact height
      while (text.offsetHeight < text.scrollHeight) {
        text.style.height = `${(height++)}px`;
      }

      /// reset the ta back to it's original height
      text.style.height = origHeight;

      /// put the overflow back
      text.style.overflow = overflow;

      return height;
    }
  } else {
    return scrollHeight;
  }
}

let calculateLineWidth = (text) => {
  let style = (window.getComputedStyle) ?
    window.getComputedStyle(text) : text.currentStyle,

    // This will get the line-height only if it is set in the css,
    // otherwise it's "normal"
    textLineHeight = parseInt(style.lineHeight, 10),

    // Get the scroll height of the textarea
    textHeight = calculateContentHeight(text, textLineHeight),

    // calculate the number of lines
    numberOfLines = Math.ceil(textHeight / textLineHeight),

    // get the amount of characters in the textarea
    numOfChars = text.value.length,

    // this number will vary depending on how the width of your 
    // lines character count is calculated and rounded in terms 
    // of what the actual width in character count actually is
    // you will have to adjust this number accordingly
    adjustor = 14,

    // divide the number of characters by the amount of lines
    percentage = numOfChars / numberOfLines + adjustor;

  return percentage;
}

const moveCursor = (text) => {
  const btns = document.querySelectorAll('.btns');
  btns.forEach((btn) => {
    btn.addEventListener('click', (e) => {
      text.focus();
      let pos = text.selectionStart;
      if (e.target.id === 'left') {
        pos--;
        text.setSelectionRange(pos, pos);
      } else if (e.target.id === 'right') {
        pos++;
        text.setSelectionRange(pos, pos);
      } else if (e.target.id === 'up') {
        if (pos - Number(calculateLineWidth(text)) > 0) {
          pos = pos - Number(calculateLineWidth(text));
          text.setSelectionRange(pos, pos);
        } else {
          text.setSelectionRange(0, 0);
        }
      } else {
        pos = pos + Number(calculateLineWidth(text));
        text.setSelectionRange(pos, pos)
      }
    })
  })
}
moveCursor(text);
#text {
  line-height: 1.5;
  text-align: justify;
  resize: none;
}

#btns {
  display: flex;
  align-items: center;
}

#mid {
  display: flex;
  flex-direction: column;
}

.btns {
  height: 20px;
}
<textarea id='text' cols="50" rows="6"></textarea>
<div id="btns">
  <button id="left" class="btns">&larr;</button>
  <div id="mid">
    <button id="up" class="btns">&#129045;</button>
    <button id="down" class="btns">&#129043;</button>
  </div>
  <button id="right" class="btns">&rarr;</button>
</div>

Leave a Reply

Your email address will not be published. Required fields are marked *