Looping an array of modals in Javascript - javascript

I'm having trouble looping the following function.
It works when changing i='x' for each 1,2,3... However, when trying to loop for 'i', it becomes unresponsive. I feel there's something obvious I'm missing!
var i=1;
var modal = [];
var btn = [];
var span = [];
//for(i=1;i<5;i++)
//{
modal[i]= document.getElementById('challenge'+i+'Modal');
btn[i] =document.getElementById("challenge"+i);
span[i] = document.getElementById('challenge'+i+'Close');
btn[i].onclick = function() {
modal[i].style.display = "block";
}
span[i].onclick = function() {
modal[i].style.display = "none";
}
window.onclick = function(event) {
if (event.target == modal[i]) {
modal[i].style.display = "none";
}
}
//}

Thanks #epascarello for the pointer, here is the solution:
var i;
var modal = [];
var btn = [];
var span = [];
function open(i) {
return function() {
modal[i].style.display = "block";
}
}
function closex(i) {
return function() {
modal[i].style.display = "none";
}
}
function close(i) {
return function(event) {
if (event.target == modal[i]) {
modal[i].style.display = "none";
}
}
}
for(i=1;i<3;i++)
{
modal[i]= document.getElementById('challenge'+i+'Modal');
btn[i] =document.getElementById("challenge"+i);
span[i] = document.getElementById('challenge'+i+'Close');
// When the user clicks the button, open the modal
btn[i].onclick = open(i);
// When the user clicks on <span> (x), close the modal
span[i].onclick = closex(i);
// When the user clicks anywhere outside of the modal, close it
window.onclick = close(i);
}

Related

pointerEvents = "none" not working as expected

I'm creating a memory card game and it works until I try to click on the cards too fast. When I open two cards, I am calling compareCards function which adds document.body.style.pointerEvents = "none"; but obviously I can click on the third card if I am fast enough. How can I fix it? Here is my full JS code, note that class .flip adds pointer-events: none; among other things while .match adds short animation. I guess it probably has something to do with setTimeouts but I need them in order to show animatioins.
const playBtn = document.querySelector(".intro button");
const restartBtn = document.querySelectorAll(".restartBtn");
const introScreen = document.querySelector(".intro");
const game = document.querySelector(".game");
const gameContainer = document.querySelector("#gameContainer");
const timer = document.querySelector(".timer span");
const moves = document.querySelector(".moves span");
let time,
minutes = 0,
seconds = 0;
let numberOfMoves = 0;
moves.innerHTML = numberOfMoves;
let openCards = [];
let matchedCards = [];
function startGame() {
let shuffledDeck = shuffle(deckCards);
for (let i = 0; i < shuffledDeck.length; i++) {
const card = document.createElement("div");
card.classList.add("card");
const image = document.createElement("img");
image.setAttribute("src", "img/" + shuffledDeck[i]);
card.appendChild(image);
gameContainer.appendChild(card);
}
runTimer();
}
const deckCards = [
... images to add to game ...];
gameContainer.addEventListener("click", function (e) {
if (e.target.className === "card") {
flipCard();
}
function flipCard() {
e.target.classList.add("flip");
addCard();
}
function addCard() {
if (openCards.length == 0 || openCards.length == 1) {
openCards.push(e.target.firstElementChild);
}
compareCards();
}
});
function compareCards() {
if (openCards.length == 2) {
document.body.style.pointerEvents = "none";
}
if (openCards[0].src == openCards[1].src && openCards.length == 2) {
cardsMatched();
} else if (openCards[0].src !== openCards[1].src && openCards.length == 2) {
cardsNotMatched();
}
}
function countMoves() {
numberOfMoves++;
moves.innerHTML = numberOfMoves;
}
function cardsMatched() {
setTimeout(function () {
openCards[0].parentElement.classList.add("match");
openCards[1].parentElement.classList.add("match");
matchedCards.push(...openCards);
document.body.style.pointerEvents = "auto";
gameWon();
openCards = [];
}, 500);
countMoves();
}
function cardsNotMatched() {
setTimeout(function () {
openCards[0].parentElement.classList.remove("flip");
openCards[1].parentElement.classList.remove("flip");
document.body.style.pointerEvents = "auto";
openCards = [];
}, 500);
countMoves();
}
function gameWon() {
if (matchedCards.length == 16) {
stopTimer();
showModal();
}
}
const modal = document.querySelector(".modal");
function showModal() {
const closeModal = document.querySelector(".closeBtn");
modal.style.display = "block";
closeModal.addEventListener("click", () => {
modal.style.display = "none";
});
window.onclick = function (event) {
if (event.target == modal) {
modal.style.display = "none";
}
};
}
function resetEverything() {
modal.style.display = "none";
stopTimer();
timer.innerHTML = `00:00`;
numberOfMoves = 0;
moves.innerHTML = numberOfMoves;
matchedCards = [];
openCards = [];
startGame();
}
function shuffle(array) {
let currentIndex = array.length,
randomIndex;
while (currentIndex != 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
}
function runTimer() {
time = setInterval(() => {
seconds++;
if (seconds == 60) {
minutes++;
seconds = 0;
}
timer.innerHTML = `${minutes < 10 ? `0${minutes}` : minutes}:${
seconds < 10 ? `0${seconds}` : seconds
}`;
}, 1000);
}
function stopTimer() {
seconds = 0;
minutes = 0;
clearInterval(time);
}
playBtn.addEventListener("click", () => {
introScreen.classList.remove("fadeIn");
introScreen.classList.add("fadeOut");
game.classList.add("fadeIn");
startGame();
});
What you're running into is a common problem where it takes time for things like CSS to propagate through the website, especially when you have timeouts. What you should do is VERY RARELY rely on CSS classes for your logic and let CSS be what it's meant to be: a purely visual medium.
The solution is simple. Instead of relying on pointer events, set up a flag that toggles whether the user is allowed to click on something or not, and then reference that flag for all your logic. This allows you to decouple what you see from what's happening underneath the hood. Something like this (only the relevant bits):
let canAction = true; // add a flag
function startGame() {
// ... start game logic
canAction = true; // set the flag to allow actions
}
gameContainer.addEventListener("click", function (e) {
if (canAction) {
// ... card click logic
}
});
function compareCards() {
if (openCards.length == 2) {
canAction = false; // stop user from taking further action
}
// ... rest of compare card logic
}
function cardsMatched() {
canAction = true; // re allow the user to click on cards - put this inside the setTimeout or outside depending on what you need
setTimeout(function () {
// ...
}, 500);
countMoves();
}
function cardsNotMatched() {
canAction = true; // re allow the user to click on cards - put this inside the setTimeout or outside depending on what you need
setTimeout(function () {
// ...
}, 500);
countMoves();
}
Of course you are free to keep the pointer-events CSS stuff as well, but don't rely on it for your logic. You'll just be inviting a whole lot of messy situations like this.

Trying to shorten my code to make multiple modals work javascript

I am trying to make modal-contents on 5 images, I've tried for several hours to shorten my code with for loops but I still have lots of lines which I don't know how to shorten. I'm just starting with javascript. This is my code below, working but way too long.
var modal = [];
for (i=0; i<5; i++)
{
modal[i] = document.getElementById('simplemodal'+i);
}
var modalbtn = [];
for (i=0; i<5; i++) {
modalbtn[i] = document.getElementById('pics-post'+i);
modalbtn[i].addEventListener('click', eval('openmodal'+i));
window.addEventListener('click', eval('clickoutside'+i));
}
var closebtn0 = document.getElementsByClassName('closebtn')[0];
var closebtn1 = document.getElementsByClassName('closebtn')[1];
var closebtn2 = document.getElementsByClassName('closebtn')[2];
var closebtn3 = document.getElementsByClassName('closebtn')[3];
var closebtn4 = document.getElementsByClassName('closebtn')[4];
for (i=0; i<5; i++) {
eval('closebtn'+i.addEventListener('click', eval('closemodal'+i)));
}
function openmodal0(){
modal[0].style.display = 'block';
}
function openmodal1(){
modal[1].style.display = 'block';
}
function openmodal2(){
modal[2].style.display = 'block';
}
function openmodal3(){
modal[3].style.display = 'block';
}
function openmodal4(){
modal[4].style.display = 'block';
}
function closemodal0(){
modal[0].style.display = 'none';
}
function closemodal1(){
modal[1].style.display = 'none';
}
function closemodal2(){
modal[2].style.display = 'none';
}
function closemodal3(){
modal[3].style.display = 'none';
}
function closemodal4(){
modal[4].style.display = 'none';
}
function closemodal5(){
modal[5].style.display = 'none';
}
function clickoutside0(e){
if(e.target == modal[0])
modal[0].style.display = 'none';
}
function clickoutside1(e){
if(e.target == modal[1])
modal[1].style.display = 'none';
}
function clickoutside2(e){
if(e.target == modal[2])
modal[2].style.display = 'none';
}
function clickoutside3(e){
if(e.target == modal[3])
modal[3].style.display = 'none';
}
function clickoutside4(e){
if(e.target == modal[4])
modal[4].style.display = 'none';
}
I've tried to use eval to increment the variables name but was not able to find the right way to make it work.
You can put everything into a single loop and use anonymous functions.
var modal = [];
var modalbtn = [];
var closebtn = [];
for (i=0; i<5; i++) {
modal[i] = document.getElementById('simplemodal'+i);
modalbtn[i] = document.getElementById('pics-post'+i);
modalbtn[i].addEventListener('click', () => {
modal[i].style.display = 'block';
});
window.addEventListener('click', eval('clickoutside'+i));
closebtn[i] = document.getElementsByClassName('closebtn')[i];
closebtn[i].addEventListener('click', () => {
modal[i].style.display = 'none';
});
}
You could use a common function for opening model and closing model like:
function openmodel (i) {
model[i].style.display = 'block'
}
function clickoutside(e, i){
if(e.target == modal[i])
modal[i].style.display = 'none';
}
And change you event listener like:
modalbtn[i].addEventListener('click', function () {
openmodel(i)
})
window.addEventListener('click', function (e) {
clickoutside(e, i)
})
And insted of this
eval('closebtn'+i.addEventListener('click', eval('closemodal'+i)))
you can use:
var closebtn = document.getElementsByClassName('closebtn')
for (let i = 0; i < closebtn.length; i++) {
closebtn[i].addEventListener('click', function () {
closeModel(i)
})
}
And change closeModel function like:
function closemodal(i){
modal[i].style.display = 'none';
}

Creating a tab controller in javascript?

I'm trying to make a tab controller on a div that has divs beneath it that are for content (each sibling is a tab).
Given this html:
<div id="myTabCtl">
<div id="tab1">
<img src="https://images.unsplash.com/photo-1546238232-20216dec9f72?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1000&q=80" style="width:240px;">
</div>
<div id="tab2">
Cute puppies.
<img src="https://www.telegraph.co.uk/content/dam/news/2016/05/06/rexfeatures_4950182a_trans_NvBQzQNjv4Bqeo_i_u9APj8RuoebjoAHt0k9u7HhRJvuo-ZLenGRumA.jpg?imwidth=240" style="width:240px;">
</div>
<div id="tab3">
No puppies here.
</div>
</div>
And this javascript:
Element.prototype.tabs = function () {
var tabs = [];
this.classList.add('xTabCtl');
var tab_bar = document.createElement('ul');
tab_bar.classList.add('xTabRow');
this.insertBefore(tab_bar, this.firstChild);
this.addTab = function (div,label,title,click) {
if (typeof div == 'string')
div = document.getElementById(div);
if (!div) return false;
div.style.display = 'none';
var tab_ctl = document.createElement('li');
tab_ctl.innerHTML = label;
if (title) tab_ctl.title = title;
var cnt = tabs.length;
if (click) {
tab_ctl.onclick = function () {
this.setTab(cnt);
click();
};
} else {
tab_ctl.onclick = function () {
this.setTab(cnt);
};
}
tab_ctl.style.cursor = 'pointer';
tab_ctl.style.userSelect = 'none';
tab_ctl.classList.add('xTabItem');
tab_bar.appendChild(tab_ctl);
var tab = {};
tab.elem = tab_ctl;
tab.div = div;
tab.loaded = false;
tabs[tabs.length] = tab;
return true;
}
this.setTab = function (tab_no) {
for (var i=0; i<tabs.length; i++) {
tabs[i].elem.classList.remove('xTabItemSel');
tabs[i].div.style.display = 'none';
tabs[i].loaded = false;
}
tabs[tab_no].div.style.display = 'block';
tabs[tab_no].div.classList.add('xTabItemSel');
tabs[tab_no].loaded = true;
return;
}
};
document.getElementById('myTabCtl').tabs();
document.getElementById('myTabCtl').addTab('tab1','Cute Puppies 1','Title 1');
document.getElementById('myTabCtl').addTab('tab2','Cute Puppies 2','Title 2');
document.getElementById('myTabCtl').addTab('tab3','No Puppies','Title 3',function(){alert('hello.');});
Obviously, this uses some classes that are defined in a theme css file, but that's not at issue.
addTab works, but I cannot call setTab from the onclick of each li that is added. Why not?
Perhaps bigger, there has to be a cleaner way of doing this. I like that it adds the tabs as an li so the dopes I work with only have to add the wrapper and that content. There could be multiple tab controls on a screen. I'm not using the libraries that are available because most go way too far and I want to confine it to addTab and setTab.
This problem is with clouser, when you invoke this.setTab inside event handle, the reference of this is local so it will not get actual setTab method.
Element.prototype.tabs = function () {
var tabs = [], _this = this;
this.classList.add('xTabCtl');
var tab_bar = document.createElement('ul');
tab_bar.classList.add('xTabRow');
this.insertBefore(tab_bar, this.firstChild);
this.addTab = function (div,label,title,click) {
if (typeof div == 'string')
div = document.getElementById(div);
if (!div) return false;
div.style.display = 'none';
var tab_ctl = document.createElement('li');
tab_ctl.innerHTML = label;
if (title) tab_ctl.title = title;
var cnt = tabs.length;
if (click) {
tab_ctl.onclick = function () {
_this.setTab(cnt);
click();
};
} else {
tab_ctl.onclick = function () {
_this.setTab(cnt);
};
}
tab_ctl.style.cursor = 'pointer';
tab_ctl.style.userSelect = 'none';
tab_ctl.classList.add('xTabItem');
tab_bar.appendChild(tab_ctl);
var tab = {};
tab.elem = tab_ctl;
tab.div = div;
tab.loaded = false;
tabs[tabs.length] = tab;
return true;
}
this.setTab = function (tab_no) {
for (var i=0; i<tabs.length; i++) {
tabs[i].elem.classList.remove('xTabItemSel');
tabs[i].div.style.display = 'none';
tabs[i].loaded = false;
}
tabs[tab_no].div.style.display = 'block';
tabs[tab_no].div.classList.add('xTabItemSel');
tabs[tab_no].loaded = true;
return;
}
};
document.getElementById('myTabCtl').tabs();
document.getElementById('myTabCtl').addTab('tab1','Cute Puppies 1','Title 1');
document.getElementById('myTabCtl').addTab('tab2','Cute Puppies 2','Title 2');
document.getElementById('myTabCtl').addTab('tab3','No Puppies','Title 3',function(){alert('hello.');});

How to pass event target to a function

I am trying to modularize the following javascript but I have a problem to pass the event target. The function open was inside the the $('.myImg').on('click', function (event) { ...}.
I can't get it working.
Any Input would be appreciated.
var Modal = function () {
var modal = document.getElementById('myModal'),
modalcontent = document.getElementById('modal-img-content'),
close_btn = document.getElementsByClassName("close")[0],
img = document.getElementsByClassName('.myImg'),
modalImg = document.getElementById("img01"),
visible = false;
function getScrollBarWidth () {
var $outer = $('<div>').css({visibility: 'hidden', width: 100, overflow: 'scroll'}).appendTo('body'),
widthWithScroll = $('<div>').css({width: '100%'}).appendTo($outer).outerWidth();
$outer.remove();
return 100 - widthWithScroll;
}
function open(event){
visible = true;
modal.style.display = "block";
document.getElementsByTagName("BODY")[0].style.overflow = "hidden";
modal.style.overflow = "auto";
var newSrc = this.src;
var caption = this.alt;
modalImg.setAttribute('src', newSrc);
}
return {
getScrollBarWidth : getScrollBarWidth,
open : open
}
}();
$('.myImg').on('click', function(event) {
event.stopPropagation();
Modal.open(event);
});
You pass event but not use it in your handler.
There is correct code:
function open(event){
visible = true;
modal.style.display = "block";
document.getElementsByTagName("BODY")[0].style.overflow = "hidden";
modal.style.overflow = "auto";
var newSrc = event.target.getAttribute('src');
var caption = event.target.getAttribute('alt');
modalImg.setAttribute('src', newSrc);
}
Since I already commented with the solution, I post a cleaned version.
You need to use the event you passed.
You want
var newSrc = event.target.src;
var caption = event.target.alt;
which is the only place you need DOM in your code
var Modal = function() {
var $modal = $('#myModal'),
$modalcontent = $('#modal-img-content'),
$close_btn = $(".close"),
$img = $('.myImg'),
$modalImg = $("#img01"),
visible = false;
function getScrollBarWidth() {
var $outer = $('<div>').css({
visibility: 'hidden',
width: 100,
overflow: 'scroll'
}).appendTo('body'),
widthWithScroll = $('<div>').css({
width: '100%'
}).appendTo($outer).outerWidth();
$outer.remove(); // WHY?
return 100 - widthWithScroll;
}
function open(event) {
visible = true;
$modal.show();
$("body").css({'overflow':'hidden'});
$modal..css({'overflow':'auto'});
var newSrc = event.target.src;
var caption = event.target.alt;
$modalImg.attr('src', newSrc);
}
return {
getScrollBarWidth: getScrollBarWidth,
open: open
}
}();
$('.myImg').on('click', function(event) {
event.stopPropagation();
Modal.open(event);
});

'this.function' is not a function - OO JS

I have a modal object that has various functions as its properties, but when I click to open a modal I'm getting the error this.openModal is not a function. I've tried debugging but it just falls over as soon as it hits the function I want it to run. I suspect it might be an issue with the 'this' binding, but I'm not sure where the problem is. All my JS is below, one function kind of chains onto another so it's easier to see the whole object.
var modal = function () {
// Modal Close Listeners
this.closeModalListen = function(button) {
var modalFooter = button.parentElement;
var modalContent = modalFooter.parentElement;
var modalElement = modalContent.parentElement;
var backdrop = document.getElementById("modal-backdrop");
this.closeModal(modalElement, backdrop);
}
// Open Modal
this.openModal = function(button) {
var button = button;
var modal = button.getAttribute("data-modal");
var modalElement = document.getElementById(modal);
var backdrop = document.createElement('div');
backdrop.id="modal-backdrop";
backdrop.classList.add("modal-backdrop");
document.body.appendChild(backdrop);
var backdrop = document.getElementById("modal-backdrop");
backdrop.className += " modal-open";
modalElement.className += " modal-open";
}
// Close Modal
this.closeModal = function(modalElement, backdrop) {
modalElement.className = modalElement.className.replace(/\bmodal-open\b/, '');
backdrop.className = backdrop.className.replace(/\bmodal-open\b/, '');
}
// Initialise Function
this.init = function () {
// Create Events for creating the modals
if (document.addEventListener) {
document.addEventListener("click", this.handleClick, false);
}
else if (document.attachEvent) {
document.attachEvent("onclick", this.handleClick);
}
}
// Handle Button Click
this.handleClick = function(event) {
var thisModal = this;
event = event || window.event;
event.target = event.target || event.srcElement;
var element = event.target;
while (element) {
if (element.nodeName === "BUTTON" && /akela/.test(element.className)) {
thisModal.openModal(element);
break;
} else if (element.nodeName === "BUTTON" && /close/.test(element.className)) {
thisModal.closeModalListen(element);
break;
} else if (element.nodeName === "DIV" && /close/.test(element.className)) {
thisModal.closeModalListen(element);
break;
}
element = element.parentNode;
}
}
}
// Initalise Modal Engine
var akelaModel = new modal();
akelaModel.init();
I'm looking to fix this with pure js and no jquery.
You need to bind the function to the object before giving it as a event listener:
document.addEventListener("click", this.handleClick.bind(this), false);
document.attachEvent("onclick", this.handleClick.bind(this));
Otherwise it becomes detached from its original object and doesn't have the expected this.
As an alternative to using bind, you can move:
var thisModal = this;
from the handleClick method to the constructor.
Also, it's convention to give constructors names starting with a capital letter so it's easier to see that they're constructors.
var Modal = function () {
var thisModal = this;
// Open Modal
this.openModal = function(button) {
console.log('openModal called on ' + button.tagName);
}
// Initialise Function
this.init = function () {
// Create Events for creating the modals
if (document.addEventListener) {
document.addEventListener("click", this.handleClick, false);
}
}
// Handle Button Click
this.handleClick = function(event) {
thisModal.openModal(event.target);
}
}
// Initalise Modal Engine
var akelaModel = new Modal();
akelaModel.init();
<div>Click <span>anywhere</span></div>

Categories