When I want to yield execution of my javascript and allow the browser to apply styles etc. before I continue, I tend to use the common technique of setTimeout with a delay of 0 to have a callback queued at the end of the event loop. However, I came across a situation where this doesn't seem to be working reliably.
In the snippet below, I have an active class that applies a transition to the chaser element.
When I hover over a target div, I want to remove the active class from the chaser element, move the chaser to a new location, then reapply the active class. The effect should be that the o should immediately vanish, and then fade in its new location. Instead, both the opacity and top have the transition applied, so the o slides from position to position, most of the time.
If I increase the inner timeout's delay to 10, it starts to behave as I originally intended. If I set it to 5, then it sometimes does and sometimes doesn't.
I would have expected any setTimeout to have queued my callback until after the style updates have been applied, but there's a clear race condition here. Am I missing something? Is there a way to guarantee the order of updates?
I'm on Chrome 56 on macOS and Windows, haven't tested other browsers yet.
(I know I can achieve this in other ways such as applying the transition to only the opacity property - please consider this a contrived example to demonstrate the particular question about ordering style updates).
var targets = document.querySelectorAll('.target');
var chaser = document.querySelector('#chaser');
for (var i = 0; i < targets.length; i++) {
targets[i].addEventListener('mouseenter', function(event) {
chaser.className = '';
setTimeout(function() {
// at this point, I'm expecting no transition
// to be active on the element
chaser.style.top = event.target.offsetTop + "px";
setTimeout(function() {
// at this point, I'm expecting the element to
// have finished moving to its new position
chaser.className = 'active';
}, 0);
}, 0);
});
}
#chaser {
position: absolute;
opacity: 0;
}
#chaser.active {
transition: all 1s;
opacity: 1;
}
.target {
height: 30px;
width: 30px;
margin: 10px;
background: #ddd;
}
<div id="chaser">o</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
What you need to listen for is transitionend event before doing anything else. You can read up on MDN about transitionend event. Btw, setTimeout should never be used to guarantee timing.
EDIT: This is for reference after clarification from OP. Whenever a style change occurs to an element, there is either a reflow and/or repaint. You can read more about them here. If the second setTimeout is ran before the first reflow, then you get the sliding effect. The reason why 10ms will lead to the desired effect is because .active class is added after the offsetTop property has been adjusted (leading to transition property applied after the element has changed it's offsetTop). Usually, there are 60fps (i.e: ~16ms per frame) which means that you have a 16 ms window to do anything before the new styles are applied. This is why a small delay of 5ms will sometimes lead different results.
TL:DR - The browser asks JS and CSS every 16ms for any updates, and calculates what to draw. If you miss the 16ms window, you can have completely different results.
You are calling a setTimeout() timing method inside another setTimeout() timing method.
My thought is, why not call the both setTimeout() method separately like so:
The first setTimeout() method should execute first, then at end of execution, it should call the second setTimeout() method.
Here is a working script for moving the chaser:
function _( id ) { return document.getElementById( id ); }
window.addEventListener( 'load', function() {
var targets = document.querySelectorAll('.target');
var chaser = document.querySelector('#chaser');
setTopPosition( targets );
function setTopPosition( targets ) {
for (var i = 0; i < targets.length; i++) {
targets[i].addEventListener('mouseenter', function(event) {
chaser.className = '';
_( 'status' ).innerText = chaser.className; // to inspect the active class
setTimeout(function() {
/* at this point, I'm expecting no transition // to be active on the element */
chaser.style.top = event.target.offsetTop + "px";
}, 0);
// check if charser.className == ''
if ( chaser.className == '') {
setClassName();
} else {
alert( 0 );
}
}); // addEventListener
} // for
} //function setTopPosition( targets )
function setClassName() {
setTimeout(function() {
/* at this point, I'm expecting the element to have finished moving to its new position */
chaser.className = 'active';
_( 'status' ).innerText = chaser.className;
}, 0);
} // function setClassName()
});
HTML:
<div id="chaser">o</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
<div class="target">x</div>
<div id="status">status</div>
CSS:
#chaser { position: absolute; opacity: 0; }
#chaser.active { transition: all 1s; opacity: 1; }
.target { height: 30px; width: 30px; margin: 10px; background: #ddd; }
Related
Im creating a fixed header where on load, the logo is flat white. On scroll, it changes to the full color logo.
However, when scrolling back to the top, it stays the same colored logo instead of going back to white.
Here's the code (and a pen)
$(function() {
$(window).scroll(function() {
var navlogo = $('.nav-logo-before');
var scroll = $(window).scrollTop();
if (scroll >= 1) {
navlogo.removeClass('.nav-logo-before').addClass('nav-logo-after');
} else {
navlogo.removeClass('.nav-logo-after').addClass('nav-logo-before');
}
});
});
http://codepen.io/bradpaulp/pen/gmXOjG
There's a couple of things here:
1) You start with a .nav-logo-before class but when the logo becomes black you remove that class and then try to get the same element using a class selector that doesn't exist anymore
2) removeClass('.nav-logo-before') is different than removeClass('nev-logo-before), notice the "." in the first selector.
3) You get the element using the $('.selector')in every scroll event, this can be a performance issue, it's better to cache them on page load and then use the element stored in memory
4) It's not a good practice to listen to scroll events as this can be too performance demanding, it's usually better to use the requestAnimationFrame and then check if the scroll position has changed. Using the scroll event it could happen that you scroll up really fast and the scroll event doesn't happen at 0, so your logo won't change. With requestAnimationFrame this can't happen
$(function() {
var navlogo = $('.nav-logo');
var $window = $(window);
var oldScroll = 0;
function loop() {
var scroll = $window.scrollTop();
if (oldScroll != scroll) {
oldScroll = scroll;
if (scroll >= 1) {
navlogo.removeClass('nav-logo-before').addClass('nav-logo-after');
} else {
navlogo.removeClass('nav-logo-after').addClass('nav-logo-before');
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
});
body {
background-color: rgba(0,0,0,.2);
}
.space {
padding: 300px;
}
.nav-logo-before {
content: url(https://image.ibb.co/kYANyv/logo_test_before.png)
}
.nav-logo-after {
content: url(https://image.ibb.co/jYzFJv/logo_test_after.png)
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<img class="nav-logo nav-logo-before">
</div>
<div class="space">
</div>
Dont need to add the dot . in front of the class name in removeClass and addClass:
Use this:
navlogo.removeClass('nav-logo-before')
Secondly, you are removing the class that you are using to get the element in the first place.
I have an updated codepen, see if this suits your needs: http://codepen.io/anon/pen/ZeaYRO
You are removing the class nav-logo-before, so the second time the function runs, it can't find any element with nav-logo-before.
Just give a second class to your navlogo element and use that on line 3.
Like this:
var navlogo = $('.second-class');
working example:
http://codepen.io/anon/pen/ryYajx
You are getting the navlogo variable using
var navlogo = $('.nav-logo-before');
but then you change the class to be 'nav-logo-after', so next time the function gets called you won't be able to select the logo using jquery as it won't have the '.nav-logo-before'class anymore.
You could add an id to the logo and use that to select it, for example.
Apart from that, removeClass('.nav-logo-before') should be removeClass('nav-logo-before') without the dot before the class name.
The problem is that you removes nav-logo-before and then you want to select element with such class but it doesn't exist.
I've rafactored you code to avert it.
Another problem is that you uses dot in removeClass('.before') while it should be removeClass('before') - without dot
$(function() {
var navlogo = $('.nav-logo');
$(window).scroll(function() {
var scroll = $(window).scrollTop();
if (scroll >= 1) {
navlogo.removeClass('before').addClass('after');
} else {
navlogo.removeClass('after').addClass('before');
}
});
});
body {
background-color: rgba(0,0,0,.2);
}
.space {
padding: 300px;
}
.before {
content: url(https://image.ibb.co/kYANyv/logo_test_before.png)
}
.after {
content: url(https://image.ibb.co/jYzFJv/logo_test_after.png)
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<img class="nav-logo before">
</div>
<div class="space">
</div>
So I am making a clicker game and am kind of stuck. I want a popup like cookieClicker has when you get an achievement. It pops up and tells you what happened, you can click the x or it will just fade away after a few seconds.
I tried making something with pure javascript and CSS to no avail, it would fade away nicely but not automatically.
So how do I make it so whenever X element is made/displayed then it goes away after 3 seconds?
Also, if it matters the element would be created by a javascript function, and multiples might be created at the same time.
P.S. I tried searching and found something about auto-fading in javascript but nothing in there seemed to work either.
EDIT: After trying to view cookieclicker source and playing the game again it appears it doesn't even have this functionality. The closest thing I can compare it to is when you would add something to your cart on a website, then it alerts you the item was added and then fades away.
Here is one approach which uses Javascript to trigger a CSS transition:
var button = document.getElementsByTagName('button')[0];
var div = document.getElementsByTagName('div')[0];
function autoFader() {
if (window.getComputedStyle(div).getPropertyValue('display') === 'none') {
div.style.display = 'block';
setTimeout(function(){
div.style.opacity = '0';
},10);
setTimeout(function(){
div.removeAttribute('style');
},4010);
}
}
button.addEventListener('click',autoFader,false);
div {
display: none;
width: 200px;
height: 100px;
margin: 20px auto;
padding: 6px;
font-size: 20px;
color: rgb(255,255,255);
text-align: center;
border: 3px solid rgb(127,0,0);
background-color: rgb(255,0,0);
opacity: 1;
transition: opacity 3s linear 1s;
}
<button type="button">Click Me</button>
<div>
<p>Hi, I'm an auto-fading pop-up.</p>
</div>
So, your openPopup function might look like this:
function openPopup(/* options here */) {
const popup = actuallyOpenPopup();
waitSomeTimeAndCloseIfNotClosedYet(popup);
}
where 2nd function should take a popup instance (which has .close method probably, or dismiss)
and start a timeout. You need to keep that timeout, so if close was called, you need to cancel it.
Something like this:
function waitSomeTimeAndCloseIfNotClosedYet(popup) {
const originalClose = popup.close;
/* monkey patching, decorating,
separate method - whatever you prefer */
popup.close = function () {
clearTimeout(this.timeout);
originalClose.call(this);
};
popup.timeout = setTimeout(() => popup.close(), 3000);
}
So, if was closed manually - it wont be called twice, if not, will fire up a timeout and close automatically.
Via CSS you can only achieve visible closing, but not removal of nodes. (Google for transition visibility, fade out modal etc.)
Hope this helps!
I have an hidden div, and I want to show the hidden div only when user's mouse over another a trigger element for several seconds instead show the hidden div once the user hover the trigger element
here is my javascript code
$('.c_like_icon').mouseover(
function() {
var timeout = setTimeout(function(){
var comment_id=$(this).attr('data-commentId');
$.ajax({
url: 'ajax_c_like_user.php',
method:'post',
data:{comment_id:comment_id},
success:function(data){
var like_num=$('#'+comment_id+'c_like_number').text();
if(like_num>=1){
$('#'+comment_id+'like_user_w').html(data);
$('#'+comment_id+'like_user_w').show();
}
else{
$('#'+comment_id+'like_user_w').hide();
}
}
})
}, 2000); //2 seconds
},
function(){
var comment_id=$(this).attr('data-commentId');
clearTimeout(timeout); //cancel the timeout if they hover off
$('#'+comment_id+'like_user_w').hide();
// do stuff when hover off
}
)
define a timeout in your hover in function and clear in the hover out function, to prevent it being fired if they leave before the time runs out, like this:
var timeout;
$('#trigger').hover(
function() {
timeout = setTimeout(function(){
// do stuff on hover
$('#hiddenDiv').show();
}, 2000); //2 seconds
},
function(){
clearTimeout(timeout); //cancel the timeout if they hover off
// do stuff when hover off
$('#hiddenDiv').hide();
}
);
You can very easily do this CSS only. No jquery is required which presents a huge benefit as it is a big library to download.
Just use delayed transitions. Here is my example (live demo here: http://codepen.io/anon/pen/jbGhi):
HTML
<div id="first"></div>
<div id="second"></div>
In this example, the ids are not necessary but I find it better to understand what happens.
CSS
for the purpose of this example, I'll stylize the divs (to make the hover effect more obvious) but none of the following really matters:
div{
height: 50vmin;
width: 50vmin;
border: solid 5px black;
float: left;
margin-right: 10vmin;
}
and this is where the magic happens:
div#first:hover ~ div#second{
transition: all 0.2s ease 1s;
background-color: green;
}
We are using the css selector "~" that means "any sibling element after (and their children)". In that example it means "a div called #second that is sibling and after a div called #first that is hovered". Basically, as long as the second div is a sibling and after or contained within a sibling (that is after) of the first one, you'll get the desired effect.
And there you go. You can add more delay (change "1s" to whatever duration) before the change occurs, and you can smoothen the transition itself (change "0.2s" to whatever duration).
PS: in the CSS, don't forget to add all vendor prefixes for transition and transform. Always check caniuse.com to know which prefixes are needed. Example:
-webkit-transition: all 1s;
transition: all 1s;
i know its an old question, but i think it should have a vanilla solution
// Element will be the triggerer
let timeOut;
element.addEventListener('mouseover', (e) => {
timeOut = setTimeout(() => {
// Do your stuff here
}, 400);
});
element.addEventListener('mouseout', (e) => {
clearTimeout(timeOut);
});
Assume we have three light bulbs, and I want to glow the first one, keep it on for a few milliseconds, turn it off, turn on the next and continue in the same way.
Turning the light bulbs on and off is done by adding and removing a class. How do I achieve this?
P.S. I used light bulbs just to make my question clearer. Basically what I need is, how to add a class to a div, keep it for some time, remove class, apply a class to another div, keep it for some time and remove it and so on...
*Edit
Clarification: The number of bulbs is dynamic
You can use a simple combination of setInterval and jquery selecors
Check this fiddle http://jsfiddle.net/aqXtL/1/
The Javascript function setInterval(code, interval) will repeatedly execute code with your interval. Just keep a variable with a counter, and use jquery's addClass and removeClass to turn the lights on and off.
you can use setTimeout like setTimeout(yourfuncname, 3000) == wait 3 seconds then run that function. If you need to use animations then the jquery animate method has a completed callback as a parameter.
very simplified example...
function startDimming(){
setTimeout(_interval,function(){
// Remove all "glowing" bulbs.
// Add class to first "bulb".
setTimeout(_interval,function(){
// Remove all "glowing" bulbs.
// Add class to second "bulb".
setTimeout(_interval,function(){
// Remove all "glowing" bulbs.
// Add class to last "bulb".
startDimming();
});
});
});
}
You can use the animation of jquery and use the complete function to do other tasks.
.animate( properties [, duration] [, easing] [, complete] )
$('#clickme').click(function() {
$('#book').animate({
opacity: 0.25,
left: '+=50',
height: 'toggle'
}, 5000, function() {
// Here you have animated the object and the animation is complete, so
// you can start a new animation here
});
});
HTML :
<div class="bulb"> </div>
<div class="bulb"> </div>
<div class="bulb"> </div>
CSS:
.bulb {
width: 50px;
height: 100px;
border: 2px #000 solid;
float: left;
margin: 10px;
}
.bulb.on {
background: #ff0;
}
Jquery :
var timer;
function light ( i ) {
i = i % $(".bulb").length;
$(".bulb").eq(i).addClass("on").siblings().removeClass("on");
timer = setTimeout(function(){
light( ++i );
}, 1000);
}
$(document).ready( function() {
light(0);
});
Edit : here's a working sample, http://jsfiddle.net/QdWgM/
http://jsfiddle.net/jmPCt/18/
I'm quite new to JS and jQuery. I've written all the code by hand in the link above. It works and does what I want it to save for but one thing. If you click rapidly on the 'next' link, you'll see either a flash of the next container to display or, if you click rapidly enough, the code will display two containers but I only want one to show only at a time. Is there some way of handling this in jQuery? I've tried using stops as discussed here: How to prevent jquery hover event from firing when not complete? but this does not solve the issue.
You are looking for .stop(). It's implementation changes with the desired behavior but the documentation should clear that up for you: http://api.jquery.com/stop
Here is a demo: http://jsfiddle.net/jmPCt/19/
Because of how .stop() works, when you use it with .fadeIn() or .fadeOut() you can chop-up your animations to the point where they no longer work. The best fix I've found is to always animate to absolute values with .fadeTo(): http://api.jquery.com/fadeTo
Here is the code I added to your JSFiddle, this overwrites the default .fadeIn() and .fadeOut() jQuery functions with ones that use .fadeTo() and .stop():
$.fn.fadeOut = function (duration, callback) {
$(this).stop().fadeTo(duration, 0, function () {
$(this).css('display', 'none');
if (typeof callback == 'function') {
callback();
}
});
};
$.fn.fadeIn = function (duration, callback) {
$(this).css('display', 'block').stop().fadeTo(duration, 1, function () {
if (typeof callback == 'function') {
callback();
}
});
};
Update
If you set the position property for the "slide" elements then they can animate on top of each other which will remove the jumpiness that your code exhibits:
HTML --
<div id="controls">
<div id="countah"></div>
<a href="#" id=prev>prev</a> |
<a href="#" id=next>next</a>
</div>
CSS --
.js .staceyPort {
display: none;
position : absolute;
top : 0;
left : 0;
}
#controls{
position : fixed;
bottom : 0;
left : 0;
z-index : 1000;
background : gold;
}
Here is a demo: http://jsfiddle.net/jmPCt/21/