I'm trying to make an image popup using the .toggle function in javascript however it only works 1.5 times.
I can click it and it opens it, click the background to close it, open it again but then i cannot close it.
Here is the codepen with everything needed, any guidance would be great so thanks in advance. (im pretty new to js
https://codepen.io/user843/pen/gOXobZZ
function prodImgPopup(a) {
popUpModal = document.querySelector(".prodimgpop")
popupImgID = document.getElementById("popID")
popUpModal.classList.toggle("product-popup-show")
popupImgID.src = a.src
popUpModal.addEventListener("click", e => {
popUpModal.classList.toggle("product-popup-show")
})
every time you click the image you add the event listener ... therefore, when you've clicked the image twice, it has the event listener running twice ... which means it does toggle twice
Of course toggling twice === do nothing
Change your code to
const popUpModal = document.querySelector(".prodimgpop")
const popupImgID = document.getElementById("popID")
popUpModal.addEventListener("click", e => {
popUpModal.classList.remove("product-popup-show")
});
function prodImgPopup(a) {
popupImgID.src = a.src
popUpModal.classList.add("product-popup-show")
}
edit: not sure why every second time you load that page you get that prodImgPopup is undefined
anyway - here's an alternative
const popUpModal = document.querySelector(".prodimgpop");
const popupImgID = document.getElementById("popID");
popUpModal.addEventListener("click", (e) => {
popUpModal.classList.remove("product-popup-show");
});
document.querySelectorAll("img[loading=lazy]").forEach((img) =>
img.addEventListener("click", (prodImgPopup) => {
popupImgID.src = img.src;
popUpModal.classList.add("product-popup-show");
})
);
and remove the onclick= from those <img loading="lazy" ...> images
You also need CSS change to fix the latent image
.prodimgpop {
display: none;
}
.prodimgpop.product-popup-show {
display: block;
}
Here's the code all fixed up and running
const popUpModal = document.querySelector(".prodimgpop");
const popupImgID = document.getElementById("popID");
popUpModal.addEventListener("click", (e) => {
popUpModal.classList.remove("product-popup-show");
});
function prodImgPopup(a) {
popupImgID.src = this.src;
popUpModal.classList.add("product-popup-show");
}
document.querySelectorAll("img[loading=lazy]").forEach((img) =>
img.addEventListener("click", (prodImgPopup) => {
popupImgID.src = img.src;
popUpModal.classList.add("product-popup-show");
})
);
.product-popup {
display: none;
}
.product-popup-show {
width: 100%;
height: 100%;
position: absolute;
top: 0;
right: 0;
z-index: 100000;
background-color: rgba(32,32,32, .8);
position: fixed;
}
.popup-img {
display: flex;
justify-content: center;
align-items: center;
padding: 5rem;
}
.popup-img .img {
width: 100%;
max-width: 750px;
object-fit: cover;
}
/* fix popup display */
.prodimgpop {
display: none;
}
.prodimgpop.product-popup-show {
display: block;
}
<div class="prod">
<img loading="lazy" src="https://images.unsplash.com/photo-1602471615287-d733c59b79c4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1171&q=80" class="img prod-img" alt="">
<img loading="lazy" src="https://images.unsplash.com/photo-1533689476487-034f57831a58?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1171&q=80" class="img prod-img" alt="">
</div>
<div id="productPop" class="prodimgpop">
<div class="popup-img">
<img id="popID" class="img">
</div>
</div>
As mentioned by Jaromanda, the event listener is being added each time you click. Try changing the Javascript to this.
function prodImgPopup(a) {
popUpModal = document.querySelector(".prodimgpop")
popupImgID = document.getElementById("popID")
popUpModal.classList.toggle("product-popup-show")
popupImgID.src = a.src
}
const popupdisplay = document.querySelector("#productPop");
popupdisplay.addEventListener("click", e => {
popUpModal.classList.toggle("product-popup-show")
});
Related
My onmouseenter function isn't triggering.
I created a button that would allow you to go to the top. However, once there I want the button to have a display property of none. I tried doing it in two ways. One with classList and the other with style.display = 'none'. Both didn't work, am I missing something in the logic ?
EDIT-------------
onmouseleave the button should reappear. I added the function.
Here is a code pen
const topDiv = document.getElementById('topDiv')
const arrowup = document.getElementById('arrowup')
const hideArrow = () => {
if (topDiv) {
arrowup.classList.remove(showme');
arrowup.classlist.add('hideme');
} else {
arrowup.classList.add('showme');
}
}
const showArrow = () => {
if (!topDiv) {
arrowup.classList.remove('hideme');
arrowup.classList.add('showme');
}
}
#top {
height: 1000px;
}
a {
position: fixed;
top: 10px;
}
.showme {
display: block;
}
.hideme {
display: none;
}
<div onmouseleave="showArrow() onmouseenter="hideArrow()" id="top">
hello
</div>
<a class="showme" id="arrowup" onClick="hideArrow()" href="#top">
click me
</a>
There are some issues:
onmouseenter="hideArrow" is missing brackets -> onmouseenter="hideArrow()"
add and remove are functions, that get the class as param -> add('showme')
the return is wrong -> remove it
Working example:
const topDiv = document.getElementById('topDiv')
const arrowup = document.getElementById('arrowup')
const hideArrow = () => {
if (topDiv) {
arrowup.classList.remove('showme');
arrowup.classList.add('hideme');
}
else {
arrowup.classList.add('showme');
}
}
#top {
height: 1000px;
}
a {
position: fixed;
top: 50px;
}
.showme {
display: block;
}
.hideme {
display: none;
}
<div onmouseenter="hideArrow()" id="topDiv">
hello
</div>
<a class="showme" id="arrowup" onClick="hideArrow()" href="#topDiv">
click me
</a>
Remove the return keyword from the if condition because it causes the function to stop running at that point
element.classList.add() is a function while you are assigning classes to the element using the '=' operator. So the correct way to do it is arrowup.classList.add('hideme') and arrowup.classList.remove('showme').
I have some containers that have child containers inside which contain links.
What happens is when the user hovers over the parent container to show the child, the links are disabled for the first 2 seconds. If a user moves the mouse away before clicking the link this behaviour is reset with a 'hasBeenHovered' variable that changes from true to false inside the mouseleave event.
The two issues I'm facing are:
a) I can't get it to work solely on the parent being hovered - it loops through all of them and shows them all;
b) On mobile is there anyway of returning the opacity to 1 and disabling the links again by re-tapping (so the re-tap effectively works as the mouseleave event?). If this is very complex to do I may just have it so it stays visible until the parent container hits the top of the viewport.
Although the code sandbox is below it says "Uncaught TypeError: allowLinks is not a function", yet on CodePen the demo works?
Many thanks in advance for any help.
Emily
CodePen: https://codepen.io/emilychews/pen/rNjXMee
var parent = document.querySelectorAll(".parent");
var child = document.querySelectorAll(".child");
var link = document.querySelectorAll(".link");
var hasBeenHovered = false;
var listener = function (e) {
e.preventDefault();
};
// prevent default on all specific links
link.forEach(function (item) {
item.addEventListener("click", listener);
});
// mouseover that changes opacity to 1 and removes prevent default on links
parent.forEach(function (item) {
item.addEventListener("mouseover", function (event) {
child.forEach(function (item) {
item.style.opacity = "1";
item.style.transition = "opacity .5s";
});
// remove prevent Default
if (hasBeenHovered === false) {
function allowLinks() {
link.forEach(function (item) {
item.removeEventListener("click", listener);
hasBeenHovered = true;
});
}
}
setTimeout(function () {
allowLinks();
}, 2000);
}, false );
});
// mouseleave event re-adds opacity: 0 and re-adds prevent default
parent.forEach(function (item) {
item.addEventListener("mouseleave", function (event) {
child.forEach(function (item) {
item.style.opacity = "0";
item.style.transition = "opacity .5s";
});
// re-add prevent Default
if (hasBeenHovered === true) {
link.forEach(function (item) {
item.addEventListener("click", listener);
hasBeenHovered = false;
});
}
}, false );
});
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
}
.parent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 20rem;
height: 20rem;
background: lightblue;
margin: 1rem;
}
.child {
opacity: 0; /* hides child container*/
padding: 1rem;
background: red;
}
a {
color: #fff;
display: block;
margin: 1rem 0;
}
<div class="parent">
<div class="child">
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
</div>
</div>
<div class="parent">
<div class="child">
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
</div>
</div>
Trying to keep most of your original code logic, I've modified quite a bit of it but this should be what your are aiming for. For the mobile part I'd recommend a flag for the touch handler, but do note it'll get a bit more complicated since mobile also responds to the onclick handlers.
The links will not work in the snippet due to StackOverflow security so added a console log but will work if you copy to CodePen
const parents = document.querySelectorAll(".parent");
parents.forEach(parent => {
const children = parent.querySelectorAll(".child");
const links = parent.querySelectorAll(".link");
let timeoutId = null; // track the timeout so we can clear it
let enableLinks = false; // should we allow links?
links.forEach(link =>
link.addEventListener("click", evt => {
if (!enableLinks) {
evt.preventDefault(); // hold them hostage
} else {
console.log('StackOverflow prevents links'); // just a placeholder for SO snippet
}
}, true)
);
parent.addEventListener("mouseover", function() {
enableLinks = false; // ensure links are disabled at first
children.forEach(child => child.style.opacity = "1"); // let the children be seen
if (!timeoutId) // make sure there isn't already a timeout
timeoutId = setTimeout(() => enableLinks = true, 2000); // enable links after the 2s
}, false);
parent.addEventListener("mouseleave", function(event) {
children.forEach(child => child.style.opacity = "0"); // hide your children
clearTimeout(timeoutId); // remove the timeout so it can't overlap
timeoutId = null; // clear timeout id
enableLinks = false; // turn off links
}, false);
});
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
}
.parent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 20rem;
height: 20rem;
background: lightblue;
margin: 1rem;
}
.child {
opacity: 0;
/* hides child container*/
padding: 1rem;
background: red;
transition: opacity .5s;
}
a {
color: #fff;
display: block;
margin: 1rem 0;
}
<div class="parent">
<div class="child">
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
</div>
</div>
<div class="parent">
<div class="child">
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
<a target="_blank" href="https://google.com" class="link">This will work after 2 seconds of mouseover</a>
</div>
</div>
I have made a modal in a component, the data works fine, it's dynamic which is perfect. Problem is, when I open it, it seems that whenever I click anywhere inside the modal, it closes it again.
The modal is managed using useState hook. I think the problem lies in my onClick calls further down. Any advise please?
const LeaveRequestsUnits = () => {
let [data, setData] = useState([]);
let [modalState, setModalState] = useState(false);
let modalOnOff = () => {
setModalState(!modalState);
};
let [selectedUnit, setSelectedUnit] = useState('');
let updateSelectedUnit = (item) => {
setSelectedUnit(item);
const getLeaveUnits = data.map((item) => {
// fct to update the modalState and display-block the modal
const openModal = (item) => {
updateSelectedUnit(item);
modalOnOff();
$('.modalBackground').css('display', 'block');
};
const modal = () => {
return (
<div>
<p>{selectedUnit.note}</p>
<p>{selectedUnit.start}</p>
<p>{selectedUnit.end}</p>
Google
<h1>Close</h1>
</div>
);
};
// display:none the modal if the modalState is false
if (!modalState) {
$('.modalBackground').css('display', 'none');
}
if (item.end >= today && item.approved !== false) {
return (
<div
className={unitColour}
key={item.reqID}
onClick={() => openModal(item)}
>
<div className='unitLeft'>
<img src={statusIcon} alt='Status Icon' id='statusIcon' />
</div>
<div className='unitMiddle'>
<p id='unitLeaveType'>{leaveTypeName}</p>
<p id='unitDate'>{startEndDate(item.start, item.end)}</p>
</div>
<div className='unitDivider'></div>
<div className='unitRight'>
<p id='unitDuration'>
{convertTimestamp(item.duration, item.type)}
</p>
</div>
{/* modal */}
<div className={`modalBackground modalShowing-${modalState}`}>
{modal()}
</div>
{/* end modal */}
</div>
);
}
});
return <div className='requestsContainer
CSS below:
.modalBackground {
display: none;
z-index: 10000;
width: 80vw;
height: 250px;
background-color: white;
border-radius: 15px;
position: absolute;
top: 10vh;
overflow: hidden;
color: black;
opacity: 0;
pointer-events: none;
cursor: auto;
}
.modalShowing-true {
/* display: none;
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-color: black;
opacity: 0.3; */
opacity: 1;
pointer-events: auto;
}
When you define the modal component, you need to tell it to prevent clicks on it from bubbling up to the parent elements click listener that will try to close the modal.
const cancelClick = useEffect( (event) => {
event && event.stopPropagation();
}, []) // only need to create this function once
const modal = () => {
return (
<div onClick={cancelClick}>
<p>{selectedUnit.note}</p>
<p>{selectedUnit.start}</p>
<p>{selectedUnit.end}</p>
Google
<h1>Close</h1>
</div>
);
};
I would HIGHLY recommend you stop using jquery here as well. In this component its a super easy change, just remove the jquery calls to change the display css property on the backdrop and instead use the state variable to control showing that.
<div className={`modalBackground${!!modalState ? ' open' : ''}`}>
{modal()}
</div>
and then you can clean up the css for it. I dropped the display: none; style and went with transform: scale(0);. This gives more flexibility in how you decide how you want to show the modal (fade in would do nicely).
.modalBackground {
position: absolute;
top: 10vh;
z-index: 10000;
overflow: hidden;
width: 80vw;
height: 250px;
opacity: 0;
transform: scale(0);
background-color: white;
color: black;
border-radius: 15px;
cursor: auto;
/* here you can add a transition on both opacity and scale to make the modal animate in. */
}
.modalBackground.open {
opacity: 1;
transform: scale(1);
}
I've built this gallery
https://jsfiddle.net/ramamamagagaulala/do4yLxcz/
let images = document.querySelectorAll('.work-item');
let best = document.querySelector('.work-modal');
let main = document.querySelector('.work-modal__item');
console.log(images)
let closeButton = document.getElementById("closee");
images.forEach(function(ref) {
ref.addEventListener('click', function(){
let newImage = this.getElementsByTagName('img')[0].src;
best.classList.add('work-modal--show');
main.style.backgroundImage = `url( ${newImage} )`;
})
})
closeButton.addEventListener('click', function() {
best.classList.remove('work-modal--show');
});
basically, it works like this:
you click an item.
JavaScript checks what IMG this item contains.
a modal window opens up.
then the IMG that is associated with the item, is going to be displayed as the background image of this modal.
So far so good, however, I would like to build a function so I can press the arrow keys on my keyboard and the next image is going to be displayed.
What I've tried is to select the IMG of the nextSibling while clicking. Then I have used this variable to set up the background image of the modal window. But this only worked once.
Any ideas what to try next?
I would suggest have list of images urls in an array in .js file, and then you show one modal, click right/left and just change img src value to next/previous array element, untill get to either end of array.
There are three things we need to do for this problem
Storing the image source in an array
Keep track of the position of the image index
Add an event listener to track the keypress for next & prev button on your keyboard
let images = document.querySelectorAll('.work-item');
let best = document.querySelector('.work-modal');
let main = document.querySelector('.work-modal__item');
let closeButton = document.getElementById("closee");
let currentIndex = -1;
let imgSrc = [];
images.forEach(function(ref,index) {
imgSrc.push(ref.children[0].getAttribute("src"));
ref.addEventListener('click', function(){
let newImage = this.getElementsByTagName('img')[0].src;
best.classList.add('work-modal--show');
main.style.backgroundImage = `url( ${newImage} )`;
currentIndex = index
});
})
closeButton.addEventListener('click', function() {
best.classList.remove('work-modal--show');
});
let doc = document.getElementById("work");
window.addEventListener("keydown", event => {
if(event.keyCode === 39){
// next event
if(currentIndex < imgSrc.length -1 ){
main.style.backgroundImage = `url( ${imgSrc[currentIndex+1]} )`;
currentIndex=currentIndex+1;
} else {
alert("Reached last image")
}
} else if(event.keyCode === 37){
// prev event
if(currentIndex > 0){
main.style.backgroundImage = `url( ${imgSrc[currentIndex-1]} )`;
currentIndex=currentIndex-1;
} else {
alert("Reached first image")
}
}
});
.work-container{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 1rem;
}
img {
width: 250px;
}
.work-item__img{
width: 100%;
height: 100%;
display: block;
transition: all 1s linear;
opacity: 1;
object-fit: cover;
transform: scale(1.1);
}
/* modal */
.work-modal{
display: none;
}
.work-modal--show{
position: fixed;
background: rgba(0,0,0,0.5);
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 999;
display: grid;
justify-content: center;
align-items: center;
}
.work-modal__item{
height: 70vh;
width: 80vw;
border:0.5rem solid var(--yellow);
border-radius: 0.4rem;
}
#media screen and (min-width:768px){
.work-modal__item{
height: 80vh;
width: 60vw;
}
}
.work-modal__close{
position: fixed;
font-size: 3rem;
color: var(--brightYellow);
bottom: 5%;
right: 5%;
transition: color 0.5s linear;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.work-modal__close:hover{
color: red;
}
<section class="work section-padding" id="work">
<div class="work-container">
<div class="work-item item-1">
<img src="https://images.pexels.com/photos/2683138/pexels-photo-2683138.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940" alt="" class="work-item__img">
</div>
<div class="work-item item-2">
<img src="https://images.pexels.com/photos/2736220/pexels-photo-2736220.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940" alt="" class="work-item__img">
</div>
<div class="work-item item-3">
<img src="https://images.pexels.com/photos/2928178/pexels-photo-2928178.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500" alt="" class="work-item__img">
</div>
</div>
</section>
<div class="work-modal">
<div class="work-modal__item"></div>
<div class="work-modal__close">
<i id="closee" class="fas fa-window-close">close</i>
</div>
</div>
JS Fiddle
https://jsfiddle.net/aamin89/b5wp3kez/1/
CodePen of the nav
On the first interaction with the mobile nav, it will open and close as expected but anything after that and there's a bug. It will begin to open and close instantly or the links will appear is weird orders.
What I need is for the mobile nav to first open from right to left, have each of the links to cascade into the view, starting from About all the way to Blog, and then I would like it to reverse when leaving the view.
Right now I don't have the logic implemented for the reverse but I need to work out this bug before I get to that.
SNIPPET
const bars = document.querySelector('.fa-bars');
bars.addEventListener('click', () => {
const navItemsContainer = document.querySelector('.navbar__links-container');
const navItems = document.querySelectorAll('.navbar__links-container__item');
const sleep = ms => {
return new Promise(resolve => {
setTimeout(() => {
return resolve();
}, ms);
});
};
const startNavAnimation = async () => {
let count = 0;
for (let item of navItems) {
if (item.classList.contains('navbar__links-container__item--show')) {
setTimeout(() => {
item.style.transitionDelay = `${ count }s`
item.classList.remove('navbar__links-container__item--show');
count += .15;
}, count);
}
else {
item.style.transitionDelay = `${ count }s`
item.classList.add('navbar__links-container__item--show');
count += .15;
}
}
};
if (navItemsContainer.classList.contains('navbar__links-container--open')) {
navItems[ navItems.length - 1 ].addEventListener('transitionend', () => {
navItemsContainer.classList.remove('navbar__links-container--open');
});
}
else {
navItemsContainer.classList.add('navbar__links-container--open');
}
startNavAnimation();
});
body {
margin: 0;
}
.navbar {
background: #f2f2f2;
display: flex;
flex-wrap: wrap;
}
.navbar__mobile-container {
display: flex;
justify-content: space-around;
width: 100%;
}
.fa-bars {
cursor: pointer;
}
.navbar__links-container {
background: inherit;
display: flex;
flex-direction: column;
align-items: flex-end;
list-style: none;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
top: 20px;
left: 100%;
transition: left .25s, width .25s;
width: 0%;
}
.navbar__links-container__item {
left: 52px;
margin-bottom: 1rem;
position: relative;
transition: left .25s;
width: auto;
}
.navbar__links-container--open {
left: 0;
width: 100%;
}
.navbar__links-container__item--show {
left: -63px;
}
<nav class="navbar">
<div class="navbar__mobile-container">
<div class="navbar__logo-container">
<a class="navbar__logo-container__logo">BB</a>
</div>
<div class="navbar__hamburger-container">
<i class="fas fa-bars">MENU</i>
</div>
</div>
<ul class="navbar__links-container">
<li class="navbar__links-container__item">
<a class="navbar__links-container__item__link">About</a>
</li>
<li class="navbar__links-container__item">
<a class="navbar__links-container__item__link">Portfolio</a>
</li>
<li class="navbar__links-container__item">
<a class="navbar__links-container__item__link">Blog</a>
</li>
</ul>
</nav>
NOTES
I think the problem is the first if statement in the bars event-handler. Something about the way it's waiting for the transitionend event but the startNavAnimation hasn't been called.
There are two issues.
One is that you are adding a new event listener inside of the click event listener. I moved that outside.
The second issue is that the --open class is going to be there while the menu is opening or closing so you need another way to test open or closed status. To make the Codepen clear to understand, I just used an isOpen flag.
https://codepen.io/Jason_B/pen/jzGwQX?editors=0010
I like using classes for this, and your code shows that you do too, so you might want to have a class for open status and a class for visibility.