Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get number of rows in ContentEditable area and current caret line position?

my question has two parts, but they are related.

Firstly - I have Contenteditable DIV with some text and I need to get total number of rows (lines) of this DIV. Is it possible ?

Secondly - I need to get caret row position, if it is on row number 1, 2, 3, etc....

Can anybody help ?

like image 382
Frodik Avatar asked Apr 03 '11 06:04

Frodik


People also ask

How do you get the caret position in an editable div?

To get contentEditable caret position with JavaScript, we change the selection to start from the beginning and then get the length of the full selected string. const cursorPosition = () => { const sel = document. getSelection(); sel.

How to use contenteditable in HTML?

Definition and Usage The contenteditable attribute specifies whether the content of an element is editable or not. Note: When the contenteditable attribute is not set on an element, the element will inherit it from its parent.

Is editable HTML?

In HTML, any element can be editable. By using some JavaScript event handlers, you can transform your web page into a full and fast rich text editor.


1 Answers

The direct answer is that there is no method that actually gets you those numbers. However, there are a number of different work arounds which you can apply to estimate (I use estimate, because I don't think they can be made 100% accurate) those values.

To answer your first question, how to get the number of lines in the element. Assuming the element uses a consistent line-height, you can find the number of lines by dividing the element height with the line-height. This of course gets significantly more complicated if you got elements with margins, paddings, or differentiating line-heights within the element. If that isn't enough of a problem, the line-height property doesn't necessarily have to be a numerical value, i.e. it can be normal or a % etc. There's quite a few questions regarding the line-height property and how to get a numerical representation of it here on SO so I won't go too much into detail on that. For the sake of rest of my answer, I did specifically assign a common line-height across the element.

The far more problematic part of your question is finding the caret position within the element. Again, there isn't a method which will just return you the right answer. Instead, one approach you can take to estimate the caret row position is to make a range at the current caret position (selection), insert a dummy node there, get the nodes offset relative to the containers offset, calculate the number of lines based on the top offset of it, and then remove the dummy element.

Additional problems with the dummy element arise when you can't hide() it or leave it empty.

It seems like a very bad way of doing it, and it is. It certainly doesn't work flawlessly when trying to navigate with arrows. So why not just use getClientRects() for the selection? It works fine when it is just text without any space or such, but immidiately when you throw a few <br /><br /> in there, it won't return you the line position there anymore. Additionally, when you are at the first or last character of a line, it would give sometimes incorrect line rows, as it throws the element either up or down depending on the amount of space available.

If you are looking for a fool-proof way of finding the caret position and number of lines, there isn't one. If rough estimates are enough, than my solution is a good start (it certainly could use some more work, and support for IE which should be lot easier in this case as the TextRange object actually gives offsets in pixels).

My take on this:

var lineHeight = parseInt($('#editable').css('line-height'));
//var ce = $('#editable')[0].getClientRects();
var ce = $('#editable').position();
console.log("Lines: "+$('#editable').height()/lineHeight);
$('#editable').bind('click keyup keydown',function(){
   if(window.getSelection){
        range = window.getSelection().getRangeAt(0);
      range.insertNode($('<canvas />').attr('id','tempCaretFinder')[0]);
       var p = $('canvas#tempCaretFinder').position();
       $('canvas#tempCaretFinder').remove();
       console.log("Caret line: "+(Math.ceil((p.top-ce.top)/lineHeight)+1));

    }else if(document.selection) { 
        // the IE way, which should be relatively easier. as TextRange objects return offsets directly.
        range = document.selection.createRange();  
    }

});

Example: http://jsfiddle.net/niklasvh/mKQUH/

EDIT: Try 2 http://jsfiddle.net/niklasvh/mKQUH/129/

As mentioned, creating dummy elements made the caret jump all over the place sometimes, so I had a go again with the getClientRects() for ranges. To make them more feasible, I made the selection extend itself one character, then create the range, then check the Rect position, and then move the caret back one character.

Why?

Because the caret on the first character of a line, would have its top parameter at the same level as the previous line, so with applying an extra character to it, it can check whether it is actually a new line. Another problem with the getClientRects() was that it didn't work on empty line breaks. To accomodate this, I used the previous creating of a dummy element as a fallback in those situations.

End result:

var lineHeight = parseInt($('#editable').css('line-height'));
//var ce = $('#editable')[0].getClientRects();
var ce = $('#editable').offset();
console.log("Lines: "+$('#editable').height()/lineHeight);
$('#editable').bind('click keyup keydown',function(e){
    //alert($(window).scrollTop());
   if(window.getSelection){
       var save = window.getSelection();
       var s = window.getSelection();
       s.modify('extend','forward','character');
     // s.modify('extend','forward','line');
       range = s.getRangeAt(0);
       var p = range.getClientRects();
       var top;
       //console.log(p);
       if (typeof p[1] != "undefined"){
           top = p[1].top+$(window).scrollTop();
               }else if (typeof p[0] != "undefined"){
                top = p[0].top+$(window).scrollTop();    
               }
       else{
          // sigh... let's make a real work around then
           range.insertNode($('<canvas />').attr('id','tempCaretFinder')[0]);
       var p = $('canvas#tempCaretFinder').offset();
       $('canvas#tempCaretFinder').remove();
           top = p.top;

       }

     // console.log(top-ce.top);
       console.log("Caret line: "+(Math.ceil((top-ce.top)/lineHeight)));
       save.modify('move','backward','character');
       /*
       range = s.getRangeAt(0);
      range.insertNode($('<canvas />').attr('id','tempCaretFinder')[0]);
       var p = $('canvas#tempCaretFinder').position();
       $('canvas#tempCaretFinder').remove();
       console.log("Caret line: "+(Math.ceil((p.top-ce.top)/lineHeight)+1));
       console.log(e.which);

       switch(e.which){
           case 40:
             s.modify("move", "forward","line");

             break;  
                    case 38:
             s.modify("move", "backward","lineboundary");

             break;  
       }
       */


    }else if(document.selection) {
        // the IE way, which should be relatively easier. as TextRange objects return offsets directly.
        range = document.selection.createRange();  
    }

});
like image 132
Niklas Avatar answered Oct 14 '22 22:10

Niklas