Find out the 'line' (row) number of the cursor in a textarea
Asked Answered
P

5

37

I would like to find out and keep track of the 'line number' (rows) of the cursor in a textarea. (The 'bigger picture' is to parse the text on the line every time a new line is created/modified/selected, if of course the text was not pasted in. This saves parsing the whole text un-necessarily at set intervals.)

There are a couple of posts on StackOverflow however none of them specifically answer my question, most questions are for cursor position in pixels or displaying lines numbers besides the textarea.

My attempt is below, it works fine when starting at line 1 and not leaving the textarea. It fails when clicking out of the textarea and back onto it on a different line. It also fails when pasting text into it because the starting line is not 1.

My JavaScript knowledge is pretty limited.

<html>

<head>
<title>DEVBug</title>

<script type="text/javascript">

    var total_lines = 1; // total lines
    var current_line = 1; // current line
    var old_line_count;

    // main editor function
    function code(e) {

        // declare some needed vars
        var keypress_code = e.keyCode; // key press
        var editor = document.getElementById('editor'); // the editor textarea
        var source_code = editor.value; // contents of the editor

        // work out how many lines we have used in total    
            var lines = source_code.split("\n");
            var total_lines = lines.length;

    // do stuff on key presses
    if (keypress_code == '13') { // Enter
        current_line += 1;
    } else if (keypress_code == '8') { // Backspace
        if (old_line_count > total_lines) { current_line -= 1; }
    } else if (keypress_code == '38') { // Up
        if (total_lines > 1 && current_line > 1) { current_line -= 1; }
    } else if (keypress_code == '40') { // Down
        if (total_lines > 1 && current_line < total_lines) { current_line += 1; }
    } else {
        //document.getElementById('keycodes').innerHTML += keypress_code;
    }

    // for some reason chrome doesn't enter a newline char on enter
    // you have to press enter and then an additional key for \n to appear
    // making the total_lines counter lag.
    if (total_lines < current_line) { total_lines += 1 };

    // putput the data
    document.getElementById('total_lines').innerHTML = "Total lines: " + total_lines;
    document.getElementById('current_line').innerHTML = "Current line: " + current_line;

    // save the old line count for comparison on next run
    old_line_count = total_lines;

}

</script>

</head>

<body>

<textarea id="editor" rows="30" cols="100" value="" onkeydown="code(event)"></textarea>
<div id="total_lines"></div>
<div id="current_line"></div>

</body>

</html>
Papilloma answered 7/2, 2012 at 23:24 Comment(0)
P
51

You would want to use selectionStart to do this.

<textarea onkeyup="getLineNumber(this, document.getElementById('lineNo'));" onmouseup="this.onkeyup();"></textarea>
<div id="lineNo"></div>

<script>

    function getLineNumber(textarea, indicator) {

        indicator.innerHTML = textarea.value.substr(0, textarea.selectionStart).split("\n").length;
    }

</script>

This works when you change the cursor position using the mouse as well.

Pathless answered 7/2, 2012 at 23:45 Comment(2)
This solution won't work if there are soft linebreaks in the textarea. Example: make a textarea with 10 columns, put in a couple words in it so the text overflows to 2-3 lines - but do NOT add newlines in it. The code above will always return 1 because there is no "\n" character in the textarea - but the user actually sees more than 1 row. That's the real difficulty with TEXTAREAs... I'm really surprised there isn't any standard API for this in modern browsers...Microbicide
This doesn't work for all cases. If you type the text without linebreak, the above function always returns 1. But it could be 2 or more.Sanborne
S
30

This is tough because of word wrap. It's a very easy thing to count the number of line breaks present, but what happens when the new row is because of word wrap? To solve this problem, it's useful to create a mirror (credit: github.com/jevin). Here's the idea:

  1. Create a mirror of the textarea
  2. Send the content from the beginning of the textarea to the cursor to the mirror
  3. Use the height of the mirror to extract the current row

On JSFiddle

jQuery.fn.trackRows = function() {
    return this.each(function() {

    var ininitalHeight, currentRow, firstIteration = true;

    var createMirror = function(textarea) {
        jQuery(textarea).after('<div class="autogrow-textarea-mirror"></div>');
        return jQuery(textarea).next('.autogrow-textarea-mirror')[0];
    }

    var sendContentToMirror = function (textarea) {
        mirror.innerHTML = String(textarea.value.substring(0,textarea.selectionStart-1)).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br />') + '.<br/>.';
        calculateRowNumber();
    }

    var growTextarea = function () {
        sendContentToMirror(this);
    }

    var calculateRowNumber = function () {
        if(firstIteration){
            ininitalHeight = $(mirror).height();
            currentHeight = ininitalHeight;
            firstIteration = false;
        } else {
            currentHeight = $(mirror).height();
        }
        // Assume that textarea.rows = 2 initially
        currentRow = currentHeight/(ininitalHeight/2) - 1;
        //remove tracker in production
        $('.tracker').html('Current row: ' + currentRow);
    }

    // Create a mirror
    var mirror = createMirror(this);

    // Style the mirror
    mirror.style.display = 'none';
    mirror.style.wordWrap = 'break-word';
    mirror.style.whiteSpace = 'normal';
    mirror.style.padding = jQuery(this).css('padding');
    mirror.style.width = jQuery(this).css('width');
    mirror.style.fontFamily = jQuery(this).css('font-family');
    mirror.style.fontSize = jQuery(this).css('font-size');
    mirror.style.lineHeight = jQuery(this).css('line-height');

    // Style the textarea
    this.style.overflow = "hidden";
    this.style.minHeight = this.rows+"em";

    var ininitalHeight = $(mirror).height();

    // Bind the textarea's event
    this.onkeyup = growTextarea;

    // Fire the event for text already present
    // sendContentToMirror(this);

    });
};

$(function(){
    $('textarea').trackRows();
});
Stairway answered 16/1, 2014 at 19:29 Comment(3)
This should be the accepted answer. I haven't tested the code but it at least attempts a working solution that handles soft line breaks.Octahedral
Is the styling a mandatory part of the solution? I implemented this without the CSS and set my textarea wrap property to 'off'. If I then type text into the textarea such that it runs off the edge of the textarea (forcing a horizontal scrollbar to appear), the row number reports as increased even though I haven't created a new row.Alpine
Unfortunately, this has unhandled corner cases as well. E.g.: in a 10 col textarea, if you press ctrl+right in the first line – it will move cursor to 10 (textarea.selectionStart=10) at the end of the first line. But if you try to move cursor to that same textarea.selectionStart=10 position by right or left – cursor visibly will be at the start of the second line.Legalize
S
2

I cracked this when I found out you could get the bounding rect of the cursor, and thus it's y coordinate relative to the parent

const editorCords = document.querySelector('#editor').getClientRects()[0]
const cursorCords = window.getSelection()?.getRangeAt(0).getClientRects()[0]
    
if (editorCords && cursorCords) {
   const line = Math.floor((cursorCords.y - editorCords.y) / cursorCords.height)
   console.log(line)
}
Spurry answered 14/3, 2023 at 7:42 Comment(1)
I haven't tried this yet, but for my use case of wanting to know when a cursor is at the first or last row of a textarea this approach is probably the best oneBlondellblondelle
G
0

This worked for me:

function getLineNumber(textarea) {
  return textarea.value.substr(0, textarea.selectionStart) // get the substring of the textarea's value up to the cursor position
    .split("\n") // split on explicit line breaks
    .map((line) => 1 + Math.floor(line.length / textarea.cols)) // count the number of line wraps for each split and add 1 for the explicit line break
    .reduce((a, b) => a + b, 0); // add all of these together
};

Inspired by colab's answer as a starting point, this includes the number of word wraps without having to introduce a mirror (as in bradbarbin's answer).

The trick is simply counting how many times the number of columns textarea.cols can divide the length of each segment between explicit line breaks \n.

Note: this starts counting at 1.

Glutathione answered 7/5, 2020 at 13:17 Comment(1)
This unfortunately won't work because not all rows reach the full number of columns before doing a soft line break.Jaundiced
O
0

Using elem.selectionEnd I could get the current position of your cursor (which is the end of your selected highlight) then used substr and split("\n") to get the line number when you start typing using onkeyup.

The information is displayed like this:

Ln:3, Col:8, Pos:18, Len:55

This all refers to the current position of your cursor:

  • Ln : Line number
  • Col : Column number (starts at 1)
  • Pos : Offset position
  • Len : Length of the entire text

I was able to do this onclick by calling onkeyup() on that event.

<textarea id=d rows=7
  onkeyup="P=d.selectionEnd;V=d.value;A=V.substr(0,P).split`\n`;p.innerHTML=`Ln:${A.length}, Col:${A.pop().length+1}, Pos:${P}, Len:${V.length}`"
  onclick=this.onkeyup()
  style=width:100%></textarea>

<p id=p></p>

You can also copy-paste this code into your address bar and save it as a bookmark to use later:

data:text/html,<textarea id=d rows=7 onkeyup="P=d.selectionEnd;V=d.value;A=V.substr(0,P).split`\n`;p.innerHTML=`Ln:${A.length}, Col:${A.pop().length+1}, Pos:${P}, Len:${V.length}`" onclick=this.onkeyup() onkeypress=e=event;k=e.keyCode;(e.ctrlKey&&k==13)||k==10?f.srcdoc=d.value:0 style=width:100%></textarea> <p id=p></p>

I think this is a good place to start of you wanted to create your own custom text editor that displays that information. I made a tiny HTML code editor with this code as an example.

Orwin answered 11/6 at 20:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.