single intersection observer for multiple entries - javascript

cannot fully understand the IntersectionObserver
in the example below, everything works fine, but I'm trying to write only one single observer for multiple entries
and I'm getting various error messages.
Pls, help
let io = new IntersectionObserver((entries)=>{
entries.forEach(entry=>{
if(entry.isIntersecting){navt.classList.remove('navt1');}
else{navt.classList.add('navt1');}
})
})
let io2 = new IntersectionObserver((entries)=>{
entries.forEach(entry=>{
if(entry.isIntersecting){gotopw.style.display = 'block';}
else{gotopw.style.display = 'none';}
})
})
$(document).ready(function(){
io.observe(document.querySelector('#wrapt'));
io2.observe(document.querySelector('#apanel'));
});

Every intersecting entity refers to the element that is intersecting. So to create a single IntersectionObserver you simply have to take advantage of that.
This is a simplified example to show the concept. Note there are two "boxes" that can scroll into view. As they scroll into view the background color changes individually. I used an intersection ratio so you can see the change happen.
The modify() and revert() functions represent operations you would perform in one of the two intersection thresholds.
The test for the element id is the trick that allows the use of one IntersectionObserver for multiple elements.
Scroll slowly to see both boxes.
let io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
modify(entry.target);
} else {
revert(entry.target);
}
})
}, {
threshold: 0.5
})
function modify(el) {
if (el.id === "wrapt") {
el.style.backgroundColor = 'red';
}
if (el.id === "apanel") {
el.style.backgroundColor = 'green';
}
}
function revert(el) {
if (el.id === "wrapt") {
el.style.backgroundColor = 'initial';
}
if (el.id === "apanel") {
el.style.backgroundColor = 'initial';
}
}
io.observe(document.querySelector('#wrapt'));
io.observe(document.querySelector('#apanel'));
#wrapt {
border: 2px solid black;
height: 100px;
width: 100px;
}
#apanel {
border: 2px solid blue;
height: 100px;
width: 100px;
}
.empty {
height: 400px;
width: 100px;
}
<div class="empty"> </div>
<div id="wrapt">Wrapt</div>
<div class="empty"></div>
<div id="apanel">aPanel</div>

Related

how do i trigger onMouseEnter for elements behind other elements

I'm trying to trigger mouseEnter event when mouse is on top of multiple elements.
I want both mouseEnter events to trigger when the mouse is at the center, and preferably for both to turn yellow.
Run the code snippet below for an example:
<!DOCTYPE html>
<html>
<head>
<style>
div:hover {
background-color: yellow;
}
div {
width: 100px;
height:100px;
background:green;
border: 2px solid black;
}
.second {
transform:translateX(50%) translateY(-50%);
}
</style>
<script>
function onhover(){console.log('hovered')}
</script>
</head>
<body>
<div onmouseenter=onhover()></div>
<div onmouseenter=onhover() class='second'></div>
</body>
</html>
According to MDN, the mouseenter event does not bubble, whereas the mouseover event does. However, even if it DID bubble, your elements currently have no relation to one another, thus the mouse events are captured by the upper element.
One possible way around this is with the amazing elementsFromPoint function in JavaScript, which makes quick work of solving your issue:
// Only the IDs of the elments you are interested in
const elems = ["1", "2"];
// Modified from https://stackoverflow.com/a/71268477/6456163
window.onload = function() {
this.addEventListener("mousemove", checkMousePosition);
};
function checkMousePosition(e) {
// All the elements the mouse is currently overlapping with
const _overlapped = document.elementsFromPoint(e.pageX, e.pageY);
// Check to see if any element id matches an id in elems
const _included = _overlapped.filter((el) => elems.includes(el.id));
const ids = _included.map((el) => el.id);
for (const index in elems) {
const id = elems[index];
const elem = document.getElementById(id);
if (ids.includes(id)) {
elem.style.background = "yellow";
} else {
elem.style.background = "green";
}
}
}
div {
width: 100px;
height: 100px;
background: green;
border: 2px solid black;
}
.second {
transform: translateX(50%) translateY(-50%);
}
<div id="1"></div>
<div id="2" class="second"></div>
I think that you can not without javascript, and with it it's a bit tricky, you have to check on every mousemove if the coordinates of the mouse are in de bounding box of the element, this fill fail with elements with border radius but for the others it's ok
<script>
var hovered=[]
function addHover(element){hovered.push(element)}
function onhover(element){console.log("hovered",element)}
function onCustomHover(e){
hovered.forEach((el,i)=>{
let bounds=el.getBoundingClientRect()
if (e.pageX > bounds.left && e.pageX < bounds.bottom &&
e.pageY > bounds.top && e.pageY < bounds.right ) {
onhover(i);
}
})
}
</script>
</head>
<body>
<div id="div1"></div>
<div id="div2" class='second'></div>
<script>
document.body.addEventListener('mousemove', onCustomHover, true);//{capture :false});
addHover(document.getElementById("div1"))
addHover(document.getElementById("div2"));
</script>
I would appreciate if you could rate the answer if that was usefull to you because I can not make comments yet <3
It will be easier to change your code a little bit.
ex. Add to your div elements class box.
Add to your styles class with name hovered which will look like:
.hovered {
background-color: yellow;
}
Into JS(between script tag) add event listeners (code not tested, but idea is shown), also move script to place before closing body tag:
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
box.addEventListener('mouseover', () => {
boxes.forEach(b => b.classList.add('hovered'));
});
box.addEventListener('mouseout', () => {
boxes.forEach(b => b.classList.remove('hovered'));
});
});
The problem is that elements are blocking the mouse such that elements in the background do not receive the event. With the exception that events bubble to the parent.
Given that you could change your markup slightly to get this effect.
First add a class to your boxes so we can easily find them in JavaScript:
<div class="box"></div>
<div class="box second"></div>
Then adapt the CSS such that this background change is toggled with a class instead:
.box.hovered {
background-color: yellow;
}
And then the JavaScript:
// Get all box elements
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
// For each box attach a listener to when the mouse moves
box.addEventListener('mousemove', (ev) => {
// Get the position of the mouse
const { x, y } = ev;
boxes.forEach(b => {
// for each box get it's dimension and location
const rect = b.getBoundingClientRect();
// check if the pointed is in the box
const flag = x > rect.left && x < rect.right && y > rect.top && y < rect.bottom;
// toggle the class
b.classList.toggle('hovered', flag);
});
});
});
This can be improved a lot, especially if you have more boxes by getting the rectangles beforehand and then using the index in the forEach to link the box to it's rectangle:
const boxes = document.querySelectorAll('.box');
const rects = [...boxes].map(box => box.getBoundingClientRect());
Another improvement is to use the fact that events bubble to the parent, that means you could wrap all boxes in one parent and only add a listener to this parent.

Change button color based on screen size

What I am trying to achieve is when my device size is less than 736 px, the button should animate. I got the button working correctly, however, I’m struggling to work with the specific screen size.
$(window).resize(function() {
if ($(window).width() <= 736) {
// do something
let myBtn = document.querySelector(".btn");
let btnStatus = false;
myBtn.style.background = "#FF7F00";
function bgChange() {
if (btnStatus == false) {
myBtn.style.background = "#FF0000";
btnStatus = true;
}
else if (btnStatus == true) {
myBtn.style.background = "#FF7F00";
btnStatus = false;
}
}
myBtn.onclick = bgChange;
}
});
.btn {
width: 200px;
height: 100px;
text-align: center;
padding: 40px;
text-decoration: none;
font-size: 20px;
letter-spacing: .6px;
border-radius: 5px;
border: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="btn">CLICK ME</button>
Here's an implementation of what you're trying to do that uses:
class to alter button styling instead of style,
vanilla JavaScript instead of jQuery.
Using class is a good idea, as it keeps the styling in the CSS and out of the JavaScript code.
Using vanilla JavaScript whenever you can is preferable.
Here are the two new classes:
.btn-small-screen {
background: #FF7F00;
}
.btn-clicked {
background: #FF0000;
}
.btn-small-screen class is applied when the window is small, .btn-clicked is toggled whenever the button is clicked.
Here's the JavaScript code:
let myBtn = document.querySelector('.btn');
let isSmallWindow = () => window.innerWidth <= 736;
function toggleButtonOnClick () {
myBtn.classList.toggle('btn-clicked');
}
function setButtonMode () {
if (isSmallWindow()) {
myBtn.classList.add('btn-small-screen');
myBtn.addEventListener('click', toggleButtonOnClick);
} else {
myBtn.classList.remove('btn-small-screen');
myBtn.classList.remove('btn-clicked');
myBtn.removeEventListener('click', toggleButtonOnClick);
}
}
// setup mode on resize
window.addEventListener('resize', setButtonMode);
// setup mode at load
window.addEventListener('load', setButtonMode);
References:
Document.querySelector()
Window.innerWidth
Element.classList
DOMTokenList.toggle()
DOMTokenList.add()
DOMTokenList.remove()
EventTarget.addEventListener()
A working example:
let myBtn = document.querySelector('.btn');
let isSmallWindow = () => window.innerWidth <= 736;
function toggleButtonOnClick () {
myBtn.classList.toggle('btn-clicked');
}
function setButtonMode () {
if (isSmallWindow()) {
myBtn.classList.add('btn-small-screen');
myBtn.addEventListener('click', toggleButtonOnClick);
} else {
myBtn.classList.remove('btn-small-screen');
myBtn.classList.remove('btn-clicked');
myBtn.removeEventListener('click', toggleButtonOnClick);
}
}
// setup small mode on resize
window.addEventListener('resize', setButtonMode);
// setup small mode at load
window.addEventListener('load', setButtonMode);
.btn {
width: 200px;
height: 100px;
text-align: center;
padding: 40px;
text-decoration: none;
font-size: 20px;
letter-spacing: .6px;
border-radius: 5px;
border: none;
}
.btn-small-screen {
background: #FF7F00;
}
.btn-clicked {
background: #FF0000;
}
<button class="btn">CLICK ME</button>
Note: There is one optimization that I left out, so the code would be easier to follow.
Notice that setButtonMode() changes the DOM every time, even though it might already be set to the desired mode. This is inefficient.
To improve efficiency and only change the DOM when necessary, you could introduce a state variable (call it smallMode), and set it true whenever appropriate. Like so:
let smallMode = false;
function setButtonMode () {
if (isSmallWindow()) {
if (!smallMode) {
myBtn.classList.add('btn-small-screen');
myBtn.addEventListener('click', toggleButtonOnClick);
smallMode = true;
}
} else if (smallMode) {
myBtn.classList.remove('btn-small-screen');
myBtn.classList.remove('btn-clicked');
myBtn.removeEventListener('click', toggleButtonOnClick);
smallMode = false;
}
}

Refactor jQuery inview event listener

I want to convert the following code to pure JavaScript.
$(function () {
$('h2,.single').on('inview', function () {
$(this).addClass('is-show');
});
});
I tried much time but still can not figure out how. Any helps?
Inview was an old plugin to solve a problem that now has a web API, as long as you don't need to support IE you can use intersection observer to do this.
Without any example or further explanation of how you need things to function it's hard to guess what you want to achieve. But here's a basic implementation that would mimic the tiny bit of JQuery you provided.
const sections = [].slice.call(document.querySelectorAll(".single"));
function createObserver(el) {
let observer;
const options = {
root: null,
rootMargin: "0px",
threshold: 0.5
};
observer = new IntersectionObserver(handleIntersect, options);
observer.observe(el);
}
function handleIntersect(entries, observer) {
entries.forEach((entry) => {
let box = entry.target;
let visible = entry.intersectionRatio;
if(visible > 0.5) {
box.classList.add('is-show');
} else {
box.classList.remove('is-show');
}
});
}
const setup = (sections) => {
for (let i in sections) {
const el = sections[i];
createObserver(el);
}
}
setup(sections);
.single {
padding: 2rem;
background: tomato;
color: #fff;
margin: 600px 0;
transition: all .5s ease-out;
opacity: 0;
}
.is-show {
opacity: 1
}
<h2 class="single">I'm an H2 Element in frame</h2>
<h2 class="single">I'm an H2 Element in frame</h2>
<h2 class="single">I'm an H2 Element in frame</h2>
<h2 class="single">I'm an H2 Element in frame</h2>
Except the event you can use this:
(function() {
document.querySelector("h2, .single").addEventListener("click", function(){
this.classList.add("is-show");
});
})();
I didn't find an equivalent for inview. You can refer to this.

JS DOM manipulation 'mouseleave' triggers unexpectedly in Safari browser

EDIT: 'mouseleave' event is constantly being triggered, although the mouse does not leave the element.
Code works as intended in: chrome, mozilla, edge, opera. But not safari!
I have a vanilla JavaScript solution that changes images every 1000ms when mouse hovered on parent element. There can be any amount of images inside wrapper and this should still work. To be more clear, javascript adds "hidden" class for every image and removes it from the one who's turn is to be displayed. (Code is in fiddle).
In safari it seems to be stuck swapping 2-3rd image. Am I using wrong dom-manipulation approach? How can I find the error?
Problem presentation: https://jsfiddle.net/pcwudrmc/65236/
let imageInt = 0;
let timeOut;
let imagesWrapper = document.querySelectorAll('.items-box__item');
// Events for when mouse enters/leaves
imagesWrapper.forEach(el => {
el.addEventListener('mouseenter', () => startAnim(el));
el.addEventListener('mouseleave', () => stopanim(el));
});
// DOM Manipulation functions
function changeImages(el) {
imageInt += 1;
if (imageInt === el.children[0].children.length) {
// reset to 0 after going through all images
imageInt = 0;
}
for (let i = 0; i < el.children[0].children.length; i++) {
// Adds "hidden" class to ALL of the images for a product
el.children[0].children[i].classList.add('hidden');
}
// Removes "hidden" class for one
el.children[0].children[imageInt].classList.remove('hidden');
// changeImage calls itself again after 1 second, if hovered
timeOut = setTimeout(changeImages.bind(null, el), 1000);
}
function changeBack(el) {
for (let i = 0; i < el.children[0].children.length; i++) {
// Adds "hidden" class to ALL of the images for a product
el.children[0].children[i].classList.add('hidden');
}
// Removes "hidden" class for the first image of the item
el.children[0].children[0].classList.remove('hidden');
}
startAnim = element => { changeImages(element) }
stopanim = element => {
changeBack(element);
clearTimeout(timeOut);
imageInt = 0;
}
.items-box__item {
width: 300px;
height: 300px;
}
.items-box__item--main-image {
object-fit: contain;
width: 90%;
height: 265px;
}
.hidden {
display: none;
}
<h3>Hover on pic and hold mouse</h3>
<div class="items-box__item">
<a href="/">
<img class="items-box__item--main-image" src="https://res.cloudinary.com/keystone-demo/image/upload/c_limit,h_300,w_300/v1525948251/yrllszgndxzlydbycewc.jpg"/>
<img class="items-box__item--main-image hidden" src="https://res.cloudinary.com/keystone-demo/image/upload/c_limit,h_300,w_300/v1525948251/e96i5zbvxxuxsdczbh9d.jpg"/>
<img class="items-box__item--main-image hidden" src="https://res.cloudinary.com/keystone-demo/image/upload/c_limit,h_300,w_300/v1525948252/boaqfs3yuc4r7mvhsqqu.jpg"/>
</a>
</div>
You need to look at relatedTarget of mouseleave event, as both mouseenter and mouseleave happen every time the displayed image changes.
Also your code might be simplified. See the snippet below. Hope it helps.
const play = (box) => {
while (!box.classList.contains('items-box__item')) box = box.parentElement;
var img = box.querySelector('.show');
img.classList.remove('show');
(img.nextElementSibling || box.firstElementChild).classList.add('show');
}
const stop = ({target: box, relatedTarget: rt}) => {
while (!box.classList.contains('items-box__item')) box = box.parentElement;
while (rt != box && rt) rt = rt.parentElement;
if (rt === box) return;
box.querySelector('.show').classList.remove('show');
box.firstElementChild.classList.add('show');
box.play = clearInterval(box.play);
}
[...document.querySelectorAll('.items-box__item')]
.forEach((box) => {
box.addEventListener(
'mouseenter',
function() {
if (box.play) return;
play(box);
box.play = setInterval(() => play(box), 1000);
}
);
box.addEventListener('mouseleave', stop);
});
.items-box__item {
display: block;
width: 300px;
height: 300px;
}
.items-box__item img {
object-fit: contain;
width: 90%;
height: 265px;
display: none;
}
img.show {
display: initial
}
<h3>Hover on pic and hold mouse</h3>
<a class="items-box__item" href="/">
<img class="show" src="https://res.cloudinary.com/keystone-demo/image/upload/c_limit,h_300,w_300/v1525948251/yrllszgndxzlydbycewc.jpg">
<img src="https://res.cloudinary.com/keystone-demo/image/upload/c_limit,h_300,w_300/v1525948251/e96i5zbvxxuxsdczbh9d.jpg">
<img src="https://res.cloudinary.com/keystone-demo/image/upload/c_limit,h_300,w_300/v1525948252/boaqfs3yuc4r7mvhsqqu.jpg">
</a>

MutationObserver hangs in IE11

I am using MutationObserver to check when some nodes are removed and replaced by other new nodes to an element.
The following code works totally fine in Chrome, but on IE11 it just hangs.
If I change the addedNodes check with removedNodes, it works on IE11. I just don't understand why it hangs when I check for new nodes being added.
Any idea? I can't find any resources for this issue.
var nodeToObserve = document.querySelector('#targetNode');
var callback = function(mutations, observer) {
for (var index = 0; index < mutations.length; index) {
var mutation = mutations[index];
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
console.log(mutation);
break;
}
}
observer.disconnect();
}
const observer = new MutationObserver(callback);
observer.observe(nodeToObserve, {
childList: true, // target node's children
subtree: true // target node's descendants
});
html, body {
height: 100%;
}
#targetNode {
border: 1px solid black;
height: 100%;
}
.childNode {
//height: 20px;
background-color: blue;
margin: 10px;
padding: 10px;
}
.grandChildNode {
height: 20px;
background-color: red;
margin: 10px;
}
<div id="targetNode">
</div>
You aren't incrementing the index in your for loop. Probably the results appear in a different order depending on the browser so the if statement will be triggered on some browsers but not others. Thus, the system will hang when the if statement isn't executed b/c of the infinite loop.

Categories