skip to Main Content

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


  1. 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:

    listener = thing.addEventListener(...
    

    Here listener will be undefined since addEventListener does not return anything at all; therefore, this can not be used with removeEventListener. 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 the addEventListener. 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 had abort() 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 for click 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 to document. 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 elements style 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 with thing.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 the ballIsOn var since we can know that by just knowing if currentCursorBallEl 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 the mouseover or mousemove 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 replace setTimeout. This will fire when the browser is ready to paint. When you combine that with some bool that prevents a new animation frame relating to mousemove 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.

    const cursorExplainingDiv = document.querySelector(".cursor-explaining-div");
    
    let controller;
    let currentCursorBallEl = null;
    
    const landingBallVar = function landingBalls(e) {
      if (currentCursorBallEl === null) {
        controller = new AbortController();
        currentCursorBallEl = e.target;
    
        currentCursorBallEl.style.width = "30px";
        currentCursorBallEl.style.height = "30px";
        currentCursorBallEl.style.zIndex = "99";
        currentCursorBallEl.style.opacity = "1";
        currentCursorBallEl.style.transition = ".1s ease";
        currentCursorBallEl.style.pointerEvents = "none";
    
        let x = e.clientX;
        let y = e.clientY;
        currentCursorBallEl.style.left = x - 15 + "px";
        currentCursorBallEl.style.top = y - 15 + "px";
    
        let isThrottled = false;
        window.addEventListener(
          "mousemove",
          function (e) {
            if (!isThrottled) {
              isThrottled = true;
              let newX = e.clientX;
              let newY = e.clientY;
              requestAnimationFrame(() => {
                currentCursorBallEl.style.left = newX - 15 + "px";
                currentCursorBallEl.style.top = newY - 15 + "px";
                isThrottled = false;
              });
            }
          },
          { signal: controller.signal }
        );
    
        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.addEventListener(
              "mouseover",
              function (e) {
                requestAnimationFrame(() => {
                  currentCursorBallEl.style.width = "40px";
                  currentCursorBallEl.style.height = "40px";
                  currentCursorBallEl.style.backgroundColor = "#bc7025";
                  let newX = e.clientX;
                  let newY = e.clientY;
    
                  currentCursorBallEl.style.left = newX - 10 + "px";
                  currentCursorBallEl.style.top = newY - 20 + "px";
                });
    
                // console.log(cursorExplainingDiv);
              },
              { signal: controller.signal }
            );
    
            element.addEventListener(
              "mouseout",
              function () {
                requestAnimationFrame(() => {
                  currentCursorBallEl.style.width = `30px`;
                  currentCursorBallEl.style.height = `30px`;
                  currentCursorBallEl.transition = ".1s ease";
                  currentCursorBallEl.style.backgroundColor = "";
                  currentCursorBallEl.innerHTML = "";
                });
              },
              { signal: controller.signal }
            );
          });
        }
      }
    };
    
    function resetBallToInitialState(e) {
      currentCursorBallEl.removeAttribute("style");
      currentCursorBallEl = null;
      controller.abort();
    }
    
    document.addEventListener("click", (e) => {
      if (currentCursorBallEl !== null) {
        resetBallToInitialState(e);
      }
    });
    
    for (let i = 1; i <= 4; i++) {
      const ball = document.getElementById("ball" + i);
      if (ball) {
        console.log(ball);
        ball.addEventListener("click", function (e) {
          e.stopPropagation();
          landingBallVar(e);
        });
      }
    }
    
    Login or Signup to reply.
  2. 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.

    let ballIsOn = false;
    let bodyIsClicked = false;
    
    let cursor = document.querySelector(".ball1");
    
    const cursorExplainingDiv = document.querySelector(".cursor-explaining-div");
    
    // NOTE You of course could change initialization() to an iife if you prefer
    initialization( );
    
    function initialization( ) {
      window.addEventListener("mousemove", function (e) {
        if( ballIsOn ) {
          let newX = e.clientX;
          let newY = e.clientY;
    
          cursor.style.left = newX - 15 + "px";
          cursor.style.top = newY - 15 + "px";
    
          if( ballIsOn ) {
            let tag = e.target.tagName
            if( tag == "H1" || tag == "H2" || tag == "P" || tag == "BUTTON" || tag == "IMG" ) {
              cursor.classList.add( "special_cursor" )
            }
            else cursor.classList.remove( "special_cursor" );
          }
        }
      });
    
      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);
          });
        }
      }
    }
    
    
    const landingBallVar = function landingBalls(e) {
      if (ballIsOn === false) {
        ballIsOn = true;
        let clickedBall = e.target;
        let cursor = clickedBall;
    
        cursor.classList.add( "cursor" )
    
        let x = e.clientX;
        let y = e.clientY;
        cursor.style.left = x - 15 + "px";
        cursor.style.top = y - 15 + "px";
      }
    };
    
    function resetBallToInitialState() {
      cursor.classList.remove( "cursor" );
      cursor.classList.remove( "special_cursor" );
      cursor.style.left = "0px";
      cursor.style.top = "0px";
    
      ballIsOn = false;
    }
    
    document.addEventListener("click", (e) => {
      if (ballIsOn && !e.target.closest(".ball1")) {
        resetBallToInitialState();
      }
    });
    /* NOTE Add this: When the div is a cursor */
    .ball.cursor {
      width: 30px;
      height: 30px;
      z-index: 99;
      opacity: 1;
      transition: .1s ease;
      pointer-events: none;
      /* NOTE Add this: Need "absolute" to make the ball <div> moveable */
      position: absolute;
    }
    
    /* NOTE Add this: When the cursor is over some element types */
    .ball.cursor.special_cursor {
      width: 40px;
      height: 40px;
      background-color: #bc7025;
    }
    
    /* NOTE The rest of this css is just here to make the snippet work
            I assume it can just be replaced with your current css
     */
    .flex {
      display: flex;
    }
    
    a {
      margin: 0px 6px;
      padding: 0px 4px;
      box-shadow: 1px 1px 3px black;
    }
    
    .ball {
      position: relative;
    
      box-shadow: 2px 2px 6px green;
      border: solid 1px gray;
      width: 40px;
      height: 20px;
      border-radius: 100%;
    
      margin: 10px;
    
      cursor: pointer;
    }
    .ball:hover {
      background-color: cyan;
    }
    
    .left-landing {
      box-shadow: 2px 2px 6px blue;
      border: solid 1px gray;
      margin: 10px;
      padding: 6px;
    }
    
    .right-landing {
      box-shadow: 2px 2px 6px yellow;
      border: solid 1px gray;
      margin: 10px;
    }
    
    .landing-image {
      width: 64px;
      margin: 6px;
      padding: 6px;
      border-radius: 10px;
    }
    <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 ball1" id="ball1"></div>
      <!-- <div class="ball ball2" id="ball2""></div> -->
      <div class="ball ball2" id="ball2"></div>
      <!-- <div class="ball ball3" id="ball3"()"></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="img/What is Web Design.jpg" class="landing-image" alt=""> -->
        <img src="images/card_back.png" class="landing-image" alt="">
      </div>
    </section>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search