Make For Loop pause until click on div - JavaScript - javascript

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. The loop must stop when all boxes have been displayed.
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 class=" box 'type' "> // type -> can be '"success" or "warning"
// BOX BODY
<div class="box-close" 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";
}

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, set click event to toggle box
closeBox; closes a box
openNext; Call closeBox for current box, clear any timeout's that are set 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;
}
// Stop the loop if we've reached the last box
if (index >= nBoxes) {
console.log('Done!')
return;
}
// 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>

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 class="box-wrapper">
<div class=" box type success">
one
<div class="box-close"> X </div>
</div>
<div class=" box type ">
two
<div class="box-close"> X </div>
</div>
<div class=" box type ">
three
<div class="box-close"> X </div>
</div>
</div>
<div class="not-a-box">I'm not thier brother, don't involve me in this!</div>

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

Related

Prevent paragraph from adding new line (JAVASCRIPT, HTML)

I have a program I'm writing that will display countries and sub-county via an array of information. I've decided to include a part where instead of displaying in a text area, I just want it to display via paragraph output.
However, if the user clicks the button again, it will keep copying and pasting the output. I want to prevent this in case the user does this action
[Current Result after button is pressed multiple times ][1] https://i.stack.imgur.com/enZVW.png
It displays the result multiple times if the button is clicked again.
[How I want it to look like after pressing the button multiple times][2] https://i.stack.imgur.com/dXqYE.png
HTML
<input type="input" id="city"><br><br>
<button id="button2"> <!-- Giving button an ID to be called out in our init function and add an eventlistener -->
Show country/subcountry</button><br><br><br>
<!-- <textarea readonly id="countryOut" style="overflow-y:scroll;
resize: none; margin-left: 2.7em; " ></textarea><br><br> -->
<p id = "countryOut"></p><br><br>
JAVASCRIPT
// display += `${sub}, ${co}\n \n`; // display subcountry, and country with new lines included for spacing
p2.innerHTML += `${sub}, ${co}\n \n`;
}
}
}
function init() {
var button = document.getElementById("button1"); // When country is entered, cities will display
button.addEventListener("click", getCountrySub); // when click event/action is performed, the function of getCountry will execute
var button2 = document.getElementById("button2"); // when city is entered, the region, country, sub country, etc. will display
button2.addEventListener("click", getCities); // when click event/action is performed, the function of getCities will execute
}```
+= sign is making duplicated texts.
Fix this to = will work what you intended.
// AS-IS
p2.innerHTML += `${sub}, ${co}`
// TO-BE
p2.innerHTML = `${sub}, ${co}`
Feels like the code is incomplete, assuming that this is a loop that iterates through both lists
p2.innerHTML += `${sub}, ${co}`
Then I think you are missing a cleanup before you start the output, so before the loops start try this:
p2.innerHTML = ""; // trick is here, when button is clicked clear old results, then show new information
for (const co of countries) { // Please fix to your variable names
for (const sub of co.sub) {
p2.innerHTML += `${sub}, ${co}`;
}
}

Filter from duplicate when executing a function

Good afternoon, I have a script that searches for divs by content and changes it, the script is executed when I click on the button, after the click they are rendered or deleted, but if I click on the button, and the content of the div of the last button will match the content of the next button of the div of the past the buttons will not disappear, and because of this, the script will repeatedly try to change the contents of the past divs.
I need that when switching to a new button, the script does not touch the divs that were in the previous button. how to implement it please tell me.
My code:
const boxes = Array.from(document.getElementsByClassName("btn-default"));
boxes.forEach((box) => {
box.addEventListener("click", function handleClick(event) {
console.log("box clicked", event);
var textProp = "textContent" in document ? "textContent" : "innerText";
[].slice
.call(document.querySelectorAll("button"), 0)
.forEach(function (aEl) {
if (aEl[textProp].indexOf("All") > -1) {
function convert() {
var pageDivs = document.getElementsByClassName("product__price");
for (i = 0; i < pageDivs.length; i++) {
const exchange = 57;
let sum = document
.getElementsByClassName("product__price")
[i].innerText.replace("EUR", "USD");
sum = sum.split(" ")[0];
sum = sum / exchange;
document.getElementsByClassName("product__price")[i].innerHTML =
sum + " EUR";
}
}
convert();
}
});
});
});
The code scans the buttons by class, and when I click on them, the page content is updated, and the script changes the contents of the divs, and as I said, if the contents of the buttons match, they will remain on the page and the script will try to change them again, I need the script to not changed past divas, if they remained.
I click on this button
<button role="button" class="btn btn-default active" ng-class="{'active': cat.id === Store.categoryId}" ng-click="Store.setCategory(cat.id)">All</button>
After clicking, about 5-10 such divs are created with different numbers
<div class="product__price">
111 USD
</div>
<div class="product__price">
555 USD
</div>
After i click on the button again and several divs are created again, but the old div remain, and some old div disappear, I need my script to not change the divs that remained after the click.
When you click on the button, the divs appear in the DOM, on the next click, some of them disappear, some remain, those that remain should not be changed by the script on the next click.

Replace DIV Element When Clicked On It 3 Times

I want to replace a specific div element with a different one, when it has reached 3 clicks on it. That is the only task, I am trying to accomplish with the code.
I have tried looking at some code that does this but all of them replace it with get go, they don't give you a number amount to specify when to replace it with.
Example: <div id="1"></div> has been clicked on 3 times by a user. Once it exceeds that amount replace it with <div id="3"></div>
Changing the id attribute is not a good idea, instead you can use data- attribute like the following way:
var count = 0; // Declare a variable as counter
$('#1').click(function(){
count++; // Increment the couter by 1 in each click
if(count == 3) // Check the counter
$(this).data('id', '3'); // Set the data attribute
console.log($(this).data('id'));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="1" data-id="1">Click</div>
You could write a JavaScript function that keeps track how often you clicked on a specific DOM element (i. e. the div element with id="1"). As soon as the element was clicked three times, it will be replaced by another DOM element which can be created in JavaScript as well.
var clicks = 0;
function trackClick(el) {
clicks++;
if(clicks === 3) {
var newEl = document.createElement('div');
newEl.textContent = 'Div3';
newEl.id = '3';
el.parentNode.replaceChild(newEl, el);
}
}
<div id="1" onclick="trackClick(this)">Div1</div>
In case you should use a library like jQuery or have another HTML structure, please specify your question to improve this code snippet so that it fits for your purpose.
The main idea is to start listening click events on the first div and count them.
The below code shows this concept. Firstly we put first div into variable to be able to create event listeners on it and also create count variable with initial value: 0. Then pre-make the second div, which will replace the first one later.
And the last part is also obvious: put event listener on a div1 which will increment count and check if it is equal 3 each time click happens.
const div1 = document.querySelector('#id-1');
let count = 0;
// pre-made second div for future replacement
const divToReplace = document.createElement('div');
divToReplace.id = 'id-2';
divToReplace.innerText = 'div 2';
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
div1.parentNode.replaceChild(divToReplace, div1);
}
});
<div id="id-1"> div 1 </div>
Note that this approach is easy to understand, but the code itself is not the best, especially if you will need to reuse that logic. The below example is a bit more complicated - we create a function which takes 2 arguments: one for element to track and another - the element to replace with. Such approach will allow us to reuse functionality if needed.
function replaceAfter3Clicks(elem, newElem) {
let count = 0;
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
elem.parentNode.replaceChild(newElem, elem);
}
});
}
const div1 = document.querySelector('#id-1');
// pre-made second div for future replacement
const div2 = document.createElement('div');
div2.id = 'id-2';
div2.innerText = 'div 2';
replaceAfter3Clicks(div1, div2);
<div id="id-1"> div 1 </div>
If you know, how to use JQuery, just put a click event handler on your div 1. On that handler, increment a click counter to 3. If it reaches 3, replace the div with JQuery again.
If there are multiple divs to replace, use an array of counters instead of a single one, or modify a user-specific data attribute via JQuery.
Using native JavaScript, rather than relying upon library (for all the benefits that might offer), the following approach is possible:
// A named function to handle the 'click' event on the relevant elements;
// the EventObject is passed in, automatically, from EventTarget.addEventListener():
const replaceOn = (event) => {
// caching the element that was clicked (because I'm using an Arrow function
// syntax we can't use 'this' to get the clicked element):
let el = event.target,
// creating a new <div> element:
newNode = document.createElement('div'),
// retrieving the current number of clicks set on the element, after this
// number becomes zero we replace the element. Here we use parseInt() to
// convert the string representation of the number into a base-10 number:
current = parseInt(el.dataset.replaceOn, 10);
// here we update the current number with the decremented number (we use the
// '--' operator to reduce the number by one) and then we update the
// data-replace-on attribute value with the new number:
el.dataset.replaceOn = --current;
// here we discover if that number is now zero:
if (current === 0) {
// if so, we write some content to the created <div> element:
newNode.textContent = "Original element has been replaced.";
// and here we use Element.replaceWith() to replace the current
// 'el' element with the new newNode element:
el.replaceWith(newNode);
}
};
// here we use the [data-replace-on] attribute-selector to search
// through the document for all elements with that attribute, and
// use NodeList.forEach() to iterate over that NodeList:
document.querySelectorAll('[data-replace-on]').forEach(
// using an Arrow function we pass a reference to the current
// Node of the NodeList to the function, and here we use
// EventTarget.addEventListener() to bind the replaceOn function
// (note the deliberate lack of parentheses) to handle the
// 'click' event:
(element) => element.addEventListener('click', replaceOn)
);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
div {
border: 1px solid #000;
display: inline-block;
padding: 0.5em;
border-radius: 1em;
}
div[data-replace-on] {
cursor: pointer;
}
div[data-replace-on]::before {
content: attr(data-replace-on);
}
<div data-replace-on="3"></div>
<div data-replace-on="13"></div>
<div data-replace-on="1"></div>
<div data-replace-on="21"></div>
<div data-replace-on="1"></div>
<div data-replace-on="6"></div>
<div data-replace-on="4"></div>
References:
CSS:
Attribute-selectors ([attribute=attribute-value]).
JavaScript:
Arrow function syntax.
ChildNode.replaceWith().
document.querySelectorAll().
EventTarget.addEventListener().
NodeList.prototype.forEach().

JavaScript function to replace HTML contents for a few seconds, then revert back.

I'm making a hangman game using JavaScript and need to hide some HTML for a few seconds to display an error message, and then revert back to the original HTML. I've tried using setTimeout(); and setInterval(); but those seem to just wait a few seconds before displaying the error message.
Here's the code for reference:
<div class="row text-center">
<div id="alredGuess" class="col">
<div>
<span id="guessedLetters"></span> <br>
</div>
<div>Guesses left:<span id="guessesLeft">10</span></div>
<div>Wins:<span id="wins">0</span></div>
<div>Losses:<span id="losses">0</span></div>
</div>
</div>
JS:
if (gameRunning === true && guessedLetterBank.indexOf(letter) === -1) {
// run game logic
guessedLetterBank.push(letter);
// Check if letter is in picked word
for (var i = 0; i < pickedWord.length; i++) {
//convert to lower case
if (pickedWord[i].toLowerCase() === letter.toLowerCase()) {
//if match, swap placeholder
pickedWordPlaceholderArr[i] = pickedWord[i];
}
}
$placeholders.textContent = pickedWordPlaceholderArr.join("");
checkIncorrect(letter);
} else if (gameRunning === false) {
$placeholders.textContent = "Press \"A\" To Begin!"
} else {
//alert("You've already guessed this letter.")
function newAlert() {
var hideDiv = document.getElementById("alredGuess");
if (hideDiv.style.display = "block") {
hideDiv.style.display = "none";
}
}
hideDiv.textContent("You've already guessed this letter!");
function showDiv() {
var showDiv = document.getElementById("alredGuess");
if (hideDiv.style.display = "none") {
hideDiv.style.display = "block";
}
}
}
}
setInterval(newAlert, 3000);
}
Tip 1
Well, first of all i don't recommend using display: block|none to show or hide DOM elements. Instead try using visibility: visible|hidden or better, toggle a css class name such as : .hidden. That's because when you set a DOM element's display to none, its width and height are gonna be set to zero, often causing an unwanted loss of space because the DOM node visually collapses. With the visibility property, for example, the element just disappears without loss of space.
Tip 2
Error/status messages should always live within their own containers. Do not display messages in substitution of some content you need to revert back after.
It is always better to prepare an empty <div>, hide it by default with a generic .hiddenCSS class and then remove this one as soon as you need to display the container.
Suggested solution
Now, in your case, i think you're using setIntervalin the wrong way. You have to immediately show the alert message, then make it disappear after a few seconds.
As suggested above, this should be done by toggling CSS classes, using different containers and using setTimeout in order to remove/add the CSS classes as soon as the interval is over. Basically, the setTimeout restores everything to its original state.
So, given this HTML code:
<div id="alredGuess">This is the original text</div>
<div id="alertbox" class="hidden"></div>
and this CSS code:
.hidden { visibility: hidden; }
try this:
var alertTimeout = 1000; // Timeout in milliseconds.
function showAlertMessage() {
// This is your original text container.
var alredGuess = document.getElementById("alredGuess");
// This is the new error message container named #alertbox
var alertBox = document.getElementById("alertbox");
// Now let's fill it with the specific error text (better using HTML here).
alertBox.innerHTML = "You've already guessed this letter!";
// Hide the original container by adding an .hidden css class.
alredGuess.classList.add('hidden');
// Show the error message container by removing its default .hidden css class.
alertBox.classList.remove('hidden');
// Then set up an interval: as it ends, revert everything to its original state.
setTimeout(function() {
alertBox.classList.add('hidden');
alredGuess.classList.remove('hidden');
}, alertTimeout);
}
// Call the function.
showAlertMessage();
Fiddle: https://jsfiddle.net/qyk4jspd/
Hope this helps.

Increase value of counter by one upon class creation/append

Creating a system that increases the 'fault' counter by a value of one upon every wrong answer submitted. I'm doing this by having my system listen out for the creation of a class called "incorrectResponse". However, it appears to just add to the counter as soon as the page finishes loading and doesn't add any more values after that.
Here is the code I've tried that isn't working.
//*-- Fault counter --*//
if (document.querySelectorAll('incorrectResponse')) {
$('#fault-counter').html(function(i, val) { return val*1+1 });
};
Any reason as to why this is the case?
Assumming you're using jQuery (you mention it in one of your comments), one thing you could do is attach an event listener to the container of your 'wrong answers'.
A custom event would be triggered manually any time a wrong answer is received/appended to the page and the event listener would react to it recalculating the number of wrong answers and updating the counter.
function updateNumberOfIncorrectMsgs() {
$('.counter').text($('.incorrectAnswer').length);
}
updateNumberOfIncorrectMsgs();
var $container = $('.wrongAnswersContainer')
.on('newWrongAnswerAdded', function() {
updateNumberOfIncorrectMsgs();
});
// The for and setTimeout is only to simulate msgs appended to the page, the important part is the custom event that gets triggered when an element is added.
for (var i = 1; i < 6; i++) {
setTimeout(function() {
$container
.append('<p class="incorrectAnswer">Incorrect answer</p>')
.trigger('newWrongAnswerAdded');
}, 1000 * i);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>Incorrect answer counter: <span class="counter"></span>
</p>
<div class="wrongAnswersContainer"></div>

Categories