IntersactionObserver() only observes the first element of a row instead of all - javascript

I'm experimenting with IntersectionObserver(), and it's behaving weirdly: I'm using a very simple layout, just 8 images in the same div with flexbox, using wrap, so basically the 8 images are positioning themself in different rows, according to the size of the viewport. I'm applying a filter class (which adds a blur filter) to each element first and then removing it when they are displayed on the screen:
HTML:
<div class="flex--cont">
<div class="box box-2">
<img src="img/1.jpg" alt="" class="img img-1">
</div>
<div class="box box-1">
<img src="img/2.jpg" alt="" class="img img-2">
</div>
<div class="box box-3">
<img src="img/3.jpg" alt="" class="img img-3">
</div>
<div class="box box-4">
<img src="img/4.jpg" alt="" class="img img-4">
</div>
<div class="box box-5">
<img src="img/5.jpg" alt="" class="img img-5">
</div>
<div class="box box-6">
<img src="img/6.jpg" alt="" class="img img-6">
</div>
<div class="box box-7">
<img src="img/7.jpg" alt="" class="img img-7">
</div>
<div class="box box-8">
<img src="img/8.jpg" alt="" class="img img-8">
</div>
</div>
JAVASCRIPT
const allImage = Array.from(document.querySelectorAll(".img"));
allImage.forEach((img) => img.classList.add("filter"));
const removeFilter = function (entries, observer) {
const [entry] = entries;
const image = entry.target;
image.classList.remove("filter");
};
const ImageObserver = new IntersectionObserver(removeFilter, {
root: null,
threshold: 0.15,
});
allImage.forEach((img) => ImageObserver.observe(img));
The thing is that the observer actually only observes the very first element of each row, so if I have 2 rows, it only gets the 1st and the 5th image, if I have 3 rows it gets the 1rst, the 4th and the 7th image and so on. I do have applied it to all of the images. Why is it doing that? Thanks for your answers!

Only the first one was changing because that was all you were targeting in the change color function by destructuring only the first array element:
const [entry] = entries;
However, the InteractionObserver callback is not called per entry but for all entries that are triggered simultaneously; hence the entries array contains all of the items being observed and you need to check the isIntersecting property like this:
const changeColor = function(entries) {
entries.forEach(entry => {
if(entry.isIntersecting) {
entry.target.style.background = 'blue';
} else {
entry.target.style.background = 'red';
}
})
}
From the MDN docs
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};

Related

How to - When image is clicked set opacity to 1 and lower opacity for other images

I couldn't for the life of me figure this out! How can I make this happen - When image is clicked set opacity to 1 and lower opacity for other images? I was told to add a class to the clicked image, store it, then remove the class when another image is clicked? but i cant figure it out!
let items = document.getElementsByClassName("item");
document.body.addEventListener('click', (event) => {
const el = event.target;
for(var i=0; i< smallImg.length; i++) {
if (el.className !== 'sec') return;
const wasSelected = el.classList.contains('selected');
for (const d of document.querySelectorAll('div >img'))
d.classList.remove('selected');
el.classList.toggle('selected', !wasSelected)
console.log(".selected");
}
});
.sec:not(:first-child) {
opacity: 0.3;
}
.sec:not:active{
opacity: 0.3;
}
<div class="image-container">
<img
src="https://source.unsplash.com/400x400/?stationery"
class="item main-image"
/>
<div class="secondary-image">
<img
src="https://source.unsplash.com/100x100/?pen"
class="item sec item-1 active "
/>
<img
src="https://source.unsplash.com/100x100/?pencil"
class="item sec item-2"
/>
<img
src="https://source.unsplash.com/100x100/?notepad"
class="item sec item-3"
/>
<img
src="https://source.unsplash.com/100x100/?eraser"
class="item sec item-4"
/>
</div>
Just select the element that currently has the active class, and if such an element exists, remove that class from that. And then add it to the one that was clicked.
(Any check on whether the clicked element was actually one of those images, is not currently included. I simply kept your general click handler for body, please refine this yourself to apply only where needed.)
document.body.addEventListener('click', (event) => {
let el = event.target;
let prev = document.querySelector('.secondary-image .active');
if(prev) {
prev.classList.remove('active');
}
el.classList.add('active');
});
.secondary-image .sec:not(.active) {
opacity: 0.3;
}
<div class="image-container">
<img
src="https://source.unsplash.com/400x400/?stationery"
class="item main-image"
/>
<div class="secondary-image">
<img
src="https://source.unsplash.com/100x100/?pen"
class="item sec item-1 active "
/>
<img
src="https://source.unsplash.com/100x100/?pencil"
class="item sec item-2"
/>
<img
src="https://source.unsplash.com/100x100/?notepad"
class="item sec item-3"
/>
<img
src="https://source.unsplash.com/100x100/?eraser"
class="item sec item-4"
/>
</div>

How to randomly re-render the DOM nodes' order of precedence from a node-list?

I'm trying to shuffle cards by reordering the index of the nodes in my NodeList.
How can I achieve removing and appending the children of a class attribute I have?
HTML:
<div class="card" data-card="1" onclick="cardClicked(this)">
<img src="img/cards/1.png" alt="">
<img class="back" src="img/cards/back.png" alt="">
</div>
<div class="card" data-card="7" onclick="cardClicked(this)">
<img src="img/cards/7.png" alt="">
<img class="back" src="img/cards/back.png" alt="">
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
<img src="img/cards/1.png" alt="">
<img class="back" src="img/cards/back.png" alt="">
</div>
JavaScript:
function shuffleCards() {
let cards = document.querySelectorAll('.card');
let cardsArray = Array.from(cards);
// reorder the nodes of the nodelist (cards)
}
There are several ways you can go about "shuffling" an array. I chose the Fisher-Yates method in an experimental "war" card game.
https://github.com/scottm85/war/blob/master/src/Game/Deck.js#L80
shuffle()
{
/* Use a Fisher-Yates shuffle...If provides a much more reliable shuffle than using variations of a sort method https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle */
let cards = this.cards;
for (let i = cards.length - 1; i > 0; i--)
{
let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i
[cards[i], cards[j]] = [cards[j], cards[i]];
}
this.cards = cards;
console.log("----------------- Deck Shuffled -----------------");
}
https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
Granted my provided example is slightly different than your needs as I was doing this in react, had a Deck array built out, and wasn't using JS for direct DOM manipulation. In theory though, you could modify this to work with your method. Something like this:
function shuffleCards
{
let cards = document.querySelectorAll('.card');
let cardsArray = Array.from(cards);
for (let i = cardsArray.length - 1; i > 0; i--)
{
let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i
[cardsArray[i], cardsArray[j]] = [cardsArray[j], cardsArray[i]];
cards[i].remove();
}
cardsArray.forEach(t => document.body.appendChild(t));
}
You can use sort and Math.random() methods like this:
function shuffleCards() {
var parent = document.getElementById("parent");
let cards = document.querySelectorAll('.card');
let cardsArray = Array.from(cards);
//console.log(cardsArray)
cardsArray.sort(() => Math.random() - 0.5);
//console.log(cardsArray)
cardsArray.forEach(t => parent.appendChild(t));
// reorder the nodes of the nodelist (cards)
}
<h3> Click on card to Shuffle at the same postion</h3>
<div id="parent">
<div class="card" data-card="1" onclick="shuffleCards(this)">
card 1
<img src="img/cards/1.png" alt="">
<img class="back" src="img/cards/back.png" alt="">
</div>
<div class="card" data-card="7" onclick="shuffleCards(this)">
card 2
<img src="img/cards/7.png" alt="">
<img class="back" src="img/cards/back.png" alt="">
</div>
<div class="card" data-card="1" onclick="shuffleCards(this)">
card 3
<img src="img/cards/1.png" alt="">
<img class="back" src="img/cards/back.png" alt="">
</div>
</div>
Others have provided algorithms for the randomization portion, but here is how you can handle the detaching and reattaching portion. removeChild() will get you the node detached from the DOM and appendChild() will allow you to add it. Just be careful with attaching and detaching. Events can get a bit messy especially if you end up cloning the node somewhere down the line rather than just attaching it again.
(function shuffleCards() {
let container = document.querySelector('#container');
let cards = container.querySelectorAll('.card');
[...cards].map(
node => container.removeChild(node)
).sort(
() => Math.random() - 0.5 // shamelessly stolen from Alireza Ahmadi
).forEach(
node => container.appendChild(node)
);
})()
<div id="container">
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 1
</div>
<div class="card" data-card="7" onclick="cardClicked(this)">
Card 2
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 3
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 4
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 5
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 6
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 7
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 8
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 9
</div>
<div class="card" data-card="1" onclick="cardClicked(this)">
Card 10
</div>
</div>
The following approach is agnostic to where the queried nodes need to be removed from, and after shuffling, have to be inserted into again.
Thus one does not need to know anything about the DOM structure except for the element query.
Basically ...
Use only reliable/proven shuffle functions like _.shuffle.
Create an array from the queried node list.
Map it with node items in a way that each (child) node gets removed from the DOM but preserves not only its own reference but also the reference of its parent node.
Shuffle the mapped array.
Iterate the shuffled items in a way where from each item one can restore the former parent-node child-node relationship ...
function shuffleArray(arr) {
let idx;
let count = arr.length;
while (--count) {
idx = Math.floor(Math.random() * (count + 1));
[arr[idx], arr[count]] = [arr[count], arr[idx]];
}
return arr;
}
function shuffleCards() {
const removedNodeDataList = Array
.from(document.querySelectorAll('.card'))
.map(elmNode => ({
parentNode: elmNode.parentNode,
elementNode: elmNode.parentNode.removeChild(elmNode),
}));
shuffleArray(removedNodeDataList)
.forEach(({ parentNode, elementNode }) =>
parentNode.appendChild(elementNode)
);
}
function init() {
document
.querySelector('button')
.addEventListener('click', shuffleCards);
}
init();
img { background-color: #eee; }
button { display: block; margin: 0 0 10px 0; }
<button>Shuffle Cards</button>
<div class="card" data-card="1">
<img src="https://picsum.photos/140/50?grayscale" alt="">
<img class="back" src="https://picsum.photos/120/50?grayscale" alt="">
</div>
<div class="card" data-card="2">
<img src="https://picsum.photos/100/50?grayscale" alt="">
<img class="back" src="https://picsum.photos/160/50?grayscale" alt="">
</div>
<div class="card" data-card="3">
<img src="https://picsum.photos/180/50?grayscale" alt="">
<img class="back" src="https://picsum.photos/80/50?grayscale" alt="">
</div>

I need to move a node list from one parent to another

I'm trying to move a node list (in my case img HTML tags with a class name of ".images") when I reach a specific breakpoint to another parent as the first child of that new parent.
/* BEFORE BREAKPOINT */
<div class="old-parent">
<img class="images">
<div class="new-parent">
</div>
</div>
<div class="old-parent">
<img class="images">
<div class="new-parent">
</div>
</div>
<div class="old-parent">
<img class="images">
<div class="new-parent">
</div>
</div>
/*AFTER REACHING THE BREAKPOINT*/
<div class="old-parent">
<div class="new-parent">
<img class="images">
</div>
</div>
<div class="old-parent">
<div class="new-parent">
<img class="images">
</div>
</div>
<div class="old-parent">
<div class="new-parent">
<img class="images">
</div>
</div>
So far is working when I use a single selector, however when I try to select them all and transfer them to a new parent is not working.
const newParent = document.querySelectorAll('.new-parent');
const images = document.querySelectorAll('.images');
const mediaQ = window.matchMedia('(max-width: 494px)');
const changeImg = (e) => {
if (e.matches) {
newParent.forEach(elem => {
elem.insertBefore(images, elem.childNodes[0]);
})
}
};
mediaQ.addEventListener('change', changeImg);
Then returns an error in the console:
Uncaught TypeError: Failed to execute 'insertBefore' on 'Node': parameter 1 is not of type 'Node'
You need to index images to move one image at a time.
const changeImg = (e, i) => {
if (e.matches) {
newParent.forEach(elem => {
elem.insertBefore(images[i], elem.childNodes[0]);
})
}
};

How to check if all the input values are equal to my data?

I have 32 items in my array, all of them have these properties: id, word, image. User has to guess what's in all the images and write their guess in inputs (so 32 inputs in total). I need to check if the input equals my arrays property "word" and then when clicked a button (type submit, all my pic's and inputs are in a form) display some text for example "Oops! Guess again" if wrong and "Yay! You got it correctly" if right. The text should appear below every input. I displayed all the pictures and inputs with a forEach, and i'm using bulma framework for this page:
const wordBox = info.forEach((words) => {
mainColumns.innerHTML += `
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src=${words.image} alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<input class="input" id="text" type="text" placeholder="Įvesk žodį">
</div>
</div>
<div class="content">
Content
</div>
</div>
</div>
</div>`;
});
Any ideas?
This is how it should look like (the result should appear in content place)
Something like this
I use change instead of a button click
const info = [
{word:"flower",image:"flower.gif"},
{word:"boat",image:"boat.gif"}
];
const mainColumns = document.getElementById("mainColumns");
mainColumns.innerHTML = info.map(({image,word}) =>
`<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src=${image} alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<input class="input" data-word="${word}" type="text" placeholder="Įvesk žodį">
<span class="correct hide">Yay</span>
<span class="wrong hide">NOO</span>
</div>
</div>
<div class="content">
Content
</div>
</div>
</div>
</div>`).join("");
mainColumns.addEventListener("change",function(e) {
const correct = [...mainColumns.querySelectorAll("[data-word]")].map(input => {
if (input.value) {
const correct = input.value === input.dataset.word;
parent = input.closest("div");
parent.querySelector(".correct").classList.toggle("hide",!correct)
parent.querySelector(".wrong").classList.toggle("hide",correct);
return correct ? 1 : 0;
}
else return 0;
}).reduce((a,b)=>a+b);
document.getElementById("correct").innerText = correct;
})
#mainColumns { display:flex; }
.hide { display: none; }
<div id="mainColumns"></div>
Correct: <span id="correct"></span>
What you can do is to filter the word array with word from the input value. Then check if the length is equal zero, No match, if the length is greater than one, then there is a match.
const status = wordBox.filter(item => item.word === inputWord)
I'd move towards keeping the objects and the HTML separate, binding the HTML to the object and vice versa. This means including a couple more properties to your array elements.
let info = [{
image: 'flower.png',
word: 'flower',
content: '',
guess: ''
}];
function bindWords() {
info.forEach((words) => {
mainColumns.innerHTML = `
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src=${words.image} alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<input class="input" data-word="${words.word}" type="text" placeholder="Įvesk žodį" value="${words.guess}">
</div>
</div>
<div class="content">
${words.content}
</div>
</div>
</div>
</div>`;
});
}
bindWords();
check.addEventListener('click', () => {
info = Array.from(document.querySelectorAll('.card')).map(el => ({
image: el.querySelector('img').src,
word: el.querySelector('.input').dataset.word,
guess: el.querySelector('.input').value,
content: el.querySelector('.input').value === el.querySelector('.input').dataset.word ?
'Correct' : 'Incorrect'
}));
bindWords();
});
<div id="mainColumns"></div>
<button id="check">Check Answers</button>

Hide a section if the images inside are not present

I have an HTML code with more section like this:
<section class="grid-1">
<div class="item-2">
<div class="item-6">
<img src="../img/image1.png" onclick="openModal();currentSlide(4)" class="hover-shadow cursor" style="display: none;">
</div>
<div class="item-6">
<img id="currentPhoto" src="../img/image2.png" onclick="openModal();currentSlide(5)" class="hover-shadow cursor" style="display: none;">
</div>
<div class="item-6">
<img id="currentPhoto" src="../img/image3.png" onclick="openModal();currentSlide(6)" class="hover-shadow cursor" style="display: none;">
</div>
</div>
</section>
the style="display: none;" has been added from this code:
document.addEventListener("DOMContentLoaded", function(event) {
document.querySelectorAll('img').forEach(function(img){
img.onerror = function(){this.style.display='none';};
})
});
but is it possible to hide all the section class grid-1 if all the images sources are not available?
You could add a class to the not loaded images (or a data-attribute if you may) and then compare they to the total images amount in the grid.
function checkGrid(image) {
const grid = image.closest('section');
const gridImages = grid.querySelectorAll('img');
const gridImagesWithError = grid.querySelectorAll('img.invalid-src');
if(gridImagesWithError.length === gridImages.length) {
grid.style.display = 'none';
}
}
document.addEventListener("DOMContentLoaded", function(event) {
const images = document.querySelectorAll('img');
images.forEach(function(image) {
image.onerror = function() {
this.classList.add('invalid-src');
this.style.display = 'none';
checkGrid(this);
};
});
});
Although this works, in order to check multiple grids, it is recommended that you add a class to the grids so the query selector wouldn't have to rely solely on the section tag which can be unsafe.

Categories