How can I count text lines inside an DOM element? Can I? - javascript

I'm wondering if there's a way to count lines inside a div for example. Say we have a div like so:
<div id="content">hello how are you?</div>
Depending on many factors, the div can have one, or two, or even four lines of text. Is there any way for the script to know?
In other words, are automatic breaks represented in DOM at all?

If the div's size is dependent on the content (which I assume to be the case from your description) then you can retrieve the div's height using:
var divHeight = document.getElementById('content').offsetHeight;
And divide by the font line height:
document.getElementById('content').style.lineHeight;
Or to get the line height if it hasn't been explicitly set:
var element = document.getElementById('content');
document.defaultView.getComputedStyle(element, null).getPropertyValue("lineHeight");
You will also need to take padding and inter-line spacing into account.
EDIT
Fully self-contained test, explicitly setting line-height:
function countLines() {
var el = document.getElementById('content');
var divHeight = el.offsetHeight
var lineHeight = parseInt(el.style.lineHeight);
var lines = divHeight / lineHeight;
alert("Lines: " + lines);
}
<body onload="countLines();">
<div id="content" style="width: 80px; line-height: 20px">
hello how are you? hello how are you? hello how are you? hello how are you?
</div>
</body>

Check out the function getClientRects() which can be used to count the number of lines in an element. Here is an example of how to use it.
var message_lines = $("#message_container")[0].getClientRects();
It returns a javascript DOM object. The amount of lines can be known by doing this:
var amount_of_lines = message_lines.length;
A few things to note is it only works if the containing element is inline, however you can surround the containing inline element with a block element to control the width like so:
console.log( message_container.getClientRects().length )
<div style="display:inline;" id="message_container">
..Text of the post..<br>
nice ha?
</div>
Though I don't recommend hard coding the style like that. It's just for example purposes.

One solution is to enclose every word in a span tag using script. Then if the Y dimension of a given span tag is less than that of it's immediate predecessor then a line break has occurred.

I wasnt satisfied with the answers here and on other questions. The highest rated answer doesn't take padding or border into account, and therefore obviously ignores box-sizing as well. My answer combines some techniques here and and on other threads to get a solution that works to my satisfaction.
It isnt perfect: When no numerical value was able to be retrieved for the line-height (e.g. normal or inherit), it just uses the font-size multiplied by 1.2. Perhaps someone else can suggest a reliable way to detect the pixel value in those cases.
Other than that, it has been able to correctly handle most of the styles and cases I have thrown at it.
jsFiddle for playing around and testing. Also inline below.
function countLines(target) {
var style = window.getComputedStyle(target, null);
var height = parseInt(style.getPropertyValue("height"));
var font_size = parseInt(style.getPropertyValue("font-size"));
var line_height = parseInt(style.getPropertyValue("line-height"));
var box_sizing = style.getPropertyValue("box-sizing");
if(isNaN(line_height)) line_height = font_size * 1.2;
if(box_sizing=='border-box')
{
var padding_top = parseInt(style.getPropertyValue("padding-top"));
var padding_bottom = parseInt(style.getPropertyValue("padding-bottom"));
var border_top = parseInt(style.getPropertyValue("border-top-width"));
var border_bottom = parseInt(style.getPropertyValue("border-bottom-width"));
height = height - padding_top - padding_bottom - border_top - border_bottom
}
var lines = Math.ceil(height / line_height);
alert("Lines: " + lines);
return lines;
}
countLines(document.getElementById("foo"));
div
{
padding:100px 0 10% 0;
background: pink;
box-sizing: border-box;
border:30px solid red;
}
<div id="foo">
x<br>
x<br>
x<br>
x<br>
</div>

Clone the container object and write 2 letters and calculate the height. This return the real height with all style applied, line height, etc. Now, calculate the height object / the size of a letter. In Jquery, the height excelude the padding, margin and border, it is great to calculate the real height of each line:
other = obj.clone();
other.html('a<br>b').hide().appendTo('body');
size = other.height() / 2;
other.remove();
lines = obj.height() / size;
If you use a rare font with different height of each letter, this does not works. But works with all normal fonts, like Arial, mono, comics, Verdana, etc. Test with your font.
Example:
<div id="content" style="width: 100px">hello how are you? hello how are you? hello how are you?</div>
<script type="text/javascript">
$(document).ready(function(){
calculate = function(obj){
other = obj.clone();
other.html('a<br>b').hide().appendTo('body');
size = other.height() / 2;
other.remove();
return obj.height() / size;
}
n = calculate($('#content'));
alert(n + ' lines');
});
</script>
Result: 6 Lines
Works in all browser without rare functions out of standards.
Check: https://jsfiddle.net/gzceamtr/

For those who use jQuery http://jsfiddle.net/EppA2/3/
function getRows(selector) {
var height = $(selector).height();
var line_height = $(selector).css('line-height');
line_height = parseFloat(line_height)
var rows = height / line_height;
return Math.round(rows);
}

I am convinced that it is impossible now. It was, though.
IE7’s implementation of getClientRects did exactly what I want. Open this page in IE8, try refreshing it varying window width, and see how number of lines in the first element changes accordingly. Here’s the key lines of the javascript from that page:
var rects = elementList[i].getClientRects();
var p = document.createElement('p');
p.appendChild(document.createTextNode('\'' + elementList[i].tagName + '\' element has ' + rects.length + ' line(s).'));
Unfortunately for me, Firefox always returns one client rectangle per element, and IE8 does the same now. (Martin Honnen’s page works today because IE renders it in IE compat view; press F12 in IE8 to play with different modes.)
This is sad. It looks like once again Firefox’s literal but worthless implementation of the spec won over Microsoft’s useful one. Or do I miss a situation where new getClientRects may help a developer?

based on GuyPaddock's answer from above, this seems to work for me
function getLinesCount(element) {
var prevLH = element.style.lineHeight;
var factor = 1000;
element.style.lineHeight = factor + 'px';
var height = element.getBoundingClientRect().height;
element.style.lineHeight = prevLH;
return Math.floor(height / factor);
}
the trick here is to increase the line-height so much that it will "swallow" any browser / OS differences in the way that they render fonts
Checked it with various stylings and different font sizes / families
only thing that it doesn't take into account (since in my case it didnt matter), is the padding - which can easily be added to the solution.

No, not reliably. There are simply too many unknown variables
What OS (different DPIs, font variations, etc...)?
Do they have their font-size scaled up because they are practically blind?
Heck, in webkit browsers, you can actually resize textboxes to your heart's desire.
The list goes on. Someday I hope there will be such a method of reliably accomplishing this with JavaScript, but until that day comes, your out of luck.
I hate these kinds of answers and I hope someone can prove me wrong.

You should be able to split('\n').length and get the line breaks.
update: this works on FF/Chrome but not IE.
<html>
<head>
<script src="jquery-1.3.2.min.js"></script>
<script>
$(document).ready(function() {
var arr = $("div").text().split('\n');
for (var i = 0; i < arr.length; i++)
$("div").after(i + '=' + arr[i] + '<br/>');
});
</script>
</head>
<body>
<div>One
Two
Three</div>
</body>
</html>

getClientRects return the client rects like this and if you want to get the lines, use the follow function like this
function getRowRects(element) {
var rects = [],
clientRects = element.getClientRects(),
len = clientRects.length,
clientRect, top, rectsLen, rect, i;
for(i=0; i<len; i++) {
has = false;
rectsLen = rects.length;
clientRect = clientRects[i];
top = clientRect.top;
while(rectsLen--) {
rect = rects[rectsLen];
if (rect.top == top) {
has = true;
break;
}
}
if(has) {
rect.right = rect.right > clientRect.right ? rect.right : clientRect.right;
rect.width = rect.right - rect.left;
}
else {
rects.push({
top: clientRect.top,
right: clientRect.right,
bottom: clientRect.bottom,
left: clientRect.left,
width: clientRect.width,
height: clientRect.height
});
}
}
return rects;
}

I found a way to calc the line number when I develop a html editor.
The primary method is that:
In IE you can call getBoundingClientRects, it returns each line as a
rectangle
In webkit or new standard html engine, it returns each element or
node's client rectangles, in this case you can compare each
rectangles, I mean each there must be a rectangle is the largest, so
you can ignore those rectangles that height is smaller(if there is a
rectangle's top smaller than it and bottom larger than it, the
condition is true.)
so let's see the test result:
The green rectangle is the largest rectangle in each row
The red rectangle is the selection boundary
The blue rectangle is the boundary from start to selection after expanding, we see it may larger than red rectangle, so we have to check each rectangle's bottom to limit it must smaller than red rectangle's bottom.
var lineCount = "?";
var rects;
if (window.getSelection) {
//Get all client rectangles from body start to selection, count those rectangles that has the max bottom and min top
var bounding = {};
var range = window.getSelection().getRangeAt(0);//As this is the demo code, I dont check the range count
bounding = range.getBoundingClientRect();//!!!GET BOUNDING BEFORE SET START!!!
//Get bounding and fix it , when the cursor is in the last character of lineCount, it may expand to the next lineCount.
var boundingTop = bounding.top;
var boundingBottom = bounding.bottom;
var node = range.startContainer;
if (node.nodeType !== 1) {
node = node.parentNode;
}
var style = window.getComputedStyle(node);
var lineHeight = parseInt(style.lineHeight);
if (!isNaN(lineHeight)) {
boundingBottom = boundingTop + lineHeight;
}
else {
var fontSize = parseInt(style.fontSize);
if (!isNaN(fontSize)) {
boundingBottom = boundingTop + fontSize;
}
}
range = range.cloneRange();
//Now we have enougn datas to compare
range.setStart(body, 0);
rects = range.getClientRects();
lineCount = 0;
var flags = {};//Mark a flags to avoid of check some repeat lines again
for (var i = 0; i < rects.length; i++) {
var rect = rects[i];
if (rect.width === 0 && rect.height === 0) {//Ignore zero rectangles
continue;
}
if (rect.bottom > boundingBottom) {//Check if current rectangle out of the real bounding of selection
break;
}
var top = rect.top;
var bottom = rect.bottom;
if (flags[top]) {
continue;
}
flags[top] = 1;
//Check if there is no rectangle contains this rectangle in vertical direction.
var succ = true;
for (var j = 0; j < rects.length; j++) {
var rect2 = rects[j];
if (j !== i && rect2.top < top && rect2.bottom > bottom) {
succ = false;
break;
}
}
//If succ, add lineCount 1
if (succ) {
lineCount++;
}
}
}
else if (editor.document.selection) {//IN IE8 getClientRects returns each single lineCount as a rectangle
var range = body.createTextRange();
range.setEndPoint("EndToEnd", range);
rects = range.getClientRects();
lineCount = rects.length;
}
//Now we get lineCount here

Following #BobBrunius 2010 suggestion I created this with jQuery. No doubt it could be improved but it may help some.
$(document).ready(function() {
alert("Number of lines: " + getTextLinesNum($("#textbox")));
});
function getTextLinesNum($element) {
var originalHtml = $element.html();
var words = originalHtml.split(" ");
var linePositions = [];
// Wrap words in spans
for (var i in words) {
words[i] = "<span>" + words[i] + "</span>";
}
// Temporarily replace element content with spans. Layout should be identical.
$element.html(words.join(" "));
// Iterate through words and collect positions of text lines
$element.children("span").each(function () {
var lp = $(this).position().top;
if (linePositions.indexOf(lp) == -1) linePositions.push(lp);
});
// Revert to original html content
$element.html(originalHtml);
// Return number of text lines
return linePositions.length;
}
#textbox {
width: 200px;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div id="textbox">Lorem ipsum dolor sit amet, consectetuer adipiscing elit,
<br>sed diam nonummy</div>

Try this solution:
function calculateLineCount(element) {
var lineHeightBefore = element.css("line-height"),
boxSizing = element.css("box-sizing"),
height,
lineCount;
// Force the line height to a known value
element.css("line-height", "1px");
// Take a snapshot of the height
height = parseFloat(element.css("height"));
// Reset the line height
element.css("line-height", lineHeightBefore);
if (boxSizing == "border-box") {
// With "border-box", padding cuts into the content, so we have to subtract
// it out
var paddingTop = parseFloat(element.css("padding-top")),
paddingBottom = parseFloat(element.css("padding-bottom"));
height -= (paddingTop + paddingBottom);
}
// The height is the line count
lineCount = height;
return lineCount;
}
You can see it in action here:
https://jsfiddle.net/u0r6avnt/
Try resizing the panels on the page (to make the right side of the page wider or shorter) and then run it again to see that it can reliably tell how many lines there are.
This problem is harder than it looks, but most of the difficulty comes from two sources:
Text rendering is too low-level in browsers to be directly queried from JavaScript. Even the CSS ::first-line pseudo-selector doesn't behave quite like other selectors do (you can't invert it, for example, to apply styling to all but the first line).
Context plays a big part in how you calculate the number of lines. For example, if line-height was not explicitly set in the hierarchy of the target element, you might get "normal" back as a line height. In addition, the element might be using box-sizing: border-box and therefore be subject to padding.
My approach minimizes #2 by taking control of the line-height directly and factoring in the box sizing method, leading to a more deterministic result.

In certain cases, like a link spanning over multiple rows in non justified text, you can get the row count and every coordinate of each line, when you use this:
var rectCollection = object.getClientRects();
https://developer.mozilla.org/en-US/docs/Web/API/Element/getClientRects
This works because each line would be different even so slightly. As long as they are, they are drawn as a different "rectangle" by the renderer.

You can compare element height and element height with line-height: 0
function lineCount(elm) {
const originalStyle = elm.getAttribute('style')
// normalize
elm.style.padding = 0
elm.style.border = 0
// measure
elm.style.lineHeight = 1
const totalHeight = elm.offsetHeight
elm.style.lineHeight = 0
const singleLineHeight = elm.scrollHeight * 2
const lineCount = Math.round(totalHeight / singleLineHeight)
// undo above style changes
elm.setAttribute('style', originalStyle)
return (isNaN(lineCount) || singleLineHeight == 0) ? 0 : lineCount
}
function printElmLineCount(elm){
console.log(
lineCount(elm)
)
}
p{ border:2em black solid ; padding:1em; line-height: 3em; }
<p contentEditable id='elm'>
one<br>
two<br>
three
</p>
<button onclick='printElmLineCount(elm)'>Get lines count</button>

Easiest way to do this is calculating line height and divide it by element height.
This code works for any Kind of elements:
function getStyle(el,styleProp)
{
var x = el;
if (x.currentStyle)
var y = x.currentStyle[styleProp];
else if (window.getComputedStyle)
var y = document.defaultView.getComputedStyle(x,null).getPropertyValue(styleProp);
return y;
}
function calculateLineHeight (element) {
var lineHeight = parseInt(getStyle(element, 'line-height'), 10);
var clone;
var singleLineHeight;
var doubleLineHeight;
if (isNaN(lineHeight)) {
clone = element.cloneNode();
clone.innerHTML = '<br>';
element.appendChild(clone);
singleLineHeight = clone.offsetHeight;
clone.innerHTML = '<br><br>';
doubleLineHeight = clone.offsetHeight;
element.removeChild(clone);
lineHeight = doubleLineHeight - singleLineHeight;
}
return lineHeight;
}
function getNumlines(el){return Math.ceil(el.offsetHeight / calculateLineHeight (el))}
console.log(getNumlines(document.getElementById('g1')))
.Text{font-size: 28px;}
#media screen and (max-width: 780px) {
.Text{font-size: 50px;}
}
<div><span class="Text" id="g1" >
This code works for any Kind of elements: bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli bla blo bli </span>
</div>

My solution(may be duplicated): make this element nowrap to calculate lineHeight, then calculate lineNumber by oneLineHeight / lineHeight
function getLineInfo(root: HTMLElement): {
lineNumber: number;
lineHeight: number;
elHeight: number;
} {
const oldOverFlow = root.style.overflow;
const oldWhiteSpace = root.style.whiteSpace;
root.style.overflow = "hidden";
root.style.whiteSpace = "nowrap";
const lineHeight = root.offsetHeight;
root.style.overflow = oldOverFlow;
root.style.whiteSpace = oldWhiteSpace;
const lineNumber = Math.round(root.offsetHeight / lineHeight);
return {
lineNumber: lineNumber,
lineHeight,
elHeight: root.offsetHeight,
};
}

You could count the number of line breaks in the element's innerText, like this:
const text = anyDivElement.innerText;
const lines = text.split(/\r\n|\r|\n/).length;

Another simple solution, by using getClientRects(), not well tested:
export function getLineInfo(root: HTMLElement): {
lineNumber: number;
lineHeight: number;
elHeight: number;
} {
const test = document.createElement("span");
test.textContent = " ";
root.appendChild(test);
const first = test.getClientRects();
root.insertBefore(test, root.firstChild);
const second = test.getClientRects();
const lineHeight = first[0].y - second[0].y;
test.remove();
const lastPadding = lineHeight - first[0].height;
const offsetHeight = first[0].bottom - second[0].top + lastPadding;
const lineNumber = Math.round(offsetHeight / lineHeight);
return {
lineNumber: lineNumber,
lineHeight,
elHeight: offsetHeight,
};
}

Related

InDesign script, resize images after word import

Sometimes we have big images in word file and after importing this word file inside InDesign, the image goes inside the overflow text and the text flow stops at this point.
We couldn't resize these images or can't get hold of this image for applying any scripting logic.
Basically, I will search for figure parastyle, then check for rectangles inside the para, and do resize logic. Sample jsx code here:
app.findTextPreferences.appliedParagraphStyle= 'figure';
var founds = app.findText();
// find 92% text width area
var pageWidth = this.props.textAreaWidth * 92 /100;
for(var i=0, len=founds.length; i<len; i++){
// find the rectangles inside the para
var rect = founds[i].rectangles;
if(rect.length == 0) continue;
var vb = rect[0].visibleBounds;
var imgWidth = vb[3] - vb[1];
// image resize logic
if(imgWidth > pageWidth){
vb[3] = pageWidth;
rect[0].visibleBounds = vb;
rect[0].fit(FitOptions.PROPORTIONALLY);
rect[0].fit(FitOptions.FRAME_TO_CONTENT);
}
How to apply some logic to the images which are in the overflow text? how to resize the image which is in overflow text?
We can just import the below word file into any InDesign template
Sample word file
You could iterate through all the graphics and resize the big ones.
var images = app.activeDocument.allGraphics;
var max = {};
max.w = 100; // max width
max.h = 100; // max height
for (var i=0; i<images.length; i++) {
var gb = images[i].geometricBounds;
var size = {};
size.w = -gb[1] + gb[3];
size.h = -gb[0] + gb[2];
if (size.w > max.w || size.h > max.h) {
var scale = (size.w > size.h) ? max.w/size.w : max.h/size.h;
images[i].horizontalScale *= scale;
images[i].verticalScale *= scale;
images[i].parent.fit(FitOptions.FRAME_TO_CONTENT);
}
}

move fake cursor to position taken from clicked character

I have a div with text using monospace font, and I need to display a cursor in the place where I click, I have functions that display text with cursor:
function draw() {
var text = textarea.val();
var html;
if (pos == text.length) {
html = encode(text) + '<span class="cursor"> </span>';
} else {
html = encode(text.slice(0, pos)) + '<span class="cursor">' +
encode(text[pos+1]) + '</span>' + encode(text.slice(pos+1));
}
output.html(html);
}
and function that get cursor position based on x/y coordinate of the mouse event:
function get_char_pos(div, text, event) {
var num_chars = get_num_chars(div);
var cursor = div.find('.cursor');
var rect = cursor[0].getBoundingClientRect();
var width = rect.width;
var height = rect.height;
var offset = div.offset();
var col = Math.floor((event.pageX-offset.left)/width);
var row = Math.floor((event.pageY-offset.top)/height);
var try_pos = col + (row > 0 ? num_chars * row : 0);
return try_pos;
}
It almost working except when text contain tabs (tabs are replaced by 4 spaces by encode function). I've try to fix tabs using this:
var before = text.slice(0, try_pos);
var tabs = before.match(/\t/g);
var fix = tabs ? tabs * 3 : 0;
try_pos += fix;
return try_pos > text.length ? text.lenght : try_pos;
but this don't work. It should also work for a case when I click on space that may be part of tab. How to fix it when text contain tabs?
Here is codepen demo
The tab character is the issue. It's a single character which means the string it's not calculated as four characters in the text.slice. If you replace \t with four spaces your issue is solved.

slide text (book page) from top to bottom

i am working on a small application (phonegap) that scrolls a page of a book when the users pushed the audio-button to listen to the text at the same time. The general idea :-)
I have looked into the Marquee version, what works so far but it has some strange behaviour:
<marquee behavior="scroll" height="100%" vspace="0%" direction="up" id="mymarquee" scrollamount="3" scolldelay="1000" loop="1"> TEXT HERE </marquee>
with the "id="mymarquee" connected to the audio play button. This works but not recommanded as they say. Better to use a javascript version. So i found a cool version so far on the web, but it goes from the right to the left. Now i am not the best programmer in the world so i was wondering if someone could help adjust the script below so we can add a direction to it. This way the script would be multi-functional (for others as well) since i only need a scroll from top to bottom.
Here is the HTML part:
<script src="js/slideandfade.js" type="text/javascript"></script>
<DIV ID="fader" STYLE="text-align:right;"></DIV>
<SCRIPT TYPE="text/javascript">
fadeandscroll('TEXT HERE', '#676F77', '#DFF5FF', 40, 70, 250, 10);
</SCRIPT>
And this is the slideandfade.js
//Text fade
var bgcolor;
var fcolor;
var heading;
//Number of steps to fade
var steps;
var colors;
var color = 0;
var step = 1;
var interval1;
var interval2;
//fade: fader function
// Fade from backcolor to forecolor in specified number of steps
function fade(headingtext,backcolor,forecolor,numsteps) {
if (color == 0) {
steps = numsteps;
heading = "<font color='{COLOR}'>"+headingtext+"</strong></font>";
bgcolor = backcolor;
fcolor = forecolor;
colors = new Array(steps);
getFadeColors(bgcolor,fcolor,colors);
}
// insert fader color into message
var text_out = heading.replace("{COLOR}", colors[color]);
// write the message to the document
document.getElementById("fader").innerHTML = text_out;
// select next fader color
color += step;
if (color >= steps) clearInterval(interval1);
}
//getFadeColors: fills colors, using predefined Array, with color hex strings fading from ColorA to ColorB
//Note: Colors.length equals the number of steps to fade
function getFadeColors(ColorA, ColorB, Colors) {
len = Colors.length;
//Strip '#' from colors if present
if (ColorA.charAt(0)=='#') ColorA = ColorA.substring(1);
if (ColorB.charAt(0)=='#') ColorB = ColorB.substring(1);
//Substract red green and blue components from hex string
var r = HexToInt(ColorA.substring(0,2));
var g = HexToInt(ColorA.substring(2,4));
var b = HexToInt(ColorA.substring(4,6));
var r2 = HexToInt(ColorB.substring(0,2));
var g2 = HexToInt(ColorB.substring(2,4));
var b2 = HexToInt(ColorB.substring(4,6));
// calculate size of step for each color component
var rStep = Math.round((r2 - r) / len);
var gStep = Math.round((g2 - g) / len);
var bStep = Math.round((b2 - b) / len);
// fill Colors array with fader colors
for (i = 0; i < len-1; i++) {
Colors[i] = "#" + IntToHex(r) + IntToHex(g) + IntToHex(b);
r += rStep;
g += gStep;
b += bStep;
}
Colors[len-1] = ColorB; // make sure we finish exactly at ColorB
}
//IntToHex: converts integers between 0 - 255 into a two digit hex string.
function IntToHex(n) {
var result = n.toString(16);
if (result.length==1) result = "0"+result;
return result;
}
//HexToInt: converts two digit hex strings into integer.
function HexToInt(hex) {
return parseInt(hex, 16);
}
var startwidth = 0;
//scroll: Make the text scroll using the marginLeft element of the div container
function scroll(startw) {
if (startwidth == 0) {
startwidth=startw;
}
document.getElementById("fader").style.marginLeft = startwidth + "px";
if (startwidth > 1) {
startwidth -= 1;
} else {
clearInterval(interval2);
}
}
function fadeandscroll(txt,color1,color2,numsteps,fademilli,containerwidth,scrollmilli) {
interval1 = setInterval("fade('"+txt+"','"+color1+"','"+color2+"',"+numsteps+")",fademilli);
interval2 = setInterval("scroll("+containerwidth+")",scrollmilli);
}

Get cursor or text position in pixels for input element

IE allows me to create a text range in an input element, upon which I can call getBoundingClientRect() and get the position in pixels of a certain character or the cursor/caret. Is there any way of getting the position of a certain character in pixels in other browsers?
var input = $("#myInput")[0];
var pixelPosition = null;
if (input.createTextRange)
{
var range = input.createTextRange();
range.moveStart("character", 6);
pixelPosition = range.getBoundingClientRect();
}
else
{
// Is there any way to create a range on an input's value?
}
I'm using jQuery, but I doubt it will be able to address my situation. I expect a pure JavaScript solution, if any, but jQuery answers are welcome.
Demo
I have written a function which behaves as expected. A very detailed demonstration panel can be found here: Fiddle: http://jsfiddle.net/56Rep/5/
The interface in the demo is self-explanatory.
The functionality as requested in the question would be implemented in my function as follows:
var pixelPosition = getTextBoundingRect(input, 6)
Function dependencies
Updated: The function is pure JavaScript, and not dependent on any plugin or framework!
The function assumes that the getBoundingClientRect method exist. Text ranges are used when they're supported. Otherwise, the functionality is achieved using my function logic.
Function logic
The code itself contains several comments. This part goes in a deeper detail.
One temporary <div> container is created.
1 - 3 <span> elements are created. Each span holds a part of the input's value (offsets 0 to selectionStart, selectionStart to selectionEnd, selectionEnd to end of string, only the second span is meaninngful).
Several significant style properties from the input element are copied to these <div> and <span> tags. Only significant style properties are copied. For example, color is not copied, because it does not affect the offsets of a character in any way.#1
The <div> is positioned at the exact position of the text node (input's value). Borders and paddings are taken into account, to make sure that the temporary <div> is correctly positioned.
A variable is created, which holds the return value of div.getBoundingClientRect().
The temporary <div> is removed, unless parameter debug is set to true.
The function returns the ClientRect object. For more information about this object, see this page. The demo also shows a list of properties: top, left, right, bottom, height and width.
#1: getBoundingClientRect() (and some minor properties) is used to determine the position of the input element. Then, the padding and border width are added, to get the real position of a text node.
Known issues
The only case of an inconsistency was encountered when getComputedStyle returned a wrong value for font-family: When a page hasn't defined a font-family property, the computedStyle returns an incorrect value (even Firebug is experiencing this issue; environment: Linux, Firefox 3.6.23, font "Sans Serif").
As visible in the demo, the positioning is sometimes slightly off (almost zero, always smaller than 1 pixel).
Technical restrictions prevents the script from getting the exact offset of a text fragment when the contents has been moved, e.g. when the first visible character in an input field does not equal the first value's character.
Code
// #author Rob W http://stackoverflow.com/users/938089/rob-w
// #name getTextBoundingRect
// #param input Required HTMLElement with `value` attribute
// #param selectionStart Optional number: Start offset. Default 0
// #param selectionEnd Optional number: End offset. Default selectionStart
// #param debug Optional boolean. If true, the created test layer
// will not be removed.
function getTextBoundingRect(input, selectionStart, selectionEnd, debug) {
// Basic parameter validation
if(!input || !('value' in input)) return input;
if(typeof selectionStart == "string") selectionStart = parseFloat(selectionStart);
if(typeof selectionStart != "number" || isNaN(selectionStart)) {
selectionStart = 0;
}
if(selectionStart < 0) selectionStart = 0;
else selectionStart = Math.min(input.value.length, selectionStart);
if(typeof selectionEnd == "string") selectionEnd = parseFloat(selectionEnd);
if(typeof selectionEnd != "number" || isNaN(selectionEnd) || selectionEnd < selectionStart) {
selectionEnd = selectionStart;
}
if (selectionEnd < 0) selectionEnd = 0;
else selectionEnd = Math.min(input.value.length, selectionEnd);
// If available (thus IE), use the createTextRange method
if (typeof input.createTextRange == "function") {
var range = input.createTextRange();
range.collapse(true);
range.moveStart('character', selectionStart);
range.moveEnd('character', selectionEnd - selectionStart);
return range.getBoundingClientRect();
}
// createTextRange is not supported, create a fake text range
var offset = getInputOffset(),
topPos = offset.top,
leftPos = offset.left,
width = getInputCSS('width', true),
height = getInputCSS('height', true);
// Styles to simulate a node in an input field
var cssDefaultStyles = "white-space:pre;padding:0;margin:0;",
listOfModifiers = ['direction', 'font-family', 'font-size', 'font-size-adjust', 'font-variant', 'font-weight', 'font-style', 'letter-spacing', 'line-height', 'text-align', 'text-indent', 'text-transform', 'word-wrap', 'word-spacing'];
topPos += getInputCSS('padding-top', true);
topPos += getInputCSS('border-top-width', true);
leftPos += getInputCSS('padding-left', true);
leftPos += getInputCSS('border-left-width', true);
leftPos += 1; //Seems to be necessary
for (var i=0; i<listOfModifiers.length; i++) {
var property = listOfModifiers[i];
cssDefaultStyles += property + ':' + getInputCSS(property) +';';
}
// End of CSS variable checks
var text = input.value,
textLen = text.length,
fakeClone = document.createElement("div");
if(selectionStart > 0) appendPart(0, selectionStart);
var fakeRange = appendPart(selectionStart, selectionEnd);
if(textLen > selectionEnd) appendPart(selectionEnd, textLen);
// Styles to inherit the font styles of the element
fakeClone.style.cssText = cssDefaultStyles;
// Styles to position the text node at the desired position
fakeClone.style.position = "absolute";
fakeClone.style.top = topPos + "px";
fakeClone.style.left = leftPos + "px";
fakeClone.style.width = width + "px";
fakeClone.style.height = height + "px";
document.body.appendChild(fakeClone);
var returnValue = fakeRange.getBoundingClientRect(); //Get rect
if (!debug) fakeClone.parentNode.removeChild(fakeClone); //Remove temp
return returnValue;
// Local functions for readability of the previous code
function appendPart(start, end){
var span = document.createElement("span");
span.style.cssText = cssDefaultStyles; //Force styles to prevent unexpected results
span.textContent = text.substring(start, end);
fakeClone.appendChild(span);
return span;
}
// Computing offset position
function getInputOffset(){
var body = document.body,
win = document.defaultView,
docElem = document.documentElement,
box = document.createElement('div');
box.style.paddingLeft = box.style.width = "1px";
body.appendChild(box);
var isBoxModel = box.offsetWidth == 2;
body.removeChild(box);
box = input.getBoundingClientRect();
var clientTop = docElem.clientTop || body.clientTop || 0,
clientLeft = docElem.clientLeft || body.clientLeft || 0,
scrollTop = win.pageYOffset || isBoxModel && docElem.scrollTop || body.scrollTop,
scrollLeft = win.pageXOffset || isBoxModel && docElem.scrollLeft || body.scrollLeft;
return {
top : box.top + scrollTop - clientTop,
left: box.left + scrollLeft - clientLeft};
}
function getInputCSS(prop, isnumber){
var val = document.defaultView.getComputedStyle(input, null).getPropertyValue(prop);
return isnumber ? parseFloat(val) : val;
}
}
I ended up creating a hidden mock input out of a span positioned absolutely and styled similarly to the input. I set the text of that span to the value of the input up to the character whose position I want to find. I insert the span before the input and get it's offset:
function getInputTextPosition(input, charOffset)
{
var pixelPosition = null;
if (input.createTextRange)
{
var range = input.createTextRange();
range.moveStart("character", charOffset);
pixelPosition = range.getBoundingClientRect();
}
else
{
var text = input.value.substr(0, charOffset).replace(/ $/, "\xa0");
var sizer = $("#sizer").insertBefore(input).text(text);
pixelPosition = sizer.offset();
pixelPosition.left += sizer.width();
if (!text) sizer.text("."); // for computing height. An empty span returns 0
pixelPosition.bottom = pixelPosition.top + sizer.height();
}
return pixelPosition
}
The css for my sizer span:
#sizer
{
position: absolute;
display: inline-block;
visibility: hidden;
margin: 3px; /* simulate padding and border without affecting height and width */
font-family: "segoe ui", Verdana, Arial, Sans-Serif;
font-size: 12px;
}
May 2014 update: The incredibly lightweight and robust textarea-caret-position Component library now supports <input type="text"> as well, rendering all other answers obsolete.
A demo is available at http://jsfiddle.net/dandv/aFPA7/
Thanks to Rob W for inspiration towards RTL support.
2016 update: A more modern HTML5 based solution would be to use the contenteditable property.
<div contenteditable="true"> <!-- behaves as input -->
Block of regular text, and <span id='interest'>text of interest</span>
</div>
We can now find the position of the span using jquery offset(). And of course, the <span> tags can be inserted upfront or dynamically.

Finding number of lines in an html textarea

I'm writing a mobile web application where scrollbars are not displayed on the device's browser. Due to this, I'm trying to dynamically modify the height of the textarea to make it bigger, however I don't know of any way to actually get the line count on an html textarea. Any help would be greatly appreciated!
EDIT
So I realize now that it's not newlines per se, but actual line wrapping. So when one line finishes it wraps the text to the next line. It appears as if it is a new line. Any way to count the number of these? Thanks!
The number of lines in the textarea would be
textarea.value.match(/\n/g).length + 1
I have created a plugin to handle line counting and wrap detection in a <textarea>.
I hope someone can use it.
Code on BitBucket
Sample Usage
var result = $.countLines("#textarea");
result.actual // The number of lines in the textarea.
result.wraps // The number of lines in the textarea that wrap at least once.
result.wrapped // The total number of times all lines wrap.
result.blank // The number of blank lines.
result.visual // The approximate number of lines that the user actually sees in the textarea
Working Demonstration
/*! Textarea Line Count - v1.4.1 - 2012-12-06
* https://bitbucket.org/MostThingsWeb/textarea-line-count
* Copyright (c) 2012 MostThingsWeb (Chris Laplante); Licensed MIT */
(function($) {
$.countLines = function(ta, options) {
var defaults = {
recalculateCharWidth: true,
charsMode: "random",
fontAttrs: ["font-family", "font-size", "text-decoration", "font-style", "font-weight"]
};
options = $.extend({}, defaults, options);
var masterCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
var counter;
if (!ta.jquery) {
ta = $(ta);
}
var value = ta.val();
switch (options.charsMode) {
case "random":
// Build a random collection of characters
options.chars = "";
masterCharacters += ".,?!-+;:'\"";
for (counter = 1; counter <= 12; counter++) {
options.chars += masterCharacters[(Math.floor(Math.random() * masterCharacters.length))];
}
break;
case "alpha":
options.chars = masterCharacters;
break;
case "alpha_extended":
options.chars = masterCharacters + ".,?!-+;:'\"";
break;
case "from_ta":
// Build a random collection of characters from the textarea
if (value.length < 15) {
options.chars = masterCharacters;
} else {
for (counter = 1; counter <= 15; counter++) {
options.chars += value[(Math.floor(Math.random() * value.length))];
}
}
break;
case "custom":
// Already defined in options.chars
break;
}
// Decode chars
if (!$.isArray(options.chars)) {
options.chars = options.chars.split("");
}
// Generate a span after the textarea with a random ID
var id = "";
for (counter = 1; counter <= 10; counter++) {
id += (Math.floor(Math.random() * 10) + 1);
}
ta.after("<span id='s" + id + "'></span>");
var span = $("#s" + id);
// Hide the span
span.hide();
// Apply the font properties of the textarea to the span class
$.each(options.fontAttrs, function(i, v) {
span.css(v, ta.css(v));
});
// Get the number of lines
var lines = value.split("\n");
var linesLen = lines.length;
var averageWidth;
// Check if the textarea has a cached version of the average character width
if (options.recalculateCharWidth || ta.data("average_char") == null) {
// Get a pretty good estimation of the width of a character in the textarea. To get a better average, add more characters and symbols to this list
var chars = options.chars;
var charLen = chars.length;
var totalWidth = 0;
$.each(chars, function(i, v) {
span.text(v);
totalWidth += span.width();
});
// Store average width on textarea
ta.data("average_char", Math.ceil(totalWidth / charLen));
}
averageWidth = ta.data("average_char");
// We are done with the span, so kill it
span.remove();
// Determine missing width (from padding, margins, borders, etc); this is what we will add to each line width
var missingWidth = (ta.outerWidth() - ta.width()) * 2;
// Calculate the number of lines that occupy more than one line
var lineWidth;
var wrappingLines = 0;
var wrappingCount = 0;
var blankLines = 0;
$.each(lines, function(i, v) {
// Calculate width of line
lineWidth = ((v.length + 1) * averageWidth) + missingWidth;
// Check if the line is wrapped
if (lineWidth >= ta.outerWidth()) {
// Calculate number of times the line wraps
var wrapCount = Math.floor(lineWidth / ta.outerWidth());
wrappingCount += wrapCount;
wrappingLines++;
}
if ($.trim(v) === "") {
blankLines++;
}
});
var ret = {};
ret["actual"] = linesLen;
ret["wrapped"] = wrappingLines;
ret["wraps"] = wrappingCount;
ret["visual"] = linesLen + wrappingCount;
ret["blank"] = blankLines;
return ret;
};
}(jQuery));
result = jQuery.countLines("#textarea");
jQuery('#display').html(
'<span>Actual: ' + result.actual + '</span>' +
'<span>Blank: ' + result.blank + '</span>' +
'<span>Visual: ' + result.visual + '</span>' +
'<span>Wrapped: ' + result.wrapped + '</span>' +
'<span>Wraps: ' + result.wraps + '</span>'
);
#textarea {
width: 150px;
height: 80px;
}
#display span {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea id="textarea">text
here
this is a longer line so that it will wrap in the box longer longer longer</textarea>
<div id="display"></div>
This is an efficient and accurate method to count the number of lines in a text area, including wrapped lines.
/** #type {HTMLTextAreaElement} */
var _buffer;
/**
* Returns the number of lines in a textarea, including wrapped lines.
*
* __NOTE__:
* [textarea] should have an integer line height to avoid rounding errors.
*/
function countLines(textarea) {
if (_buffer == null) {
_buffer = document.createElement('textarea');
_buffer.style.border = 'none';
_buffer.style.height = '0';
_buffer.style.overflow = 'hidden';
_buffer.style.padding = '0';
_buffer.style.position = 'absolute';
_buffer.style.left = '0';
_buffer.style.top = '0';
_buffer.style.zIndex = '-1';
document.body.appendChild(_buffer);
}
var cs = window.getComputedStyle(textarea);
var pl = parseInt(cs.paddingLeft);
var pr = parseInt(cs.paddingRight);
var lh = parseInt(cs.lineHeight);
// [cs.lineHeight] may return 'normal', which means line height = font size.
if (isNaN(lh)) lh = parseInt(cs.fontSize);
// Copy content width.
_buffer.style.width = (textarea.clientWidth - pl - pr) + 'px';
// Copy text properties.
_buffer.style.font = cs.font;
_buffer.style.letterSpacing = cs.letterSpacing;
_buffer.style.whiteSpace = cs.whiteSpace;
_buffer.style.wordBreak = cs.wordBreak;
_buffer.style.wordSpacing = cs.wordSpacing;
_buffer.style.wordWrap = cs.wordWrap;
// Copy value.
_buffer.value = textarea.value;
var result = Math.floor(_buffer.scrollHeight / lh);
if (result == 0) result = 1;
return result;
}
Demo here
I haven't tried using the function discussed in this blog, but you may find it useful.
http://kirblog.idetalk.com/2010/03/calculating-cursor-position-in-textarea.html
Basically, if you create a div and then copy the text into that div, with the same width and font characteristics, you can then get the information you need, such as the number of lines. The number of lines in this example would be easy, in that if you know how many pixels high a single line would be, then just find the width of the test div and you can get a pretty accurate idea as to how many lines are in your textarea.
Get scrollHeight, subtract top+bottom padding, divide by lineHeight.
I'm pretty sure there is no reasonable way to count the number of lines as displayed in the browser especially considering some browsers (Safari) allow the user to resize textareas.
It'd be hacky, but your best bet might be to just estimate based on the total characters divided by average number of characters per line. :-/
Maybe there is a way to get the "raw" number of "visual" lines. You should read the scrollHeight property of the textarea and divide it by the height of a line. Let's try.
Start with this HTML:
<textarea id="ta" cols="50" rows="10"></textarea>
Then:
var line_height = Math.floor($("#ta").height() / parseInt($("#ta").attr("rows")));
var dirty_number_of_lines = Math.ceil($("#ta")[0].scrollHeight / line_height);
I am not sure if that really works, just a mad theory.
You can calculate is as so:
var length = $('#textarea').val().split("\n").length;
The number of characters allowed per line is dictated by the "cols" attribute of the textarea.
<textarea rows="10" cols="80"></textarea>
Assuming 80 characters per line, a good estimate may be:
var approxNumLines = textareaElement.value.length / textareaElement.cols ;
Doesn't account for word-break and word-wrap.

Categories

Resources