Search code examples
javascriptanimationes6-promise

Animate.css promise sometimes doesn't clean the classes after animationEnd


I have few titles in my page and I animate each of them with a different animation from Animate.css. First I wrap every letter on the titles in a span element and then I use an event listener on the parent to see when is over it. I use a event.target to determine which letter has been hover it and a parentNode to determine the class of it's parent and give the corresponding animation.

The problem is that sometimes some letter doesn't animate. I see that the letter still have the "animate__animated animate__(animation name)" from the previous hover.

I use the JS code from the Animate.css web sites that use promises.

Does anyone have idea why sometimes the classes are not cleaned? I see that I'm trying to animate a lot of elements but I'm not animating all of them together and at the end I use bubbling event to avit too my event lister.

   /// wrap every letter in a span
  const words = document.querySelectorAll(".word");
  words.forEach(function (e) {
    e.innerHTML = e.textContent.replace(
      /\bCristian\b|([^\x00-\x40]|\w|\S)/g,
      (match, group) =>
        group == undefined
          ? `<span class="cristian">${match}</span>`
          : `<span class="letter">${match}</span>`
    );
  });
 // anim letters function
  const animationsSettings = [
    ["fede", "rubberBand"],
    ["myne", "bounce"],
    ["skills", "jello"],
    ["contact", "wobble"],
    ["tools", "swing"],
  ];
  const animationPrefix = "animate__";
  const defAnimationName = "jello";
  let animationName = defAnimationName;

  const regexClasses = () => {
    let regex = ``;
    for (settings of animationsSettings) {
      regex += `\\b${settings[0]}\\b|`;
    }
    return regex.slice(0, -1);
  };

  const regexConst = new RegExp(regexClasses(), "");

  function anim_letters(el, i) {
    // check if el is an event or not
    const node =
      el.originalEvent instanceof Event || el.target ? el.target : el;
    const parentClasses = node.parentNode.classList.value;
    const parentClassMatch = parentClasses.match(regexConst);

    if (
      node.tagName == "SPAN" &&
      node.className !== "word" &&
      node.className !== "cristian"
    ) {
      if (parentClassMatch) {
        for (settings of animationsSettings) {
          if (parentClassMatch[0] == settings[0]) {
            animationName = settings[1];
            break;
          }
        }
      } else {
        animationName = defAnimationName;
      }

      // We create a Promise and retu1rn it
      new Promise((resolve, reject) => {
        if (i) {
          animationName = "jello";
          node.style.animationDelay = `${i * 0.1 + 0.2}s`;
        }
        node.classList.add(
          "animate__animated",
          `${animationPrefix}${animationName}`
        );

        // When the animation ends, we clean the classes and resolve the Promise
        function handleAnimationEnd(event) {
          event.stopPropagation();
          node.classList.remove(
            "animate__animated",
            `${animationPrefix}${animationName}`
          );
          node.style.animationDelay = "";
          resolve("animation ended");
        }

        node.addEventListener("animationend", handleAnimationEnd, {
          once: true,
        });
      });
    }
  }

  const lettersStart = document.querySelectorAll(".lettersstart>.letter");
  for (const [i, letter] of lettersStart.entries()) {
    anim_letters(letter, i);
  }

  for (e of words) {
    e.addEventListener("mouseover", anim_letters);
  }
.letter{
display:inline-block
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet"/>
<h2 class="lettersstart word hi">Hi,</h2>
<h2 class="lettersstart im word">I'm
<span class="cristian">Cristian</span></h2>
<h3 class="myne word">Myne<wbr>social</h3>
<h3 class="fede word">Federico<wbr>Angeli</h3>

You can see the real page here

Thanks


Solution

  • I think the problem is that your animationName variable is global. When you animate a letter with class A, then hover over a letter with a different class B before the first animation is done, it will try to remove class B from the element at the end and be stuck with class A. You should be able to fix this by simply moving the

    let animationName = defAnimationName;
    

    declaration into the anim_letters function.

    /// wrap every letter in a span
    const words = document.querySelectorAll(".word");
    words.forEach(function (e) {
      e.innerHTML = e.textContent.replace(
        /\bCristian\b|([^\x00-\x40]|\w|\S)/g,
        (match, group) =>
          group == undefined
            ? `<span class="cristian">${match}</span>`
            : `<span class="letter">${match}</span>`
      );
      // FIXME: XSS issue if textContent contains <>
    });
    
    // anim letters function
    // use a simple lookup map instead of complicated and fragile regex stuff
    const animationsSettings = new Map([
      ["fede", "rubberBand"],
      ["myne", "bounce"],
      ["skills", "jello"],
      ["contact", "wobble"],
      ["tools", "swing"],
    ]);
    const animationPrefix = "animate__";
    const defAnimationName = "jello";
    
    function anim_letters(el, i) {
      // check if el is an event or not
      const node = el.originalEvent instanceof Event || el.target ? el.target : el;
    
      if (node.tagName != "SPAN" || node.classList.contains("word") || node.classList.contains("cristian")) {
        return Promise.resolve();
      }
      let animationName = defAnimationName
      for (const className of node.parentNode.classList) {
        if (animationsSettings.has(className)) {
          animationName = animationsSettings.get(className);
          break;
        }
      }
      if (i) {
        animationName = "jello";
        node.style.animationDelay = `${i * 0.1 + 0.2}s`;
      }
      return new Promise((resolve, reject) => {
        node.classList.add(
          "animate__animated",
          `${animationPrefix}${animationName}`
        );
        // When the animation ends, we clean the classes and resolve the Promise
        function handleAnimationEnd(event) {
          event.stopPropagation();
          node.classList.remove(
            "animate__animated",
            `${animationPrefix}${animationName}`
          );
          node.style.animationDelay = "";
          resolve("animation ended");
        }
        node.addEventListener("animationend", handleAnimationEnd, {
          once: true,
        });
      });
    }
    
    const lettersStart = document.querySelectorAll(".lettersstart>.letter");
    for (const [i, letter] of lettersStart.entries()) {
      anim_letters(letter, i);
    }
    
    for (e of words) {
      e.addEventListener("mouseover", anim_letters);
    }
    .letter {
      display:inline-block
    }
    <link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet"/>
    <h2 class="lettersstart word hi">Hi,</h2>
    <h2 class="lettersstart im word">I'm
    <span class="cristian">Cristian</span></h2>
    <h3 class="myne word">Myne<wbr>social</h3>
    <h3 class="fede word">Federico<wbr>Angeli</h3>