CSS Scroll Snap: Dynamic content insertion triggers scroll - javascript

Let's imagine an example of some content that dynamically inserted upon data fetch from server. The purpose is to display it like a bunch of items in a scroll-snapped container.
The problem is browser somehow randomly tries to scroll to some element but I want the container to stay at the first element no matter how many elements are inserted. The only exception is user that manually scrolls the container.
It works fine unless I add scroll-snap property.
Here is the code snippet
const startingIdx = 3;
const batchSize = 10;
const delay = 500;
const root = document.querySelector('.pills');
const appendBatch = (from) => {
for(let i = 0; i < batchSize; i++){
const element = document.createElement('div');
element.classList.add('pill');
element.innerText = String(from + i);
root.appendChild(element);
}
}
for(let i = 0; i < 3; i++){
setTimeout(() => appendBatch(startingIdx + i*batchSize), i*delay);
}
.pills {
display: flex;
gap: 20px;
width: 100%;
overflow: auto;
scroll-snap-type: x mandatory;
}
.pill {
width: 120px;
height: 40px;
background: rgba(92, 138, 255, 0.15);
display: flex;
align-items: center;
justify-content: center;
border-radius: 20px;
flex-shrink: 0;
scroll-snap-align: start;
}
<main>
<div class="pills">
<div class="pill">1</div>
<div class="pill">2</div>
</div>
</main>
And the demo where you can reproduce it yourself: https://977940.playcode.io/
This is what I'm talking about

Just add that in your script. When page is loaded it scrolls to the left side.
window.addEventListener("DOMContentLoaded", () => {
root.scrollLeft = 0;
});

Seems like adding scroller.scrollBy(0,0); before every batch insertion would save the day as it forces browser to snap to existing element
Source: https://web.dev/snap-after-layout/#interoperability

Related

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😃

Use IntersectionObserver To Trigger Event AFTER Element Completely Passes Threshold

I have a few IntersectionObserver's set up. observer toggles new boxes to be made as the user scrolls down the page. lastBoxObserver loads new boxes as this continuous scrolling happens.
What I would like to do is change the color of a box once it leaves the threshold set in the first observer (observer - whose threshold is set to 1). So, once box 12 enters the viewport, it passes through the observer, and once it has completely passed outside of the threshold for this observer and box 13 enters the observer, box 12's background changes from green to orange, perhaps.
Is there a way to make this happen? Maybe by adding an additional observer, or adding code to observer? Any help is greatly appreciated.
Codepen: https://codepen.io/jon424/pen/NWwReEJ
JavaScript
const boxes = document.querySelectorAll(".box");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
entry.target.classList.toggle("show", entry.isIntersecting);
if (entry.isIntersecting) observer.unobserve(entry.target);
});
},
{
threshold: 1,
}
);
const lastBoxObserver = new IntersectionObserver((entries) => {
const lastBox = entries[0];
if (!lastBox.isIntersecting) return;
loadNewBoxes();
lastBoxObserver.unobserve(lastBox.target);
lastBoxObserver.observe(document.querySelector(".box:last-child"));
}, {});
lastBoxObserver.observe(document.querySelector(".box:last-child"));
boxes.forEach((box) => {
observer.observe(box);
});
const boxContainer = document.querySelector(".container");
function loadNewBoxes() {
for (let i = 0; i < 1000; i++) {
const box = document.createElement("div");
box.textContent = `${i + 1}`;
box.classList.add("box");
observer.observe(box);
boxContainer.appendChild(box);
}
}
HTML
<div class="container">
<div class="box">0</div>
</div>
CSS
.container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.box {
background: green;
color: white;
font-size: 4rem;
text-align: center;
margin: auto;
height: 100px;
width: 100px;
border: 1px solid black;
border-radius: 0.25rem;
padding: 0.5rem;
transform: translateX(100px);
opacity: 0;
transition: 150ms;
}
.box.show {
transform: translateX(0);
opacity: 1;
}
.box.show.more {
background-color: orange;
}
You just need to add color change logic in your first observer.
I tested with the following changes to your code,
In css, change .box.show.more to -
.box.more {
background-color: orange;
}
In javascript -
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
entry.target.classList.toggle("show", entry.isIntersecting);
if (entry.isIntersecting) {
observer.unobserve(entry.target);
if(entry.target.textContent === '13')
entry.target.previousSibling.classList.toggle('more');
}
});
},
{
threshold: 1,
}
);
As you can see, the only change is that I added this part -
if(entry.target.textContent === '13')
entry.target.previousSibling.classList.toggle('more');
I also changed the number of new div to add from 1000 to 20 for easy test.
If you want to change box 12 color as soon as box 13 enters viewport, you can simply change "threshold" from 1 to 0.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<style>
.container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.box {
background: green;
color: white;
font-size: 4rem;
text-align: center;
margin: auto;
height: 100px;
width: 100px;
border: 1px solid black;
border-radius: 0.25rem;
padding: 0.5rem;
opacity: 0;
}
.box.show {
opacity: 1;
}
.box.show.more {
background-color: orange;
}
.mytest{
border:solid 10px #000;
background-color: orange;
}
</style>
<div class="container">
<div class="box">0</div>
<div class="sentinel"></div>
</div>
<script>
/* https://stackoverflow.com/questions/70994897/use-intersectionobserver-to-trigger-event-after-element-completely-passes-thresh/71054028#71054028 */
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("show");
}
if (entry.boundingClientRect.top < entry.rootBounds.top) {
/* entry is above viewport */
entry.target.classList.add("mytest");
observer.unobserve(entry.target);
}
});
},
{
threshold: 0, /* top is 200 below top of viewport - something for the box to scroll into*/
rootMargin: "-200px 0px 0px 0px"
}
);
/* last div sentinel indicates last box */
const mysentinel = new IntersectionObserver((entries) => {
var entry = entries[0];
if (entry.isIntersecting) {
loadNewBoxes();
}
},{
threshold: 0
});
const boxContainer = document.querySelector(".container");
const sentinel = document.querySelector(".sentinel");
/* setup - create extra boxes, sentinel is last after boxes */
loadNewBoxes();
mysentinel.observe(sentinel);
function loadNewBoxes() {
for (let i = 0; i < 10; i++) {
const box = document.createElement("div");
box.textContent = `${i + 1}`;
box.classList.add("box");
box.classList.add("show");
observer.observe(box);
boxContainer.appendChild(box);
}
boxContainer.appendChild(sentinel);
}
</script>
</body>
</html>
IntersectionObserver callback is async, if you scroll fast enough then some boxes will be missed. I've simplified your code. If the box/observed target is above the root top then I add a class and unobserve it. If the box is transitioning I add a class to show the box. I use sentinel to indicate the last box and to add more boxes when the sentinel hits the viewport.

Append child multiple times to its parent onclick

I want to dynamically append a child to its parent multiple times when I click the button.
let btn = document.querySelector('.btn');
let starContainer = document.querySelector('.star__container');
let starWin = document.createElement('div');
starWin.classList.add('star__win');
starWin.innerText = 'Test';
btn.addEventListener('click',addItem);
function addItem(){
starContainer.appendChild(starWin);
}
<div class="star__container"></div>
<button class='btn'>Click</button>
You need to create your starWin element each time the addItem method is called. Now, you append the same element several times. It won't be cloned.
let btn = document.querySelector('.btn');
let starContainer = document.querySelector('.star__container');
btn.addEventListener('click', addItem);
function addItem() {
starContainer.appendChild(createElement());
}
function createElement() {
let starWin = document.createElement('div');
starWin.classList.add('star__win');
starWin.innerText = 'Test';
return starWin;
}
<div class="star__container"></div>
<button class='btn'>Click</button>
<div class="star__container"></div>
<button class='btn'>Click</button>
let btn = document.querySelector('.btn');
let starContainer = document.querySelector('.star__container');
btn.addEventListener('click',addItem);
function addItem(){
let starWin = document.createElement('div');
starWin.className = 'star__win';
starContainer.appendChild(starWin);
}
Update
Issue
Expectation: A <div> should to be appended to DOM for each click of a button.
Result: The first click of the button appends a <div> to DOM, but thereafter any further clicking of said button elicits nothing.
Diagnosis: All code concerning the creation of <div> is not within a function, the only time it will run is at page load. When the handler function is triggered by a button click, it finds that <div> that was made at page load and successfully appends the <div> to the DOM. When user clicks the button again, nothing happens because the <div> was made only once.
Solution: Place all of the aforementioned code in the handler function addItem()
Demo 1
let btn = document.querySelector('.starBtn');
btn.addEventListener('click', addItem);
function addItem(event) {
const box = document.querySelector('.starBox');
let star = document.createElement('b');
star.classList.add('stellar');
star.innerText = '⭐';
box.appendChild(star);
}
body {
font-size: 3rem
}
.starBox {
word-wrap: break-word;
}
.starBtn {
position: fixed;
top: 0;
left: 0;
font: inherit;
}
<article class="starBox"></article>
<button class='starBtn'>🤩</button>
Not sure what the problem is, don't care really. If you take a look at this demo it'll help with whatever issue you may have. Details are commented line-by-line in the demo. Apologies in advance -- I'm bored ...
🥱
I'll come back and post a decent answer later. Review the demo in Full Page mode.
Demo 2
// Reference the <form>
const STARS = document.forms.starFactory;
// Define the counter
let s = 0;
// Register the form to the click event
STARS.onclick = makeStar;
// Handler function passes event object
function makeStar(event) {
// Define an array of characters✱
let galaxy = ['★', '☆', '✨', '✩', '✪', '⚝', '✫', '✭', '✯', '✰', '✴', '⭐', '🌟', '🌠', '💫', '🟊', '🤩'];
/*
- "this" is the form
- Collect all <input>, <button>, <output>, etc into a
NodeList
*/
const field = this.elements;
/*
- event.target is always the tag the user interacted with
- In this case it's the <button> because this handler
will not accept but that <button>
*/
const clicked = event.target;
/*
- The NodeList `field` can reference form tags by
suffixing the tag's #id or [name]
- The <fieldset> and <output> are referenced
*/
const jar = field.starJar;
const cnt = field.count;
/*
- By using a simple `if` condition strategically we can
control what and how tags behave when a registered
event.
- The remainder of the handler is explained at the very
end.
*/
if (clicked.id === 'STARt') {
s++;
const star = document.createElement('S');
let index = Math.floor(Math.random() * galaxy.length);
let ico = galaxy[index];
star.textContent = ico;
star.className = 'star';
star.style.zIndex = s;
star.style.left = Math.floor(Math.random() * 85) + 1 +'%';
star.style.bottom = Math.floor(Math.random() * 90) + 1 + '%';
jar.appendChild(star);
cnt.value = s;
}
}
/*
- increment `s` by one
- create a <s>trikethrough tag (aka <s>tar tag JK)
- generate a random number in the range of 0 to 15
- get a character from galaxy Array at the index number
determined from the previous step.
- render the text character in the <s>tar
- assign the class .star to <s>tar
- assign `z-index` to <s>tar (Note: it increases every
click which insures that tags won't collide)
- randomly assign `left` in the range of 1 to 85 to <s>tar
- randomly assign `bottom` in the range of 1 to 90 to
<s>tar
- append <s>tar to #starJar
- increment #count value
*/
:root,
body {
font: 400 5vw/1 Verdana;
background: #123;
}
#starFactory {
display: flex;
justify-content: center;
align-items: center;
}
#starJar {
position: relative;
display: flex;
flex-flow: row wrap;
justify-content: end;
width: 50vw;
height: 50vw;
border: 5px inset rgba(255,255,0,0.3);
border-bottom-left-radius: 12vw;
border-bottom-right-radius: 12vw;
color: gold;
}
legend {
position: relative;
z-index: 5150;
width: max-content;
font-size: 1.5rem;
color: goldenrod;
}
#STARt {
position: relative;
z-index: 5150;
font-size: 1.5rem;
background: none;
padding: 0;
border: 0;
cursor: pointer;
}
#count {
position: relative;
z-index: 9999;
font-size: 1.25rem;
width: 5vw;
overflow-x: visible;
color: cyan;
}
s.star {
position: absolute;
text-decoration: none;
font-size: 1.5rem;
background: none;
padding: 0;
margin: 0;
border: 0;
}
<form id='starFactory'>
<fieldset id="starJar">
<legend>starJar
<button id='STARt' type='button'>
✴️ <output id='count'></output>
</button>
</legend>
</fieldset>
</form>

z-index not working on Safari when manipulating it using Javascript

I am making a website that has multiple layers to it, which is brought back and forth by manipulating the z-index in Javascript through event reactions (like onclick). I have a navigation bar which has a large z-index value compared to every other element as I would like it to be at the very front regardless of anything. However, when run on Safari, the nav bar disappears from the get go, while it works fine on Google Chrome and FireFox.
I have included the css code and javascript code that dictates this role:
JAVASCRIPT:
//Global variables representing DOM elements
var introTitleElem = document.getElementById('introduction-title');
var resumeElem = document.getElementById('resume-container');
var introElem = document.getElementById('intro-content');
var eduElem = document.getElementById('edu-content');
//Layer tracker (for transition effect)
var prev = introElem;
var prevButton = "";
//Function that actually changes the layers
function changeLayer(layer, button) {
if (layer === prev) return;
introTitleElem.style.opacity = "0";
prev.style.zIndex = "40";
layer.style.zIndex = "50";
layer.style.cssText = "opacity: 1; transition: opacity 0.5s";
prev.style.zIndex = "5";
prev.style.opacity = "0";
prev = layer;
if (prevButton !== "") prevButton.style.textDecoration = "none";
button.style.textDecoration = "underline";
prevButton = button;
}
//Manages events triggered by name button toggle
function revealResume() {
introTitleElem.style.zIndex = "0";
resumeElem.style.zIndex = "10";
resumeElem.style.opacity = "1";
introElem.style.opacity = "1";
resumeElem.style.transition = "opacity 0.7s";
introElem.style.transition = "opacity 0.7s";
}
document.getElementById("name-title").addEventListener("click", revealResume);
//Manage z-index of different components of the resume and reveal them accordingly
$('#nav-education').click(function () {
onEducation = true;
changeLayer(eduElem, this);
});
CSS (SASS):
/*NAVIGATION STYLING*/
#fixed-nav {
align-self: flex-start;
overflow-x: hidden;
float: right;
z-index: 9999 !important;
display: flex;
margin: 1em;
li {
margin: 0.6em;
font: {
family: $font-plex-sans-condensed;
size: 0.8em;
}
text-align: center;
color: $lightest-grey;
transition: color 0.3s;
&:hover {
cursor: pointer;
color: $dark-grey;
transition: color 0.3s;
}
}
}
/*OVERALL DIV FORMATTING*/
.format-div {
width: 100vw;
height: 100vh;
display: flex;
position: absolute;
justify-content: center;
align-items: center;
flex-direction: column;
opacity: 0;
}
/*EDUCATION CONTENT STYLING*/
#edu-content {
background-color: $red-1;
}
HTML:
<div id="resume-container">
<ul id="fixed-nav">
<li id="nav-education">education.</li>
<li id="nav-experiences">experiences.</li>
<li id="nav-skills">skills.</li>
<li id="nav-projects">projects.</li>
<li id="nav-additional">additional.</li>
<li id="nav-contact">contact.</li>
</ul>
<div id="intro-content" class="format-div">
<h1 class="type-effect">
<h1>I'm Daniel <b>(Sang Don)</b> Joo</h1>
<span class="blinking-cursor">|</span>
</h1>
</div>
<div id="edu-content" class="format-div">
<h1>education</h1>
</div>
Sorry about the large amount of code but I'm really unsure of where this problem is rooted. Cheers!
it has to be the position feature of the elements so that it can work
wrong example ( not working )
.className { z-index: 99999 !important; }
correct example ( it work )
.className { position: 'relative'; z-index: 99999 !important; }
.className { position: 'absolute'; z-index: 99999 !important; }
etc..
good luck :)

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