13

Given a textarea with a not fixed width font, I want to know on key up if the caret (as given by element.selectionEnd) is in the first line or in the last line of the text.

Enter image description here

To avoid bad answers, here are some solutions which don't work:

  • Splitting on \n: A sentence can be broken in two lines, because it's too long for the textarea's width.
  • Measuring the text before the caret (for example by copying the text into a div with same style and measuring the height of the span): Some characters after the caret may change the wrapping point (usually between words).

Here's a fiddle for the tests and to remove some ambiguity (yes, it's a textarea element, and it's not only one line, etc.): http://jsbin.com/qifezupu/4/edit

Notes:

  • I don't care for Internet Explorer 9- or mobile browsers.
  • I need a reliable solution, working for all positions of the caret.
  • There are a lot of tricky details which make most ideas unusable in practice, please build a working fiddle before answering.
Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
  • I think with pure `textarea` not possible (in my opinion).But if you can use some editor which parse a pure html then you can define which line is last and which one is first. – Just code Jul 07 '14 at 08:17

5 Answers5

5

The best solution for now :

  • Create a temporary div with two spans, and CSS styles made to mimic (font, wordwrapping, scrollbars) the wrapping behavior of the textarea.
  • Fill the first span with the text before the caret.
  • Fill the second span with the text after the caret.
  • Use the offset.top and element height of the spans to know if we're on the first line, or on the last line.

Can I see it working ?

I made a fiddle which makes it obvious how it works : http://jsbin.com/qifezupu/31/edit

How to use it :

I made a jQuery plugin : https://github.com/Canop/taliner.js

Here's the related demonstration page : http://dystroy.org/demos/taliner/demo.html

The code :

$.fn.taliner = function(){

    var $t = this,
        $d = $('<div>').appendTo('body'),
        $s1 = $('<span>').text('a').appendTo($d),
        $s2 = $('<span>').appendTo($d),
        lh =  $s1.height();

    $d.css({
        width: $t.width(),
        height: $t.height(),
        font: $t.css('font'),
        fontFamily: $t.css('fontFamily'), // for FF/linux
        fontSize: $t.css('fontSize'),
        whiteSpace : 'pre-wrap',
        wordWrap : 'break-word',
        overflowY: 'auto',
        position: 'fixed',
        bottom: 0,
        left: 0,
        padding: 0,
        zIndex: 666
    });

    var lh = $s1.height(),
        input = this[0],
        se = input.selectionEnd,
        v = input.value,
        res = {};

    $s1.text(v);
    res.linesNumber = $s1.height()/lh|0;

    $s1.text(v.slice(0, se));
    $s2.text(v.slice(se));
    res.caretOnFirstLine = input.selectionEnd===0
        || ($s1.height()<=lh && $s1.offset().top===$s2.offset().top);
    res.caretOnLastLine = $s2.height()===lh;

    $d.remove();
    return res;
}

Does it really work ?

There's still a problem. The only information we get in JavaScript regarding the caret position is element.selectionEnd. And this is very poor.

Here are the possible caret positions on a two lines textarea :

| A | B | C |
| D | E |

As you can see, there are more possible caret positions than inter-character positions. More specifically you have two different caret positions between the C and the D but the DOM doesn't provide any way to distinguish between them.

This means that sometimes, if you click at the right of the end of the line, the library won't be able to tell if it's the end of the line or the start of the next line.

Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
  • Thank you for sharing your solution. I am trying to achieve the same functionality but for navigating between multiple contenteditable elements, hooked onto the up/down keyboard events (this seems more natural than tabbing, which works). I wonder if Rangy / Tim Down could help with this problem. – rorymorris Nov 26 '14 at 23:52
2

There are several methods of obtaining caret position (in pixels) inside a textarea:
Offset possition of the caret in a textarea in pixels

Careful, they might have issues with different browsers.
You can set a predefined line height or font size that you later use to obtain caret position.

I didn't tested any of the methods but let's assume that the function used to get caret position (in pixels) returns the y component with respect to the bottom of the caret.
You specify a line height of 15 px.
Let's say method returns (20, 45). Current line = y/15 = 45/15 = 3

JS Fiddle to demonstrate how it can be done based on Dan's solution:
Current line from caret coordinates

I added a span where the current line is displayed. Sadly, my tests on Chrome shows that this is not 100% accurate: When you select third line with mouse it says you are on 4th line.

Line:
<span id="line">0</span>

At the end of the update function (in HTML section) I added the following line:

//24 is the top padding of the textarea
//16 is the line-height
line.innerText = (coordinates.top - 24) / 16;
Community
  • 1
  • 1
B0Andrew
  • 1,725
  • 13
  • 19
  • This looks like the best solution up until now - smaller browser bugs at edge-cases don't seem to be breaking relevant behavior. Cudos for finding this! – Falco Jul 07 '14 at 13:29
2

Another solution based on what @Flater said about adding up the width of the text. The idea (in summary) is:

  1. Get the width of the textarea
  2. Get the text before the cursor position and calculate it's width
  3. Get the text after the cursor position and calculate it's width
  4. If the before text > width of text area or does not contain line-break (incase the text on first line is less), the cursor is not on first line
  5. If the after text >width of text area or does not contain line-break, the cursor is not on last line

Code:

$('#t').keyup(function(e){
  first_line="true";
  last_line="true";
  text = $(this).val();
  width = $(this).width();
  var cursorPosition = $(this).prop("selectionStart");
  txtBeforeCaret = text.substring(0, cursorPosition);
  txtAfterCaret = text.substring(cursorPosition);
  widthBefore = $('#holder').text(txtBeforeCaret).width();
  widthAfter = $('#holder').text(txtAfterCaret).width();
  match1 = txtBeforeCaret.match(/\n/);
  if(txtAfterCaret!==null){match2=txtAfterCaret.match(/\n/g);}
  if(widthBefore>width || match1){first_line="false";}
  if(widthAfter>width || (match2!==null && match2.length)) {last_line="false";}
  $('#f').html(first_line);
  $('#l').html(last_line);
});

#holder is a div created and hidden (to find the text width).

DEMO

AyB
  • 11,609
  • 4
  • 32
  • 47
  • I hadn't had time to see if they're easy to fix but there are two problems : 1) it doesn't give the right answer if you put the caret at the begining of the second line 2) it doesn't work at end of first line with [this text](http://jsbin.com/hacicitu/1/edit) – Denys Séguret Jul 07 '14 at 10:36
  • Another problem : it breaks if you type some html in the textarea (in my solution I solved that by using `text` and some css settings). – Denys Séguret Jul 07 '14 at 10:37
  • @dystroy I corrected it a little after a few mistakes. Test it and let me know. The resizing is not perfect however (when resized, you need to type for the correct output instead of moving cursor). – AyB Jul 07 '14 at 12:26
  • [there are still problems](http://i.imgur.com/ls5Juh8.png) (tested on chromium/linux) – Denys Séguret Jul 07 '14 at 12:27
  • @dystroy You're right, the problem seems to be that new-lines (automatically made because the textbox is smaller) cannot be detected. I'm out of options with this method. – AyB Jul 07 '14 at 13:01
1

You can measure the length of a piece of text by putting it in a div and getting that div's width.

In short, look at this jsFiddle. Everytime you change the text in the textbox, you'll be alerted of the text width.

So I'd suggest doing the following:

  • Find both the first and last line of your textarea's content. The next steps apply to either line.
  • To see if a line is broken up because of the textarea's width, use my suggested snippet to check if the text width is larger than the textarea's width.
  • If it is longer, you could iteratively start checking for substrings of the line. If you find one that matches your textarea's width, you can be pretty sure that's where the line break is.

I'm not sure what action you want to take, but you can use this method to approximate where your caret is. It might not be pixel perfect though, as the div width varies slightly from the text's width.

Here's the snippet to measure text width:

$(document).ready(function() {

    $("#txtbox").keyup(function() {
        var the_Text = $("#txtbox").val();

        var width = $("#testdiv").text(the_Text).width();

        alert("The text width is : " + width + " px");
    });

});

$("#testdiv") is a hidden div on the page. Nothing fancy, the only CSS used is to make sure that the user doesn't see it.

Flater
  • 12,908
  • 4
  • 39
  • 62
  • 1
    If you think this should work, can you try to adapt your solution to [the fiddle I gave](http://jsbin.com/qifezupu/4/edit) ? Note that I don't care for pixel position, just *exact* line position of the caret. – Denys Séguret Jul 07 '14 at 08:06
0

Here is a fiddle using this jQuery plugin (https://github.com/Codecademy/textarea-helper) to get the caret position (the y position is what you need): http://jsbin.com/qifezupu/11/edit

Unfortunately, some adjustments are needed, as you will see. This is just an idea.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Oliboy50
  • 2,661
  • 3
  • 27
  • 36