ReactJs carousel - how to force items to snap after a swipe? - javascript

I'm building a react carousel and have the basic app set up. It's a slider that follows your finger on mobile, basically a flexbox with overflow: scroll. The buttons work by scrolling the container by (item index * the width of the item) times, so if I scroll to item #3, it will scroll by 300% the width of the item from the starting position.
Here's the code:
function Carousel(props) {
const {children} = props
const [index, setIndex] = useState(0)
const [length, setLength] = useState(children.length)
const [touchPosition, setTouchPosition] = useState(null)
const [movement, setMovement] = useState(0) // saves the distance swiped
useEffect(() => {
setLength(children.length)
}, [children])
const next = () => {
if (index < (length-1)) {
setIndex(prevState => prevState + 1)
}
// sets the index to next if you are not on the last slide
}
const previous = () => {
if (index > 0) {
setIndex(prevState => prevState - 1)
}
// sets the index to be the previous if you are further than first slide
}
const handleTouchStart = (e) => {
const touchDown = e.touches[0].clientX
setTouchPosition(touchDown)
// saves the touch position on touch start
}
const handleTouchEnd = (e) => {
const touchDown = touchPosition
if (touchDown === null) {
return
}
const currentTouch = e.changedTouches[0].clientX
const diff = touchDown - currentTouch
console.log(diff) // for testing
setMovement(diff)
if (diff > 5) {
setTimeout(() => next(), 100)
}
if (diff < -5) {
setTimeout(() => previous(), 100)
}
setTouchPosition(null)
// compares the starting and ending touch positions, calculates the difference
}
// to test the distance swiped
const transformMultiplier =() => {
let mu = (movement / window.innerWidth) * 100
console.log(`m = ${mu}`)
}
useEffect(()=>{
transformMultiplier()
}, [movement])
return (
<div className="carousel-container">
<div className="carousel-wrapper"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{ index > 0 && <button className="left-arrow" onClick={previous}>
<
</button> }
<div className="carousel-content-wrapper">
{ index < children.length -1 && <button className="right-arrow" onClick={next}>
>
</button>}
{/* the sliding is done by the translateX below. it translates by (100% of the slides * the index of the slide) from the starting position. */}
<div className="carousel-content"
style={{transform: `translateX(-${index * (window.innerWidth > 480 ? 100 : (100 - (movement / window.innerWidth) * 100))}%)`}}
>
{ children}
</div>
</div>
</div>
</div>
)
}
.carousel-container {
width: 100vw;
display: flex;
flex-direction: column;
}
.carousel-wrapper {
display: flex;
width: 100%;
position: relative;
}
.carousel-content-wrapper {
overflow: scroll;
-webkit-overflow-scrolling: auto;
width: 100%;
height: 100%;
-ms-overflow-style: none; /* hide scrollbar in IE and Edge */
scrollbar-width: none; /* hide scrollbar in Firefox */
}
.carousel-content {
display: flex;
transition: all 250ms linear;
-ms-overflow-style: none; /* hide scrollbar in IE and Edge */
scrollbar-width: none; /* hide scrollbar in Firefox */
}
.carousel-content::-webkit-scrollbar, .carousel-content::-webkit-scrollbar {
display: none;
}
.carousel-content-wrapper::-webkit-scrollbar, .carousel-content::-webkit-scrollbar {
display: none;
}
.carousel-content > * {
width: 100%;
flex-shrink: 0;
flex-grow: 1;
}
.left-arrow, .right-arrow {
position: absolute;
z-index: 1;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
border-radius: 24px;
background-color: white;
border: 1px solid #ddd;
}
.left-arrow {
left: 24px;
}
.right-arrow {
right: 24px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
When I used to swipe on mobile, the slider transitions by the swipe amount + the default transition amount. I tried to fix that by trying to subtract the percentage of the screen the swipe took from the transition amount, so if I swipe for 15% of the screen, the slide should transition the remaining 85%.
The problems:
I try to set the amount of swipe movement as a state value "movement", which seems to be causing trouble and doesn't update properly during every swipe. So sometimes the slider uses the "movement" state from the previous swipe.
The inertial scrolling on mobile makes the swipes unpredictable. I couldn't turn it off with some methods I found online.
If anyone could take a look at the code, and maybe point me toward how I should try and fix these problems, that would be great. Or better yet, if someone knows a less jank way of making such a carousel snap to the next/previous item while maintaining the finger-following scroll that would be perfect.
I know this isn't ideal, but the SO snippet editor is giving me an unexplainable error so here's codesandbox if anyone wants to play around with it in action: https://codesandbox.io/s/hardcore-goodall-usspz?file=/src/carousel.js&resolutionWidth=320&resolutionHeight=675

for scrolling use on parent container where you have all images next styles >>
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
display: flex;
overflow-x: scroll;
and then for each slide, child of parent container each this style
scroll-snap-align: start;

Related

Resizable flex box

Intentionally posting this question on stackoverflow instead of posting it on salesforce.stackexchange.com as this problem is specific to JS/CSS
I have below LWC playground where it is not working as expected
https://app.lwc.studio/edit/CTHEQCGrn18rKmQQ0zs1 (unfortunately you might have to login to webcomponents.dev to access this link)
but on the below code pen the same is working as expected not sure what am I doing wrong.
https://codepen.io/gs650x/pen/qByPQKP
I have below HTML
<section class="resizeable-container" >
<div class="resizeable-item">
DIV1
</div>
<div class="resizer-x" onmousedown={handleOnMouseDown} onmouseup={handleOnMouseUp}></div>
<div class="resizeable-item">
DIV2
</div>
</section>
CSS
.resizeable-container {
display: flex;
min-height: 80vh;
}
.resizeable-item {
flex: 50%;
overflow: auto;
}
.resizer-x {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background: hsl(212, 100%, 17%);
padding: 4px;
}
.resizer-x {
z-index: 2;
cursor: col-resize;
}
.resizer-x::before,
.resizer-x::after {
content: "";
width: 2px;
height: 16px;
margin: 2px;
background: lightgray;
}
Below is the javascript
renderedCallback() {
if (!resizer)
resizer = this.template.querySelector(".resizer-x")
//In case mouse up event occurs outside the resizer element
document.addEventListener("mouseup", this.handleOnMouseUp)
}
handleOnMouseDown(event) {
event.preventDefault();
document.addEventListener("mousemove", this.handleOnMouseMove)
document.addEventListener("mouseup", this.handleOnMouseUp)
}
handleOnMouseMove(event) {
event.preventDefault();
const clientX = event.clientX;
const deltaX = clientX - (resizer._clientX || clientX);
resizer._clientX = clientX;
const { previousElementSibling, nextElementSibling } = resizer
// LEFT
if (deltaX < 0) {
const width = Math.round(parseInt(window.getComputedStyle(previousElementSibling).width) + deltaX)
previousElementSibling.style.flex = `0 ${clientX < 10 ? 0 : clientX}px`
nextElementSibling.style.flex = "1 0"
}
// RIGHT
if (deltaX > 0) {
const width = Math.round(parseInt(window.getComputedStyle(nextElementSibling).width) - deltaX)
nextElementSibling.style.flex = `0 ${width < 10 ? 0 : width}px`
previousElementSibling.style.flex = "1 0"
}
}
handleOnMouseUp(event) {
event.preventDefault();
document.removeEventListener("mousemove", this.handleOnMouseMove)
document.removeEventListener("mouseup", this.handleOnMouseUp)
delete event._clientX
}
This works well on my 32 inches monitor but it is not working on my 14 inch laptop
expectation is to move the cursor and resizer-x at the same time but resizer-x moves first and then the cursor moves and mouse up only works if it is coming from the resizer-x anywhere else it is not working. I have added eventListener to document not on a particular object but still removeEventListener to stop handleMouseMove function doesn't stop.
Below is the code pen where it is working as expected but in LWC it doesn't behave in the same manner
https://codepen.io/gs650x/pen/qByPQKP

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.

Svelte - hide and show nav on scroll

I want the nav to hide scrolling down 60px and to show when scrolling up 60px, no matter in which part of the page.
I did this, but it's incomplete, what am I missing?
<script>
let y = 0;
</script>
<svelte:window bind:scrollY="{y}" />
<nav class:hideNav={y > 60}>
<ul>
<li>link</li>
</ul>
</nav>
<style>
nav {
position: fixed;
top: 0;
}
.hideNav {
top: -70px;
}
</style>
Your code seems to perfectly hide the navbar after you scroll the specified amount, here is a REPL of your code in action. maybe the body of your content has no scroll ?
here is another implementation REPL that further elaborates how to use scrolling position
<script>
import {onMount, onDestroy} from 'svelte'
const scrollNavBar = 60
let show = false
onMount(() => {
window.onscroll = () => {
if (window.scrollY > scrollNavBar) {
show = true
} else {
show = false
}
}
})
onDestroy(() => {
window.onscroll = () => {}
})
</script>
<style>
.scrolled {
transform: translate(0,calc(-100% - 1rem))
}
nav {
width: 100%;
position: fixed;
box-shadow: 0 -0.4rem 0.9rem 0.2rem rgb(0 0 0 / 50%);
padding: 10px;
transition: 0.5s ease
}
:global(body) {
margin: 0;
padding: 0;
height: 200vh;
}
</style>
<nav class:scrolled={show}>
elemnt
</nav>
In your REPL, it seems like the nav does not reappear on scrolling up. It does appear only at the top of the page.
I am also trying to show the nav when the user scrolls up by 30px anywhere on the page, for instance. I think that it was what OP is asking as well.
I found a REPL successfully doing it with jQuery but I am struggling to make it work in Svelte at the moment. Any clue?
I will revert back if I succeed.
// Hide Header on on scroll down
var didScroll;
var lastScrollTop = 0;
var delta = 5;
var navbarHeight = $('header').outerHeight();
$(window).scroll(function(event){
didScroll = true;
});
setInterval(function() {
if (didScroll) {
hasScrolled();
didScroll = false;
}
}, 250);
function hasScrolled() {
var st = $(this).scrollTop();
// Make sure they scroll more than delta
if(Math.abs(lastScrollTop - st) <= delta)
return;
// If they scrolled down and are past the navbar, add class .nav-up.
// This is necessary so you never see what is "behind" the navbar.
if (st > lastScrollTop && st > navbarHeight){
// Scroll Down
$('header').removeClass('nav-down').addClass('nav-up');
} else {
// Scroll Up
if(st + $(window).height() < $(document).height()) {
$('header').removeClass('nav-up').addClass('nav-down');
}
}
lastScrollTop = st;
}
The answers here couldn't help me. So here's a REPL I made for what I'm using to achieve this in svelte:window.
How I did it;
Create a variable that will store the scroll position (in px) at the end of the scroll event - [let's call it lastScrollPosition].
let lastScrollPosition = 0
At the beginning of a scroll event; inside svelte:window, get and compare the current scroll position to the last scroll position variable we created in [1.] (lastScrollPosition)
<svelte:window on:scroll={()=>{
var currentScrollposition = window.pageYOffset || document.documentElement.scrollTop; //Get current scroll position
if (currentScrollposition > lastScrollPosition) {
showNav = false
}else{
showNav = true
}
lastScrollPosition = currentScrollposition;
}}></svelte:window>
If current scroll Position is greater than lastScrollPosition, showNav is false else, true.
NB: You can use CSS or Svelte Conditional ({#if}) to achieve the hide on scroll down and show on scroll up (This example shows CSS..).
<main>
<div class="nav {showNav == true? "show": "hide" }" >
Nav bar
</div>
<div class="content">
Content
</div>
</main>
<style>
.nav{
background-color: gray;
padding: 6px;
position: fixed;
top: 0;
width: 100%;
}
.content{
background-color: green;
margin-top: 25px;
padding: 6px;
width: 100%;
height: 2300px;
}
.hide{
display: none;
}
.show{
display: unset;
}
</style>

Stop fixed element scrolling at certain point

I have fixed sidebar which should scroll along with main content and stop at certain point when I scroll down. And vise versa when I scroll up.
I wrote script which determines window height, scrollY position, position where sidebar should 'stop'. I stop sidebar by adding css 'bottom' property. But I have 2 problems with this approach:
When sidebar is close to 'pagination' where it should stop, it suddenly jumps down. When I scroll up it suddenly jumps up.
When I scroll page, sidebar moves all the time
Here's my code. HTML:
<div class="container">
<aside></aside>
<div class="content">
<div class="pagination"></div>
</div>
<footer></footer>
</div>
CSS:
aside {
display: flex;
position: fixed;
background-color: #fff;
border-radius: 4px;
transition: 0s;
transition: margin .2s, bottom .05s;
background: orange;
height: 350px;
width: 200px;
}
.content {
display: flex;
align-items: flex-end;
height: 1000px;
width: 100%;
background: green;
}
.pagination {
height: 100px;
width: 100%;
background: blue;
}
footer {
height: 500px;
width: 100%;
background: red;
}
JS:
let board = $('.pagination')[0].offsetTop;
let filterPanel = $('aside');
if (board <= window.innerHeight) {
filterPanel.css('position', 'static');
filterPanel.css('padding-right', '0');
}
$(document).on('scroll', function () {
let filterPanelBottom = filterPanel.offset().top + filterPanel.outerHeight(true);
let bottomDiff = board - filterPanelBottom;
if(filterPanel.css('position') != 'static') {
if (window.scrollY + window.innerHeight - (bottomDiff*2.6) >= board)
filterPanel.css('bottom', window.scrollY + window.innerHeight - board);
else
filterPanel.css('bottom', '');
}
});
Here's live demo on codepen
Side bar is marked with orange background and block where it should stop is marked with blue. Than you for your help in advance.
I solved my problem with solution described here
var windw = this;
let board = $('.pagination').offset().top;
let asideHeight = $('aside').outerHeight(true);
let coords = board - asideHeight;
console.log(coords)
$.fn.followTo = function ( pos ) {
var $this = this,
$window = $(windw);
$window.scroll(function(e){
if ($window.scrollTop() > pos) {
$this.css({
position: 'absolute',
top: pos
});
} else {
$this.css({
position: 'fixed',
top: 0
});
}
});
};
$('aside').followTo(coords);
And calculated coordinates as endpoint offset top - sidebar height. You can see solution in my codepen

Categories

Resources