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
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>