Get CSS value mid-transition with native JavaScript - javascript

This question was asked before, but the answer uses jQuery, here.
So, I am going to tweak the question to specifically ask for a native solution, to minimize dependencies.
Let's say hypothetically, you have a <div> and that <div> is in mid-transition of its opacity value and top value. How would I get the value of both of those properties mid-transition using native JavaScript?

It is very easy to port the jQuery script from the linked thread into its vanilla JavaScript equivalent and below is a sample. The output is printed on the right side (output#op element) once timer expires.
All that we are doing is the following:
Attach two event handlers to the element which triggers the transition (sometimes the triggering element can be different from the one that has animation). In the other thread, the element that is triggering the transition and the one that is being transitioned is the same. Here, I have put it on two different elements just for a different demo.
One event handler is for mouseover event and this creates a timer (using setTimeout) which gets the opacity and top value of the element that is being transitioned upon expiry of timer.
The other event handler is for mouseleave event to clear the timer when the user has hovered out before the specific point at which we need the opacity and top value to be obtained.
Getting the opacity and top value of the element that is being transitioned can be obtained by using the window.getComputedStyle method.
Unlike the demo in the other thread (which uses setInterval), here I have used setTimeout. The difference is that setInterval adds an interval and so the function is executed every x seconds whereas the function passed to setTimeout is executed only once after x seconds. You can use whichever fits your needs.
var wrap = document.querySelector('.wrapper'),
el = document.querySelector('.with-transition'),
op = document.querySelector('#op');
var tmr;
wrap.addEventListener('mouseenter', function() {
tmr = setTimeout(function() {
op.innerHTML = 'Opacity: ' + window.getComputedStyle(el).opacity +
', Top: ' + window.getComputedStyle(el).top;
}, 2500);
});
wrap.addEventListener('mouseleave', function() {
clearTimeout(tmr);
});
.wrapper {
position: relative;
height: 400px;
width: 400px;
background: yellowgreen;
}
.with-transition {
position: absolute;
top: 0px;
left: 100px;
width: 200px;
height: 100px;
background: yellow;
opacity: 0;
transition: all 5s linear;
}
.wrapper:hover .with-transition {
top: 300px;
opacity: 1;
}
output {
position: absolute;
top: 50px;
right: 50px;
}
<div class='wrapper'>
<div class='with-transition'></div>
</div>
<output id='op'></output>

The answer referenced in the duplicate question is easily modified to NOT use jquery. There is no black magic happening there.
The real question is why would you want to do this?
If You need control over a transition just impliment the partial transition with javascript, do what you need, then complete the transition.

Related

Cypress assertion - element is actionable / not covered

I would like to expand on this question as I have a similar question:
I want to cy.get('#lead_name').type('foo') but it is covered by this element with opacity 0.9 while the form is loading:
<div class="blockUI blockOverlay" style="z-index: 1000; border: none; margin: 0px; padding: 0px; width: 665px; height: 100%; top: 0px; left: 0px; background-color: rgb(0, 0, 0); opacity: 0.9; cursor: wait; position: absolute;"></div>
When I start with the assertion
cy.get('#lead_name').should('be.visible)
it passes the assertion (maybe because of the opacity?) but when I then try to type in the field, I get the error message that the element is covered.
When I try to assert that the overlay is not there anymore and add
cy.get('.blockUI blockOverlay').should('not.exist')
Cypress also passes the assertion even though the element does exist and covers the other element and cy.get('#lead_name').type('foo') fails.
Is there any way to address this problem like
//This does not work it's just a sample to explain what I want to do
//test if the element I want to get is not covered
cy.get('#lead_name').should('not.be.covered')
//or test if the element is actionable
cy.get('#lead_name').should('be.actionable')
to make sure it waits until the form has loaded?
{edit} This is the error message I get from Cypress:
Timed out retrying after 4000ms: cy.type() failed because this element:
<input name="CrmLead[first_name]" id="CrmLead_first_name" type="text" maxlength="255">
is being covered by another element:
<div class="blockUI blockOverlay" style="z-index: 1000; border: none; margin: 0px; padding: 0px; width: 665px; height: 100%; top: 0px; left: 0px; background-color: rgb(0, 0, 0); opacity: 0.9; cursor: wait; position: absolute;"></div>
{edit 2} This is the code I use, condensed to the relevant parts:
it('should select new lead', () {
cy.visit(Cypress.env('lead_url'))
cy.get('#new_lead).click() //this opens the new form which takes some time to load
cy.get('#lead_name').type('foo')
cy.get('#lead_last_name').type('bar')
cy.get('.button').click()
}
{edit 3} The more I test possible solutions, the more I become convinced that the problem is not the overlay, but something in the Cypress code itself.
When I open the form manually it never takes more than a second for the loading spinner to disappear, usually just 0.1-0.2 seconds.
Yet when Cypress opens the form the form just doesn't load properly as the loading spinner stays there indefinitely.
The checking of the overlay should be done in two steps.
cy.get('.blockUI.blockOverlay')
.should('exist')
.then($overlay => $overlay.remove())
cy.get('.blockUI.blockOverlay').should('not.exist')
cy.get('#lead_name', {timeout: 10000})
.should(($el) => { // should will cause retry
return Cypress.dom.isFocusable($el) // instead of visible, more relevent to actionability
})
It's possible if you just do the second step, Cypress passes that command before the overlay is present.
It's the same principle as checjing a loading spinner, which has be tackled on SO before.
BTW you are selecting an element with two classed (according to the error message), so you need two . in the selector.
Perhaps that's the only change you need!
I added another check above that might help. Cypress.dom.isFocusable.
From the docs Is focusable
Cypress internally uses this method everywhere to figure out whether an element is hidden, mostly for actionability.
If you get really stuck, take a look at Gleb Bahmutov's walkthrough video here Debug the Element Visibility Problems in Cypress
One more idea - you can try removing the covering overlay in the test - added a line to the sample above.
You can use {force: true} with type(). This will igonre the overlapping of other element.
cy.get('#lead_name').type('foo', {force: true})
Or if you want to assert that the element has opacity 0.9 you can use:
cy.get('.blockUI blockOverlay')
.should('have.attr', 'style')
.and('include', 'opacity: 0.9')
Or, you can wait for the element to not have the opacity, in that case you can use:
cy.get('.blockUI blockOverlay', {timeout: 7000})
.should('have.attr', 'style')
.and('not.include', 'opacity: 0.9')

Question about execution sequence with toggling CSS attributes (namely animation) [duplicate]

This question already has answers here:
Restart animation in CSS3: any better way than removing the element?
(14 answers)
Closed 1 year ago.
So in this example, if I don't nest the
box.style.animation = 'myanimation 3s'
inside of the setTimeout, then the code doesn't work. But when I run it like this, with a timeout delay of ZERO, it works. Originally I thought that maybe it was an execution timing error, so I set the Timeout delay to 50, but I got curious and tried lower amounts until I got to zero, and it still ran. Essentially I was just trying to implement an animation every time the element is clicked. Is there a better/safer workaround for this? Also, for curiosity, what is the difference between running that line of code directly after the 'if' statement versus nesting it in a setTimeout with a delay of 0? I'm using Firefox with Ubuntu LTS 20.04
edit: I should add, if I don't nest inside the setTimeout function, the animation will run the first time, but not any subsequent time. But when I run the code as shown, it runs everytime.enter image description here
const box = document.querySelector('.box')
box.addEventListener('click', function() {
if(box.style.animation){
box.style.animation = ''
}
setTimeout(function() {
box.style.animation = 'myanimation 3s'
}, 0)
})
EDIT: Another answer I came across on reddit, is to listen for an animationend event, such as:
const box = document.querySelector('.box')
box.addEventListener('click', function() {
box.style.animation = 'myanimation 3s'
})
box.addEventListener('animationend', function() {
box.style.animation = ''
})
The reason why it seems like nothing is happening when you don't use the timeout is that modern browsers optimize away the first statement completely, in order to prevent unnecessary operations from happening that would have a negative effect on performance.
The browser notices that you set the animation property to an empty string, just to set it to another value in the line below. That would cause a reflow, and in many cases that wouldn't be what you want. In this case, trying to reset a CSS animation, it is exactly what you want, though.
So you need to tell the browser to intentionally cause a reflow. One way to do that is using setTimeout, so the first expression will not be optimized away. Another way is to cause reflow by doing something that will itself inherently cause a reflow. offsetLeft, offsetTop, offsetWidth and offsetHeight are such a properties that cause a reflow in order to report back accurate values when they are accessed. It's enough to simply access them, without putting the values into a variable or anything:
const box = document.querySelector('.box')
box.addEventListener('click', function() {
if (box.style.animation) {
box.style.animation = ''
}
box.offsetLeft; // this forces a reflow, just like setTimeout
box.style.animation = 'myanimation 3s'
})
.box {
background: red;
width: 120px;
height: 120px;
position: relative;
}
.box::after {
content: "click me!";
color: #fff;
display: block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
}
#keyframes myanimation {
from {
transform: translateY(0)
}
50% {
transform: translateY(1em)
}
to {
transform: translateY(0);
}
}
<div class="box">
</div>

Changing div styles at scroll position without JQuery

I am relatively new to web design and am not yet ready to dive into JQuery but I am beginning to use Javascript as needed. I am unable to figure out how to change the background color of a div menubar at a certain scroll position.
CSS
.mainMenu {
position: fixed;
top: 0px;
left: 0px;
height: 80px;
padding: 20px;
transition: all 0.2s;
}
Javascript
var scrollHeight = window.pageYOffset;
if (scrollHeight >= 100) {
document.getElementById("mainMenu").style.backgroundColor = "green";
}
From what I can tell as a noob, the if statement only runs on load and the var scrollHeight isn't updating as the user scrolls. I appreciate any help making this work! I will get around to learning JQuery but I would like to understand the language better before dabbling in libraries.
Right – you need to setup something that continues to check the scroll position and update accordingly:
function checkPosition() {
// Continue calling this function:
requestAnimationFrame(checkPosition);
// Check your position here
}
// Initial Call:
checkPosition();
Better yet, read up on Scroll Events.
Edit:
Also, instead of manipulating styles directly, I'd recommend adding or removing classes:
element.classList.add("newClass");
(By the way, jQuery is actually easier than 'regular' javascript.)

Why is setTimeout needed when applying a class for my transition to take effect?

I have an element with a transition applied to it. I want to control the transition by adding a class to the element which causes the transition to run. However, if I apply the class too quickly, the transition effect does not take place.
I'm assuming this is because the .shown is placed onto the div during the same event loop as when .foo is placed onto the DOM. This tricks the browser into thinking that it was created with opacity: 1 so no transition is put into place.
I'm wondering if there is an elegant solution to this rather than wrapping my class in a setTimeout.
Here's a snippet:
var foo = $('<div>', {
'class': 'foo'
});
foo.appendTo($('body'));
setTimeout(function(){
foo.addClass('shown');
});
.foo {
opacity: 0;
transition: opacity 5s ease;
width: 200px;
height: 200px;
background-color: red;
}
.foo.shown {
opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Actually, the point is not about the setTimeout, but about how the element is rendered.
The CSS transition will only appear if the element is rendered with a property value, and then this property is changed.
But once you append the element, it does not mean that it was rendered. Simply adding a setTimeout is not enough. Thought it may work for you, in some browser versions it won't work! (Mostly Firefox)
The point is about the element's render time. Instead of setTimeout, you can force a DOM render by requesting a visual style property, and then changing the class:
var foo = $('<div>', {
'class': 'foo'
});
foo.appendTo($('body'));
//Here I request a visual render.
var x = foo[0].clientHeight;
//And it works, without setTimeout
foo.addClass('shown');
.foo {
opacity: 0;
transition: opacity 5s ease;
width: 200px;
height: 200px;
background-color: red;
}
.foo.shown {
opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
When you do DOM manipulation that that javascript relies on immediately afterwards, you need to pause javascript execution briefly in order to allow rendering to catch up, since that will be done asynchronously. All a blank setTimeout does is move the code within to the end of the current execution pipeline. The browser must complete rendering the new layout before it will obey a trigger for your transition so the setTimeout is a good idea and in my opinion the most elegant solution.

Using addClass() and CSS transition on render function in Backbone app not working correctly

In my backbone.js application, I'm trying to fade in the view element after it's been appended. However it doesn't work.
Live example here: http://metropolis.pagodabox.com
var itemRender = view.render().el;
$('#items-list').append(itemRender);
$(itemRender).addClass('show');
However if I add a small setTimeout function, it works.
var itemRender = view.render().el;
$('#items-list').append(itemRender);
setTimeout(function(){
$(itemRender).addClass('show');
},10);
Using fadeIn() also works but I prefer to use straight CSS for the transition as it's more efficient, and prefer not to use any setTimeout "hacks" to force it to work. Is there a callback I can use for append? Or any suggestions? The full code is below:
itemRender: function (item) {
var view = new app.ItemView({ model: item }),
itemName = item.get('name'),
itemRender = view.render().el;
$('#items-list').append(itemRender);
$(itemRender).addClass('show');
app.itemExists(itemName);
}
CSS/LESS:
#items-list li {
padding: 0 10px;
margin: 0 10px 10px;
border: 1px solid #black;
.border-radius(10px);
position: relative;
.opacity(0);
.transition(opacity)
}
#items-list li.show {.opacity(1)}
This "hack" you mention (or some variant of it) is occasionally necessary for web development, simply due to the nature of how browsers render pages.
(NOTE: This is all from memory, so while the overall idea is right please take any details with a small grain of salt.)
Let's say you do the following:
$('#someElement').css('backgroundColor', 'red');
$('#someElement').css('backgroundColor', 'blue');
You might expect to see the background color of #someElement flash red for a brief moment, then turn blue right? However, that won't happen, because browsers try to optimize rendering performance by only rendering the final state at the end of the JS execution. As a result, the red background will never even appear on the page; all you'll ever see is the blue.
Similarly here, the difference between:
append
set class
and:
append
wait 1ms for the JS execution to finish
set class
Is that the latter allows the element to enter the page and AFTER the JS is executed have its style change, while the former just applies the style change before the element gets shown.
So while in general window.setTimeout should be avoided, when you need to deal with these ... complications of browser rendeering, it's really the only way to go. Personally I like using the Underscore library's defer function:
var itemRender = view.render().el;
$('#items-list').append(itemRender);
_(function(){
$(itemRender).addClass('show');
}).defer();
It's the same darn thing, but because it's encapsulated in a library function it feels less dirty to me :-) (and if the "post-render" logic is more than a line or two I can factor it in to a Backbone View method and do _(this.postRender).defer() inside my render method).
You can use CSS animations
#keyframes show {
0% { opacity: 0; }
100% { opacity: 1; }
}
#items-list li {
padding: 0 10px;
margin: 0 10px 10px;
border: 1px solid #black;
.border-radius(10px);
position: relative;
}
#items-list li.show {
animation: show 1s;
}

Categories