I'm trying to do a simple auto-expanding textarea. This is my code:
textarea.onkeyup = function () {
textarea.style.height = textarea.clientHeight + 'px';
}
But the textarea just keeps growing indefinitely as you type...
I know there is Dojo and a jQuery plugin for this, but would rather not have to use them. I looked at their implementation, and was initially using scrollHeight but that did the same thing.
You can start answering and play with the textarea for your answer to play with.
Reset the height before Using scrollHeight to expand/shrink the textarea correctly. Math.min() can be used to set a limit on the textarea's height.
Code:
var textarea = document.getElementById("textarea");
var heightLimit = 200; /* Maximum height: 200px */
textarea.oninput = function() {
textarea.style.height = ""; /* Reset the height*/
textarea.style.height = Math.min(textarea.scrollHeight, heightLimit) + "px";
};
Fiddle: http://jsfiddle.net/gjqWy/155
Note: The input event is not supported by IE8 and earlier. Use keydown or keyup with onpaste and/or oncut if you want to support this ancient browser as well.
I've wanted to have the auto-expanding area to be limited by rows number (e.g 5 rows). I've considered using "em" units, for Rob's solution however, this is error-prone and wouldn't take account stuff like padding, etc.
So this is what I came up with:
var textarea = document.getElementById("textarea");
var limitRows = 5;
var messageLastScrollHeight = textarea.scrollHeight;
textarea.oninput = function() {
var rows = parseInt(textarea.getAttribute("rows"));
// If we don't decrease the amount of rows, the scrollHeight would show the scrollHeight for all the rows
// even if there is no text.
textarea.setAttribute("rows", "1");
if (rows < limitRows && textarea.scrollHeight > messageLastScrollHeight) {
rows++;
} else if (rows > 1 && textarea.scrollHeight < messageLastScrollHeight) {
rows--;
}
messageLastScrollHeight = textarea.scrollHeight;
textarea.setAttribute("rows", rows);
};
Fiddle: http://jsfiddle.net/cgSj3/
For those interested in a jQuery version of Rob W's solution:
var textarea = jQuery('.textarea');
textarea.on("input", function () {
jQuery(this).css("height", ""); //reset the height
jQuery(this).css("height", Math.min(jQuery(this).prop('scrollHeight'), 200) + "px");
});
...and if you need an infinitely expanding textarea (as I did), just do this:
var textarea = document.getElementById("textarea");
textarea.oninput = function() {
textarea.style.height = ""; /* Reset the height*/
textarea.style.height = textarea.scrollHeight + "px";
};
2022 Vanilla JS Solution
Note: I have only tested this in Chrome but it should work everywhere.
This will handle pasting, deleting, text-wrapping, manual returns, etc. & accounts for padding and box-sizing issues.
How it Works
Forces resize and all height properties to auto/none/0 etc to
prevent interference with the event code.
Resets the rows attribute
to 1 to get accurate scrollHeight.
Sets overflow to hidden and hard locks the current computed width (minus left/right border width and left/right padding widths), then forces box-sizing to content-box to get an accurate line-height and scrollHeight reading. border-width and padding-inline is also removed to keep the textarea width consistent when switching box-sizing. This all helps keep the math accurate when dealing with text wrapping.
Grabs the computed line-height and top/bottom-padding pixel values.
Obtains the scrollHeight pixel value (rounded since chrome rounds
and we're hoping to handle all browsers consistently).
Removes overflow, box-sizing, width, padding-inline and border-width overrides.
Subtracts block_padding from scroll_height then divides that by the line_height to get the needed rows. The rows value is rounded to the nearest integer since it will always be within ~.1 of the correct whole number.
The calculated rows value is applied as the
rows attribute unless the row_limit is smaller, then the row_limit is used instead.
Edit / Update Details
I removed the loop code that was used figure out the row count because I was able to verify the math on the division formula works out within about .1 of the row count needed. Therefore a simple Math.round() ensures the row count is accurate. I was not able to break this in testing so if it turns out to be wrong please feel free to suggest a tweak.
I also ran into issues when line-height is not explicitly set on the text area as in that case the computed value for line-height comes back as "normal" and not the actual computed value. This new version accounts for this eventuality and handles it properly as well.
Layout Shift Possibility
I did not set the textarea to position: absolute; while swapping it's box-sizing out as I did not notice a need for it in my testing. It's worth mentioning because I suppose there could be a scenario where these minor changes might cause a layout shift depending on how the page is styled and if that happens you could add add that and then remove it along with the box-sizing override and removal.
Sample Code
(you only need the one JS function, everything else is just for the demo)
function autosize(textarea_id, row_limit) {
// Set default for row_limit parameter
row_limit = parseInt(row_limit ?? '5');
if (!row_limit) {
row_limit = 5;
}
// Get the element
const textarea = document.getElementById(textarea_id);
// Set required styles for this to function properly.
textarea.style.setProperty('resize', 'none');
textarea.style.setProperty('min-height', '0');
textarea.style.setProperty('max-height', 'none');
textarea.style.setProperty('height', 'auto');
// Set rows attribute to number of lines in content
textarea.oninput = function() {
// Reset rows attribute to get accurate scrollHeight
textarea.setAttribute('rows', '1');
// Get the computed values object reference
const cs = getComputedStyle(textarea);
// Force content-box for size accurate line-height calculation
// Remove scrollbars, lock width (subtract inline padding and inline border widths)
// and remove inline padding and borders to keep width consistent (for text wrapping accuracy)
const inline_padding = parseFloat(cs['padding-left']) + parseFloat(cs['padding-right']);
const inline_border_width = parseFloat(cs['border-left-width']) + parseFloat(cs['border-right-width']);
textarea.style.setProperty('overflow', 'hidden', 'important');
textarea.style.setProperty('width', (parseFloat(cs['width']) - inline_padding - inline_border_width) + 'px');
textarea.style.setProperty('box-sizing', 'content-box');
textarea.style.setProperty('padding-inline', '0');
textarea.style.setProperty('border-width', '0');
// Get the base line height, and top / bottom padding.
const block_padding = parseFloat(cs['padding-top']) + parseFloat(cs['padding-bottom']);
const line_height =
// If line-height is not explicitly set, use the computed height value (ignore padding due to content-box)
cs['line-height'] === 'normal' ? parseFloat(cs['height'])
// Otherwise (line-height is explicitly set), use the computed line-height value.
: parseFloat(cs['line-height']);
// Get the scroll height (rounding to be safe to ensure cross browser consistency)
const scroll_height = Math.round(textarea.scrollHeight);
// Undo overflow, width, border-width, box-sizing & inline padding overrides
textarea.style.removeProperty('width');
textarea.style.removeProperty('box-sizing');
textarea.style.removeProperty('padding-inline');
textarea.style.removeProperty('border-width');
textarea.style.removeProperty('overflow');
// Subtract block_padding from scroll_height and divide that by our line_height to get the row count.
// Round to nearest integer as it will always be within ~.1 of the correct whole number.
const rows = Math.round((scroll_height - block_padding) / line_height);
// Set the calculated rows attribute (limited by row_limit)
textarea.setAttribute("rows", "" + Math.min(rows, row_limit));
};
// Trigger the event to set the initial rows value
textarea.dispatchEvent(new Event('input', {
bubbles: true
}));
}
autosize('textarea');
* {
box-sizing: border-box;
}
textarea {
width: 100%;
max-width: 30rem;
font-family: sans-serif;
font-size: 1rem;
line-height: 1.5rem;
padding: .375rem;
}
<body>
<textarea id="textarea" placeholder="enter some text here :)"></textarea>
</body>
using
<div contentEditable></div>
may also do the same work, expanding it self, and requires no js
Unlike the accepted answer, my function cares about padding-{top,bottom} and border-{top,bottom}-width. And it has many parameters. Note that it doesn't set window.addEventListener('resize')
Function:
// #author Arzet Ro, 2021 <arzeth0#gmail.com>
// #license CC0 (Creative Commons Zero v1.0 Universal) (i.e. Public Domain)
// #source https://stackoverflow.com/a/70341077/332012
// Useful for elements with overflow-y: scroll and <textarea>
// Tested only on <textarea> in desktop Firefox 95 and desktop Chromium 96.
export function autoResizeScrollableElement (
el: HTMLElement,
{
canShrink = true,
minHeightPx = 0,
maxHeightPx,
minLines,
maxLines,
}: {
canShrink?: boolean,
minHeightPx?: number,
maxHeightPx?: number,
minLines?: number,
maxLines?: number,
} = {}
): void
{
const FN_NAME = 'autoResizeScrollableElement'
if (
typeof minLines !== 'undefined'
&& minLines !== null
&& Number.isNaN(+minLines)
)
{
console.warn(
'%O(el=%O):: minLines (%O) as a number is NaN',
FN_NAME, el, minLines
)
}
if (
typeof maxLines !== 'undefined'
&& maxLines !== null
&& Number.isNaN(+maxLines)
)
{
console.warn(
'%O(el=%O):: maxLines (%O) as a number is NaN',
FN_NAME, el, maxLines
)
}
canShrink = (
canShrink === true
||
// #ts-ignore
canShrink === 1 || canShrink === void 0 || canShrink === null
)
const style = window.getComputedStyle(el)
const unpreparedLineHeight = style.getPropertyValue('line-height')
if (unpreparedLineHeight === 'normal')
{
console.error('%O(el=%O):: line-height is unset', FN_NAME, el)
}
const lineHeightPx: number = (
unpreparedLineHeight === 'normal'
? 1.15 * parseFloat(style.getPropertyValue('font-size')) // 1.15 is a wrong number
: parseFloat(unpreparedLineHeight)
)
// #ts-ignore
minHeightPx = parseFloat(minHeightPx || 0) || 0
//minHeight = Math.max(lineHeightPx, parseFloat(style.getPropertyValue('min-height')))
// #ts-ignore
maxHeightPx = parseFloat(maxHeightPx || 0) || Infinity
minLines = (
minLines
? (
Math.round(+minLines || 0) > 1
? Math.round(+minLines || 0)
: 1
)
: 1
)
maxLines = (
maxLines
? (Math.round(+maxLines || 0) || Infinity)
: Infinity
)
//console.log('%O:: old ov.x=%O ov.y=%O, ov=%O', FN_NAME, style.getPropertyValue('overflow-x'), style.getPropertyValue('overflow-y'), style.getPropertyValue('overflow'))
/*if (overflowY !== 'scroll' && overflowY === 'hidden')
{
console.warn('%O:: setting overflow-y to scroll', FN_NAME)
}*/
if (minLines > maxLines)
{
console.warn(
'%O(el=%O):: minLines (%O) > maxLines (%O), '
+ 'therefore both parameters are ignored',
FN_NAME, el, minLines, maxLines
)
minLines = 1
maxLines = Infinity
}
if (minHeightPx > maxHeightPx)
{
console.warn(
'%O(el=%O):: minHeightPx (%O) > maxHeightPx (%O), '
+ 'therefore both parameters are ignored',
FN_NAME, el, minHeightPx, maxHeightPx
)
minHeightPx = 0
maxHeightPx = Infinity
}
const topBottomBorderWidths: number = (
parseFloat(style.getPropertyValue('border-top-width'))
+ parseFloat(style.getPropertyValue('border-bottom-width'))
)
let verticalPaddings: number = 0
if (style.getPropertyValue('box-sizing') === 'border-box')
{
verticalPaddings += (
parseFloat(style.getPropertyValue('padding-top'))
+ parseFloat(style.getPropertyValue('padding-bottom'))
+ topBottomBorderWidths
)
}
else
{
console.warn(
'%O(el=%O):: has `box-sizing: content-box`'
+ ' which is untested; you should set it to border-box. Continuing anyway.',
FN_NAME, el
)
}
const oldHeightPx = parseFloat(style.height)
if (el.tagName === 'TEXTAREA')
{
el.setAttribute('rows', '1')
//el.style.overflowY = 'hidden'
}
// #ts-ignore
const oldScrollbarWidth: string|void = el.style.scrollbarWidth
el.style.height = ''
// Even when there is nothing to scroll,
// it causes an extra height at the bottom in the content area (tried Firefox 95).
// scrollbar-width is present only on Firefox 64+,
// other browsers use ::-webkit-scrollbar
// #ts-ignore
el.style.scrollbarWidth = 'none'
const maxHeightForMinLines = lineHeightPx * minLines + verticalPaddings // can be float
// .scrollHeight is always an integer unfortunately
const scrollHeight = el.scrollHeight + topBottomBorderWidths
/*console.log(
'%O:: lineHeightPx=%O * minLines=%O + verticalPaddings=%O, el.scrollHeight=%O, scrollHeight=%O',
FN_NAME, lineHeightPx, minLines, verticalPaddings,
el.scrollHeight, scrollHeight
)*/
const newHeightPx = Math.max(
canShrink === true ? minHeightPx : oldHeightPx,
Math.min(
maxHeightPx,
Math.max(
maxHeightForMinLines,
Math.min(
Math.max(scrollHeight, maxHeightForMinLines)
- Math.min(scrollHeight, maxHeightForMinLines) < 1
? maxHeightForMinLines
: scrollHeight,
(
maxLines > 0 && maxLines !== Infinity
? lineHeightPx * maxLines + verticalPaddings
: Infinity
)
)
)
)
)
// #ts-ignore
el.style.scrollbarWidth = oldScrollbarWidth
if (!Number.isFinite(newHeightPx) || newHeightPx < 0)
{
console.error(
'%O(el=%O):: BUG:: Invalid return value: `%O`',
FN_NAME, el, newHeightPx
)
return
}
el.style.height = newHeightPx + 'px'
//console.log('%O:: height: %O → %O', FN_NAME, oldHeightPx, newHeightPx)
/*if (el.tagName === 'TEXTAREA' && el.scrollHeight > newHeightPx)
{
el.style.overflowY = 'scroll'
}*/
}
Usage with React (TypeScript):
<textarea
onKeyDown={(e) => {
if (!(e.key === 'Enter' && !e.shiftKey)) return true
e.preventDefault()
// send the message, then this.scrollToTheBottom()
return false
}}
onChange={(e) => {
if (this.state.isSending)
{
e.preventDefault()
return false
}
this.setState({
pendingMessage: e.currentTarget.value
}, () => {
const el = this.chatSendMsgRef.current!
engine.autoResizeScrollableElement(el, {maxLines: 5})
})
return true
}}
/>
For React onChange is like oninput in HTML5, so if you don't use React, then use the input event.
One of the answers uses rows attribute (instead of CSS's height as my code above does), here's an alternative implementation that doesn't use outside variables (BUT just like that answer there is a bug: because rows is temporaily set to 1, something bad happens with <html>'s scrollTop when you input AND <html> can be scrolled):
// #author Arzet Ro, 2021 <arzeth0#gmail.com>
// #license CC0 (Creative Commons Zero v1.0 Universal) (i.e. Public Domain)
// #source https://stackoverflow.com/a/70341077/332012
function autoResizeTextareaByChangingRows (
el,
{minLines, maxLines}
)
{
const FN_NAME = 'autoResizeTextareaByChangingRows'
if (
typeof minLines !== 'undefined'
&& minLines !== null
&& Number.isNaN(+minLines)
)
{
console.warn('%O:: minLines (%O) as a number is NaN', FN_NAME, minLines)
}
if (
typeof maxLines !== 'undefined'
&& maxLines !== null
&& Number.isNaN(+maxLines)
)
{
console.warn('%O:: maxLines (%O) as a number is NaN', FN_NAME, maxLines)
}
minLines = (
minLines
? (
Math.round(+minLines || 0) > 1
? Math.round(+minLines || 0)
: 1
)
: 1
)
maxLines = (
maxLines
? (Math.round(+maxLines || 0) || Infinity)
: Infinity
)
el.setAttribute(
'rows',
'1',
)
const style = window.getComputedStyle(el)
const unpreparedLineHeight = style.getPropertyValue('line-height')
if (unpreparedLineHeight === 'normal')
{
console.error('%O:: line-height is unset for %O', FN_NAME, el)
}
const rows = Math.max(minLines, Math.min(maxLines,
Math.round(
(
el.scrollHeight
- parseFloat(style.getPropertyValue('padding-top'))
- parseFloat(style.getPropertyValue('padding-bottom'))
) / (
unpreparedLineHeight === 'normal'
? 1.15 * parseFloat(style.getPropertyValue('font-size')) // 1.15 is a wrong number
: parseFloat(unpreparedLineHeight)
)
)
))
el.setAttribute(
'rows',
rows.toString()
)
}
const textarea = document.querySelector('textarea')
textarea.oninput = function ()
{
autoResizeTextareaByChangingRows(textarea, {maxLines: 5})
}
For those using Angular and having the same issue, use
<textarea cdkTextareaAutosize formControlName="description" name="description" matInput placeholder="Description"></textarea>
The key here is cdkTextareaAutosize which will automatically resize the textarea to fit its content. Read more here.
I hope this helps someone.
Related
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)
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
}
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;
}
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)
I just wanted to know how jQuery can generate a fade effect in IE browsers when they don't support opacity? Animating opacity is the way they do the fade in other browsers like Firefox and Chrome.
I went into the code but honestly I couldn't find anything understandable to me!
From the jquery source, they basically detect if opacity is supported and if not, use IEs alpha filter
if ( !jQuery.support.opacity ) {
jQuery.cssHooks.opacity = {
get: function( elem, computed ) {
// IE uses filters for opacity
return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
( parseFloat( RegExp.$1 ) / 100 ) + "" :
computed ? "1" : "";
},
set: function( elem, value ) {
var style = elem.style,
currentStyle = elem.currentStyle;
// IE has trouble with opacity if it does not have layout
// Force it by setting the zoom level
style.zoom = 1;
// Set the alpha filter to set the opacity
var opacity = jQuery.isNaN( value ) ?
"" :
"alpha(opacity=" + value * 100 + ")",
filter = currentStyle && currentStyle.filter || style.filter || "";
style.filter = ralpha.test( filter ) ?
filter.replace( ralpha, opacity ) :
filter + " " + opacity;
}
};
}
using the following style filter:alpha(opacity=40)