Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bug with transitionend event not correctly removing a CSS class

I recently have been learning more JavaScript as I am going to be using it a lot for an upcoming internship. The company recommended many different resources for me to look at, but a friend of mine also suggested this website:

https://javascript30.com/

I decided to give it a try and started yesterday on the first project which was a JavaScript drumkit.

So the overall project wasn't hard, but I noticed a little bug that was happening. So the project attaches a 'keydown' event listener to several different HTML elements that trigger when either ASDFGHJKJL are pressed. In addition to playing a sound, the listener also attaches a class to the element that adds a little effect to the key being pressed.

To remove the class, we attached a transitionend listener to the elements. So when the CSS property finishes the transition after the key is pressed, the class is then removed and the graphic goes back to normal in the browser.

Now if you press the keys slowly, it works perfectly fine. But I noticed that if you were to hold a key down for a second or two, it appears as though the class that gets added is never removed and the graphic is permanent. I then checked the console and noticed that the transitionend event never fires once it gets stuck like that.

Here is the code from the finished file.

function removeTransition(e) 
{
    if (e.propertyName !== 'transform') 
       return;
    e.target.classList.remove('playing');
}

function playSound(e) 
{
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
    if (!audio) 
      return;

    key.classList.add('playing');
    audio.currentTime = 0;
    audio.play();
}

And this is how we attached the transitionend listener, not sure if this would have anything to do with it

const keys = Array.from(document.querySelectorAll('.key'));
keys.forEach(key => key.addEventListener('transitionend', removeTransition));

So we put in the if loop in the removeTransition function so that it doesn't remove the class for each property that is transitioning, but I noticed that when you remove the if loop, the page works perfectly.

So I am not entirely sure what is going on with the transitionend event, and why it just stops firing entirely once it gets stuck like that. And also, why it works when the if loop is removed. Maybe it has more chances to remove the class when it loops through all of the properties? No clue. Was wondering if anyone here might know what is going on.

EDIT:

I altered the playSound function a little bit to try and get to the bottom of this.

function playSound(e)
{
    const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
    const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
    if(!audio)
    {
        return;
    }
    if(key.classList.contains("playing"))
    {
        console.log("True");
        key.classList.remove("playing");
    }
    audio.currentTime = 0;
    audio.play();
    console.log("adding");
    key.classList.add("playing");
}

So I added an if statement that checks if the element already contains the "playing" class once it gets stuck. I also added a classList.remove call to try and get rid of it in that same branch. What I found was once it gets stuck, and I press the key again, it branches off into that if statement and prints the console message, but STILL will not remove the "playing" class.

like image 874
wjvukasovic Avatar asked May 14 '17 16:05

wjvukasovic


2 Answers

Use .toggle() instead of .add():

key.classlist.toggle('playing'); 

instead of

key.classlist.add('playing');
like image 167
Abdelrahman Avatar answered Sep 25 '22 01:09

Abdelrahman


There are 2 causes for this unable to remove CSS class problem:

  1. The flag to mark whether animation is played is class playing, which is also the key to switch on/off the animation. This setting makes the problem very complicated.
  2. The flag/mark change is adding/removing class playing. It is a DOM operation which costs more time than necessary.

To solve this problem, it is better to store flag/mark information in memory (as a variable), and stop keyboard event listener if transition is not completed.

Store flag/mark information in a variable:

var keysUnderTransition = {
  "65": 0,
  "83": 0,
  "68": 0,
  "70": 0,
  "71": 0,
  "72": 0,
  "74": 0,
  "75": 0,
  "76": 0
};

As every element will have 2 transitions in one cycle. 0 means element is not in transition (no transition left), 2 means it's in the first transition (2 transitions left to complete), and 1 means it's in the last transition (1 transition left to complete).

In function playSound, check and mark the flag:

if (keysUnderTransition[e.keyCode]) {
  return;
}
keysUnderTransition[e.keyCode] = 2;

In function removeTransition, revert flag/mark:

  var dataKey = this.getAttribute('data-key');
  var underTransitionVal = keysUnderTransition[dataKey];
  if (underTransitionVal === 2) {
    setTimeout(function() {
      keysUnderTransition[dataKey] = 1;
    }, 50);
  } else if (underTransitionVal === 1) {
    keysUnderTransition[dataKey] = 0;
  }

I use 50 in the above code to change flag before the end of second transition.

Here is a working jsfiddle.

like image 20
shaochuancs Avatar answered Sep 23 '22 01:09

shaochuancs