Toggling preventDefault() On mouseover and mouseleave of Parent Container - javascript

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>

Related

Javascript .toggle only works 1.5 times

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")
});

Add scroll event listener triggers one time only when intersection observer detects true or false

I've tried to make parallax effect using only JS and I succeeded in controlling one section. Now I'm trying to make it works on multiple section on a page and I couldn't understand why.😢
The problem is scroll event listener triggers only once, when intersection observer detects true or false. I guess using only addEventListener() could work, but I what to use less resource by using IntersectionObserver().
Here's my code i'm working on.
<style>
.placeholder {
width:100%;
height: 150vh;
background: royalblue;
.divider {
height: 70vh;
background: rosybrown;
.para-section {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
background-image: url('https://picsum.photos/1280/720');
background-size: 100%;
background-position: 50% 50%;
</style>
<header class="placeholder">Header</header>
<section class="para-section">
<div class="para-content">Parallax Moving</div>
</section>
<div class="divider">Divider</div>
<section class="para-section">
<div class="para-content">Parallax Moving</div>
</section>
<footer class="placeholder">Footer</footer>
This JS code works with one element, but not with multi-elements.
(am I doing right with add, remove evert listener? also curious^^;)
const paraSection = document.querySelectorAll(".para-section");
const options = { rootMargin: "-100px" };
const paraObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
window.addEventListener("scroll", parallaxScroll(entry.target));
console.log("ON");
} else {
window.removeEventListener("scroll", parallaxScroll(entry.target));
console.log("OFF");
}
});
}, options);
paraSection.forEach((element) => {
paraObserver.observe(element);
});
// transform controll
const increment = -0.5;
const parallaxScroll = (element) => {
let centerOffest = window.scrollY - element.offsetTop;
let yOffsetRatio = centerOffest / element.offsetHeight;
console.log(yOffsetRatio);
let yOffset = 50 + yOffsetRatio * 100 * increment;
element.style.backgroundPositionY = `${yOffset}%`;
};
If you have some better approach, any suggestions are welcome.
Thanks😃

Auto scroll images on page load, stop on hover

JS newbie here. I have an issue that is probably has a fairly simple answer, but I haven't been able to figure it out yet. I wasn't sure exactly what to call this thing.
I have text in a div and when you hover over it, it displays a picture in another div. This is working fine, but I would like to have it scroll through the images automatically when the page loads. Once the user hovers over one of the text divs, I'd like the auto scroll to stop.
I have a Codepen of how I have it set up here: https://codepen.io/johnballman/pen/dwEwRz
HTML:
<div class="app-screen">
<img src="http://placehold.it/350x150">
</div>
<div id="features">
<article data-src="http://placehold.it/350x150">Link 1</article>
<article data-src="http://placehold.it/350x250">Link 2</article>
<article data-src="http://placehold.it/350x350">Link 3</article>
</div>
CSS:
.app-screen {
float: left;
margin-right: 100px;
display: block;
width: 350px;
height: 200px;
background-color: grey;
padding-top: 100px;
}
img.active{
z-index: 2 !important;
opacity: 1 !important;
transition:opacity 1s linear;
}
JS:
$("#features article").hover( function() {
var value=$(this).attr('data-src');
$(".app-screen img").attr("src", value);
});
$(this).switchClass("", "active", 1000);
Any help would be great. Thanks.
Use setInterval to loop a c current counter.
Use ++c % tot (where tot is the number of links) to: increment-loop the counter.
Use only Classes. That way you can have multiple .Features elements in a single page!
Create show, stop and play functions. show is to show a c image; stop is to stop the interval, and play to start your magic.
/**
* Features
* Auto-change articles featured images
*/
$('.Features').each((i, el) => {
const $this = $(el);
const $image = $this.find('.Features-image');
const $link = $this.find('.Features-link');
const tot = $link.length;
let c = 0; // Counter to keep track of Current image
let itv = null; // Interval loop
const show = () => {
$image.css({backgroundImage: `url("${$link.eq(c).data().src}")`});
$link.removeClass('is-active').eq(c).addClass('is-active');
}
const stop = () => clearInterval(itv);
const play = () => itv = setInterval(() => {
c = ++c % tot; // Preincrement + loop (% = reminder operator)
show(); // Show c image
}, 3000);
// Link mouseenter
$link.on({
mouseenter() {
c = $link.index(this);
stop(); // Stop ongoing auto-play
show(); // Show c image
},
mouseleave() {
play(); // Play on mouseleave
}
});
// Init
show(); // Show c image
play(); // Start play!
});
/*QuickReset*/ *{margin:0;box-sizing:border-box;}html,body{height:100%;font:14px/1.4 sans-serif;}
/**
* Features
* jQuery-handled articles with featured images
*/
.Features {
display: flex;
min-height: 200px;
}
.Features-image {
background: #aaa 50% / cover no-repeat none;
transition: background 0.5s;
flex: 0 1 40%;
}
.Features-links {
display: flex;
flex: 1;
flex-flow: column;
}
.Features-link {
flex: 1;
padding: 10px;
transition: background 0.3s;
border-bottom: 1px solid #ddd;
}
.Features-link:hover,
.Features-link.is-active{
background: #eee;
}
<div class="Features">
<div class="Features-image"></div>
<div class="Features-links">
<article class="Features-link" data-src="//placehold.it/350x350/0bf">Link 1</article>
<article class="Features-link" data-src="//placehold.it/350x350/f0b">Link 2</article>
<article class="Features-link" data-src="//placehold.it/350x350/0fb">Link 3</article>
</div>
</div>
<div class="Features">
<div class="Features-image"></div>
<div class="Features-links">
<article class="Features-link" data-src="//placehold.it/350x350/28a">Lorem</article>
<article class="Features-link" data-src="//placehold.it/350x350/a28">Ipsum</article>
<article class="Features-link" data-src="//placehold.it/350x350/8a2">Dolor</article>
</div>
</div>
<script src="//code.jquery.com/jquery-3.3.1.min.js"></script>

Weird mobile nav animation bug

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.

Prevent ghost image on dragging non-draggable elements?

I'm creating a website that utilizes the HTML5 Drag and Drop API.
However, to increase the user experience, I'd like to prevent ghost images when a user drags non-draggable elements. Is this even possible?
Further, almost every element seems " draggable " by default. One can click and then quickly drag pretty much any element in a browser, which creates a ghost image along with a no-drop cursor. Is there any way to prevent this behaviour?
draggable="false" doesn't work.
user-select: none doesn't work.
pointer-events: none doesn't work.
event.preventDefault() on mousedown doesn't work.
event.preventDefault() on dragstart doesn't work either.
I'm out of ideas and so far it's proven incredibly difficult to find information about this online. I have found the following thread, but, again, draggable="false" doesn't seem to work in my case.
Below is a screenshot that demonstrates it doesn't work; of course you can't see my cursor in the screenshot, but you can see how I've dragged the numbers to the left despite that.
I believe the issue might have something to do with its parent having dragover and drop events associated with it. I'm still dumbfounded nonetheless.
HTML
...
<body>
...
<div id="backgammon-board-container">
<div class="column" id="left-column">
<div class="quadrant" id="third-quadrant">
<div class="point odd top-point" id="point-13-12"><text>13</text>
<div class="checker player-one-checker" id="checker-03" draggable="true"></div>
</div>
</div>
</div>
...
</div>
</body>
</html>
CSS
#backgammon-board-container {
height: 100vh;
width: 60vw;
position: absolute;
right: 0;
display: flex;
}
.column {
height: 100%;
display: flex;
flex-direction: column; /* column-reverse for player two perspective */
}
#left-column {
flex: 6;
}
.quadrant {
flex: 1;
display: flex;
}
.point {
flex: 1;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.checker {
z-index: 1;
width: 48px;
height: 48px;
border-radius: 50%;
}
text {
position: fixed;
font-family: impact;
font-size: 24px;
color: white;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
pointer-events: none;
}
JS
const p1checkers = document.getElementsByClassName('player-one-checker');
const p2checkers = document.getElementsByClassName('player-two-checker');
const pointClass = document.getElementsByClassName('point');
function setTurn(player) {
if (player === 'p1') {
allowCheckerMovement = p1checkers;
disallowCheckerMovement = p2checkers;
} else {
allowCheckerMovement = p2checkers;
disallowCheckerMovement = p1checkers;
}
// enable checker control for player
for (var i = 0; i < allowCheckerMovement.length; i++) {
allowCheckerMovement[i].style.cursor = 'pointer';
allowCheckerMovement[i].setAttribute('draggable', true);
allowCheckerMovement[i].addEventListener('dragstart', start); // for drag-and-drop.js
allowCheckerMovement[i].addEventListener('dragend', stop); // for drag-and-drop.js
}
// disable checker control for player
for (var i = 0; i < disallowCheckerMovement.length; i++) {
disallowCheckerMovement[i].style.cursor = 'default';
disallowCheckerMovement[i].setAttribute('draggable', false);
disallowCheckerMovement[i].removeEventListener('dragstart', start); // for drag-and-drop.js
disallowCheckerMovement[i].removeEventListener('dragend', stop); // for drag-and-drop.js
}
// allow drag and drop
for (var i = 0; i < pointClass.length; i++) {
pointClass[i].addEventListener('dragover', allowDrop); // for drag-and-drop.js
pointClass[i].addEventListener('drop', droppedOn); // for drag-and-drop.js
}
}
function start(event) {
var checker = event.target;
event.dataTransfer.setData('text/plain', checker.id);
event.dataTransfer.effectAllowed = 'move';
window.requestAnimationFrame(function(){
checker.style.visibility = 'hidden';
});
}
function allowDrop(event) {
event.preventDefault();
}
function droppedOn(event) {
event.preventDefault();
var data = event.dataTransfer.getData('text/plain');
event.target.appendChild(document.getElementById(data));
}
function stop(event){
var element = event.srcElement;
window.requestAnimationFrame(function(){
element.style.visibility = 'visible';
});
}
This is the solution you're looking for. ;)
For anything that DOES need to be draggable, just add the 'enable-drag' CSS class.
$('*:not(".enable-drag")').on('dragstart', function (e) {
e.preventDefault();
return false;
});

Categories

Resources