Home > Net >  Make For Loop pause until click on div - JavaScript
Make For Loop pause until click on div - JavaScript

Time:09-22

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:

  1. openBox; opens a box, starts the timeout, set click event to toggle box
  2. closeBox; closes a box
  3. openNext; Call closeBox for current box, clear any timeout's that are set, wrap the loop counter back to 0 and ofc call openBox 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.

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

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

  • Related