I'm trying to write a code that displays several boxes one after the other, but only after the previous one has been closed. This can be done in two ways. Either the box closes automatically after 10 seconds, or it stays up indefinitely until it is closed by clicking "X".
I am trying to use a for loop to iterate over an array (mandatory) of these boxes, but I cannot work out how to 'pause' the loop to wait for user action. Does anyone know how this could be done (without jQuery)? I've tried using setTimeout, but then realized it cannot be done this way. I'm new to programming, so it's all a bit confusing, if anyone could help I'd really appreciate it! It may be worth mentioning that I'd prefer not to use id's.
I've tried to simplify my code to be easier to read:
HTML:
// Simplified - every element has this structure, only the class changes for the parent div
<div > // type -> can be '"success" or "warning"
// BOX BODY
<div onClick="removeBox()"> X </div>
</div>
CSS
.box{display="none";}
JavaScript
// Simplified - each box div present in page is stored in array allBoxes
allBoxes = array of boxes
//Show boxes 1 by 1
for (j = 0; j < allBoxes.length; j ) {
showBox(allBoxes[j]);
}
function showBox() {
box=allBoxes[j];
box.style.display= "block";
if (box.classList.contains("success")==true){
setTimeout(removeBox, 10000); //PROBLEM: only executes after for loop is done, meaning it 'removes' the last div in the array being looped, regardless of type class
//alternative
setTimeout(removeBox(box), 10000); //Problem: executes remove immediately, without waiting the 10s
}
else{
//something to make the For Loop pause until the user clicks on X
box.querySelector(".box-close").addEventListener("click",removeBox); //doesn't do anything, loop continues
//alternative
box.querySelector(".box-close").addEventListener("click",removeBox(box)); //simply removes box immediately (not in response to click or anything), loop continues
}
}
function removeBox() {
box.style.display = "none";
}
CodePudding user response:
My take on this is to actually use setTimeout()
. We can assign an onClick
next to the timeout that both will show the next box. If needed, the timeout can be canceled using clearTimeout()
So the next box will be shown after 3
seconds, or when the previous box is closed (clicked in my demo below)
To give an example, please see the demo below, were we have 3 main functions:
openBox
; opens a box, starts the timeout, setclick
event to toggle boxcloseBox
; closes a boxopenNext
; CallcloseBox
for current box, clear any timeout's that are set, wrap the loop counter back to0
and ofc callopenBox
to open the next one
Please see additional explanation in the code itself.
const nBoxes = 5; // Number of boxes
let index = 0; // Current box index
let count = null; // setTimeout pid
// Function to open boxes, assign onClick and start the count-down
const openBox = (n) => {
var e = document.getElementById('box_' n);
e.style.display = 'block';
e.onclick = openNext;
count = setTimeout(openNext, 3000);
}
// Function to close a box
const closeBox = (n) => document.getElementById('box_' n).style.display = 'none';
// Function to cycle to the next box
const openNext = () => {
// Close current open box
if (index > 0) {
closeBox(index);
}
// Stop any count-downs
if (count) {
clearTimeout(count);
count = null;
}
// Reset the loop if we've reached the last box
if (index >= nBoxes) {
index = 0;
}
// Bump index and open new box
index ;
openBox(index);
};
// Start; open first box
openNext()
.box {
display: none;
}
<div id='box_1' class='box'>1</div>
<div id='box_2' class='box'>2</div>
<div id='box_3' class='box'>3</div>
<div id='box_4' class='box'>4</div>
<div id='box_5' class='box'>5</div>
CodePudding user response:
The only thing you need the loop for is to assign the click event listener to the close buttons. Inside the click event listener we hide the current box and then find the next box and show it, if it exists.
Note: In the following snippet, the .box-wrapper
element is necessary to isolate all the boxes from any other siblings so that box.nextElementSibling
will properly return null
when there are no more boxes left to open.
const autoBoxAdvanceTime = 10000
const allBoxes = document.querySelectorAll('.box')
allBoxes.forEach(box => box.querySelector('.box-close').addEventListener('click', () => nextBox(box)))
//Show first box
showBox(allBoxes[0]);
function showBox(box) {
box.style.display = "block";
if (box.classList.contains("success")) {
console.log(`Going to next box in ${autoBoxAdvanceTime/1000} seconds`)
setTimeout(() => {
// only advance automaticaly if the box is still showing
if (box.style.display === "block")
nextBox(box)
}, autoBoxAdvanceTime);
}
}
function nextBox(box) {
box.style.display = "none"
const next = box.nextElementSibling
if (next) {
console.log('going to box:', next.textContent)
showBox(next)
} else {
console.log('last box closed')
}
}
.box,
.not-a-box {
display: none;
}
.box-close {
cursor: pointer;
}
<div >
<div >
one
<div > X </div>
</div>
<div >
two
<div > X </div>
</div>
<div >
three
<div > X </div>
</div>
</div>
<div >I'm not thier brother, don't involve me in this!</div>
CodePudding user response:
Instead of a real loop, use a function that re-invokes itself after a delay.
showBox
should not take the box index as an argument
Instead, it should look at the allBoxes
array directly. You might also need to keep track of which boxes have already been dealt with, which could be done either by maintaining a second list of boxes, or with a simple counter; either way, you'd define that variable outside the function so that it would retain its value across invocations of showBox
.
showBox
should call itself as its final step
Instead of relying on the loop to call showBox
, have showBox
do that itself. (This is known as "tail recursion" -- a function that calls itself at its own end.)
To add a 6-second delay, you'd wrap that invocation in a setTimeout
.
nextBoxTimer = setTimeout(showBox, 6000)
- integrate the tail-recursing loop with manual box closing
When a user manually closes a box, you want to stop the current timer and start a new one. Otherwise, a person could wait 5 seconds to close the first box, and then the second box would automatically close 1 second later.
So, make showBox
begin by canceling the current timer. This will be pointless when showBox
is running because it called itself, but it's crucial in cases when showBox
is running because the user pre-empted the timer by closing the previous box herself.
For this to work, you'll need to define the variable outside of showBox
(which is why the snippet in step 2 doesn't use let
when assigning into nextBoxTimer
).