There seems to be no easy way to do this, I have the following working example. There's a bit of processing there, so it's a little slow, and it can be an odd character when moving up and down between paragraphs.
Please inform me of any improvements that may be made.
http://jsfiddle.net/zQUhV/47/
What I did was split the paragraph into each work, insert them into the new element one by one, check the change in height - when changing a new line.
This function returns an array of line objects containing the text of the line, index of the beginning and end of the index:
(function($) { $.fn.lines = function(){ words = this.text().split(" ");
Now you can use this to measure the number of characters to the cursor point and apply this while moving the paragraph to keep the cursor position relative to the beginning of the line. However, this can lead to incredibly inaccurate results when considering widths "i" to widths "m".
Instead, it would be better to find the line width to the cursor point:
function distanceToCaret(textElement,caretIndex){ line = findLineViaCaret(textElement,caretIndex); if(line.startIndex == 0) { // +1 needed for substring to be correct but only first line? relativeIndex = caretIndex - line.startIndex +1; } else { relativeIndex = caretIndex - line.startIndex; } textToCaret = line.text.substring(0, relativeIndex); hiddenElement = textElement.clone(); //copies font settings and width hiddenElement.empty();//clear text hiddenElement.css("visibility", "hidden"); hiddenElement.css("width", "auto"); //so width can be measured hiddenElement.css("display", "inline-block"); //so width can be measured jQuery('body').append(hiddenElement); // doesn't exist until inserted into document hiddenElement.text(textToCaret); //add to get width width = hiddenElement.width(); hiddenElement.remove(); return width; } function findLineViaCaret(textElement,caretIndex){ jQuery.each(textElement.lines(), function() { if(this.startIndex <= caretIndex && this.endIndex >= caretIndex) { r = this; return false; // exits loop } }); return r; }
Then divide the target string into characters and find the point that most closely matches the width above, adding characters one by one until the point is reached:
function getCaretViaWidth(textElement, lineNo, width) { line = textElement.lines()[lineNo-1]; lineCharacters = line.text.replace(/^\s+|\s+$/g, '').split(""); hiddenElement = textElement.clone(); //copies font settings and width hiddenElement.empty();//clear text hiddenElement.css("visibility", "hidden"); hiddenElement.css("width", "auto"); //so width can be measured hiddenElement.css("display", "inline-block"); //so width can be measured jQuery('body').append(hiddenElement); // doesn't exist until inserted into document if(width == 0) { //if width is 0 index is at start caretIndex = line.startIndex; } else {// else loop through each character until width is reached hiddenElement.empty(); jQuery.each(lineCharacters, function() { text = hiddenElement.text(); prevWidth = hiddenElement.width(); hiddenElement.text(text + this); elWidth = hiddenElement.width(); caretIndex = hiddenElement.text().length + line.startIndex; if(hiddenElement.width() > width) { // check whether character after width or before width is closest if(Math.abs(width - prevWidth) < Math.abs(width - elWidth)) { caretIndex = caretIndex -1; // move index back one if previous is closes } return false; } }); } hiddenElement.remove(); return caretIndex; }
To use the following keydown function to intersect fairly well between valid paragraphs:
$(document).on('keydown', 'p[contenteditable="true"]', function(e) { //if cursor on first line & up arrow key if(e.which == 38 && (cursorIndex() < $(this).lines()[0].text.length)) { e.preventDefault(); if ($(this).prev().is('p')) { prev = $(this).prev('p'); getDistanceToCaret = distanceToCaret($(this), cursorIndex()); lineNumber = prev.lines().length; caretPosition = getCaretViaWidth(prev, lineNumber, getDistanceToCaret); prev.focus(); setCaret(prev.get(0), caretPosition); } // if cursor on last line & down arrow } else if(e.which == 40 && cursorIndex() >= $(this).lastLine().startIndex && cursorIndex() <= ($(this).lastLine().startIndex + $(this).lastLine().text.length)) { e.preventDefault(); if ($(this).next().is('p')) { next = $(this).next('p'); getDistanceToCaret = distanceToCaret($(this), cursorIndex()); caretPosition = getCaretViaWidth(next, 1, getDistanceToCaret); next.focus(); setCaret(next.get(0), caretPosition); } //if start of paragraph and left arrow } else if(e.which == 37 && cursorIndex() == 0) { e.preventDefault(); if ($(this).prev().is('p')) { prev = $(this).prev('p'); prev.focus(); setCaret(prev.get(0), prev.text().length); } // if end of paragraph and right arrow } else if(e.which == 39 && cursorIndex() == $(this).text().length) { e.preventDefault(); if ($(this).next().is('p')) { $(this).next('p').focus(); } };