TextArea Cursor Position with JavaScript

I’d recently searched around for some good-quality JavaScript snippets to determine the cursor position within an HTML TextArea, but haven’t had any luck.  So, like any fellow geek would do, I came up with my own solution.

The DOM in IE does not provide information regarding relative position in terms of characters; however, it does provide bounding and offset values for browser-rendered controls.  Thus, I used these values to determine the relative bounds of a character. Then, using the JavaScript TextRange, I created a mechanism for working with such measures to calculate the Line and Column position for fixed-width fonts within a given TextArea.

First, the relative bounds for the TextArea must be calculated based upon the size of the fixed-width font used.  To do this, the original value of the TextArea must be stored in a local JavaScript variable and clear the value.  Then, a TextRange is created to determine the Top and Left bounds of the TextArea.

var storedValue = textBox.value;

textBox.value = "";

textBox.select();

var caretPos = document.selection.createRange();

textBox.__boundingTop = caretPos.boundingTop;

textBox.__boundingLeft = caretPos.boundingLeft;

Next, in order to capture the bounding width of a single fixed-width character, the value of the TextArea is set to a single character, selected, and another TextRange is created.

textBox.value = " ";

textBox.select();

caretPos = document.selection.createRange();

textBox.__boundingWidth = caretPos.boundingWidth;

textBox.__boundingHeight = caretPos.boundingHeight;

 

textBox.value = storedValue;

The values obtained from the initialization will persist in the instance of the TextArea, textBox.  Also, in order for this initialization to occur at least once, it must be registered with the onLoad event.

 

Next, the TextArea must be configured to capture the cursor position.  Mouse and keyboard activity will be captured as follows.

 <textarea name="txtLayoutViewer"

   onmouseup="updatePosition(this)"

   onmousedown="updatePosition(this)"

   onkeyup="updatePosition(this)"

   onkeydown="updatePosition(this)"

   onfocus="updatePosition(this)"

   rows="15"

   cols="75"></textarea>

When the updatePosition method is called, another TextRange is created to capture the cursor selection.  Then, using the values calculated during the initialization and those in the TextRange, the Line and Column values are calculated as follows.

var caretPos = document.selection.createRange();

var boundingTop = (caretPos.offsetTop + textBox.scrollTop) - textBox.__boundingTop;

var boundingLeft = (caretPos.offsetLeft + textBox.scrollLeft) - textBox.__boundingLeft; 

textBox.__Line = (boundingTop / textBox.__boundingHeight) + 1;

textBox.__Column = (boundingLeft / textBox.__boundingWidth) + 1;

 

As with the bounds captured during initialization, the Line and Column values persist in the instance of the TextArea.  They can then be used be other elements throughout the DOM. 

Below is the complete code listing.

<html>

<head>

    <script language="JavaScript" type="text/javascript">

    <!--

        function initPosition(textBox) {

            var storedValue = textBox.value;

            textBox.value = "";

            textBox.select();

            var caretPos = document.selection.createRange();

            textBox.__boundingTop = caretPos.boundingTop;

            textBox.__boundingLeft = caretPos.boundingLeft;

            textBox.value = " ";

            textBox.select();

            caretPos = document.selection.createRange();

            textBox.__boundingWidth = caretPos.boundingWidth;

            textBox.__boundingHeight = caretPos.boundingHeight;

            textBox.value = storedValue;

        }

        function storePosition(textBox) {

            var caretPos = document.selection.createRange();

            var boundingTop = (caretPos.offsetTop + textBox.scrollTop) - textBox.__boundingTop;

            var boundingLeft = (caretPos.offsetLeft + textBox.scrollLeft) - textBox.__boundingLeft;

            textBox.__Line = (boundingTop / textBox.__boundingHeight) + 1;

            textBox.__Column = (boundingLeft / textBox.__boundingWidth) + 1;

        } 

        function updatePosition(textBox) {

            storePosition(textBox);

            document.forms[0].txtLine.value = textBox.__Line;

            document.forms[0].txtColumn.value = textBox.__Column;

        }

    //-->

    </script>

    <style type="text/css">

        body, td, tg, input, select {

            font-family: Verdana;

            font-size: 10px;

        }

    </style>

</head>

    <form>

        <table cellspacing="0" cellpadding="3">

            <tr>

                <td colspan="3">

                    Change Font Size

                    <select onchange="this.form.txtLayoutViewer.style.fontSize = this.options[this.selectedIndex].value; initPosition(this.form.txtLayoutViewer);">

                        <option value="10">10px</option>

                        <option value="12">12px</option>

                        <option value="14">14px</option>

                        <option value="16">16px</option>

                        <option value="18">18px</option>

                        <option value="20">20px</option>

                        <option value="24">24px</option>

                        <option value="36">36px</option>

                    </select>

                </td>

            </tr>

            <tr>

                <td colspan="3">

                    <textarea name="txtLayoutViewer"

                        onmouseup="updatePosition(this)"

                        onmousedown="updatePosition(this)"

                        onkeyup="updatePosition(this)"

                        onkeydown="updatePosition(this)"

                        onfocus="updatePosition(this)"

                        rows="15"

                        cols="75">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec ornare aliquam quam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Pellentesque et quam in dui consequat tempor. Etiam lorem lectus, sollicitudin laoreet, tincidunt nec, pharetra in, magna. Mauris accumsan velit et augue. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.</textarea></td>

            </tr>

            <tr>

                <td width="70%">

                     </td>

                <td width="10%">

                    Line <input type="text" name="txtLine" style="width: 25px" readonly></td>

                <td width="20%">

                    Column <input type="text" name="txtColumn" style="width: 25px" readonly></td>

            </tr>

        </table>

    </form>

</body>

</html>

 

Please note that this has only been tested on IE 6.  As always, I welcome your feedback and constructive criticism.

UPDATE 3/29/3005:

A friend of mine filled me in on a limitation to this approach.  If the TextArea extends beyond the screen, thus requiring scrolling, the calculated values will be incorrect due to the fact that the bounds used in the calculation change when the window has scrolled.  Moreover, if the text you are working with requires that you scroll beyond the viewable portion of the TextArea, the calculated values will reverse.  In order to fix this, the scolling index of the screen and/or TextArea must be obtained to offset the calcuation.  Thus, if you plan on using this approach ensure that the requirements are such that the TextArea does not require scrolling.  Otherwise, if you feel so inclined, email with your thoughts on obtaining the offset value required when scrolling.  Thanks!

UPDATE 4/12/2005:

Thanks to Mr. Noisy, the previous bug listed (TextArea scrolling) has been fixed, and now accounts for the scrollTop and scrollLeft properties of the TextArea.  I've updated the code in this post to reflect the changes.

New on Geeks with Blogs

  • We Won The One Award I Actually Care About

    Full Scale made the Inc. 5000 for the fifth year straight, the 12th listing across my three companies. Here is why the one award you cannot buy is worth stopping for.

  • Your Customers Build the Features Now

    I let a tool I liked sit dead for a year rather than build the features I wanted. An MCP server meant I never had to, and your customers can do the same to your product.

  • Get the Size of a Directory in Linux the Easy Way

    du -sh for the quick answer, ncdu for the cleanup, df for the disk itself: every command for checking directory size in Linux, plus why du and df never agree.

  • Vim Search and Replace: The Ultimate Guide

    One :%s command replaces every match in a file before a find dialog would even open. The Vim substitute patterns worth the muscle memory: flags, ranges, capture groups, and multi-file edits.