How to Get the Caret Column (Not Pixels) Position in a Textarea, in Characters, from the Start

How to get the caret column (not pixels) position in a textarea, in characters, from the start?

With Firefox, Safari (and other Gecko based browsers) you can easily use textarea.selectionStart, but for IE that doesn't work, so you will have to do something like this:

function getCaret(node) {
if (node.selectionStart) {
return node.selectionStart;
} else if (!document.selection) {
return 0;
}

var c = "\001",
sel = document.selection.createRange(),
dul = sel.duplicate(),
len = 0;

dul.moveToElementText(node);
sel.text = c;
len = dul.text.indexOf(c);
sel.moveStart('character',-1);
sel.text = "";
return len;
}

(complete code here)

I also recommend you to check the jQuery FieldSelection Plugin, it allows you to do that and much more...

Edit: I actually re-implemented the above code:

function getCaret(el) { 
if (el.selectionStart) {
return el.selectionStart;
} else if (document.selection) {
el.focus();

var r = document.selection.createRange();
if (r == null) {
return 0;
}

var re = el.createTextRange(),
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint('EndToStart', re);

return rc.text.length;
}
return 0;
}

Check an example here.

Get the offset position of the caret in a textarea in pixels

Here's an approach using rangyinputs, rangy and jQuery.

It basically copies the whole text from inside the textarea into a div of the same size. I have set some CSS to ensure that in every browser, the textarea and the div wrap their content in exactly the same way.

When the textarea is clicked, I read out at which character index the caret is positioned, then I insert a caret span at the same index inside the div. By only doing that I ended up having an issue with the caret span jumping back to the previous line if the user clicked at the start of a line. To fix that I check if the previous character is a space (which would allow a wrap to occur), if that is true, I wrap it in a span, and I wrap the next word (the one directly after the caret position) in a span. Now I compare the top values between these two span's, if they differ, there was some wrapping going on, so I assume that the top and the left value of the #nextword span are equivalent to the caret position.

This approach can still be improved upon, I'm sure I haven't thought of everything that could possibly go wrong, and even if I have, then I haven't bothered implementing a fix for all of them as I don't have the time to do so at the moment, a number of things that you would need to look at:

  • it doesn't yet handle hard returns inserted with Enter (fixed)
  • positioning breaks when entering multiple spaces in a row (fixed)
  • I think hyphens would allow a content wrap to occur as well..

Currently it works exactly the same way across browsers here on Windows 8 with the latest versions of Chrome, Firefox, IE and Safari. My testing has not been very rigorous though.

Here's a jsFiddle.

I hope it will help you, at the very least it might give you some ideas to build on.

Some Features:

  • I have included a ul for you which is positioned in the right spot, and fixed a Firefox issue where the textarea selection was not re-set back to its original spot after the DOM manipulations.

  • I have added IE7 - IE9 support and fixed the multiple word selection issue pointed out in the comments.

  • I have added support for hard returns inserted with Enter and multiple spaces in a row.

  • I have fixed an issue with the default behaviour for the ctrl+shift+left arrow text selection method.

JavaScript

function getTextAreaXandY() {

// Don't do anything if key pressed is left arrow
if (e.which == 37) return;

// Save selection start
var selection = $(this).getSelection();
var index = selection.start;

// Copy text to div
$(this).blur();
$("div").text($(this).val());

// Get current character
$(this).setSelection(index, index + 1);
currentcharacter = $(this).getSelection().text;

// Get previous character
$(this).setSelection(index - 1, index)
previouscharacter = $(this).getSelection().text;

var start, endchar;
var end = 0;
var range = rangy.createRange();

// If current or previous character is a space or a line break, find the next word and wrap it in a span
var linebreak = previouscharacter.match(/(\r\n|\n|\r)/gm) == undefined ? false : true;

if (previouscharacter == ' ' || currentcharacter == ' ' || linebreak) {
i = index + 1; // Start at the end of the current space
while (endchar != ' ' && end < $(this).val().length) {
i++;
$(this).setSelection(i, i + 1)
var sel = $(this).getSelection();
endchar = sel.text;
end = sel.start;
}

range.setStart($("div")[0].childNodes[0], index);
range.setEnd($("div")[0].childNodes[0], end);
var nextword = range.toHtml();
range.deleteContents();
var position = $("<span id='nextword'>" + nextword + "</span>")[0];
range.insertNode(position);
var nextwordtop = $("#nextword").position().top;
}

// Insert `#caret` at the position of the caret
range.setStart($("div")[0].childNodes[0], index);
var caret = $("<span id='caret'></span>")[0];
range.insertNode(caret);
var carettop = $("#caret").position().top;

// If preceding character is a space, wrap it in a span
if (previouscharacter == ' ') {
range.setStart($("div")[0].childNodes[0], index - 1);
range.setEnd($("div")[0].childNodes[0], index);
var prevchar = $("<span id='prevchar'></span>")[0];
range.insertNode(prevchar);
var prevchartop = $("#prevchar").position().top;
}

// Set textarea selection back to selection start
$(this).focus();
$(this).setSelection(index, selection.end);

// If the top value of the previous character span is not equal to the top value of the next word,
// there must have been some wrapping going on, the previous character was a space, so the wrapping
// would have occured after this space, its safe to assume that the left and top value of `#nextword`
// indicate the caret position
if (prevchartop != undefined && prevchartop != nextwordtop) {
$("label").text('X: ' + $("#nextword").position().left + 'px, Y: ' + $("#nextword").position().top);
$('ul').css('left', ($("#nextword").position().left) + 'px');
$('ul').css('top', ($("#nextword").position().top + 13) + 'px');
}
// if not, then there was no wrapping, we can take the left and the top value from `#caret`
else {
$("label").text('X: ' + $("#caret").position().left + 'px, Y: ' + $("#caret").position().top);
$('ul').css('left', ($("#caret").position().left) + 'px');
$('ul').css('top', ($("#caret").position().top + 14) + 'px');
}

$('ul').css('display', 'block');
}

$("textarea").click(getTextAreaXandY);
$("textarea").keyup(getTextAreaXandY);

HTML

<div></div>
<textarea>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</textarea>
<label></label>
<ul>
<li>Why don't you type this..</li>
</ul>

CSS

body {
font-family: Verdana;
font-size: 12px;
line-height: 14px;
}
textarea, div {
font-family: Verdana;
font-size: 12px;
line-height: 14px;
width: 300px;
display: block;
overflow: hidden;
border: 1px solid black;
padding: 0;
margin: 0;
resize: none;
min-height: 300px;
position: absolute;
-moz-box-sizing: border-box;
white-space: pre-wrap;
}
span {
display: inline-block;
height: 14px;
position: relative;
}
span#caret {
display: inline;
}
label {
display: block;
margin-left: 320px;
}
ul {
padding: 0px;
margin: 9px;
position: absolute;
z-index: 999;
border: 1px solid #000;
background-color: #FFF;
list-style-type:none;
display: none;
}
@media screen and (-webkit-min-device-pixel-ratio:0) {
span {
white-space: pre-wrap;
}
}
div {
/* Firefox wrapping fix */
-moz-padding-end: 1.5px;
-moz-padding-start: 1.5px;
/* IE8/IE9 wrapping fix */
padding-right: 5px\0/;
width: 295px\0/;
}
span#caret
{
display: inline-block\0/;
}

Find caret position in textarea in pixels

This "raw code" works at least in IE.

Using the code you can put your <TEXTAREA> where ever you want in page, and the <DIV id="db"> will follow it. Even despite of the scrolling position of the page. You can fix the position of <DIV> by changing the literal numbers at d.style...6-statements.

<body>
<div id="db" style="position:absolute;left:-20px;top:-20px;border:1px solid red;">V</div>
<br><br><br>
<form id="props">
<textarea id="ta" style="width:200px;height:100px;" onkeyup="moveDiv();"></textarea>
</form>

<script>
<!--
var b=document.body;
var d=document.getElementById('db');
var a=document.getElementById('ta');

function moveDiv(){
var sel=document.selection;
var targ=sel.createRange();
d.style.top=a.offsetTop+a.clientTop-d.clientHeight-6;
d.style.left=targ.offsetLeft+b.scrollLeft-6;
return;
}
// -->
</script>
</body>

Positioning of the <DIV> is not quite exact, but gets better when using fixed-width font in <TEXTAREA>.

inserting text in current (or last) caret position inside of TextArea, in Angular; Element is not interpreted as TextArea

When you're dealing with Angular you're in TypeScript world.

document.querySelector method is a generic method where by default it returns Element:

querySelector<E extends Element = Element>(selectors: string): E | null;

Simply choose the proper type and you're done:

const myTA = document.querySelector<HTMLTextAreaElement>(`#${this.BODY_TEXTAREA}`);
^^^^^^^^^^^^^^^^^^^

Get caret position in textarea (IE)

I would argue that counting line breaks as two characters when calculating selection boundaries/cursor position is the correct approach. Fundamentally, those two characters \r\n are there in the text. They are present in the textarea's value property and they are in the value that is submitted to the server.

The only arguments I can see for counting line breaks as one character are:

  1. Consistency with other browsers
  2. IE's TextRange methods consider the line breaks as one character

I think neither is valid. Firstly, IE is already inconsistent with other browsers in counting line breaks as two characters rather than one. Secondly, IE's TextRange character-based methods are a little insane anyway, causing problems wherever they're used.

I think it makes total sense to consider the selection/caret position as a position relative to the actual text in the textarea. This allows easy manipulation of the text.

Here are two main functions. The first is the only textarea selection/caret position getting function I've seen that works correctly with all line breaks. You can find this here:
How to get the start and end points of selection in text area?. Second, here's a complementary setSelection function:

function offsetToRangeCharacterMove(el, offset) {
return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
}

function setSelection(el, startOffset, endOffset) {
var range = el.createTextRange();
var startCharMove = offsetToRangeCharacterMove(el, startOffset);
range.collapse(true);
if (startOffset == endOffset) {
range.move("character", startCharMove);
} else {
range.moveEnd("character", offsetToRangeCharacterMove(el, endOffset));
range.moveStart("character", startCharMove);
}
range.select();
}

Caret position relative to screen coordinates in javaFX

Just wanted to follow-up with an answer to this question for TextField controls in JavaFX. I'm sure the same could apply to other text input controls as well. I got the idea from looking at some code that involved changing the default colour of the caret using a subclass of TextFieldSkin class. If you look closely, the TextFieldSkin superclass holds a reference to the Path instance which represents the caret in a protected field called caretPath. Although this is kind of a hack'ish solution, it does provide developers with the absolute coordinates of the Caret in a much safer way than most of the hacks I've seen out there.

public class TextFieldCaretControlSkin extends TextFieldSkin {
public TextFieldCaretControlSkin(TextField textField, Stage stage) {
super(textField);
Popup popup = new Popup();

// Make the popup appear to the right of the caret
popup.setAnchorLocation(PopupWindow.AnchorLocation.CONTENT_BOTTOM_LEFT);
// Make sure its position gets corrected to stay on screen if we go out of screen
popup.setAutoFix(true);
// Add list view (mock autocomplete popup)
popup.getContent().add(new ListView<String>());

// listen for changes in the layout bounds of the caret path
caretPath.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
popup.hide();
// get the caret's x position relative to the textfield.
double x = newValue.getMaxX();
// get the caret's y position relative to the textfield.
double y = newValue.getMaxY();
Point2D p = caretPath.localToScene(x, y);

/*
* If the coordinates are negatives then the Path is being
* redrawn and we should just skip further processing.
*/
if (x == -1.0 || y == -1.0)
return;

// show the popup at these absolute coordinates.
popup.show(textField,
p.getX() + caretPath.getScene().getX() +
caretPath.getScene().getWindow().getX(),
p.getY() + caretPath.getScene().getY() +
caretPath.getScene().getWindow().getY() -
newValue.getHeight()); // put the popup on top of the caret
}
});
}
}

To use you'd have to embed this in some sort of subclassed text input control and remember to do textField.setSkin(new TextFieldCaretControlSkin(textField)). There may be better ways to do this since I am not a JavaFX expert but I just wanted to share this solution with the rest of the world just in case it provided some insight.

Hope this helps!

How to know if caret resides in the last character in textarea?

As you're always going to be checking for the last character, you could grab the length property of the textarea value on every keydown and compare the length with the textarea's selectionEnd property:

selectionEnd - Specifies or returns the end position of the selected
text within the current element.

Example:

document.getElementById("textAreaIdHere").onkeydown = function(){
// Get length of textarea value
var len = this.value.length;
// If character entered is at the end of the textarea (therefore cursor)
if(this.selectionEnd === len) {
// Is at the end
}
}

jsFiddle here


selectionEnd is only available to use as of IE9+. For implementing this feature in older browsers, this shim may be of use.



Related Topics



Leave a reply



Submit