disable click event for nested div elements until setTimeout is over - javascript

I have this script where I highlight the clicked div and its parents, then I make them white again respectively. However, I would like to stop consecutive clicks, allow only one click until setTimeout finishes. Basically, user should wait for the whole animation to finish before clicking again.
const allDivElements = document.querySelectorAll("div");
let timeout = 300;
allDivElements.forEach((div) => {
div.addEventListener("click", function (e) {
setTimeout(() => {
changeBg(this, true);
setTimeout(() => {
changeBg(this, false);
timeout = 300;
}, timeout);
}, timeout);
timeout += 300;
});
});
function changeBg(div, phase) {
if (phase) div.style.backgroundColor = "lightblue";
else div.style.backgroundColor = "#fff";
}
As far as I looked over the possible solutions, I was not able to find one that prevents click events until setTimeout methods. Any detailed help will be appreciated.
EDIT: Sorry if I cause confusion. This is the link to the whole application in case you'd like to test it out: https://codesandbox.io/s/busy-goldstine-ope9w?file=/src/index.js
Thanks in advance!

It looks like you're trying to make this too complicated. Just use a Boolean value for each element to track whether the click should work or not:
const timeout = 300;
allDivElements.forEach((div) => {
let clickAllowed = true;
div.addEventListener("click", function (e) {
if (clickAllowed) {
clickAllowed = false;
changeBg(this, true);
setTimeout(() => {
changeBg(this, false);
clickAllowed = true;
}, timeout);
}
});
});

Handle your listeners using addEventListener and removeEventListener
Once all the elements are processed, add back the events using addEventListener
A recursion could also come handy to iterate over parent Elements:
const divs = document.querySelectorAll("div");
const EVT = (el, t, n, f, o = {}) => el.forEach(e => e[`${{on:"add",off:"remove"}[t]}EventListener`](n, f, o));
const changeBg = (div) => {
if ([...divs].indexOf(div) < 0) return EVT(divs, "on", "click", clickHandler); // Add
div.style.backgroundColor = "lightblue";
setTimeout(() => {
div.style.backgroundColor = "white";
changeBg(div.parentElement); // Recursive call, this time pass the parent
}, 300);
};
const clickHandler = async(ev) => {
EVT(divs, "off", "click", clickHandler); // Remove listeners
changeBg(ev.currentTarget); // Start
};
EVT(divs, "on", "click", clickHandler); // Add listeners
body {
font-family: sans-serif;
}
div {
border: 1px solid black;
padding: 5px;
background: #fff;
}
<div id="1">1
<div id="2">2
<div id="3">3
<div id="4">4
<div id="5">5
</div>
</div>
</div>
</div>
</div>

Related

How to wait for appendChild() to update and style the DOM, before trying to measure the new element's width

I'm appending a DOM element like this:
this.store.state.runtime.UIWrap.appendChild(newElement)
When I immediately try to measure the new element's width I get 2.
I tried:
setTimeout()
double nested window.requestAnimationFrame()
MutationObserver
The above works very unreliably, like 50% of the time. Only when I set a large timeout like 500ms it worked.
This happens only on mobile.
This is the workaround that I'm using, but it's ugly:
function getWidthFromStyle(el) {
return parseFloat(getComputedStyle(el, null).width.replace('px', ''))
}
function getWidthFromBoundingClientRect(el) {
return el.getBoundingClientRect().width
}
console.log(getWidthFromBoundingClientRect(newElement))
while (getWidthFromBoundingClientRect(newElement) < 50) {
await new Promise(r => setTimeout(r, 500))
console.log(getWidthFromBoundingClientRect(newElement))
}
I tried with both functions getWidthFromStyle() and getWidthFromBoundingClientRect() - no difference. The width gets calculated properly after a couple of cycles.
I also tried using MutationObserver without success.
Is there a way to know when the DOM is fully updated and styled before I try to measure an element's width/height?
P.S. I'm not using any framework. this.store.state.runtime... is my own implementation of a Store, similar to Vue.
EDIT: The size of the element depended on an image inside it and I was trying to measure the element before the image had loaded. Silly.
it can done with MutationObserver.
doesn't this method solve your problem?
const div = document.querySelector("div");
const span = document.querySelector("span");
const observer = new MutationObserver(function () {
console.log("new width", size());
});
observer.observe(div, { subtree: true, childList: true });
function addElem() {
setTimeout(() => {
const newSpan = document.createElement("span");
newSpan.innerHTML = "second";
div.appendChild(newSpan);
console.log("element added");
}, 3000);
}
function size() {
return div.getBoundingClientRect().width;
}
console.log("old width", size());
addElem();
div {
display: inline-block;
border: 1px dashed;
}
span {
background: gold;
}
<div>
<span>one</span>
</div>
You can use something like this:
export function waitElement(elementId) {
return new Promise((resolve, reject) => {
const element = document.getElementById(elementId);
if (element) {
resolve(element);
} else {
let tries = 10;
const interval = setInterval(() => {
const element = document.getElementById(elementId);
if (element) {
clearInterval(interval);
resolve(element);
}
if (tries-- < 0) {
clearInterval(interval);
reject(new Error("Element not found"));
}
}, 100);
}
});
}

How to execute an event in a loop until the event is true

I'm stuck in Javascript at looping a piece of code when an event listener is being
placed.
For example, let's say I have a div:
<div id="option">
</div>
and I added a Javascript mouseenter event listener:
const divItem = document.getElementById("option")
divItem.addEventListner("mouseenter", () => {
console.log("Mouse is entered")
})
Now the console log happens once and after I hover the mouse, but I want it to happen every after 4 seconds
and log the same message in the console until the mouse is moving out of the div.
I tried using setTimeout:
divItem.addEventListner("mouseenter", () => {
const timeoutEvent = () => {
console.log("Mouse is entered")
setTimeout( () => { timeoutEvent() }, 4000 )
}
timeoutEvent()
})
but it is logging even after the mouse left the div,
so how can I solve this?
You're on the right track. If you want every four seconds, you want to:
Use setInterval (or set a new setTimeout every time), and
Cancel it when you see mouseleave
const divItem = document.getElementById("option")
// The timer handle so we can cancel it
let timer = 0; // A real timer handle is never 0, so we can use it as a flag
divItem.addEventListener("mouseenter", () => {
console.log("Mouse is entered");
timer = setInterval(() => {
if (timer) {
console.log("Mouse is still here");
}
}, 1000);
})
divItem.addEventListener("mouseleave", () => {
console.log("Mouse left");
clearInterval(timer);
timer = 0;
});
<div id="option">
this is the div
</div>
(I've used one second in that example instead of four so it's easier to see it working.)
Or using setTimeout rescheduling itself instead:
const divItem = document.getElementById("option")
// The timer handle so we can cancel it
let timer = 0; // A real timer handle is never 0, so we can use it as a flag
const timerInterval = 1000;
function tick() {
if (timer) {
console.log("Mouse is still here");
timer = setTimeout(tick, timerInterval);
}
}
divItem.addEventListener("mouseenter", () => {
console.log("Mouse is entered");
timer = setTimeout(tick, timerInterval);
})
divItem.addEventListener("mouseleave", () => {
console.log("Mouse left");
clearTimeout(timer);
timer = 0;
});
<div id="option">
this is the div
</div>
You should use setInterval instead of setTimeout.
You can define your 'interval' function and the interval in global scope and then use them to set and start the interval execution and clearInterval ( stop de execution ) on mouseenter/mouseleave events.
const divItem = document.getElementById("option")
let myInterval;
const timeoutEvent = () => {
console.log("Mouse is entered")
}
divItem.addEventListener("mouseenter", () => {
myInterval = setInterval(timeoutEvent, 1000);
})
divItem.addEventListener("mouseleave", () => clearInterval(myInterval))
<div id="option">
My Option
</div>
divItem.addEventListener("mouseenter", () => {
console.log("Mouse is entered");
timer = setInterval(() => {
console.log("Mouse is still here");
}
}, 4000);
})

How to constantly trigger an event when hovered over that element?

So, I have a button and I want to constantly have an event triggering when the button is hovered. If I use mouseover, then this event triggers only once when the cursor comes on it from somewhere outside.
btn.addEventListener("mouseover", function(){
console.log("Hello");
});
For instance, I want this console log to happen constantly while the cursor is over the button.
Please check if mousemove event works for you.
let interval = null;
btn.addEventListener("mousemove", function(){
console.log("Hello");
});
btn.addEventListener('mouseenter', function(){
interval= setInterval(()=>console.log('Hello'), 100)
})
btn.addEventListener('mouseout', function(){
clearInterval(interval)
})
#btn{
width: 300px;
height: 60px;
border-radius: 8px;
}
<button id="btn">
hover me
</button>
You could do this by using mouseover and mouseout (and a setInterval). Basically, start an interval when the mouse enters, and clear it when it exits.
let interval;
btn.addEventListener("mouseover", function(){
interval = setInterval(function() {
console.log("Hello");
}, 100);
});
btn.addEventListener("mouseout", function(){
clearInterval(interval);
});
You can use a combination of
flags, either on the elements themselves as here, or as separate variable(s)
a setInterval function
to figure out which elements are being hovered at any given time. (This is obviously a slightly silly example since you can't very well mouse-over multiple elements that sit next to each other at a time.)
function addEventListeners(button) {
button.addEventListener("mouseover", () => button.dataset.hovered = "1");
button.addEventListener("mouseout", () => button.dataset.hovered = "0");
}
function reportHovers(elements) {
const hoveredIds = elements.filter(el => el.dataset.hovered === "1").map(el => el.id);
document.getElementById("out").value = `${Math.floor(new Date() / 1000)}\n${JSON.stringify(hoveredIds)}`;
}
var buttons = Array.from(document.querySelectorAll("button"));
buttons.forEach(button => addEventListeners(button));
setInterval(() => reportHovers(buttons), 100);
<button id="b1">Button 1</button>
<button id="b2">Button 2</button>
<button id="b3">Button 3</button>
<textarea id="out"></textarea>

MutationObserver for when parent changes

Is there a way to detect when the parent of an element changes (namely when changing from null to !null -- i.e., when the element is initially added to the DOM) using a MutationObserver? I can't find any documentation that shows how this could be achieved.
I am programmatically creating elements with document.createElement(). I return the created element from a function, but want to create a listener from within the function to react when the element is eventually added to the DOM, without knowing where or which parent it will be added to.
I'm not quite sure how else to phrase this, honestly.
const elem = document.createElement('div');
let added = false;
elem.addEventListener('added-to-dom', () => { added = true; });
// ^ how do I achieve this?
assert(added == false);
document.body.addChild(elem);
assert(added == true);
I don't see what's so hard about understanding this or why it was closed.
An easy but inelegant way is to monkeypatch Node.prototype.appendChild (and, if necessary, Element.prototype.append, Element.prototype.insertAdjacentElement, and Node.prototype.insertBefore) to watch for when an element is added to the DOM:
const elementsToWatch = new Set();
const { appendChild } = Node.prototype;
Node.prototype.appendChild = function(childToAppend) {
if (elementsToWatch.has(childToAppend)) {
console.log('Watched child appended!');
elementsToWatch.delete(childToAppend);
}
return appendChild.call(this, childToAppend);
};
button.addEventListener('click', () => {
console.log('Element created...');
const div = document.createElement('div');
elementsToWatch.add(div);
setTimeout(() => {
console.log('About to append element...');
container.appendChild(div);
}, 1000);
});
<button id="button">Append something after 1000ms</button>
<div id="container"></div>
Mutating built-in prototypes generally isn't a good idea, though.
Another option would be to use a MutationObserver for the whole document, but this may well result in lots of activated callbacks for a large page with frequent mutations, which may not be desirable:
const elementsToWatch = [];
new MutationObserver(() => {
// instead of the below, another option is to iterate over elements
// observed by the MutationObserver
// which could be more efficient, depending on how often
// other elements are added to the page
const root = document.documentElement; // returns the <html> element
const indexOfElementThatWasJustAdded = elementsToWatch.findIndex(
elm => root.contains(elm)
);
// instead of the above, could also use `elm.isConnected()` on newer browsers
// if an appended node, if it has a parent,
// will always be in the DOM,
// instead of `root.contains(elm)`, can use `elm.parentElement`
if (indexOfElementThatWasJustAdded === -1) {
return;
}
elementsToWatch.splice(indexOfElementThatWasJustAdded, 1);
console.log('Observed an appended element!');
}).observe(document.body, { childList: true, subtree: true });
button.addEventListener('click', () => {
console.log('Element created...');
const div = document.createElement('div');
div.textContent = 'foo';
elementsToWatch.push(div);
setTimeout(() => {
console.log('About to append element...');
container.appendChild(div);
}, 1000);
});
<button id="button">Append something after 1000ms</button>
<div id="container"></div>
You could listen for the DOMNodeInserted-event and compare the elements id.
Notice: This event is marked as Depricated and will probably stop working in modern modern browsers at some point in the near
future.
let container = document.getElementById('container');
let button = document.getElementById('button');
document.body.addEventListener('DOMNodeInserted', function(event) {
if (event.originalTarget.id == button.id) {
console.log('Parent changed to: ' + event.originalTarget.parentElement.id);
}
});
button.addEventListener('click', function(event) {
container.appendChild(button);
});
#container {
width: 140px;
height: 24px;
margin: 10px;
border: 2px dashed #c0a;
}
<div id="container"></div>
<button id="button">append to container</button>

How to make a delay between capturing/bubbling phase

I'm learning event phasing of nested elements so I create small project. Codepen JS starts on 43rd line.
So here's simple nested divs.
<div id="zzz" class="thir">
0
<div id="xxx" class="thir">
0
<div id="sss" class="thir">
0
</div>
</div>
</div>
And here what we do with them.
const ar2 = [zzz, xxx, sss];
ar2.map(e => {
e.addEventListener('click', nestedClick, phase);
})
function nestedClick(e) {
// e.stopPropagation();
const meow = this;
const prevColor = this.style.backgroundColor;
this.style.backgroundColor = '#757575';
window.setTimeout(() => { meow.style.backgroundColor = prevColor}, 500);
}
To visually show how capturing/bubbling works I'd like to change background color and set timeout on each step, wait until it's done and trigger next click with the same strategy.
But here I see after I click on any element event still goes through, changing color and forces all .setTimeout() like at the same time. How can I repair it?
Side question: why e.stopPropagation() works whether it's capturing or bubbling phase?
Thank you for attention!
You need to shift the start time of the timers. And for a flashing effect having a second timer would be good.
let counter = 1;
const ar2 = [...document.getElementsByClassName('thir')];
ar2.map(e => {
e.addEventListener('click', nestedClick);
e.addEventListener('mouseup', function() {
counter = 1;
});
});
function nestedClick(e) {
const prevColor = this.style.backgroundColor;
debugger;
setTimeout( () => {
this.style.backgroundColor = '#757575';
setTimeout( () => {
this.style.backgroundColor = prevColor;
}, 50 * (counter++));
}, 500 * (counter++));
}
<div id="zzz" class="thir">
CLICK ME
<div id="xxx" class="thir">
CLICK ME
<div id="sss" class="thir">
CLICK ME
</div>
</div>
</div>

Categories