How jQuery do "fade" in IE8 and below? - javascript

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)

Related

Can I modify this waypoint so that it doesnt take effect until scrolling 500px down the screen

A WordPress theme I am using has this code which is what I assume initiates the themes fixed header class once the user starts to scroll down.
Is there a way to modify this so that the class is not initiated on scroll rather initiated when the user gets to a certain point down the screen such as 100px or something liek that. Here is the code.
if ( et_is_fixed_nav ) {
$('#main-content').waypoint( {
offset: function() {
if ( etRecalculateOffset ) {
et_calculate_header_values();
etRecalculateOffset = false;
}
return et_header_offset;
},
handler : function( direction ) {
if ( direction === 'down' ) {
$('#main-header').addClass( 'et-fixed-header' );
} else {
$('#main-header').removeClass( 'et-fixed-header' );
}
}
} );
It looks like your theme is using Waypoints. The link I attached shows how to adjust the offset. The function that is currently in place looks like it handles the offset somewhere else though, possibly in your Wordpress options panel. I haven't tested it but I'd imagine your code would end up looking like this:
$('#main-content').waypoint( {
offset: 100px,
handler : function( direction ) {
if ( direction === 'down' ) {
$('#main-header').addClass( 'et-fixed-header' );
} else {
$('#main-header').removeClass( 'et-fixed-header' );
}
}
} );
Thanks for the help Austin. You were right about it being handled elsewhere. With the help f a friend, I found this bit elsewhere in the file...
function et_calculate_header_values() {
var $top_header = $( '#top-header' ),
secondary_nav_height = $top_header.length && $top_header.is( ':visible' ) ? $top_header.innerHeight() : 0,
admin_bar_height = $( '#wpadminbar' ).length ? $( '#wpadminbar' ).innerHeight() : 0;
et_header_height = $( '#main-header' ).innerHeight() + secondary_nav_height - 1,
et_header_modifier = et_header_height <= 90 ? et_header_height - 29 : et_header_height - 56,
et_header_offset = et_header_modifier + admin_bar_height;
et_primary_header_top = secondary_nav_height + admin_bar_height;
}
function et_fix_slider_height() {
if ( ! $et_pb_slider.length ) return;
$et_pb_slider.each( function() {
var $slide = $(this).find( '.et_pb_slide' ),
$slide_container = $slide.find( '.et_pb_container' ),
max_height = 0;
$slide_container.css( 'min-height', 0 );
$slide.each( function() {
var $this_el = $(this),
height = $this_el.innerHeight();
if ( max_height < height )
max_height = height;
} );
And then I changed this line...
et_header_offset = et_header_modifier + admin_bar_height;
to this...
et_header_offset = et_header_modifier + admin_bar_height - 1000;
Mission accomplished. Thanks for your help austinthedeveloper and Dan Mossop. By the way, this is what is being used in the custom.js in the Divi theme by Elegant Themes just in case in helps someone else.

Draggable code not working with hammer.js

I've successfully implemented jQueryUI draggable, but as soon as I add hammer.js code, the draggable code no longer works.
It is not as soon as I include hammer.js, but as soon as I use the script.
Why is this? How can I get them both to work?
Both the draggable and hammer are applied to .dataCard and #main
The draggable code works fine here ( with hammer implementation commented out ): http://goo.gl/MO5Pde
Here is an example of the draggable code:
$('#main').draggable({
axis:'y',
revert:true,
start: function(event, ui){
topValue = ui.position.top;
},
drag: function(event, ui){
if(pastBreakpoint === false){
$('#searchInput').blur();
if(topValue > ui.position.top) return false;
if(ui.position.top >= 161){
if(pastBreakpoint === false){
pastBreakpoint = true;
if($('.loadingRefresh').length === 0) $('#main').before('<div class="loadingRefresh"></div>');
else{
$('.loadingRefresh').remove();
$('#main').before('<div class="loadingRefresh"></div>');
}
$('.loadingRefresh').fadeIn();
$('#main').mouseup();
setTimeout(function(){
location.reload();
}, 1000);
}
}
}
}
});
Here is the hammer code uncommented and the draggable code not working: http://goo.gl/994pxF
Here is the hammer code:
var hammertime = Hammer(document.getElementById('main'), {
transform_always_block: true,
transform_min_scale: 0
});
var posX = 0,
posY = 0,
lastPosX = 0,
lastPosY = 0,
bufferX = 0,
bufferY = 0,
scale = 1,
last_scale = 1;
hammertime.on('touch transform transformend', function(ev) {
if ((" " + ev.target.className + " ").indexOf(" dataCard ") < 0) return;
else manageMultitouch(ev, ev.target); });
function manageMultitouch(ev, element) {
switch (ev.type) {
case 'touch':
last_scale = scale;
return;
case 'transform':
scale = Math.min(last_scale * ev.gesture.scale, 10);
break;
}
if(scale <= 0.5) $(element).hide('clip');
if(scale > 1.0) $(element).addClass('focused');
var transform = "translate(" + 0 + "px," + 0 + "px) " + "scale(" + 1 + "," + scale + ")";
var style = element.style;
style.transform = transform;
style.oTransform = transform;
style.msTransform = transform;
style.mozTransform = transform;
style.webkitTransform = transform;
}
I had the same problem in my app, even with touch punch included. I had to do a good research to find what was the problem stopping the jquery ui drag.
The problem occurring is a preventDefault set at the event ( only when hammer is included ) changing the result of a trigger method from jquery ui.
Well, lets get back a little bit:
The first method you should see is the _mouseMove(), which is connected with the mousemove event.
The drag will be trigged only when the condition (this._mouseStart(this._mouseDownEvent, event) !== false) be true.
_mouseMove: function (event) {
// IE mouseup check - mouseup happened when mouse was out of window
if ($.ui.ie && (!document.documentMode || document.documentMode < 9) && !event.button) {
return this._mouseUp(event);
}
if (this._mouseStarted) {
this._mouseDrag(event);
return event.preventDefault();
}
if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
this._mouseStarted =
(this._mouseStart(this._mouseDownEvent, event) !== false);
(this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event));
}
return !this._mouseStarted;
}
The next method will create the helper ( element's clone ), set some css in the element and return true ( value we expect ), unless this._trigger("start", event) returns false.
_mouseStart: function(event) {
var o = this.options;
//Create and append the visible helper
this.helper = this._createHelper(event);
this.helper.addClass("ui-draggable-dragging");
//Cache the helper size
this._cacheHelperProportions();
//If ddmanager is used for droppables, set the global draggable
if($.ui.ddmanager) {
$.ui.ddmanager.current = this;
}
/*
* - Position generation -
* This block generates everything position related - it's the core of draggables.
*/
//Cache the margins of the original element
this._cacheMargins();
//Store the helper's css position
this.cssPosition = this.helper.css( "position" );
this.scrollParent = this.helper.scrollParent();
this.offsetParent = this.helper.offsetParent();
this.offsetParentCssPosition = this.offsetParent.css( "position" );
//The element's absolute position on the page minus margins
this.offset = this.positionAbs = this.element.offset();
this.offset = {
top: this.offset.top - this.margins.top,
left: this.offset.left - this.margins.left
};
//Reset scroll cache
this.offset.scroll = false;
$.extend(this.offset, {
click: { //Where the click happened, relative to the element
left: event.pageX - this.offset.left,
top: event.pageY - this.offset.top
},
parent: this._getParentOffset(),
relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
});
//Generate the original position
this.originalPosition = this.position = this._generatePosition(event);
this.originalPageX = event.pageX;
this.originalPageY = event.pageY;
//Adjust the mouse offset relative to the helper if "cursorAt" is supplied
(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
//Set a containment if given in the options
this._setContainment();
//Trigger event + callbacks
if(this._trigger("start", event) === false) {
this._clear();
return false;
}
//Recache the helper size
this._cacheHelperProportions();
//Prepare the droppable offsets
if ($.ui.ddmanager && !o.dropBehaviour) {
$.ui.ddmanager.prepareOffsets(this, event);
}
this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position
//If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003)
if ( $.ui.ddmanager ) {
$.ui.ddmanager.dragStart(this, event);
}
return true;
}
Below is the first _trigger called, its from drag widget.
_trigger: function (type, event, ui) {
ui = ui || this._uiHash();
$.ui.plugin.call(this, type, [event, ui]);
//The absolute position has to be recalculated after plugins
if(type === "drag") {
this.positionAbs = this._convertPositionTo("absolute");
}
return $.Widget.prototype._trigger.call(this, type, event, ui);
}
At this point the result will call another trigger method (this time from the $.Widget) and that's the point where we have the problem.
_trigger: function (type, event, data) {
var prop, orig,
callback = this.options[type];
data = data || {};
event = $.Event(event);
event.type = (type === this.widgetEventPrefix ?
type :
this.widgetEventPrefix + type).toLowerCase();
// the original event may come from any element
// so we need to reset the target on the new event
event.target = this.element[0];
// copy original event properties over to the new event
orig = event.originalEvent;
if (orig) {
for (prop in orig) {
if (!(prop in event)) {
event[prop] = orig[prop];
}
}
}
return !($.isFunction(callback) && callback.apply(this.element[0], [event].concat(data)) === false || event.isDefaultPrevented());
}
return !($.isFunction(callback) && callback.apply(this.element[0], [event].concat(data)) === false || event.isDefaultPrevented());
Our problem is exactly at this line. More specific the || before event.isDefaultPrevented().
When hammer is included the method event.isDefaultPrevented() is resulting true, once the value is denied before return, the final value would be false.
(Without the hammer included the event.isDefaultPrevented() returns false as expected.)
Backing in our _moseMouve(), instead of calling the _mouseDrag() method it'll invoke _mouseUp().
U can see it will unbind the events and call _mouseStop().
_mouseUp: function (event) {
$(document)
.unbind("mousemove."+this.widgetName, this._mouseMoveDelegate)
.unbind("mouseup."+this.widgetName, this._mouseUpDelegate);
if (this._mouseStarted) {
this._mouseStarted = false;
if (event.target === this._mouseDownEvent.target) {
$.data(event.target, this.widgetName + ".preventClickEvent", true);
}
this._mouseStop(event);
}
return false;
}
If you change the OR (||) operator by an AND (&&) it'll work fine. Off course it's not a little change, I had been testing it and until this moment I haven't find any problem at all. The line would be like this:
return !($.isFunction(callback) && callback.apply(this.element[0], [event].concat(data)) === false && event.isDefaultPrevented());
As I said, its not 100% secure, but until now I didn't find a reason to keep || instead of &&. I'll keep testing it for a few days.
Besides I've already sent an email to the lead developer from jquery ui asking about it.
Similar problem for me using it with isomorphic smartclient.
I fixed it by handling the start event and resetting isDefaultPrevented to false
$(element).draggable({
start: function (event, ui) {
event.isDefaultPrevented = function () { return false; }
}
});
I had the same problem and found this issue at github.
the shown solution using event delegation worked fine for me:
$(document).hammer().on('touch transform transformend', '#main', function() {...});

CSS3 Keyframe injected with javascript does not work with Chrome

I am creating a keyframe with javascript because I want to know a specific element's width in order to apply an animation style using that.
Here is the code:
var animation = false,
animationstring = 'animation',
prefix = '',
domPrefixes = 'Webkit Moz O ms'.split(' '),
pfx = '',
elm = document.querySelector('.marquee');
// Checks if the animation implementation is unprefixed
if( elm.style.animationName ) { animation = true; }
// Apply correct prefixes if the animation implementation has prefixes
if( animation === false ) {
for( var i = 0; i < domPrefixes.length; i++ ) {
if( elm.style[ domPrefixes[i] + 'AnimationName' ] !== undefined ) {
pfx = domPrefixes[ i ];
animationstring = pfx + 'Animation';
prefix = '-' + pfx.toLowerCase() + '-';
animation = true;
break;
}
}
}
elm.style[ animationstring ] = 'marquee 20s linear infinite';
var keyframes = '#' + prefix + 'keyframes marquee { '+
'0% { ' + prefix + 'transform: translateX(100%);}'+
'100% { ' + prefix + 'transform: translateX(-' + elm.scrollWidth + 'px);}'+
'}';
document.styleSheets[0].insertRule( keyframes, 0 );
http://jsfiddle.net/69PXa/
My problem (I think) is on row 27, I apply elm.scrollWidth as the value for translateX. This apparently breaks the keyframes in Chrome while it works as it should in Firefox. If I instead just use a fixed number the animation works.
Is there a way to solve this?
You must apply the CSS to your .marquee after you define the actual keyframes. Right now you are telling the browser to animate using keyframes that don't exist yet.
JSFiddle
Maybe this: scrollWidth/scrollHeight give invalid dimensions
Or this: Is "ScrollWidth" property of a span not working on Chrome?
can help you ;-)
Regards.

Javascript fade in fade out without Jquery and CSS3

I am really squeezing my head to make the simple fade in and fade out of the background image work only with javascript without JQuery and CSS3. I know how easy is to call a fadeIn() and fadeOut() in Jquery. Unfortunately in my project I am working, they don't support Jquery. I want to support the animation from IE6 for your info.
On click of the links the corresponding background of the div to be faded in and out from the previously existing background. I am trying to make it work based on setinterval but could not do it.
function handleClick(evt){
var element = document.getElementsByClassName(evt.target.id);
fade(element);
}
function fade(element) {
var op = 1; // initial opacity
var timer = setInterval(function () {
if (op <= 0.1){
clearInterval(timer);
element.style.display = 'none';
}
element.style.opacity = op;
element.style.filter = 'alpha(opacity=' + op * 100 + ")";
op -= op * 0.1;
}, 50);
}
​
http://jsfiddle.net/meetravi/2Pd6e/4/
Here are my full implementations of fadeIn and fadeOut for cross-browser support (including IE6) which does not require jQuery or any other 3rd-party JS library:
function fadeIn( elem, ms )
{
if( ! elem )
return;
elem.style.opacity = 0;
elem.style.filter = "alpha(opacity=0)";
elem.style.display = "inline-block";
elem.style.visibility = "visible";
if( ms )
{
var opacity = 0;
var timer = setInterval( function() {
opacity += 50 / ms;
if( opacity >= 1 )
{
clearInterval(timer);
opacity = 1;
}
elem.style.opacity = opacity;
elem.style.filter = "alpha(opacity=" + opacity * 100 + ")";
}, 50 );
}
else
{
elem.style.opacity = 1;
elem.style.filter = "alpha(opacity=1)";
}
}
function fadeOut( elem, ms )
{
if( ! elem )
return;
if( ms )
{
var opacity = 1;
var timer = setInterval( function() {
opacity -= 50 / ms;
if( opacity <= 0 )
{
clearInterval(timer);
opacity = 0;
elem.style.display = "none";
elem.style.visibility = "hidden";
}
elem.style.opacity = opacity;
elem.style.filter = "alpha(opacity=" + opacity * 100 + ")";
}, 50 );
}
else
{
elem.style.opacity = 0;
elem.style.filter = "alpha(opacity=0)";
elem.style.display = "none";
elem.style.visibility = "hidden";
}
}
As others have said, you need to fix your handleClick to properly select a single element, then pass that element to the fade function (which I named fadeOut for clarity). The default time for a jQuery fade is 400ms, so if you want to mimic that, your call might look like this:
function handleClick( evt )
{
fadeOut( document.getElementById(evt.target.id), 400 );
}
getElementById givies you one element (or null), getElementsByClassName gives an array.
function handleClick(evt){
var element = document.getElementById(evt.target.id);
fade(element);
}
You seem to aim for usage of ID's, so this should answer your needs. I updated the whole thing: IDs
However, you should realize that this method of fading is much more costly than using GPU accelerated transitions.
Update
JSfiddle webkit opacity fade
If you do not care about IE7 - IE9, you can use very useful CSS3 transitions, something like this:
.element {
-webkit-transition: opacity 0.3s ease;
}
.element[faded=true] {
opacity: 0;
}
You will get very fast, native fade out effect without jQuery.
UPDATE:
Sorry, i hadn't read quiestion title thoroughly.
element.style is undefined because you're not referencing the correct object. Use element[0] for your function call:
function handleClick(evt){
var element = document.getElementsByClassName(evt.target.id);
fade(element[0]);
}
Fiddle
Side note: Using console.log() and some type of developer console (like the one included in Chrome) can work wonders for debugging.
You should really do this via CSS3 since all modern browsers support it, and for older browsers fallback to just using show/hide. Do this by adding a "fadeOut" class or removing it via JavaScript. CSS3 (transitions) handle everything else, including hiding and showing it for older browsers.
Remember: As much as possible, do things in CSS before doing them in JavaScript. Not only is it cleaner and easier to maintain but CSS3 animations render smoother as it often hardnesses the GPU (video card) and not just the CPU. This is especially important on mobile devices but is the standard, modern way for doing it in any device.
See this Opera article for greater detail:
http://dev.opera.com/articles/view/css3-show-and-hide/
I'll point you off in the right direction and leave the rest of the coding to you.
This is how the setInterval() function works. It takes a function to execute and then the milliseconds it should run for.
setInterval(function() {
if(fade(element[0]))
clearInterval();
}, 50);
I made a JS fiddle for you here It's semicomplete but shows off how you should go about making your fadeout/fadein.
This is tested in Chrome on a Mac. Not sure about FF nor IE unfortunately.
Also as several pointed out, when getting stuff by any function that ends with s you can be 100% sure that it gives you an array with elements and thus you have to refer to the element you want as such. In your case its element[0].
Hope I help you further a little ways! :) Good luck!
For a Single Function to toggle Fade IN or Out depending the case, here's my function
function toggleFade(elem, speed ) {
//Add Opacity Property if it doesnt exist
if (!elem.style.opacity) elem.style.opacity = 1;
if (elem.style.opacity <= 0) {
var inInterval = setInterval(function() {
elem.style.opacity = Number(elem.style.opacity)+0.02;
if (elem.style.opacity >= 1)
clearInterval(inInterval);
}, speed/50 );
}else{ // end if
var outInterval = setInterval(function() {
elem.style.opacity -= 0.02;
if (elem.style.opacity <= 0)
clearInterval(outInterval);
}, speed/50 );
}
}
I modified the function of #Raptor007
if (!Element.prototype.fadeIn) {
Element.prototype.fadeIn = function(){
let ms = !isNaN(arguments[0]) ? arguments[0] : 400,
func = typeof arguments[0] === 'function' ? arguments[0] : (
typeof arguments[1] === 'function' ? arguments[1] : null
);
this.style.opacity = 0;
this.style.filter = "alpha(opacity=0)";
this.style.display = "inline-block";
this.style.visibility = "visible";
let $this = this,
opacity = 0,
timer = setInterval(function() {
opacity += 50 / ms;
if( opacity >= 1 ) {
clearInterval(timer);
opacity = 1;
if (func) func('done!');
}
$this.style.opacity = opacity;
$this.style.filter = "alpha(opacity=" + opacity * 100 + ")";
}, 50 );
}
}
if (!Element.prototype.fadeOut) {
Element.prototype.fadeOut = function(){
let ms = !isNaN(arguments[0]) ? arguments[0] : 400,
func = typeof arguments[0] === 'function' ? arguments[0] : (
typeof arguments[1] === 'function' ? arguments[1] : null
);
let $this = this,
opacity = 1,
timer = setInterval( function() {
opacity -= 50 / ms;
if( opacity <= 0 ) {
clearInterval(timer);
opacity = 0;
$this.style.display = "none";
$this.style.visibility = "hidden";
if (func) func('done!');
}
$this.style.opacity = opacity;
$this.style.filter = "alpha(opacity=" + opacity * 100 + ")";
}, 50 );
}
}
How to use:
// fadeIn with default: 400ms
document.getElementById(evt.target.id).fadeIn();
// Calls the "alert" function with the message "done!" after 400ms - alert('done!');
document.getElementById(evt.target.id).fadeIn(alert);
// Calls the "alert" function with the message "done!" after 1500ms - alert('done!');
document.getElementById(evt.target.id).fadeIn(1500, alert);
JSfiddle fadeIn / fadeOut example

Auto-expanding textarea

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.

Categories