HTML text-overflow ellipsis detection - javascript

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)

Related

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)

Check if element is visible in DOM

Is there any way that I can check if an element is visible in pure JS (no jQuery) ?
So, given a DOM element, how can I check if it is visible or not? I tried:
window.getComputedStyle(my_element)['display']);
but it doesn't seem to be working. I wonder which attributes should I check. It comes to my mind:
display !== 'none'
visibility !== 'hidden'
Any others that I might be missing?
According to this MDN documentation, an element's offsetParent property will return null whenever it, or any of its parents, is hidden via the display style property. Just make sure that the element isn't fixed. A script to check this, if you have no position: fixed; elements on your page, might look like:
// Where el is the DOM element you'd like to test for visibility
function isHidden(el) {
return (el.offsetParent === null)
}
On the other hand, if you do have position fixed elements that might get caught in this search, you will sadly (and slowly) have to use window.getComputedStyle(). The function in that case might be:
// Where el is the DOM element you'd like to test for visibility
function isHidden(el) {
var style = window.getComputedStyle(el);
return (style.display === 'none')
}
Option #2 is probably a little more straightforward since it accounts for more edge cases, but I bet its a good deal slower, too, so if you have to repeat this operation many times, best to probably avoid it.
All the other solutions broke for some situation for me..
See the winning answer breaking at:
http://plnkr.co/edit/6CSCA2fe4Gqt4jCBP2wu?p=preview
Eventually, I decided that the best solution was $(elem).is(':visible') - however, this is not pure javascript. it is jquery..
so I peeked at their source and found what I wanted
jQuery.expr.filters.visible = function( elem ) {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
};
This is the source: https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
If you're interested in visible by the user:
function isVisible(elem) {
if (!(elem instanceof Element)) throw Error('DomUtil: elem is not an element.');
const style = getComputedStyle(elem);
if (style.display === 'none') return false;
if (style.visibility !== 'visible') return false;
if (style.opacity < 0.1) return false;
if (elem.offsetWidth + elem.offsetHeight + elem.getBoundingClientRect().height +
elem.getBoundingClientRect().width === 0) {
return false;
}
const elemCenter = {
x: elem.getBoundingClientRect().left + elem.offsetWidth / 2,
y: elem.getBoundingClientRect().top + elem.offsetHeight / 2
};
if (elemCenter.x < 0) return false;
if (elemCenter.x > (document.documentElement.clientWidth || window.innerWidth)) return false;
if (elemCenter.y < 0) return false;
if (elemCenter.y > (document.documentElement.clientHeight || window.innerHeight)) return false;
let pointContainer = document.elementFromPoint(elemCenter.x, elemCenter.y);
do {
if (pointContainer === elem) return true;
} while (pointContainer = pointContainer.parentNode);
return false;
}
Tested on (using mocha terminology):
describe.only('visibility', function () {
let div, visible, notVisible, inViewport, leftOfViewport, rightOfViewport, aboveViewport,
belowViewport, notDisplayed, zeroOpacity, zIndex1, zIndex2;
before(() => {
div = document.createElement('div');
document.querySelector('body').appendChild(div);
div.appendChild(visible = document.createElement('div'));
visible.style = 'border: 1px solid black; margin: 5px; display: inline-block;';
visible.textContent = 'visible';
div.appendChild(inViewport = visible.cloneNode(false));
inViewport.textContent = 'inViewport';
div.appendChild(notDisplayed = visible.cloneNode(false));
notDisplayed.style.display = 'none';
notDisplayed.textContent = 'notDisplayed';
div.appendChild(notVisible = visible.cloneNode(false));
notVisible.style.visibility = 'hidden';
notVisible.textContent = 'notVisible';
div.appendChild(leftOfViewport = visible.cloneNode(false));
leftOfViewport.style.position = 'absolute';
leftOfViewport.style.right = '100000px';
leftOfViewport.textContent = 'leftOfViewport';
div.appendChild(rightOfViewport = leftOfViewport.cloneNode(false));
rightOfViewport.style.right = '0';
rightOfViewport.style.left = '100000px';
rightOfViewport.textContent = 'rightOfViewport';
div.appendChild(aboveViewport = leftOfViewport.cloneNode(false));
aboveViewport.style.right = '0';
aboveViewport.style.bottom = '100000px';
aboveViewport.textContent = 'aboveViewport';
div.appendChild(belowViewport = leftOfViewport.cloneNode(false));
belowViewport.style.right = '0';
belowViewport.style.top = '100000px';
belowViewport.textContent = 'belowViewport';
div.appendChild(zeroOpacity = visible.cloneNode(false));
zeroOpacity.textContent = 'zeroOpacity';
zeroOpacity.style.opacity = '0';
div.appendChild(zIndex1 = visible.cloneNode(false));
zIndex1.textContent = 'zIndex1';
zIndex1.style.position = 'absolute';
zIndex1.style.left = zIndex1.style.top = zIndex1.style.width = zIndex1.style.height = '100px';
zIndex1.style.zIndex = '1';
div.appendChild(zIndex2 = zIndex1.cloneNode(false));
zIndex2.textContent = 'zIndex2';
zIndex2.style.left = zIndex2.style.top = '90px';
zIndex2.style.width = zIndex2.style.height = '120px';
zIndex2.style.backgroundColor = 'red';
zIndex2.style.zIndex = '2';
});
after(() => {
div.parentNode.removeChild(div);
});
it('isVisible = true', () => {
expect(isVisible(div)).to.be.true;
expect(isVisible(visible)).to.be.true;
expect(isVisible(inViewport)).to.be.true;
expect(isVisible(zIndex2)).to.be.true;
});
it('isVisible = false', () => {
expect(isVisible(notDisplayed)).to.be.false;
expect(isVisible(notVisible)).to.be.false;
expect(isVisible(document.createElement('div'))).to.be.false;
expect(isVisible(zIndex1)).to.be.false;
expect(isVisible(zeroOpacity)).to.be.false;
expect(isVisible(leftOfViewport)).to.be.false;
expect(isVisible(rightOfViewport)).to.be.false;
expect(isVisible(aboveViewport)).to.be.false;
expect(isVisible(belowViewport)).to.be.false;
});
});
Use the same code as jQuery does:
jQuery.expr.pseudos.visible = function( elem ) {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
};
So, in a function:
function isVisible(e) {
return !!( e.offsetWidth || e.offsetHeight || e.getClientRects().length );
}
Works like a charm in my Win/IE10, Linux/Firefox.45, Linux/Chrome.52...
Many thanks to jQuery without jQuery!
This may help :
Hide the element by positioning it on far most left position and then check the offsetLeft property. If you want to use jQuery you can simply check the :visible selector and get the visibility state of the element.
HTML :
<div id="myDiv">Hello</div>
CSS :
<!-- for javaScript-->
#myDiv{
position:absolute;
left : -2000px;
}
<!-- for jQuery -->
#myDiv{
visibility:hidden;
}
javaScript :
var myStyle = document.getElementById("myDiv").offsetLeft;
if(myStyle < 0){
alert("Div is hidden!!");
}
jQuery :
if( $("#MyElement").is(":visible") == true )
{
alert("Div is visible!!");
}
jsFiddle
The accepted answer did not work for me.
Year 2020 breakdown.
The (elem.offsetParent !== null) method works fine in Firefox but not in Chrome. In Chrome position: fixed will also make offsetParent return null even the element if visible in the page.
User Phrogz conducted a large test (2,304 divs) on elements with varying properties to demonstrate the issue. https://stackoverflow.com/a/11639664/4481831 . Run it with multiple browsers to see the differences.
Demo:
//different results in Chrome and Firefox
console.log(document.querySelector('#hidden1').offsetParent); //null Chrome & Firefox
console.log(document.querySelector('#fixed1').offsetParent); //null in Chrome, not null in Firefox
<div id="hidden1" style="display:none;"></div>
<div id="fixed1" style="position:fixed;"></div>
The (getComputedStyle(elem).display !== 'none') does not work because the element can be invisible because one of the parents display property is set to none, getComputedStyle will not catch that.
Demo:
var child1 = document.querySelector('#child1');
console.log(getComputedStyle(child1).display);
//child will show "block" instead of "none"
<div id="parent1" style="display:none;">
<div id="child1" style="display:block"></div>
</div>
The (elem.clientHeight !== 0). This method is not influenced by position: fixed and it also check if element parents are not-visible. But it has problems with simple elements that do not have a css layout and inline elements, see more here
Demo:
console.log(document.querySelector('#inline1').clientHeight); //zero
console.log(document.querySelector('#div1').clientHeight); //not zero
console.log(document.querySelector('#span1').clientHeight); //zero
<div id="inline1" style="display:inline">test1 inline</div>
<div id="div1">test2 div</div>
<span id="span1">test3 span</span>
The (elem.getClientRects().length !== 0) may seem to solve the problems of the previous 3 methods. However it has problems with elements that use CSS tricks (other then display: none) to hide in the page.
Demo
console.log(document.querySelector('#notvisible1').getClientRects().length);
console.log(document.querySelector('#notvisible1').clientHeight);
console.log(document.querySelector('#notvisible2').getClientRects().length);
console.log(document.querySelector('#notvisible2').clientHeight);
console.log(document.querySelector('#notvisible3').getClientRects().length);
console.log(document.querySelector('#notvisible3').clientHeight);
<div id="notvisible1" style="height:0; overflow:hidden; background-color:red;">not visible 1</div>
<div id="notvisible2" style="visibility:hidden; background-color:yellow;">not visible 2</div>
<div id="notvisible3" style="opacity:0; background-color:blue;">not visible 3</div>
Conclusion.
So what I have showed you is that no method is perfect. To make a proper visibility check, you must use a combination of the last 3 methods.
2021 solution
According to MDN docs an interaction observer asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport. This means every time the element intersects with the viewport the interaction observer will trigger.
As of 2021, all the current browser supports intersection observer except IE.
Implementation
const el = document.getElementById("your-target-element");
const observer = new IntersectionObserver((entries) => {
if(entries[0].isIntersecting){
// el is visible
} else {
// el is not visible
}
});
observer.observe(el); // Asynchronous call
The handler will fire when initially created. And then it will fire every time that it becomes slightly visible or becomes completely not visible. An element is deemed to be not-visible when it's not actually visible within the viewport. So if you scroll down and element goes off the screen, then the observer will trigger and the // el is not visible code will be triggered - even though the element is still "displayed" (i.e. doesn't have display:none or visibility:hidden). What matters is whether there are any pixels of the element that are actually visible within the viewport.
Combining a couple answers above:
function isVisible (ele) {
var style = window.getComputedStyle(ele);
return style.width !== "0" &&
style.height !== "0" &&
style.opacity !== "0" &&
style.display!=='none' &&
style.visibility!== 'hidden';
}
Like AlexZ said, this may be slower than some of your other options if you know more specifically what you're looking for, but this should catch all of the main ways elements are hidden.
But, it also depends what counts as visible for you. Just for example, a div's height can be set to 0px but the contents still visible depending on the overflow properties. Or a div's contents could be made the same color as the background so it is not visible to users but still rendered on the page. Or a div could be moved off screen or hidden behind other divs, or it's contents could be non-visible but the border still visible. To a certain extent "visible" is a subjective term.
Chrome 105 (and Edge and Opera) and Firefox 106 introduced Element.checkVisibility() which returns true if the element is visible, and false otherwise.
The function checks a variety of factors that would make an element invisible, including display:none, visibility, content-visibility, and opacity:
let element = document.getElementById("myIcon");
let isVisible = element.checkVisibility({
checkOpacity: true, // Check CSS opacity property too
checkVisibilityCSS: true // Check CSS visibility property too
});
Sidenote: checkVisibility() was previously called isVisible(). See this GitHub issue.
See checkVisibility() specification draft here.
I've got a more performant solution compared to AlexZ's getComputedStyle() solution when one has position 'fixed' elements, if one is willing to ignore some edge cases (check comments):
function isVisible(el) {
/* offsetParent would be null if display 'none' is set.
However Chrome, IE and MS Edge returns offsetParent as null for elements
with CSS position 'fixed'. So check whether the dimensions are zero.
This check would be inaccurate if position is 'fixed' AND dimensions were
intentionally set to zero. But..it is good enough for most cases.*/
return Boolean(el.offsetParent || el.offsetWidth || el.offsetHeight);
}
Side note: Strictly speaking, "visibility" needs to be defined first. In my case, I am considering an element visible as long as I can run all DOM methods/properties on it without problems (even if opacity is 0 or CSS visibility property is 'hidden' etc).
If element is regular visible (display:block and visibillity:visible), but some parent container is hidden, then we can use clientWidth and clientHeight for check that.
function isVisible (ele) {
return ele.clientWidth !== 0 &&
ele.clientHeight !== 0 &&
(ele.style.opacity !== '' ? parseFloat(ele.style.opacity) > 0 : true);
}
Plunker (click here)
So what I found is the most feasible method:
function visible(elm) {
if(!elm.offsetHeight && !elm.offsetWidth) { return false; }
if(getComputedStyle(elm).visibility === 'hidden') { return false; }
return true;
}
This is build on these facts:
A display: none element (even a nested one) doesn't have a width nor height.
visiblity is hidden even for nested elements.
So no need for testing offsetParent or looping up in the DOM tree to test which parent has visibility: hidden. This should work even in IE 9.
You could argue if opacity: 0 and collapsed elements (has a width but no height - or visa versa) is not really visible either. But then again they are not per say hidden.
A little addition to ohad navon's answer.
If the center of the element belongs to the another element we won't find it.
So to make sure that one of the points of the element is found to be visible
function isElementVisible(elem) {
if (!(elem instanceof Element)) throw Error('DomUtil: elem is not an element.');
const style = getComputedStyle(elem);
if (style.display === 'none') return false;
if (style.visibility !== 'visible') return false;
if (style.opacity === 0) return false;
if (elem.offsetWidth + elem.offsetHeight + elem.getBoundingClientRect().height +
elem.getBoundingClientRect().width === 0) {
return false;
}
var elementPoints = {
'center': {
x: elem.getBoundingClientRect().left + elem.offsetWidth / 2,
y: elem.getBoundingClientRect().top + elem.offsetHeight / 2
},
'top-left': {
x: elem.getBoundingClientRect().left,
y: elem.getBoundingClientRect().top
},
'top-right': {
x: elem.getBoundingClientRect().right,
y: elem.getBoundingClientRect().top
},
'bottom-left': {
x: elem.getBoundingClientRect().left,
y: elem.getBoundingClientRect().bottom
},
'bottom-right': {
x: elem.getBoundingClientRect().right,
y: elem.getBoundingClientRect().bottom
}
}
for(index in elementPoints) {
var point = elementPoints[index];
if (point.x < 0) return false;
if (point.x > (document.documentElement.clientWidth || window.innerWidth)) return false;
if (point.y < 0) return false;
if (point.y > (document.documentElement.clientHeight || window.innerHeight)) return false;
let pointContainer = document.elementFromPoint(point.x, point.y);
if (pointContainer !== null) {
do {
if (pointContainer === elem) return true;
} while (pointContainer = pointContainer.parentNode);
}
}
return false;
}
If we're just collecting basic ways of detecting visibility, let me not forget:
opacity > 0.01; // probably more like .1 to actually be visible, but YMMV
And as to how to obtain attributes:
element.getAttribute(attributename);
So, in your example:
document.getElementById('snDealsPanel').getAttribute('visibility');
But wha? It doesn't work here. Look closer and you'll find that visibility is being updated not as an attribute on the element, but using the style property. This is one of many problems with trying to do what you're doing. Among others: you can't guarantee that there's actually something to see in an element, just because its visibility, display, and opacity all have the correct values. It still might lack content, or it might lack a height and width. Another object might obscure it. For more detail, a quick Google search reveals this, and even includes a library to try solving the problem. (YMMV)
Check out the following, which are possible duplicates of this question, with excellent answers, including some insight from the mighty John Resig. However, your specific use-case is slightly different from the standard one, so I'll refrain from flagging:
How to tell if a DOM element is visible in the current viewport?
How to check if an element is really visible with javascript?
(EDIT: OP SAYS HE'S SCRAPING PAGES, NOT CREATING THEM, SO BELOW ISN'T APPLICABLE)
A better option? Bind the visibility of elements to model properties and always make visibility contingent on that model, much as Angular does with ng-show. You can do that using any tool you want: Angular, plain JS, whatever. Better still, you can change the DOM implementation over time, but you'll always be able to read state from the model, instead of the DOM. Reading your truth from the DOM is Bad. And slow. Much better to check the model, and trust in your implementation to ensure that the DOM state reflects the model. (And use automated testing to confirm that assumption.)
Just for the reference it should be noted that getBoundingClientRect() can work in certain cases.
For example, a simple check that the element is hidden using display: none could look somewhat like this:
var box = element.getBoundingClientRect();
var visible = box.width && box.height;
This is also handy because it also covers zero-width, zero-height and position: fixed cases. However, it shall not report elements hidden with opacity: 0 or visibility: hidden (but neither would offsetParent).
Improving on #Guy Messika's answer above, breaking and returning false if the center point' X is < 0 is wrong as the element right side may go into the view. here's a fix:
private isVisible(elem) {
const style = getComputedStyle(elem);
if (style.display === 'none') return false;
if (style.visibility !== 'visible') return false;
if ((style.opacity as any) === 0) return false;
if (
elem.offsetWidth +
elem.offsetHeight +
elem.getBoundingClientRect().height +
elem.getBoundingClientRect().width === 0
) return false;
const elementPoints = {
center: {
x: elem.getBoundingClientRect().left + elem.offsetWidth / 2,
y: elem.getBoundingClientRect().top + elem.offsetHeight / 2,
},
topLeft: {
x: elem.getBoundingClientRect().left,
y: elem.getBoundingClientRect().top,
},
topRight: {
x: elem.getBoundingClientRect().right,
y: elem.getBoundingClientRect().top,
},
bottomLeft: {
x: elem.getBoundingClientRect().left,
y: elem.getBoundingClientRect().bottom,
},
bottomRight: {
x: elem.getBoundingClientRect().right,
y: elem.getBoundingClientRect().bottom,
},
};
const docWidth = document.documentElement.clientWidth || window.innerWidth;
const docHeight = document.documentElement.clientHeight || window.innerHeight;
if (elementPoints.topLeft.x > docWidth) return false;
if (elementPoints.topLeft.y > docHeight) return false;
if (elementPoints.bottomRight.x < 0) return false;
if (elementPoints.bottomRight.y < 0) return false;
for (let index in elementPoints) {
const point = elementPoints[index];
let pointContainer = document.elementFromPoint(point.x, point.y);
if (pointContainer !== null) {
do {
if (pointContainer === elem) return true;
} while (pointContainer = pointContainer.parentNode);
}
}
return false;
}
To elaborate on everyone's great answers, here is the implementation that was used in the Mozilla Fathom project:
/**
* Yield an element and each of its ancestors.
*/
export function *ancestors(element) {
yield element;
let parent;
while ((parent = element.parentNode) !== null && parent.nodeType === parent.ELEMENT_NODE) {
yield parent;
element = parent;
}
}
/**
* Return whether an element is practically visible, considering things like 0
* size or opacity, ``visibility: hidden`` and ``overflow: hidden``.
*
* Merely being scrolled off the page in either horizontally or vertically
* doesn't count as invisible; the result of this function is meant to be
* independent of viewport size.
*
* #throws {Error} The element (or perhaps one of its ancestors) is not in a
* window, so we can't find the `getComputedStyle()` routine to call. That
* routine is the source of most of the information we use, so you should
* pick a different strategy for non-window contexts.
*/
export function isVisible(fnodeOrElement) {
// This could be 5x more efficient if https://github.com/w3c/csswg-drafts/issues/4122 happens.
const element = toDomElement(fnodeOrElement);
const elementWindow = windowForElement(element);
const elementRect = element.getBoundingClientRect();
const elementStyle = elementWindow.getComputedStyle(element);
// Alternative to reading ``display: none`` due to Bug 1381071.
if (elementRect.width === 0 && elementRect.height === 0 && elementStyle.overflow !== 'hidden') {
return false;
}
if (elementStyle.visibility === 'hidden') {
return false;
}
// Check if the element is irrevocably off-screen:
if (elementRect.x + elementRect.width < 0 ||
elementRect.y + elementRect.height < 0
) {
return false;
}
for (const ancestor of ancestors(element)) {
const isElement = ancestor === element;
const style = isElement ? elementStyle : elementWindow.getComputedStyle(ancestor);
if (style.opacity === '0') {
return false;
}
if (style.display === 'contents') {
// ``display: contents`` elements have no box themselves, but children are
// still rendered.
continue;
}
const rect = isElement ? elementRect : ancestor.getBoundingClientRect();
if ((rect.width === 0 || rect.height === 0) && elementStyle.overflow === 'hidden') {
// Zero-sized ancestors don’t make descendants hidden unless the descendant
// has ``overflow: hidden``.
return false;
}
}
return true;
}
It checks on every parent's opacity, display, and rectangle.
The jQuery code from http://code.jquery.com/jquery-1.11.1.js has an isHidden param
var isHidden = function( elem, el ) {
// isHidden might be called from jQuery#filter function;
// in that case, element will be second argument
elem = el || elem;
return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
};
So it looks like there is an extra check related to the owner document
I wonder if this really catches the following cases:
Elements hidden behind other elements based on zIndex
Elements with transparency full making them invisible
Elements positioned off screen (ie left: -1000px)
Elements with visibility:hidden
Elements with display:none
Elements with no visible text or sub elements
Elements with height or width set to 0
let element = document.getElementById('element');
let rect = element.getBoundingClientRect();
if(rect.top == 0 &&
rect.bottom == 0 &&
rect.left == 0 &&
rect.right == 0 &&
rect.width == 0 &&
rect.height == 0 &&
rect.x == 0 &&
rect.y == 0)
{
alert('hidden');
}
else
{
alert('visible');
}
const isVisible = (selector) => {
let selectedElement
let topElement
let selectedData
selectedElement = document.querySelector(selector)
if (!selectedElement) {
return false
}
selectedData = selectedElement.getBoundingClientRect()
if (!selectedData || !Object.keys(selectedData)) {
return false
}
if (!(selectedData.width > 0) || !(selectedData.height > 0)) {
return false
}
topElement = document.elementFromPoint(selectedData.top, selectedData.left)
if (selectedElement !== topElement) {
return false
}
return true
}
const output = document.querySelector('.text')
output.innerHTML = '.x element is visible: ' + isVisible('.x')
.block {
width: 100px;
height: 100px;
background: black;
}
.y {
background: red;
margin-top: -100px;
}
<div class="text"></div>
<div class="x block"></div>
<div class="y block"></div>
Here's the code I wrote to find the only visible among a few similar elements, and return the value of its "class" attribute without jQuery:
// Build a NodeList:
var nl = document.querySelectorAll('.myCssSelector');
// convert it to array:
var myArray = [];for(var i = nl.length; i--; myArray.unshift(nl[i]));
// now find the visible (= with offsetWidth more than 0) item:
for (i =0; i < myArray.length; i++){
var curEl = myArray[i];
if (curEl.offsetWidth !== 0){
return curEl.getAttribute("class");
}
}
If you are scraping sites, one very inefficient method which worked for me was highlighting whatever element, and screenshotting, and then checking if the screenshot has changed.
//Screenshot
function makeSelected(element){
let range = new Range()
range.selectNode(element)
let selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
}
// screenshot again and check for diff
Here is a (pure vanilla JS) function that performs an extensive number of checks, ensuring that a given element is visible to the user:
function isVisible(element) {
// Check if the element is null or undefined
if (!element) return false;
// Get the element's bounding client rect
const boundingRect = element.getBoundingClientRect();
// Check if the element has a positive width and height
if (boundingRect.width <= 0 || boundingRect.height <= 0) return false;
// Check if the element's top and left values are within the viewport
const top = boundingRect.top;
const left = boundingRect.left;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
if (top > viewportHeight || left > viewportWidth) return false;
// Check if the element's right and bottom values are within the viewport
const right = boundingRect.right;
const bottom = boundingRect.bottom;
if (right < 0 || bottom < 0) return false;
// Check if the element is hidden by the overflow property
const parentNode = element.parentNode;
if (parentNode && getComputedStyle(parentNode).overflow === 'hidden') {
const parentRect = parentNode.getBoundingClientRect();
if (top < parentRect.top || bottom > parentRect.bottom || left < parentRect.left || right > parentRect.right) {
return false;
}
}
const elementComputedStyle = getComputedStyle(element);
// Check if the element has a z-index of less than 0
const zIndex = elementComputedStyle.zIndex;
if (zIndex < 0) return false;
// Check if the element has a display value of 'none' or an opacity of 0
const display = elementComputedStyle.display;
const opacity = elementComputedStyle.opacity;
if (display === 'none' || opacity === '0') return false;
// Check if the element is hidden by an ancestor element with a display value of 'none' or an opacity of 0
let ancestorElement = element.parentElement;
while (ancestorElement) {
const ancestorComputedStyle = getComputedStyle(ancestorElement);
const ancestorDisplay = ancestorComputedStyle.display;
const ancestorOpacity = ancestorComputedStyle.opacity;
if (ancestorDisplay === 'none' || ancestorOpacity === '0') return false;
ancestorElement = ancestorElement.parentElement;
}
// Initialize a variable to keep track of whether the element is obscured by another element
let obscured = false;
// Check if the element is obscured by another element according to its position
if (elementComputedStyle.position === 'absolute' || elementComputedStyle.position === 'fixed' ||
elementComputedStyle.position === 'relative' || elementComputedStyle.position === 'sticky' ||
elementComputedStyle.position === 'static') {
let siblingElement = element.nextElementSibling;
while (siblingElement) {
if (siblingElement.getBoundingClientRect().top > boundingRect.bottom || siblingElement.getBoundingClientRect().left > boundingRect.right) {
break;
}
if (siblingElement.getBoundingClientRect().bottom > boundingRect.top && siblingElement.getBoundingClientRect().right > boundingRect.left) {
obscured = true;
break;
}
siblingElement = siblingElement.nextElementSibling;
}
if (obscured) return false;
}
// If all checks have passed, the element is visible
return true;
}
This is a way to determine it for all css properties including visibility:
html:
<div id="element">div content</div>
css:
#element
{
visibility:hidden;
}
javascript:
var element = document.getElementById('element');
if(element.style.visibility == 'hidden'){
alert('hidden');
}
else
{
alert('visible');
}
It works for any css property and is very versatile and reliable.
This is what I did:
HTML & CSS: Made the element hidden by default
<html>
<body>
<button onclick="myFunction()">Click Me</button>
<p id="demo" style ="visibility: hidden;">Hello World</p>
</body>
</html>
JavaScript: Added a code to check whether the visibility is hidden or not:
<script>
function myFunction() {
if ( document.getElementById("demo").style.visibility === "hidden"){
document.getElementById("demo").style.visibility = "visible";
}
else document.getElementById("demo").style.visibility = "hidden";
}
</script>
There are many situations where this will not necessarily work, however in my case I'm using this and it works fine for what I need. So if you are looking for a basic solution (Which does not cover every eventuality) it 'might' be helpful to you, if this simple solution suits your particular need.
var element= document.getElementById('elementId');
if (element.style.display == "block"){
<!-- element is visible -->
} else {
<!-- element is hidden-->
}
var visible = document.getElementById("yourelementID's");
if (visible){
// make events
} else
{
//other events
}

Word wrap: ellipsis without css

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.

Why does adding a class affect the reported height within the same loop using JS/jQuery

I am building a timelined comments feature for part of my app. Inspired by facebook's new timelined profiles.
As an example here is a screenshot of the current display.
As you can see the comment/reply boxes fill up all vertical space.
In order to do this I have used jQuery to do the following to the <li> elements in the ordered list of comments with nested replies.
collect the items into an object
iterate over each one and get its individual height all the while building a left and right variable with the total height of the left
hand side boxes and the total of the right hand side boxes
I then check which out of the left and right have the higher total height and apply either a timeline-left or timeline-right class to
the next comment box.
This all seems very simple however I ran into a huge problem which I am at a loss to explain.
If I apply the class within the initial loop (iteration over the elements) The height variable gets effected within the loop. It appears to be adding them together rather than replacing the current one. This of course causes the boxes to be switched to the incorrect sides and therefore fails to close the gaps. Although not relevant to the functionality I added the heights as an attribute to each element in order to better see what was going on. And without a shadow of a doubt. As soon I add the class 'timeline-left' within the same loop as the height detection, the height variable combines each loop rather than reflecting the individual elements value.
To work around this I have come up with what I consider to be a very ugly method solve the issue.
I iterate through the objects and build an array of values (height,side) for each box.
I then iterate AGAIN through this new array and apply the classes to each box.
Now this actually works fine and performance is also fine. While I usually adhere to the whole pre-optimization is bad mentality. In this case I just cannot let it lie as surely there should be no reason to iterate through the elements more than once?
Here is the current (ugly) code I am using which collects the data about each element and then re-loops a second time to apply the classes:
var side = "left",
$timeline = $("#timeline"),
$elements = $timeline.children("li").not(".timeline-spine"),
newwidth = parseInt(($("#rightcolumnwrapper").width() / 2) - 26),
length = $elements.length,
array = [],
$i = 0,
leftheight = 0,
rightheight = 0,
side = "left";
while ($i < length) {
var $this = $elements.eq($i),
height = $this.outerHeight(true);
if (leftheight > rightheight) {
side = "right";
} else {
side = "left";
}
array.push({
"side": side,
"height": height
});
if (side == "right") {
var rightheight = rightheight + height + 25;
} else if (side == "left") {
var leftheight = leftheight + height + 25;
}
$i++;
}
$.each(array, function (index, value) {
var $timelineItem = $("#timeline").children("li").not(".timeline-spine").eq(index);
$timelineItem.attr("data-height", array[index]["height"]).addClass("timeline-" + array[index]["side"]).children(".window-wrapper").width(newwidth).siblings("#pointer").addClass("pointer-" + array[index]["side"]);
//The code below is not relevant to the question but is left in for completeness
if ($timelineItem.is("#timeline-bottom")) {
$timelineItem.find("#pointer").css({
"left": (newwidth + 21) + "px"
});
} else if ($timelineItem.is("#loves-capsule")) {
if ($timelineItem.find("#love-main").height() > $this.find("#loves-wrapper").height() - 33) {
$timelineItem.height("176px").find("#loves-wrapper").css({
"height": "174px",
"margin-bottom": "-35px"
}).end().find("#love-footer").show();
}
}
});
Is there anything I can do to get this into one loop without (and I really cannot explain why) effecting the heights reported for each element?
Note: There is some other stuff going on at the end is not really relevant to the question however I have left it in for code completeness.
EDIT: Just to be clear there is no css styling other than clear:left; float:left and clear:right; float:right; in either of the css rules that I am adding. Just in case anyone thought I may be styling them to modify the height.
This is a possible "one loop" implimentation:
jQuery(document).ready(function($){
var $timeline, $elements, $wrapper, columns;
$timeline = $("#timeline");
$elements = $timeline.children("li").not(".timeline-spine");
$wrapper = $("#rightcolumnwrapper");
columns = {
left : {
height : 0,
stack : []
},
right : {
height : 0,
stack : []
}
};
$elements.each(function(index){
var $obj = $(this), col, side, obj_height;
obj_height = $obj.outerHeight(true);
if ( index === 0 || columns.left.height < columns.right.height ) {
// create a local reference to the column
col = columns.left;
side = "left";
}
else {
col = columns.right;
side = "right";
}
// Do stuff with this node
$obj.addClass('timeline-'+side); //...
// Add object height to container height
col.height = col.height + obj_height;
// push raw dom node to stack
col.stack.push(this);
});
});
However manipulating an entire "stack" of nodes may be faster than doing the same operations on every iteration of .each()
$.each(columns,function(index){
// index is left or right
var $stack = $( this.stack );
$stack.addClass('timeline-'+index)//.doMoreThings()...
});
UPDATE: After further review, I think your problem with height is this part:
.attr("data-height", height)
Try calling "data-height" something else, or use .data() instead of .attr();
I believe this (single loop) is equivalent to what you posted. Updated source, comments in source below.
var side = "left",
$timeline = $("#timeline"),
$elements = $timeline.children("li").not(".timeline-spine"),
newwidth = parseInt(($("#rightcolumnwrapper").width() / 2), 10) - 26,
length = $elements.length,
array = [],
$i = 0,
leftheight = 0,
rightheight = 0,
side = "left",
$this = {},
height = 0;
while ($i < length) {
$this = $elements.eq($i);
height = $this.outerHeight(true);
side = (leftheight > rightheight) ? "right" : "left";
// unnecessary with only one loop
/*
array.push({
"side": side,
"height": height
});
*/
// var $timelineItem = $("#timeline").children("li").not(".timeline-spine").eq(index); === var $this = $elements.eq($i)
$this.attr("data-heightY", height).addClass("timeline-" + side).children(".window-wrapper").width(newwidth).siblings("#pointer").addClass("pointer-" + side);
if ($this.is("#timeline-bottom")) {
$this.find("#pointer").css({
"left": (newwidth + 21) + "px"
});
} else if ($this.is("#loves-capsule")) {
if ($this.find("#love-main").height() > ($this.find("#loves-wrapper").height() - 33)) {
$this.height("176px").find("#loves-wrapper").css({
"height": "174px",
"margin-bottom": "-35px"
}).end().find("#love-footer").show();
}
}
if (side == "right") {
rightheight += height + 25;
} else if (side == "left") {
leftheight += height + 25;
}
$i++;
}
// now unnecessary
/*
$.each(array, function (index, value) {
var $timelineItem = $("#timeline").children("li").not(".timeline-spine").eq(index);
$timelineItem.attr("data-height", array[index]["height"]).addClass("timeline-" + array[index]["side"]).children(".window-wrapper").width(newwidth).siblings("#pointer").addClass("pointer-" + array[index]["side"]);
if ($timelineItem.is("#timeline-bottom")) {
$timelineItem.find("#pointer").css({
"left": (newwidth + 21) + "px"
});
} else if ($timelineItem.is("#loves-capsule")) {
if ($timelineItem.find("#love-main").height() > $this.find("#loves-wrapper").height() - 33) {
$timelineItem.height("176px").find("#loves-wrapper").css({
"height": "174px",
"margin-bottom": "-35px"
}).end().find("#love-footer").show();
}
}
});
*/

Textarea auto resizer resize at every keypress on chrome

I'm implementing an auto textarea resizer that auto-expands a textarea when user presses ENTER and after a minimum scrollHeight size.
It works well in IE, Firefox and Opera, but not in Chrome.
In Chrome the textarea is resized at any keypress (not only ENTER) because when settings e.style.height it also changes scrollHeight (this doesn't happen to occur on other browsers.
I marked the problematic line with a comment:
function resize(e)
{
e = e.target || e;
var $e = $(e);
console.log( 'scrollHeight:'+e.scrollHeight );
console.log( 'style.height:'+e.style.height );
var h = Math.min( e.scrollHeight, $e.data('textexp-max') );
h = Math.max( h, $e.data('textexp-min') );
if( $e.data('h')!=h )
{
e.style.height = "";
e.style.height = h + "px"; //this changes the scrollHeight even if style.height is smaller thatn scrollHeight
//e.style.overflow = (e.scrollHeight > h ? "auto" : "hidden");
$e.data('h',h);
}
return true;
}
Here you'll find full code: http://jsfiddle.net/NzTYd/9/
UPDATE
I've found that when I update the e.style.height in Firefox/IE/Opera, e.scrollHeight is updated to the exact same value:
Example:
Setting 80px to e.style.height, sets to 80px e.scrollHeight
However, Chrome updates e.scrollHeight with a higher value (4px more).
Example::
Settings 80px to e.style.height, sets to 84px e.scrollHeight
This causes a textarea that is growing 4px for EVERY keypress it receives!
You can get the answer to this here.
Textarea to resize based on content length
Ok, I've found there is a bug with webkit: http://code.google.com/p/chromium/issues/detail?id=34224
Finally I solve it detecting if e.scrollHeight changed an amount of only 4 px, which is the difference chrome is adding.
If that condition is arised, I substract 4px from e.style.height to fix it.
I built an auto-expanding textarea in jQuery a few months back and haved used it with great success on many sites. This doesn't address your issue directly, but you might find it helpful.
If the user provides a ## a paragraph is inserted and extra line breaks are removed.
I have my textarea set to fit 40 characters per line. The line increments 1px at a time to accommodate the new characters.
You can see a working example on my site at evikjames.com > jQuery examples > Expandable Textarea.
$(".Expandable").live("keyup", function() {
// ADJUST ROWS ROWS
var ThisText = $(this).val();
var Rows = calculateRows(ThisText);
$(this).attr("rows", Rows);
// CALCULTE COUNTER
var ThisTextMax = $(this).attr("max");
if (ThisText.length > ThisTextMax) {
ThisText = ThisText.substring(ThisText, ThisTextMax);
$(this).val(ThisText);
alert("You have exceeded the maximum allowable text. Revise!");
}
$(this).next(".Count").html(ThisText.length + "/" + ThisTextMax + " characters.");
});
var calculateRows = function calculateRows(String) {
var NumCharacters = String.length;
var NumRows = Math.ceil(NumCharacters/40);
var Match = String.match(/##/g);
var NumParagraphs = 0;
if (Match != null) {
NumParagraphs = String.match(/##/g).length;
NumRows = NumRows + NumParagraphs;
}
return NumRows;
}

Categories