user1406440
user1406440

Reputation: 1655

Stop running JS when class is added to HTML - Run again when class is removed

I have a 'theme toggle' which changes the look/feel of my UI. There is also a custom JS cursor that I don't need on one of the themes. I thought it would be a good idea to kill the script when the relevant class is on html as there's a lot of calculation going on with the pointer position. Rather than just use display: none and leave the script running.

There are 3 modes/themes:

I tried to achieve this using the following method. To test, I add the .retro class to html so it's the first theme, it does stop the script from running and clicking the toggle turns the script on again. But it only does it once.

Obviously removing the class onload doesn't work either but I just wanted to do the above to see if anything was happening. Can someone tell me where I'm going wrong. What's the best approach?

Essentially what I'm trying to achieve is: Run this script, unless the class .retro is present, if so stop it. If the class .retro is removed, run the script again.

/* Toggle */

const html = document.querySelector("html");
const button = document.querySelector(".contrast__link");

button.addEventListener("click", (e) => {
    e.preventDefault();
    if (html.classList.contains("dark-mode")) {
        html.classList.remove("dark-mode");
        html.classList.add("retro");
        let slideIndex = swiper.activeIndex;
        swiper.destroy(true, true);
        swiper = initSwiper("slide", false, 999999999); // Retro: This should slide, no autoplay or loop
        swiper.slideTo(slideIndex, 0);
    } else if (html.classList.contains("retro")) {
        html.classList.remove("retro");
        let slideIndex = swiper.activeIndex;
        swiper.destroy(true, true);
        swiper = initSwiper("fade", true, 1200); // Default: This should fade, autoplay & loop
        swiper.slideTo(slideIndex, 0);
    } else {
        html.classList.add("dark-mode");
    }
});

/* Cursor */

function createHandler(callback) {
    return function(event) {
        if (!document.documentElement.classList.contains('retro')) {
            callback.call(this, event)
        }
    }
}

window.addEventListener('mousemove', createHandler(function() {

    var cursor = document.querySelector(".cursor");
    var cursorTrail = document.querySelector(".cursor-trail");
    var a = document.querySelectorAll("a");
    var timeout;

    window.addEventListener(
        "mousemove",
        function(e) {
            var x = e.clientX;
            var y = e.clientY;
            cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
            if (!timeout) {
                timeout = setTimeout(function() {
                    timeout = null;
                    cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`;
                }, 24);
            }
        },
        false
    );

    /**
     * Add/remove classes on click (anywhere).
     */

    document.addEventListener("mousedown", function() {
        cursor.classList.add("cursor--click");
    });

    document.addEventListener("mouseup", function() {
        cursor.classList.remove("cursor--click");
    });
    
    /**
     * Add/remove set classes on hover.
     * 
     * 1. This used to start with `a.forEach((item) => {` but changed to `let` so
     *    that an additional (non-anchor) item could be targeted. `#hello` is for
     *    the image on the 404 page.
     */

    // a.forEach((item) => {
    let links = document.querySelectorAll('a, #hello'); /* [1] */
    links.forEach((item) => { /* [1] */
        item.addEventListener("mouseover", () => {
            cursorTrail.classList.add("cursor-trail--hover");
        });
        item.addEventListener("mouseleave", () => {
            cursorTrail.classList.remove("cursor-trail--hover");
        });
    });
    
    /**
     * Add custom classes on hover if the cursor needs to be manipulated in a
     * unique way. If an element has a `data-interaction=""` value set. This will
     * be added as a class to the cursor on hover. For example, this is used to
     * style the prev/next arrows on the carousel.
     *
     * This could be set using a specific class but I've just left it targeting all
     * `a` elements for now. Which will add a class of `undefined` if no dataset is
     * specified.
     */

    a.forEach((item) => {
        const interaction = item.dataset.interaction;

        item.addEventListener("mouseover", () => {
            cursor.classList.add(interaction);
        });
        item.addEventListener("mouseleave", () => {
            cursor.classList.remove(interaction);
        });
    });
}))
.contrast__link {
  background: white;
}

.cursor {
  background: red;
  height: 20px;
  width: 20px;
  position: fixed;
}

.dark-mode {
  background: black;
}

.retro {
  background: blue;
}
<a href="" class="contrast__link">Toggle Classes</a>

<div class="cursor"><span></span></div>
<div class="cursor-trail"></div>

Upvotes: 1

Views: 266

Answers (4)

Drom Hour
Drom Hour

Reputation: 94

You can use element insertion and window properties to load/unload code. Look at code in snippet, there's simple system for theme changing. When on retro, mouse clicks will be logged in console. In my snippet, script will be working only on retro theme, but that doesn't changes concept.

const themeList = {
  'default': {
    enable() {
      app.classList.add('default');
    },
    disable() {
      app.classList.remove('default');
    },
  },
  'dark-mode': {
    enable() {
      app.classList.add('dark-mode');
    },
    disable() {
      app.classList.remove('dark-mode');
    },
  },
  'retro': {
    enable() {
      app.classList.add('retro');
      js = document.createElement("script");
      js.id = 'retroThemeScript';
      js.innerHTML = `
        window.fnToBeRemoved = () => console.log('mouse down');
        app.addEventListener("mousedown", window.fnToBeRemoved);
      ` // you can replace code with next line, so no need to store code in RAM
      //js.src = '/scripts/retroScript.js';
      js.type = 'text/javascript';
      app.appendChild(js);
      this.scriptElement = js;
    },
    disable() {
      app.classList.remove('retro');
      app.removeEventListener('mousedown', window.fnToBeRemoved);
      delete window.fnToBeRemoved;
      app.removeChild(this.scriptElement);
    },
  }
}

const app = document.querySelector('#app');


let currentTheme;

function setTheme(themeName) {
  if (currentTheme)
    themeList[currentTheme].disable();
  themeList[themeName].enable();
  currentTheme = themeName;
}

setTheme('default');
#app {
  font-size: 24px;
  width: 500px;
  height: 500px;
  background: silver;
}

#app.retro{
  background: magenta;
}

#app.dark-mode { 
  background: gray;
}
<button onclick="setTheme('default')">Default</button>
<button onclick="setTheme('dark-mode')">Dark</button>
<button onclick="setTheme('retro')">Retro</button>

<div id="app">
  Wanna cake?
</div>

Upvotes: 0

Erick Petrucelli
Erick Petrucelli

Reputation: 14912

As @Mykhailo Svyrydovych suggested, to avoid any overhead of the unneeded events call, you would need to have explictly named functions for each event handler, then you could use removeEventListener when you're toggling to the retro theme, removing the handler of each mousemove, mouseenter, mouseleave, mouseup and mousedown events that you don't want to run on that theme. Of course, you would need to re-bind all the events again using the proper addEventListener calls when toggling to the other two themes.

But I think that, sometimes, the easiest approach is good enough. If you measure the performance and then discover that the events keeping to fire isn't a big concern for your case, go for it. You can easily avoid the unwanted effects of the custom cursor, inside each event handler, just by checking a simple boolean:

const html = document.documentElement;
const button = document.querySelector(".contrast__link");
const cursor = document.querySelector(".cursor");
const cursorTrail = document.querySelector(".cursor-trail");
const a = document.querySelectorAll("a");
let timeout;
let customCursor = true;

button.addEventListener("click", (e) => {
  e.preventDefault();
  if (html.classList.contains("dark-mode")) {
    html.classList.remove("dark-mode");
    html.classList.add("retro");
    customCursor = false;
    cursor.style.display = "none";
  } else if (html.classList.contains("retro")) {
    html.classList.remove("retro");
    customCursor = true;
    cursor.style.display = "block";
  } else {
    html.classList.add("dark-mode");
    customCursor = true;
    cursor.style.display = "block";
  }
});

window.addEventListener(
  "mousemove",
  function(e) {
    if (!customCursor) return;
    var x = e.clientX;
    var y = e.clientY;
    cursor.style.display = "block";
    cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
    if (!timeout) {
      timeout = setTimeout(function() {
        cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`;
      }, 24);
    }
  },
  false
);

document.addEventListener("mousedown", () => {
  if (!customCursor) return;
  cursor.classList.add("cursor--click");
});

document.addEventListener("mouseup", () => {
  if (!customCursor) return;
  cursor.classList.remove("cursor--click");
});

let links = document.querySelectorAll('a, #hello');
links.forEach((item) => {
  item.addEventListener("mouseover", () => {
    if (!customCursor) return;
    cursorTrail.classList.add("cursor-trail--hover");
  });
  item.addEventListener("mouseleave", () => {
    if (!customCursor) return;
    cursorTrail.classList.remove("cursor-trail--hover");
  });
});

a.forEach((item) => {
  const interaction = item.dataset.interaction;
  item.addEventListener("mouseover", () => {
    if (!customCursor) return;
    cursor.classList.add(interaction);
  });
  item.addEventListener("mouseleave", () => {
    if (!customCursor) return;
    cursor.classList.remove(interaction);
  });
});
.contrast__link {
  background: white;
}

.cursor {
  background: red;
  height: 20px;
  width: 20px;
  position: fixed;
  display: none;
}

.dark-mode {
  background: black;
}

.retro {
  background: blue;
}
<a href="" class="contrast__link">Toggle Classes</a>

<div class="cursor"><span></span></div>
<div class="cursor-trail"></div>

P.S.: I removed the Swiper calls from my code sample because they appeared to be totally unrelated to the question being asked, and were causing console errors.

Upvotes: 1

Mykhailo Svyrydovych
Mykhailo Svyrydovych

Reputation: 107

I would suggest to add and remove "mousemove" listener inside "click" listener, when you toggling the theme. So if user selects "retro", the event listener should be removed, and when it is another theme, the listener should be added.

Upvotes: 2

dev.skas
dev.skas

Reputation: 664

I have tried with simple CSS code, it's working

 .retro .cursor{
    display:none
}

but you want to stop the function also, so tried the following that also worked. When the theme is retro on mouse move the cursor changing function is not calling

/* Toggle */

const html = document.querySelector("html");
const cursor = document.querySelector(".cursor");

function addClass() {
  if (html.classList.contains("dark-mode")) {
    html.classList.remove("dark-mode");
    html.classList.add("retro");
    cursor.style.display = 'none'

  } else if (html.classList.contains("retro")) {
    html.classList.remove("retro");

  } else {
    html.classList.add("dark-mode");
  }
}

document.addEventListener('mousemove', function(e) {
  if (html.classList.contains("retro")) {
    cursor.style.display = 'none'
  } else {
    cursor.style.display = 'block'
    var x = e.clientX;
    var y = e.clientY;
    cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
  }
});
.cursor {
  background: red;
  height: 20px;
  width: 20px;
  position: fixed;
}

.dark-mode {
  background: black;
}

.retro {
  background: blue;
}
<div class="cursor"></div>
<button onclick='addClass()'>Add Class</button>

Upvotes: 1

Related Questions