Word wrap: ellipsis without css - javascript

I'm developing an app for a TV with an old Gecko engine (1.9 I think). I have a container 250x40 and I'd like to fit 2 lines of text in it, and if it's too long then an ellipsis should be shown (just like in the CSS3 property text-overflow: ellipsis).
However:
- I cannot use CSS3,
- I tried using some jQuery plugins, but they just work too slow - you can accually see the text being shrunk down until it fits in the container.
I tried counting letters, but it doesn't work, because they are all different widths.
I tried mapping each letter to its width, and counting the widht of the whole text, but the fact that it's 2 lines screws it all up - I don't know at which point the text will go to the next line.
Any help appreciated.

Slightly based on #Emissary's answer, here's a reasonably performing pair of jQuery plugins that'll handle adding ellipsis on elements that might hold more than one row of text:
(function($) {
$.fn.reflow = function() {
return this.each(function() {
var $this = $(this);
var $parent = $this.parent();
var text = $this.data('reflow');
$this.text(text); // try full text again
var words = text.split(' ');
while ($this.height() > $parent.height() ||
$this.width() > $parent.width()) {
words.pop();
$this.html(words.join(' ') + '…');
}
});
}
$.fn.overflow = function() {
return this.each(function() {
var $this = $(this);
var text = $this.text();
$this.data('reflow', text);
}).reflow();
}
})(jQuery);
The latter one registers elements for reflowing, and the first one actually does the work. The split is there to allow window resizing to automatically reformat the (original) contents of the element, e.g.:
$('.overflow').overflow();
$(window).on('resize', function() {
$('.overflow').reflow();
});
For higher performance, if it matters, you might consider replacing the .pop sequence with something that uses a O(log n) binary partitioning algorithm to find the optimal cut point instead of just removing one word at a time.
See http://jsfiddle.net/alnitak/vyth5/

It's been a while since I bothered with supporting older browsers but this is how I always did it. Should point out that it trims words rather than characters, I always thought half a word looked daft - if you care about typography...
html:
<div id="el">
<span class="overflow">Lorem ipsum dolor sit amet</span>
</div>
css:
#el {
width: 250px;
height: 40px;
overflow: hidden;
}
.overflow {
white-space: nowrap;
}
js / jQuery:
var el = $('#el'),
ov = $('#el .overflow'),
w = el.text().split(' ');
while(ov.width() > el.width()){
w.pop();
ov.html( w.join(' ') + '…' );
}
jsFiddle

This chap here has a solution that uses javascript and no JQuery: http://blog.mastykarz.nl/measuring-the-length-of-a-string-in-pixels-using-javascript/
Done in jsfiddle: http://jsfiddle.net/ZfDYG/
Edit - just read the bit about 2 lines of text: http://jsfiddle.net/ZfDYG/8/
code (for completeness):
String.prototype.visualLength = function() {
var ruler = $("ruler");
ruler.innerHTML = this;
return ruler.offsetWidth;
}
function $(id) {
return document.getElementById(id);
}
var s = "Some text that is quite long and probably too long to fit in the box";
var len = s.visualLength();
String.prototype.trimToPx = function(length,postfix) {
postfix = postfix || '';
var tmp = this;
var trimmed = this;
if (tmp.visualLength() > length) {
trimmed += postfix;
while (trimmed.visualLength() > length) {
tmp = tmp.substring(0, tmp.length-1);
trimmed = tmp + postfix;
}
}
return trimmed;
}
String.prototype.wrapToLength = function(complete) {
if(complete[this.length] == ' ' || complete[this.length - 1] == ' ') return;
var wrapped = this;
var found = false;
for(var i = this.length-1; i > 0 && !found; i--) {
if(this[i] == ' ') {
wrapped = this.substring(0, i);
found = true;
}
}
return wrapped;
}
var firstLine = s.trimToPx(240).wrapToLength(s);
var secondLine = s.substring(firstLine.length, s.length);
$('output').innerHTML= firstLine+' '+secondLine.trimToPx(240,'...');
Html:
<span id="ruler"></span>
<div id="output"></div>
css:
#ruler { visibility: hidden; white-space: nowrap; }
#output {
width: 240px;
height: 50px;
border: 1px solid red;
}
If that is still too slow on your box, I guess it should be possible to speed up the while loop by starting with bigger additions to oscillate towards the final length (kinda like a spring) rather than increment slowly up to it.

Related

Ondrag event creates white space at the bottom of my HTML page

I have some code that detects if the cursor is being dragged left or right which upon doing so, cycles through an image sequence (similar to that of the Hyundai genesis website and its rotating car).
However, evertime the function is called, it creates white space at the bottom of the html page.
Any idea what might be causing this?
Here's the Javascript for loading the image sequence into the array:
var cache = [];
function imgList(base,firstNum,lastNum) {
var imageFunction;
for(var i = firstNum;i <= lastNum; i++) {
imageFunction = new Image();
if(i <=9){ var EXT = '000'}
else if(i <= 99){var EXT = '00'}
else if(i <= 999){var EXT = '0'}
else{var EXT = ''}
imageFunction.src = base + "." + EXT + i + ".png";
cache.push(imageFunction);
console.log(cache.length);
}
}
Here is the Javascript function that is called when the drag event occurs:
var prevX = -1;
var i = 0;
var drgleft = 0;
var drgright = 0;
function sequence(event){
if(prevX == -1){
prevX = event.pageX;
return false;
}
//drag left
if(prevX > event.pageX){
console.log('dragged left');
drgleft++;
if(drgleft == 2){
drgleft = 0;
i--;
if(i < 0){
i = 30; //for optimization reasons, input the cache.length value manually (this avoids unnecessary errors in the console and laggy framerate as a result).
}
document.getElementById("TheBigOne").src = cache[i].src; //use console.log(i); as a method of verifying that the code is executing correctly
}
}
else if(prevX < event.pageX){
console.log('dragged right');
drgright++;
if(drgright == 2){
drgright = 0;
i++;
if(i > 30){ //for optimization reasons, input the cache.length value manually (this avoids unnecessary errors in the console and laggy framerate as a result).
i=0;
}
document.getElementById("TheBigOne").src = cache[i].src;
}
}
else{}
prevX = event.pageX
}
Here is the html:
<div class="The_main_event" ondrag="sequence(event)" id="PlaneTime">
<br/>
<img src="file:///C:/Users/Foo/Desktop/Website/Web_aeroplane/Web%20Test.0031.png" id="TheBigOne" class="planepic">
<br/>
</div>
If there is white space around an image, a small gap will appear underneath it as long as it is an inline element. There are a few ways to fix it, but the easiest is changing it to a block level element.
Try this:
.planepic {
display: block;
}
Here's a demo
div {
background: red;
display: inline-block;
border: 1px solid black;
}
img {
max-width: 100px;
}
.block {
display: block;
}
<div><img src="https://3.bp.blogspot.com/-W__wiaHUjwI/Vt3Grd8df0I/AAAAAAAAA78/7xqUNj8ujtY/s1600/image02.png"></div>
<div><img src="https://3.bp.blogspot.com/-W__wiaHUjwI/Vt3Grd8df0I/AAAAAAAAA78/7xqUNj8ujtY/s1600/image02.png" class="block"></div>
Alright I found the solution to the issue was something completely unrelated. There was a bit of javascript which was used to remove the issue of the image ghosting when the image was dragged:
document.getElementById("TheBigOne").addEventListener("dragstart", function(event) {
var crt = this.cloneNode(true);
crt.style.backgroundColor = "";
crt.style.opacity = "0"; /* or visibility: hidden, or any of the above */
document.body.appendChild(crt);
event.dataTransfer.setDragImage(crt, 0, 0);
}, false);
Once I removed this code, the issue ceased to occur...
looking at this code now, I can understand why, I guess I shouldn't stay up so late and code rubbish like this.

How do you test if the contents of an input element is overflowing? [duplicate]

I have a collection of block elements on a page. They all have the CSS rules white-space, overflow, text-overflow set so that overflowing text is trimmed and an ellipsis is used.
However, not all the elements overflow.
Is there anyway I can use javascript to detect which elements are overflowing?
Thanks.
Added: example HTML structure I am working with.
<td><span>Normal text</span></td>
<td><span>Long text that will be trimmed text</span></td>
The SPAN elements always fit in the cells, they have the ellipsis rule applied. I want to detect when the ellipsis is applied to the text content of the SPAN.
Try this JS function, passing the span element as argument:
function isEllipsisActive(e) {
return (e.offsetWidth < e.scrollWidth);
}
Once upon a time I needed to do this, and the only cross-browser reliable solution I came across was hack job. I'm not the biggest fan of solutions like this, but it certainly produces the correct result time and time again.
The idea is that you clone the element, remove any bounding width, and test if the cloned element is wider than the original. If so, you know it's going to have been truncated.
For example, using jQuery:
var $element = $('#element-to-test');
var $c = $element
.clone()
.css({display: 'inline', width: 'auto', visibility: 'hidden'})
.appendTo('body');
if( $c.width() > $element.width() ) {
// text was truncated.
// do what you need to do
}
$c.remove();
I made a jsFiddle to demonstrate this, http://jsfiddle.net/cgzW8/2/
You could even create your own custom pseudo-selector for jQuery:
$.expr[':'].truncated = function(obj) {
var $this = $(obj);
var $c = $this
.clone()
.css({display: 'inline', width: 'auto', visibility: 'hidden'})
.appendTo('body');
var c_width = $c.width();
$c.remove();
if ( c_width > $this.width() )
return true;
else
return false;
};
Then use it to find elements
$truncated_elements = $('.my-selector:truncated');
Demo: http://jsfiddle.net/cgzW8/293/
Hopefully this helps, hacky as it is.
Adding to italo's answer, you can also do this using jQuery.
function isEllipsisActive($jQueryObject) {
return ($jQueryObject.width() < $jQueryObject[0].scrollWidth);
}
Also, as Smoky pointed out, you may want to use jQuery outerWidth() instead of width().
function isEllipsisActive($jQueryObject) {
return ($jQueryObject.outerWidth() < $jQueryObject[0].scrollWidth);
}
For those using (or planning to use) the accepted answer from Christian Varga, please be aware of the performance issues.
Cloning/manipulating the DOM in such a way causes DOM Reflow (see an explanation on DOM reflow here) which is extremely resource intensive.
Using Christian Varga's solution on 100+ elements on a page caused a 4 second reflow delay during which the JS thread is locked. Considering JS is single-threaded this means a significant UX delay to the end user.
Italo Borssatto's answer should be the accepted one, it was approximately 10 times quicker during my profiling.
Answer from italo is very good! However let me refine it a little:
function isEllipsisActive(e) {
var tolerance = 2; // In px. Depends on the font you are using
return e.offsetWidth + tolerance < e.scrollWidth;
}
Cross browser compatibility
If, in fact, you try the above code and use console.log to print out the values of e.offsetWidth and e.scrollWidth, you will notice, on IE, that, even when you have no text truncation, a value difference of 1px or 2px is experienced.
So, depending on the font size you use, allow a certain tolerance!
This sample show tooltip on cell table with text truncated. Is dynamic based on table width:
$.expr[':'].truncated = function (obj) {
var element = $(obj);
return (element[0].scrollHeight > (element.innerHeight() + 1)) || (element[0].scrollWidth > (element.innerWidth() + 1));
};
$(document).ready(function () {
$("td").mouseenter(function () {
var cella = $(this);
var isTruncated = cella.filter(":truncated").length > 0;
if (isTruncated)
cella.attr("title", cella.text());
else
cella.attr("title", null);
});
});
Demo: https://jsfiddle.net/t4qs3tqs/
It works on all version of jQuery
elem.offsetWdith VS ele.scrollWidth
This work for me!
https://jsfiddle.net/gustavojuan/210to9p1/
$(function() {
$('.endtext').each(function(index, elem) {
debugger;
if(elem.offsetWidth !== elem.scrollWidth){
$(this).css({color: '#FF0000'})
}
});
});
All the solutions did not really work for me, what did work was compare the elements scrollWidth to the scrollWidth of its parent (or child, depending on which element has the trigger).
When the child's scrollWidth is higher than its parents, it means .text-ellipsis is active.
When el is the parent element
function isEllipsisActive(el) {
let width = el.offsetWidth;
let widthChild = el.firstChild.offsetWidth;
return (widthChild >= width);
}
When el is the child element
function isEllipsisActive(event) {
let width = el.offsetWidth;
let widthParent = el.parentElement.scrollWidth;
return (width >= widthParent);
}
My implementation)
const items = Array.from(document.querySelectorAll('.item'));
items.forEach(item =>{
item.style.color = checkEllipsis(item) ? 'red': 'black'
})
function checkEllipsis(el){
const styles = getComputedStyle(el);
const widthEl = parseFloat(styles.width);
const ctx = document.createElement('canvas').getContext('2d');
ctx.font = `${styles.fontSize} ${styles.fontFamily}`;
const text = ctx.measureText(el.innerText);
return text.width > widthEl;
}
.item{
width: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
<div class="item">Short</div>
<div class="item">Loooooooooooong</div>
If you're doing react, here's how I did it.
<div
ref={ref => {
if (!ref) return
const isOverflowing = ref.scrollWidth > ref.clientWidth
if (isOverflowing) {
// handle what to do next here
}
}}
/>
I think the better way to detect it is use getClientRects(), it seems each rect has the same height, so we can caculate lines number with the number of different top value.
getClientRects work 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;
}
getRowRects work like this
you can detect like this
None of the solutions worked for me, so I chose a totally different approach. Instead of using the CSS solution with ellipsis, I just cut the text from a specific string length.
if (!this.isFullTextShown && this.text.length > 350) {
return this.text.substring(0, 350) + '...'
}
return this.text
and show "more/less" buttons if the length is exceeded.
<span
v-if="text.length > 350"
#click="isFullTextShown = !isFullTextShown"
>
{{ isFullTextShown ? 'show less' : 'show more' }}
</span>
Adding to #Дмытрык answer, missing deduction of borders and paddings to be fully functional!!
const items = Array.from(document.querySelectorAll('.item'));
items.forEach(item =>{
item.style.color = checkEllipsis(item) ? 'red': 'black'
})
function checkEllipsis(el){
const styles = getComputedStyle(el);
const widthEl = parseFloat(styles.width);
const ctx = document.createElement('canvas').getContext('2d');
ctx.font = `${styles.fontSize} ${styles.fontFamily}`;
const text = ctx.measureText(el.innerText);
let extra = 0;
extra += parseFloat(styles.getPropertyValue('border-left-width'));
extra += parseFloat(styles.getPropertyValue('border-right-width'));
extra += parseFloat(styles.getPropertyValue('padding-left'));
extra += parseFloat(styles.getPropertyValue('padding-right'));
return text.width > (widthEl - extra);
}
.item{
width: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
<div class="item">Short</div>
<div class="item">Loooooooooooong</div>
The e.offsetWidth < e.scrollWidth solution is not always working.
And if you want to use pure JavaScript, I recommend to use this:
(typescript)
public isEllipsisActive(element: HTMLElement): boolean {
element.style.overflow = 'initial';
const noEllipsisWidth = element.offsetWidth;
element.style.overflow = 'hidden';
const ellipsisWidth = element.offsetWidth;
if (ellipsisWidth < noEllipsisWidth) {
return true;
} else {
return false;
}
}
For someone who uses e.offsetWidth < e.scrollWidth and got a bug that can show full text but still got ellipsis.
It because offsetWidth and scrollWidth always round the value. For example: offsetWidth return 161 but the actual width is 161.25.
The solution is use getBoundingClientRect
const clonedEl = e.cloneNode(true)
clonedElement.style.overflow = "visible"
clonedElement.style.visibility = "hidden"
clonedElement.style.width = "fit-content"
e.parentElement.appendChild(clonedEl)
const fullWidth = clonedElement.getBoundingClientRect().width
const currentWidth = e.getBoundingClientRect().width
return currentWidth < fullWidth
Case you are using line-clamp >= 2 line for adding ellipsis at more than one line you can use this conditioning:
if (
descriptionElement &&
descriptionElement.offsetHeight < descriptionElement.scrollHeight
) {
// has text-overflow
}
There's a small pixel problem with the answers above when comparing offsetWidth > scrollWidth.
W3C has a legacy API that returns element.scrollWidth value as rounded which is causing the comparison in some cases to to return false.
If the element width are 150px and the scrollWidth are 150.4px (rounded to 150), then this check will be returning false even if the browser are showing ellipsis in the text.
They have tried to update the APIs that return fractional pixels, but it was reverted due to webcompat.
There's a workaround using max-content and getClientRects().
Here's a sample code that I use onMouseEnter.
Note that this only works if the container has a boundary to 100% of the available width (so if you are using flexbox, your container has to be flex: 1 for example.
hasEllipsis(elementItem) {
let scrollWidth = elementItem.scrollWidth;
elementItem.style.width = 'max-content';
const itemRects = elementItem.getClientRects();
if (itemRects.length > 0 && itemRects[0].width > scrollWidth) {
scrollWidth = itemRects[0].width;
}
elementItem.style.width = 'auto';
return scrollWidth > elementItem.clientWidth;
}
Articles:
https://bugs.chromium.org/p/chromium/issues/detail?id=980476
https://github.com/w3c/csswg-drafts/issues/4123
The solution #ItaloBorssatto is perfect. But before looking at SO - I made my decision. Here it is :)
const elems = document.querySelectorAll('span');
elems.forEach(elem => {
checkEllipsis(elem);
});
function checkEllipsis(elem){
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const styles = getComputedStyle(elem);
ctx.font = `${styles.fontWeight} ${styles.fontSize} ${styles.fontFamily}`;
const widthTxt = ctx.measureText(elem.innerText).width;
if (widthTxt > parseFloat(styles.width)){
elem.style.color = 'red'
}
}
span.cat {
display: block;
border: 1px solid black;
white-space: nowrap;
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
<span class="cat">Small Cat</span>
<span class="cat">Looooooooooooooong Cat</span>
there are some mistasks in demo http://jsfiddle.net/brandonzylstra/hjk9mvcy/ mentioned by https://stackoverflow.com/users/241142/iconoclast.
in his demo, add these code will works:
setTimeout(() => {
console.log(EntryElm[0].offsetWidth)
}, 0)

Text pagination inside a DIV with image

I want to paginate a text in some div so it will fit the allowed area
Logic is pretty simple:
1. split text into words
2. add word by word into and calculate element height
3. if we exceed the height - create next page
It works quite good
here is JS function i've used:
function paginate() {
var newPage = $('<pre class="text-page" />');
contentBox.empty().append(newPage);
var betterPageText='';
var pageNum = 0;
var isNewPage = false;
var lineHeight = parseInt(contentBox.css('line-height'), 10);
var wantedHeight = contentBox.height() - lineHeight;
for (var i = 0; i < words.length; i++) {
if (isNewPage) {
isNewPage = false;
} else {
betterPageText = betterPageText + ' ' + words[i];
}
newPage.text(betterPageText + ' ...');
if (newPage.height() >= wantedHeight) {
pageNum++;
if (pageNum > 0) {
betterPageText = betterPageText + ' ...';
}
newPage.text(betterPageText);
newPage.clone().insertBefore(newPage)
betterPageText = '...';
isNewPage = true;
} else {
newPage.text(betterPageText);
}
}
contentBox.craftyslide({ height: wantedHeight });
}
But when i add an image it break everything. In this case text overflows 'green' area.
Working fiddle: http://jsfiddle.net/74W4N/7/
Is there a better way to paginate the text and calculate element height?
Except the fact that there are many more variables to calculate,not just only the word width & height, but also new lines,margins paddings and how each browser outputs everything.
Then by adding an image (almost impossible if the image is higher or larger as the max width or height) if it's smaller it also has margins/paddings. and it could start at the end of a line and so break up everything again.basically only on the first page you could add an image simply by calculating it's width+margin and height+margin/lineheight. but that needs alot math to get the wanted result.
Said that i tried some time ago to write a similar script but stopped cause of to many problems and different browser results.
Now reading your question i came across something that i read some time ago:
-webkit-column-count
so i made a different approach of your function that leaves out all this calculations.
don't judge the code as i wrote it just now.(i tested on chrome, other browsers need different prefixes.)
var div=document.getElementsByTagName('div')[0].firstChild,
maxWidth=300,
maxHeigth=200,
div.style.width=maxWidth+'px';
currentHeight=div.offsetHeight;
columns=Math.ceil(currentHeight/maxHeigth);
div.style['-webkit-column-count']=columns;
div.style.width=(maxWidth*columns)+'px';
div.style['-webkit-transition']='all 700ms ease';
div.style['-webkit-column-gap']='0px';
//if you change the column-gap you need to
//add padding before calculating the normal div.
//also the line height should be an integer that
// is divisible of the max height
here is an Example
http://jsfiddle.net/HNF3d/10/
adding an image smaller than the max height & width in the first page would not mess up everything.
and it looks like it's supported by all modern browsers now.(with the correct prefixes)
In my experience, trying to calculate and reposition text in HTML is almost an exercise in futility. There are too many variations among browsers, operating systems, and font issues.
My suggestion would be to take advantage of the overflow CSS property. This, combined with using em sizing for heights, should allow you to define a div block that only shows a defined number of lines (regardless of the size and type of the font). Combine this with a bit of javascript to scroll the containing div element, and you have pagination.
I've hacked together a quick proof of concept in JSFiddle, which you can see here: http://jsfiddle.net/8CMzY/1/
It's missing a previous button and a way of showing the number of pages, but these should be very simple additions.
EDIT: I originally linked to the wrong version for the JSFiddle concept
Solved by using jQuery.clone() method and performing all calculations on hidden copy of original HTML element
function paginate() {
var section = $('.section');
var cloneSection = section.clone().insertAfter(section).css({ position: 'absolute', left: -9999, width: section.width(), zIndex: -999 });
cloneSection.css({ width: section.width() });
var descBox = cloneSection.find('.holder-description').css({ height: 'auto' });
var newPage = $('<pre class="text-page" />');
contentBox.empty();
descBox.empty();
var betterPageText = '';
var pageNum = 0;
var isNewPage = false;
var lineHeight = parseInt(contentBox.css('line-height'), 10);
var wantedHeight = contentBox.height() - lineHeight;
var oldText = '';
for (var i = 0; i < words.length; i++) {
if (isNewPage) {
isNewPage = false;
descBox.empty();
}
betterPageText = betterPageText + ' ' + words[i];
oldText = betterPageText;
descBox.text(betterPageText + ' ...');
if (descBox.height() >= wantedHeight) {
if (i != words.length - 1) {
pageNum++;
if (pageNum > 0) {
betterPageText = betterPageText + ' ...';
}
oldText += ' ... ';
}
newPage.text(oldText);
newPage.clone().appendTo(contentBox);
betterPageText = '... ';
isNewPage = true;
} else {
descBox.text(betterPageText);
if (i == words.length - 1) {
newPage.text(betterPageText).appendTo(contentBox);
}
}
}
if (pageNum > 0) {
contentBox.craftyslide({ height: wantedHeight });
}
cloneSection.remove();
}
live demo: http://jsfiddle.net/74W4N/19/
I actually came to an easier solution based on what #cocco has done, which also works in IE9.
For me it was important to keep the backward compatibility and the animation and so on was irrelevant so I stripped them down. You can see it here: http://jsfiddle.net/HNF3d/63/
heart of it is the fact that I dont limit height and present horizontal pagination as vertical.
var parentDiv = div = document.getElementsByTagName('div')[0];
var div = parentDiv.firstChild,
maxWidth = 300,
maxHeigth = 200,
t = function (e) {
div.style.webkitTransform = 'translate(0,-' + ((e.target.textContent * 1 - 1) * maxHeigth) + 'px)';
div.style["-ms-transform"] = 'translate(0,-' + ((e.target.textContent * 1 - 1) * maxHeigth) + 'px)';
};
div.style.width = maxWidth + 'px';
currentHeight = div.offsetHeight;
columns = Math.ceil(currentHeight / maxHeigth);
links = [];
while (columns--) {
links[columns] = '<span>' + (columns + 1) + '</span>';
}
var l = document.createElement('div');
l.innerHTML = links.join('');
l.onclick = t;
document.body.appendChild(l)

Make lines of text have equal length

In centered h1 elements, if the text falls on multiple lines, line breaks make the text look like this:
This is a header that takes up two
lines
This is a header that takes up three
lines because it is really, really
long
Is there a way to manipulate these elements so that the length of the lines of text is roughly equal? Like this:
This is a header that
takes up two lines
This is a header that takes
up three lines because it
is really, really long
The jQuery plugin Widow Fix prevents single-word lines, but I'm looking for something that evens out all the lines in a multi-line element. Are there any jQuery plugins for this, or can you recommend a strategy?
I would solve it using only strict JavaScript, going this way:
1. put a class named 'truncate' to h1 tags you want to break
2. configure the javascript code on your needs knowing that
MAXCOUNT: (integer) max chars counted per line
COUNT_SPACES: (boolean) white spaces must be counted?
COUNT_PUNCTUATION: (boolean) punctuation must be counted?
EXACT: (boolean) the last word must be cut?
BLOCKS_CLASS: (string) the className of the h1 to consider
I wrote the code very quickly so it must be better tested for bugs,
but it can be a starting point I think.
I'm not using jQuery in this code to keep the code light and to avoid dependencies.
I think I'm using all cross-browser commands (cannot test it I've got only linux now). However any correction for cross-browser compatibility task (included the use of jQUery if requested) might be easy.
Here is the code:
<html>
<head>
<style>
h1 {background-color: yellow;}
#hiddenDiv {background-color: yellow; display: table-cell; visibility:hidden;}
</style>
<script>
var MAXCOUNT = 20;
var COUNT_SPACES = false;
var EXACT = false;
var COUNT_PUNCTUATION = true;
var BLOCKS_CLASS = 'truncate';
window.onload = function ()
{
var hidden = document.getElementById('hiddenDiv');
if (hidden == null)
{
hidden = document.createElement('div');
hidden.id = 'hiddenDiv';
document.body.appendChild(hidden);
}
var blocks = document.getElementsByClassName(BLOCKS_CLASS);
for (var i=0; i<blocks.length; i++)
{
var block = blocks[i];
var text = block.innerHTML;
var truncate = '';
var html_tag = false;
var special_char = false;
maxcount = MAXCOUNT;
for (var x=0; x<maxcount; x++)
{
var previous_char = (x>0) ? text.charAt(x-1) : '';
var current_char = text.charAt(x);
// Closing HTML tags
if (current_char == '>' && html_tag)
{
html_tag = false;
maxcount++;
continue;
}
// Closing special chars
if (current_char == ';' && special_char)
{
special_char = false;
maxcount++;
continue;
}
// Jumping HTML tags
if (html_tag)
{
maxcount++;
continue;
}
// Jumping special chars
if (special_char)
{
maxcount++;
continue;
}
// Checking for HTML tags
if (current_char == '<')
{
var next = text.substring(x,text.indexOf('>')+1);
var regex = /(^<\w+[^>]*>$)/gi;
var matches = regex.exec(next);
if (matches[0])
{
html_tag = true;
maxcount++;
continue;
}
}
// Checking for special chars
if (current_char == '&')
{
var next = text.substring(x,text.indexOf(';')+1);
var regex = /(^&#{0,1}[0-9a-z]+;$)/gi;
var matches = regex.exec(next);
if (matches[0])
{
special_char = true;
maxcount++;
continue;
}
}
// Shrink multiple white spaces into a single white space
if (current_char == ' ' && previous_char == ' ')
{
maxcount++;
continue;
}
// Jump new lines
if (current_char.match(/\n/))
{
maxcount++;
continue;
}
if (current_char == ' ')
{
// End of the last word
if (x == maxcount-1 && !EXACT) { break; }
// Must I count white spaces?
if ( !COUNT_SPACES ) { maxcount++; }
}
// Must I count punctuation?
if (current_char.match(/\W/) && current_char != ' ' && !COUNT_PUNCTUATION)
{
maxcount++;
}
// Adding this char
truncate += current_char;
// Must I cut exactly?
if (!EXACT && x == maxcount-1) { maxcount++; }
}
hidden.innerHTML = '<h1><nobr>'+truncate+'</nobr></h1>';
block.style.width = hidden.offsetWidth+"px";
}
}
</script>
</head>
<body>
<center>
<h1 class="truncate">
This is a header that
takes up two lines
</h1>
<br>
<h1 class="truncate">
This is a header that takes
up three lines because it
is really, really long
</h1>
<br>
<h1>
This is a header pretty short
or pretty long ... still undecided
which in any case is not truncated!
</h1>
</center>
</body>
</html>
And here is a demo of that: http://jsfiddle.net/6rtdF/
Late to this party, but here's my approach. I get the initial element height (any elements with the class balance_lines, in the code below), then incrementally shrink the width of the element. Once the height of the element changes, I've gone too far. The step before that should have lovely roughly-equal line lengths.
$('.balance_lines').each(function(){
var currentHeight = $(this).height();
var thisHeight = currentHeight;
var currentWidth = $(this).width();
var newWidth = currentWidth;
// Try shrinking width until height changes
while (thisHeight == currentHeight) {
var testWidth = newWidth - 10;
$(this).width(testWidth);
thisHeight = $(this).height();
if (thisHeight == currentHeight) {
newWidth = testWidth;
} else {
break;
}
}
$(this).width(newWidth);
});
You can see this code in action on the homepage at apollopad.com.
The CSS Text 4 draft proposes text-wrap: balance, but I don't think any browser implements it yet.
In the meantime, you can use Adobe's jQuery plugin (demo): https://github.com/adobe-webplatform/balance-text

HTML text-overflow ellipsis detection

I have a collection of block elements on a page. They all have the CSS rules white-space, overflow, text-overflow set so that overflowing text is trimmed and an ellipsis is used.
However, not all the elements overflow.
Is there anyway I can use javascript to detect which elements are overflowing?
Thanks.
Added: example HTML structure I am working with.
<td><span>Normal text</span></td>
<td><span>Long text that will be trimmed text</span></td>
The SPAN elements always fit in the cells, they have the ellipsis rule applied. I want to detect when the ellipsis is applied to the text content of the SPAN.
Try this JS function, passing the span element as argument:
function isEllipsisActive(e) {
return (e.offsetWidth < e.scrollWidth);
}
Once upon a time I needed to do this, and the only cross-browser reliable solution I came across was hack job. I'm not the biggest fan of solutions like this, but it certainly produces the correct result time and time again.
The idea is that you clone the element, remove any bounding width, and test if the cloned element is wider than the original. If so, you know it's going to have been truncated.
For example, using jQuery:
var $element = $('#element-to-test');
var $c = $element
.clone()
.css({display: 'inline', width: 'auto', visibility: 'hidden'})
.appendTo('body');
if( $c.width() > $element.width() ) {
// text was truncated.
// do what you need to do
}
$c.remove();
I made a jsFiddle to demonstrate this, http://jsfiddle.net/cgzW8/2/
You could even create your own custom pseudo-selector for jQuery:
$.expr[':'].truncated = function(obj) {
var $this = $(obj);
var $c = $this
.clone()
.css({display: 'inline', width: 'auto', visibility: 'hidden'})
.appendTo('body');
var c_width = $c.width();
$c.remove();
if ( c_width > $this.width() )
return true;
else
return false;
};
Then use it to find elements
$truncated_elements = $('.my-selector:truncated');
Demo: http://jsfiddle.net/cgzW8/293/
Hopefully this helps, hacky as it is.
Adding to italo's answer, you can also do this using jQuery.
function isEllipsisActive($jQueryObject) {
return ($jQueryObject.width() < $jQueryObject[0].scrollWidth);
}
Also, as Smoky pointed out, you may want to use jQuery outerWidth() instead of width().
function isEllipsisActive($jQueryObject) {
return ($jQueryObject.outerWidth() < $jQueryObject[0].scrollWidth);
}
For those using (or planning to use) the accepted answer from Christian Varga, please be aware of the performance issues.
Cloning/manipulating the DOM in such a way causes DOM Reflow (see an explanation on DOM reflow here) which is extremely resource intensive.
Using Christian Varga's solution on 100+ elements on a page caused a 4 second reflow delay during which the JS thread is locked. Considering JS is single-threaded this means a significant UX delay to the end user.
Italo Borssatto's answer should be the accepted one, it was approximately 10 times quicker during my profiling.
Answer from italo is very good! However let me refine it a little:
function isEllipsisActive(e) {
var tolerance = 2; // In px. Depends on the font you are using
return e.offsetWidth + tolerance < e.scrollWidth;
}
Cross browser compatibility
If, in fact, you try the above code and use console.log to print out the values of e.offsetWidth and e.scrollWidth, you will notice, on IE, that, even when you have no text truncation, a value difference of 1px or 2px is experienced.
So, depending on the font size you use, allow a certain tolerance!
This sample show tooltip on cell table with text truncated. Is dynamic based on table width:
$.expr[':'].truncated = function (obj) {
var element = $(obj);
return (element[0].scrollHeight > (element.innerHeight() + 1)) || (element[0].scrollWidth > (element.innerWidth() + 1));
};
$(document).ready(function () {
$("td").mouseenter(function () {
var cella = $(this);
var isTruncated = cella.filter(":truncated").length > 0;
if (isTruncated)
cella.attr("title", cella.text());
else
cella.attr("title", null);
});
});
Demo: https://jsfiddle.net/t4qs3tqs/
It works on all version of jQuery
elem.offsetWdith VS ele.scrollWidth
This work for me!
https://jsfiddle.net/gustavojuan/210to9p1/
$(function() {
$('.endtext').each(function(index, elem) {
debugger;
if(elem.offsetWidth !== elem.scrollWidth){
$(this).css({color: '#FF0000'})
}
});
});
All the solutions did not really work for me, what did work was compare the elements scrollWidth to the scrollWidth of its parent (or child, depending on which element has the trigger).
When the child's scrollWidth is higher than its parents, it means .text-ellipsis is active.
When el is the parent element
function isEllipsisActive(el) {
let width = el.offsetWidth;
let widthChild = el.firstChild.offsetWidth;
return (widthChild >= width);
}
When el is the child element
function isEllipsisActive(event) {
let width = el.offsetWidth;
let widthParent = el.parentElement.scrollWidth;
return (width >= widthParent);
}
My implementation)
const items = Array.from(document.querySelectorAll('.item'));
items.forEach(item =>{
item.style.color = checkEllipsis(item) ? 'red': 'black'
})
function checkEllipsis(el){
const styles = getComputedStyle(el);
const widthEl = parseFloat(styles.width);
const ctx = document.createElement('canvas').getContext('2d');
ctx.font = `${styles.fontSize} ${styles.fontFamily}`;
const text = ctx.measureText(el.innerText);
return text.width > widthEl;
}
.item{
width: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
<div class="item">Short</div>
<div class="item">Loooooooooooong</div>
If you're doing react, here's how I did it.
<div
ref={ref => {
if (!ref) return
const isOverflowing = ref.scrollWidth > ref.clientWidth
if (isOverflowing) {
// handle what to do next here
}
}}
/>
I think the better way to detect it is use getClientRects(), it seems each rect has the same height, so we can caculate lines number with the number of different top value.
getClientRects work 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;
}
getRowRects work like this
you can detect like this
None of the solutions worked for me, so I chose a totally different approach. Instead of using the CSS solution with ellipsis, I just cut the text from a specific string length.
if (!this.isFullTextShown && this.text.length > 350) {
return this.text.substring(0, 350) + '...'
}
return this.text
and show "more/less" buttons if the length is exceeded.
<span
v-if="text.length > 350"
#click="isFullTextShown = !isFullTextShown"
>
{{ isFullTextShown ? 'show less' : 'show more' }}
</span>
Adding to #Дмытрык answer, missing deduction of borders and paddings to be fully functional!!
const items = Array.from(document.querySelectorAll('.item'));
items.forEach(item =>{
item.style.color = checkEllipsis(item) ? 'red': 'black'
})
function checkEllipsis(el){
const styles = getComputedStyle(el);
const widthEl = parseFloat(styles.width);
const ctx = document.createElement('canvas').getContext('2d');
ctx.font = `${styles.fontSize} ${styles.fontFamily}`;
const text = ctx.measureText(el.innerText);
let extra = 0;
extra += parseFloat(styles.getPropertyValue('border-left-width'));
extra += parseFloat(styles.getPropertyValue('border-right-width'));
extra += parseFloat(styles.getPropertyValue('padding-left'));
extra += parseFloat(styles.getPropertyValue('padding-right'));
return text.width > (widthEl - extra);
}
.item{
width: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
<div class="item">Short</div>
<div class="item">Loooooooooooong</div>
The e.offsetWidth < e.scrollWidth solution is not always working.
And if you want to use pure JavaScript, I recommend to use this:
(typescript)
public isEllipsisActive(element: HTMLElement): boolean {
element.style.overflow = 'initial';
const noEllipsisWidth = element.offsetWidth;
element.style.overflow = 'hidden';
const ellipsisWidth = element.offsetWidth;
if (ellipsisWidth < noEllipsisWidth) {
return true;
} else {
return false;
}
}
For someone who uses e.offsetWidth < e.scrollWidth and got a bug that can show full text but still got ellipsis.
It because offsetWidth and scrollWidth always round the value. For example: offsetWidth return 161 but the actual width is 161.25.
The solution is use getBoundingClientRect
const clonedEl = e.cloneNode(true)
clonedElement.style.overflow = "visible"
clonedElement.style.visibility = "hidden"
clonedElement.style.width = "fit-content"
e.parentElement.appendChild(clonedEl)
const fullWidth = clonedElement.getBoundingClientRect().width
const currentWidth = e.getBoundingClientRect().width
return currentWidth < fullWidth
Case you are using line-clamp >= 2 line for adding ellipsis at more than one line you can use this conditioning:
if (
descriptionElement &&
descriptionElement.offsetHeight < descriptionElement.scrollHeight
) {
// has text-overflow
}
There's a small pixel problem with the answers above when comparing offsetWidth > scrollWidth.
W3C has a legacy API that returns element.scrollWidth value as rounded which is causing the comparison in some cases to to return false.
If the element width are 150px and the scrollWidth are 150.4px (rounded to 150), then this check will be returning false even if the browser are showing ellipsis in the text.
They have tried to update the APIs that return fractional pixels, but it was reverted due to webcompat.
There's a workaround using max-content and getClientRects().
Here's a sample code that I use onMouseEnter.
Note that this only works if the container has a boundary to 100% of the available width (so if you are using flexbox, your container has to be flex: 1 for example.
hasEllipsis(elementItem) {
let scrollWidth = elementItem.scrollWidth;
elementItem.style.width = 'max-content';
const itemRects = elementItem.getClientRects();
if (itemRects.length > 0 && itemRects[0].width > scrollWidth) {
scrollWidth = itemRects[0].width;
}
elementItem.style.width = 'auto';
return scrollWidth > elementItem.clientWidth;
}
Articles:
https://bugs.chromium.org/p/chromium/issues/detail?id=980476
https://github.com/w3c/csswg-drafts/issues/4123
The solution #ItaloBorssatto is perfect. But before looking at SO - I made my decision. Here it is :)
const elems = document.querySelectorAll('span');
elems.forEach(elem => {
checkEllipsis(elem);
});
function checkEllipsis(elem){
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const styles = getComputedStyle(elem);
ctx.font = `${styles.fontWeight} ${styles.fontSize} ${styles.fontFamily}`;
const widthTxt = ctx.measureText(elem.innerText).width;
if (widthTxt > parseFloat(styles.width)){
elem.style.color = 'red'
}
}
span.cat {
display: block;
border: 1px solid black;
white-space: nowrap;
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
<span class="cat">Small Cat</span>
<span class="cat">Looooooooooooooong Cat</span>
there are some mistasks in demo http://jsfiddle.net/brandonzylstra/hjk9mvcy/ mentioned by https://stackoverflow.com/users/241142/iconoclast.
in his demo, add these code will works:
setTimeout(() => {
console.log(EntryElm[0].offsetWidth)
}, 0)

Categories