With HTML and JS, I have a div called ball and it follows my cursor wherever it goes, only when I click on it, so my problem is that I want it when I click on the document element it goes to its initial values but as soon as I move my cursor back it follows my cursor again, although I want it to stay to initial values until I click on it again.
I tried making functions and variables and other many stuff to fix the problem but it never worked.
let ballIsOn = false;
let bodyIsClicked = false;
let cursor = document.querySelector(".ball1");
let initialWidth = cursor.offsetWidth;
let initialHeight = cursor.offsetHeight;
let initialZIndex = cursor.style.zIndex;
let initialOpacity = cursor.style.opacity;
let initialLeftPosition = cursor.style.left;
let initialTopPosition = cursor.style.top;
const cursorExplainingDiv = document.querySelector(".cursor-explaining-div");
let mouseMoveListener;
let mouseOverListener;
let mouseOutListener;
const landingBallVar = function landingBalls(e) {
if (ballIsOn === false) {
ballIsOn = true;
let clickedBall = e.target;
console.log(clickedBall);
let cursor = clickedBall;
cursor.style.width = "30px";
cursor.style.height = "30px";
cursor.style.zIndex = "99";
cursor.style.opacity = "1";
cursor.style.transition = ".1s ease";
cursor.style.pointerEvents = "none";
let x = e.clientX;
let y = e.clientY;
cursor.style.left = x - 15 + "px";
cursor.style.top = y - 15 + "px";
if (mouseMoveListener) {
window.removeEventListener("mousemove", mouseMoveListener);
}
mouseMoveListener = window.addEventListener("mousemove", function(e) {
setTimeout(function() {
let newX = e.clientX;
let newY = e.clientY;
cursor.style.left = newX - 15 + "px";
cursor.style.top = newY - 15 + "px";
}, 100);
});
const allBodyEl = {
h1: document.querySelectorAll("h1"),
h2: document.querySelectorAll("h2"),
p: document.querySelectorAll("p"),
button: document.querySelectorAll("button"),
img: document.querySelectorAll("img"),
};
for (const key in allBodyEl) {
const elements = allBodyEl[key];
elements.forEach((element) => {
mouseOverListener = element.addEventListener("mouseover", function(e) {
cursor.style.width = "40px";
cursor.style.height = "40px";
cursor.style.backgroundColor = "#bc7025";
let newX = e.clientX;
let newY = e.clientY;
cursor.style.left = newX - 10 + "px";
cursor.style.top = newY - 20 + "px";
// console.log(cursorExplainingDiv);
});
mouseOutListener = element.addEventListener("mouseout", function() {
setTimeout(function() {
cursor.style.width = `30px`;
cursor.style.height = `30px`;
cursor.transition = ".1s ease";
cursor.style.backgroundColor = "";
cursor.innerHTML = "";
}, 100);
});
});
}
}
};
function resetBallToInitialState() {
cursor.style.width = `${initialWidth}px`;
cursor.style.height = `${initialHeight}px`;
cursor.style.zIndex = initialZIndex;
cursor.style.opacity = initialOpacity;
cursor.style.left = initialLeftPosition;
cursor.style.top = initialTopPosition;
ballIsOn = false;
// Remove event listenerhis
const allBodyEl = {
h1: document.querySelectorAll("h1"),
h2: document.querySelectorAll("h2"),
p: document.querySelectorAll("p"),
button: document.querySelectorAll("button"),
img: document.querySelectorAll("img")
};
for (const key in allBodyEl) {
const elements = allBodyEl[key];
elements.forEach((element) => {
element.removeEventListener("mouseover", mouseOverListener);
element.removeEventListener("mouseout", mouseOutListener);
});
}
}
document.addEventListener("click", (e) => {
if (ballIsOn && !e.target.closest(".ball1")) {
resetBallToInitialState();
}
});
for (let i = 1; i <= 4; i++) {
const ball = document.getElementById("ball" + i);
if (ball) {
console.log(ball);
ball.addEventListener("click", function(e) {
landingBallVar(e);
});
}
}
<div class="cursor-explaining-div">SO MUCH OF A TEXT</div>
<nav id="nav" class="flex">
<div class="bussiness-name">
<h2>
Fashion
</h2>
</div>
<div class="links flex">
<a href="#">Services</a>
<a href="#">About</a>
<a href="#">Contact</a>
</div>
</nav>
<section class="landing flex">
<div class="ball ball1" id="ball1" "></div>
<div class="ball ball2 " id="ball2 ""></div>
<div class="ball ball3" id="ball3" () "></div>
<div class="ball ball4 " id="ball4 "></div>
<div class="left-landing ">
<h1>Photoghraphy 📷</h1>
<p class="shady-cl ">
Photography is the art 🎨 of creating durable images by recording light, either electronically by means of an image sensor 😀, or chemically by means of a light-sensitive material such as photographic film.
</p>
<button id="contact-me ">Contact Me</button>
</div>
<div class="right-landing ">
<img src="https://placehold.co/300x200.png?text=WebDesign.jpg" class="landing-image " alt=" ">
</div>
</section>
2
Answers
There are several problems here. I will try to go through them. These points reference the code at the bottom.
Broken Event Listener Removal
There was a fundamental misunderstanding of event listeners in the original code. Take the following snippet:
Here
listener
will beundefined
sinceaddEventListener
does not return anything at all; therefore, this can not be used withremoveEventListener
. When you do, nothing will happen. The events will still be attached.The usual way of removing events involves keeping around the handler function and then calling
removeEventListener
with that.However, this is a bit painful in this situation. So there is another, more ergonomic, option for this situation where you can use an
AbortController
to kill off multiple listeners in one go.Here, we create an
AbortController
before we register the events we want to cancel (and keep it in shared scope at the top of the file), and then we pass that with theaddEventListener
. We can then later use this abort controller to signal to all the listeners that they should be removed.Note we can’t reuse an
AbortController
hence it is assigned a new one each time the events are about to be registered. The reason for that is once an abort controller has hadabort()
called, it remains in an aborted state and can not go back.Mitigation of event propagation
Another issue that manifests when the above is fixed is that when the ball is clicked the function which adds the events and the function that removes them are called in quick succession. So the handlers are added and immediately after, removed again, without any additional use input.
That’s because the
document
handler forclick
where you currently do the reset stuff will receive an event when clicking on a ball to turn it on as well, even though the ball itself has a separate click handler.That’s because events propagate. Events like
click
are fired on every ancestor element, all the way up todocument
. This is also sometimes called "event bubbling".This can be fixed by calling
e.stopPropagation();
in the ball click handler, so that doesn’t inadvertently also trigger a click event on the document which would subsequently remove all the ball handlers when you were trying to do the opposite.The reason this seemingly wasn’t an issue before is because the removal of the event listeners wasn’t working anyway, as explained above.
Resetting styles
The resetting of the styles was unnecessarily complex. When you do
thing.style.whatever = 'whatever'
you are setting inline styles on the elementsstyle
DOM attribute, which take precedence over whatever styles there are associated with the element in the CSS stylesheets. It looks like the elements don’t start out with any inline styles in the base HTML, they just use the styles from the CSS stylesheets by default.This means we can get back to the original styling by simply removing all the inline styles that have been applied like
currentCursorBallEl.removeAttribute("style");
. The element will then just adopt whatever was in the original stylesheet again, because then there are no inline styles overriding those rules, which are always there untouched no matter what you do withthing.style
in the script.You didn’t previously keep a
currentCursorBallEl
in the shared scope so I added that. This also supplanted the need for theballIsOn
var since we can know that by just knowing ifcurrentCursorBallEl
is set (not null) and using that.Remaining Glitchy Behavior
This is reaching somewhat outside of the question title, but I will go over it for completeness.
You’ll notice sometimes the cursor is glitchy, like sometimes not appearing over the content it should until you move the cursor out and back in again. This is due to the way you create timeouts that overlap and are not cancelled or buffered appropriately.
When you move the cursor quickly over the page, a
mouseout
event from a previously hovered element can end up firing after themouseover
ormousemove
for a newly hovered element. This then inadvertently sets the wrong style values.Your original motivation for the timers was probably because, without them, the element does not follow correctly until the cursor stops moving, or at least slows down.
The reason that was a problem in the first place is that when the cursor is moving, many (hundreds, possibly thousands) of
mousemove
events are triggered. This causes the styles of the element to change rapidly, and the browser (especially with the attached animation) simply does not have time to keep up with that many paints.The solution is to introduce
requestAnimationFrame
to replacesetTimeout
. This will fire when the browser is ready to paint. When you combine that with some bool that prevents a new animation frame relating tomousemove
from being registered whilst another was already was pending, you avoid triggering too many style changes faster than the browser can paint them. At the same time though, you also avoid delaying them for too long.We also
requestAnimationFrame
on the other handlers as well, which ensures all the style changes fire in a predictable order.To get the ball cursor to be in the correct position when moving, you need to add
a css postion of absolute. Otherwise your ball cursor location is way off.
You don’t need the setTimeout()’s and they cause jerky cursor movement.
The way you were saving initial style values does not work, since those
calls only give you values that you have changed with javascript.
A cleaner (and working) way, is to move all the style values you want
to change, into your .css file, then your javascript only has to add and remove
a class at appropriate places.
I used .cursor and .special_cursor class names.
See the .ball.cursor and .ball.cursor.special_cursor in my css.
These are only two parts of my css that you need to keep. The other css
is just to get the code snippet to work.
You don’t need mouseover and mouseout event listeners, that code
can be done inside your mousemove event listener, with a line of code
that looks for those element tags.
I added an initialization() function and moved the event listener setup there.