Adapt vanilla js carousel to ie - javascript

I'm building a website that uses a carousel like this one:
https://codepen.io/queflojera/pen/RwwLbEY?editors=1010
It works perfectly on opera, chrome, edge but it stops working on ie and I need it to work on ie as well, if anyone knows any way around I'll really appreciate it.
//I'm not pretty sure what is causing the ie failure on this code
// Select the carousel you'll need to manipulate and the buttons you'll add events to
const carousel = document.querySelector("[data-target='carousel']");
const card = carousel.querySelector("[data-target='card']");
const leftButton = document.querySelector("[data-action='slideLeft']");
const rightButton = document.querySelector("[data-action='slideRight']");
// Prepare to limit the direction in which the carousel can slide,
// and to control how much the carousel advances by each time.
// In order to slide the carousel so that only three cards are perfectly visible each time,
// you need to know the carousel width, and the margin placed on a given card in the carousel
const carouselWidth = carousel.offsetWidth;
const cardStyle = card.currentStyle || window.getComputedStyle(card)
const cardMarginRight = Number(cardStyle.marginRight.match(/\d+/g)[0]);
// Count the number of total cards you have
const cardCount = carousel.querySelectorAll("[data-target='card']").length;
// Define an offset property to dynamically update by clicking the button controls
// as well as a maxX property so the carousel knows when to stop at the upper limit
let offset = 0;
const maxX = -((cardCount) * carouselWidth +
(cardMarginRight * cardCount) -
carouselWidth - cardMarginRight);
// Add the click events
leftButton.addEventListener("click", function() {
if (offset !== 0) {
offset += carouselWidth + cardMarginRight;
carousel.style.transform = `translateX(${offset}px)`;
}
})
rightButton.addEventListener("click", function() {
if (offset !== maxX) {
offset -= carouselWidth + cardMarginRight;
carousel.style.transform = `translateX(${offset}px)`;
}
})
.wrapper {
height: 200px;
width: 632px;
position: relative;
overflow: hidden;
margin: 0 auto;
}
.button-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
}
.carousel {
margin: 0;
padding: 0;
list-style: none;
width: 100%;
display: flex;
position: absolute;
left: 0;
transition: all .5s ease;
}
.card {
background: black;
min-width: 632px;
height: 200px;
display: inline-block;
}
.card:nth-child(odd) {
background-color: blue;
}
.card:nth-child(even) {
background-color: red;
}
<div class="wrapper">
<ul class="carousel" data-target="carousel">
<li class="card" data-target="card">1</li>
<li class="card" data-target="card">2</li>
<li class="card" data-target="card">3</li>
<li class="card" data-target="card">4</li>
<li class="card" data-target="card">5</li>
<li class="card" data-target="card">6</li>
<li class="card" data-target="card">7</li>
<li class="card" data-target="card">8</li>
<li class="card" data-target="card">9</li>
</ul>
<div class="button-wrapper">
<button data-action="slideLeft">L</button>
<button data-action="slideRight">R</button>
</div>
</div>

Invalid character
carousel.style.transform = `translateX(${offset}px)`;
IE does not support template literals (backticks)
To fix use
carousel.style.transform = "translateX("+offset+"px)";
Also getting
Unable to get property '0' of undefined or null reference
because it is auto in IE
const cardMarginRight = Number(cardStyle.marginRight.match(/\d+/g)[0]);
Fix:
const marginRight = cardStyle.marginRight;
const cardMarginRight = isNaN(parseInt(marginRight)) ? 0 : Number(cardStyle.marginRight.match(/\d+/g)[0]);
//I'm not pretty sure what is causing the ie failure on this code
// Select the carousel you'll need to manipulate and the buttons you'll add events to
const carousel = document.querySelector("[data-target='carousel']");
const card = carousel.querySelector("[data-target='card']");
const leftButton = document.querySelector("[data-action='slideLeft']");
const rightButton = document.querySelector("[data-action='slideRight']");
// Prepare to limit the direction in which the carousel can slide,
// and to control how much the carousel advances by each time.
// In order to slide the carousel so that only three cards are perfectly visible each time,
// you need to know the carousel width, and the margin placed on a given card in the carousel
const carouselWidth = carousel.offsetWidth;
const cardStyle = card.currentStyle || window.getComputedStyle(card)
const marginRight = cardStyle.marginRight;
const cardMarginRight = isNaN(parseInt(marginRight)) ? 0 : Number(cardStyle.marginRight.match(/\d+/g)[0]);
// Count the number of total cards you have
const cardCount = carousel.querySelectorAll("[data-target='card']").length;
// Define an offset property to dynamically update by clicking the button controls
// as well as a maxX property so the carousel knows when to stop at the upper limit
let offset = 0;
const maxX = -((cardCount) * carouselWidth +
(cardMarginRight * cardCount) -
carouselWidth - cardMarginRight);
// Add the click events
leftButton.addEventListener("click", function() {
if (offset !== 0) {
offset += carouselWidth + cardMarginRight;
carousel.style.transform = "translateX("+offset+"px)";
}
})
rightButton.addEventListener("click", function() {
if (offset !== maxX) {
offset -= carouselWidth + cardMarginRight;
carousel.style.transform = "translateX("+offset+"px)";
}
})
.wrapper {
height: 200px;
width: 632px;
position: relative;
overflow: hidden;
margin: 0 auto;
}
.button-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
}
.carousel {
margin: 0;
padding: 0;
list-style: none;
width: 100%;
display: flex;
position: absolute;
left: 0;
transition: all .5s ease;
}
.card {
background: black;
min-width: 632px;
height: 200px;
display: inline-block;
}
.card:nth-child(odd) {
background-color: blue;
}
.card:nth-child(even) {
background-color: red;
}
<div class="wrapper">
<ul class="carousel" data-target="carousel">
<li class="card" data-target="card">1</li>
<li class="card" data-target="card">2</li>
<li class="card" data-target="card">3</li>
<li class="card" data-target="card">4</li>
<li class="card" data-target="card">5</li>
<li class="card" data-target="card">6</li>
<li class="card" data-target="card">7</li>
<li class="card" data-target="card">8</li>
<li class="card" data-target="card">9</li>
</ul>
<div class="button-wrapper">
<button data-action="slideLeft">L</button>
<button data-action="slideRight">R</button>
</div>
</div>

Related

How to add a smooth animation to the progress bar

When I click I want to smoothly add segments to the progress bar. They are added but instantly. What could be the problem?
I tried to implement a smooth animation with setInterval, but nothing comes out. Percentages are also added instantly.
let progressBar = document.querySelector(".progressbar");
let progressBarValue = document.querySelector(".progressbar__value");
const body = document.querySelector("body");
let progressBarStartValue = 0;
let progressBarEndValue = 100;
let speed = 50;
body.addEventListener("click", function(e) {
if (progressBarStartValue === progressBarEndValue) {
alert("you have completed all the tasks");
} else {
let progress = setInterval(() => {
if (progressBarStartValue != 100) {
progressBarStartValue += 10;
clearInterval(progress);
}
progressBarValue.textContent = `${progressBarStartValue}%`;
progressBar.style.background = `conic-gradient(
#FFF ${progressBarStartValue * 3.6}deg,
#262623 ${progressBarStartValue * 3.6}deg
)`;
}, speed);
}
});
.progressbar {
position: relative;
height: 150px;
width: 150px;
background-color: #262623;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.progressbar::before {
content: "";
position: absolute;
height: 80%;
width: 80%;
background-color: #0f0f0f;
border-radius: 50%;
}
.progressbar__value {
color: #fff;
z-index: 9;
font-size: 25px;
font-weight: 600;
}
<main class="main">
<section class="statistic">
<div class="container">
<div class="statistic__inner">
<div class="statistic__text">
<h2 class="statistic__title">You're almost there!</h2>
<p class="statistic__subtitle">keep up the good work</p>
</div>
<div class="progressbar"><span class="progressbar__value">0%</span></div>
</div>
</div>
</section>
</main>
This may not be exactly what you're looking for, but with the conic-gradient() implementation you're using, I'd recommend checking out a library call anime.js.
Here's an example with your implementation (same html and css):
// your.js
let progressBar = document.querySelector(".progressbar");
let progressBarValue = document.querySelector(".progressbar__value");
const body = document.querySelector("body");
// Switched to object for target in anime()
let progressBarObject = {
progressBarStartValue: 0,
progressBarEndValue: 100,
progressBarAnimationValue: 0 * 3.6 // New value needed for smoothing the progress bar, since the progress value needs to be multiplied by 3.6
}
// Not necessary, but I recommend changing the event listener to pointerup for better support
// Also not necessary, I changed function to arrow function for my own preference
body.addEventListener("pointerup", e => {
e.preventDefault()
if (progressBarObject.progressBarStartValue === progressBarObject.progressBarEndValue) {
alert("you have completed all the tasks");
} else {
let newValue = 0 // Needed so we can set the value, before it's applied in anime()
if (progressBarObject.progressBarStartValue != 100) {
// Math.ceil() allows us to round to the nearest 10 to guarantee the correct output
newValue = Math.ceil((progressBarObject.progressBarStartValue + 10) / 10) * 10;
}
// Optional: Prevents accidentally going over 100 somehow
if (newValue > 100) {
newValue = 100
}
anime({
targets: progressBarObject,
progressBarStartValue: newValue,
progressBarAnimationValue: newValue * 3.6,
easing: 'easeInOutExpo',
round: 1, // Rounds to nearest 1 so you don't have 0.3339...% displayed in progressBarValue
update: () => {
progressBar.style.backgroundImage = `conic-gradient(
#FFF ${progressBarObject.progressBarAnimationValue}deg,
#262623 ${progressBarObject.progressBarAnimationValue}deg)`;
progressBarValue.textContent = `${progressBarObject.progressBarStartValue}%`;
},
duration: 500
});
}
});
Here's a CodePen using the anime.js CDN: Circular Progress Bar Smoothing
If you don't want to use a javascript library, then I'd recommend switching from the conic-gradient() to something else. I hear using an .svg circle with stroke and stroke-dasharray can work great with CSS transition.
You shouldn't setInterval your progress variable like this. instead, put it as a global variable outside the function then use it to gradually add 1 as long as the start value is less than progress, and you still can control the speed with your speed variable.
let progressBar = document.querySelector(".progressbar");
let progressBarValue = document.querySelector(".progressbar__value");
const body = document.querySelector("body");
let progressBarStartValue = 0;
let progressBarEndValue = 100;
let speed = 50;
let progress = 0;
body.addEventListener("click", function(e) {
if (progressBarStartValue === progressBarEndValue) {
alert("you have completed all the tasks");
} else {
progress += 10;
setInterval(() => {
if (progressBarStartValue < progress) {
progressBarStartValue += 1;
clearInterval();
}
progressBarValue.textContent = `${progressBarStartValue}%`;
progressBar.style.background = `conic-gradient(
#FFF ${progressBarStartValue * 3.6}deg,
#262623 ${progressBarStartValue * 3.6}deg
)`;
}, speed);
}
});
.progressbar {
position: relative;
height: 150px;
width: 150px;
background-color: #262623;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid red;
}
.progressbar::before {
content: "";
position: absolute;
height: 80%;
width: 80%;
background-color: #0f0f0f;
border-radius: 50%;
border: 3px solid blue;
}
.progressbar__value {
color: #fff;
z-index: 9;
font-size: 25px;
font-weight: 600;
}
<main class="main">
<section class="statistic">
<div class="container">
<div class="statistic__inner">
<div class="statistic__text">
<h2 class="statistic__title">You're almost there!</h2>
<p class="statistic__subtitle">keep up the good work</p>
</div>
<div class="progressbar"><span class="progressbar__value">0%</span></div>
</div>
</div>
</section>
</main>

Scaling carousel centering selected div

I am working on a slider that displays 5 slides (squares). The selected slide is centered on the screen and it is bigger than the others. Users can click buttons to make the slider slide left or right selecting an adjacent square and centering it on the screen.
This is the HTML:
const slider = document.querySelector(".slider");
const slideLeft = document.querySelector(".slide-left");
const slideRight = document.querySelector(".slide-right");
const getSelectedSquare = () => document.querySelector(".selected");
let translate = 0;
function changeSelectedClass(direction) {
const selected = getSelectedSquare();
const map = {
left: selected.nextElementSibling,
right: selected.previousElementSibling,
}
const nextSelected = map[direction];
selected.classList.remove("selected");
nextSelected.classList.add("selected");
}
function centerSelected() {
const selected = getSelectedSquare();
const { width, left } = selected.getBoundingClientRect();
const distanceFromCenter = (window.innerWidth / 2) - left - (width / 2);
translate += distanceFromCenter;
slider.style.transform = `translateX(${translate}px)`;
}
slideRight.onclick = () => { changeSelectedClass("right"); centerSelected(); };
slideLeft.onclick = () => { changeSelectedClass("left"); centerSelected(); };
* {
margin: 0;
box-sizing: border-box;
}
body {
position: relative;
display: grid;
place-items: center;
min-height: 100vh;
}
.slider {
display: flex;
align-items: center;
gap: 10px;
}
.square {
width: 250px;
aspect-ratio: 1 / 1;
border: 1px solid black;
}
.square.selected {
background: lightpink;
}
.square:not(.selected) {
width: 200px;
aspect-ratio: 1 / 1;
}
<div class="container">
<div class="slider">
<div class="square"> 1 </div>
<div class="square">2</div>
<div class="square selected">3</div>
<div class="square">4</div>
<div class="square">5</div>
</div>
<div class="btn-wrapper">
<button class="btn slide-left">⇦</button>
<button class="btn slide-right">⇨</button>
</div>
</div>
The problem is that at the moment the selected div is not centered correctly when a button is pressed and the slider slides. The code works fine if all the divs have the same dimensions, but when scaling comes into play I'm missing something.
I'm no expert at all in animations and if the approach I took is wrong I would rather rebuild it, than make adjustments to make it work as it is.
Additional requirements are that the dimentions of the squares should be dynamic (eg. 15vw), the distance between them should be the same (here I'm using flex gap) and the squares should become smaller and smaller the furthest away from the selected one (this is not implemented yet in the code sample).
Since I'm learning new stuff, I'd rather not use external libraries (that do that exact thing (I know)).
A codepen of the work until now: https://codepen.io/mtenti/pen/ExvNdZL

What could be the bare minimum steps to animate the following carousel implementation?

I am making a vanilla js carousel. I have laid out basic previous and next functionality using js along with html and css.
Now I tried to use css-animations (keyframes) to do left and right slide-in/slide-out animations but the code became messy for me. So here I am asking that what minimal changes would be needed to get the same animation effects in this implementation ?
Will you go for pure JS based or pure CSS based or a mix to do the same ?
My goal is get proper animation with minimal code.
(function () {
let visibleIndex = 0;
let carousalImages = document.querySelectorAll(".carousal__image");
let totalImages = [...carousalImages].length;
function makeNextVisible() {
visibleIndex++;
if (visibleIndex > totalImages - 1) {
visibleIndex = 0;
}
resetVisible();
renderVisible();
}
function makePrevVisible() {
visibleIndex--;
if (visibleIndex < 0) {
visibleIndex = totalImages - 1;
}
resetVisible();
renderVisible();
}
function resetVisible() {
for (let index = 0; index < totalImages; index++) {
carousalImages[index].className = "carousal__image";
}
}
function renderVisible() {
carousalImages[visibleIndex].className = "carousal__image--visible";
}
function renderCarousel({ autoplay = false, autoplayTime = 1000 } = {}) {
if (autoplay) {
[...document.querySelectorAll("button")].forEach(
(btn) => (btn.style.display = "none")
);
setInterval(() => {
makeNextVisible();
}, autoplayTime);
} else renderVisible();
}
renderCarousel();
// Add {autoplay:true} as argument to above to autplay the carousel.
this.makeNextVisible = makeNextVisible;
this.makePrevVisible = makePrevVisible;
})();
.carousal {
display: flex;
align-items: center;
}
.carousal__wrapper {
width: 500px;
height: 400px;
}
.carousal__images {
display: flex;
overflow: hidden;
list-style-type: none;
padding: 0;
}
.carousal__image--visible {
position: relative;
}
.carousal__image {
display: none;
}
<div class='carousal'>
<div class='carousal__left'>
<button onclick='makePrevVisible()'>Left</button>
</div>
<section class='carousal__wrapper'>
<ul class='carousal__images'>
<li class='carousal__image'>
<img src='https://fastly.syfy.com/sites/syfy/files/styles/1200x680/public/2018/03/dragon-ball-super-goku-ultra-instinct-mastered-01.jpg?offset-x=0&offset-y=0' alt='UI Goku' / width='500' height='400'/>
</li>
<li class='carousal__image'>
<img src='https://www.theburnin.com/wp-content/uploads/2019/01/super-broly-3.png' alt='Broly Legendary' width='500' height='400'/>
</li>
<li class='carousal__image'>
<img src='https://lh3.googleusercontent.com/proxy/xjEVDYoZy8-CTtPZGsQCq2PW7I-1YM5_S5GPrAdlYL2i4SBoZC-zgtg2r3MqH85BubDZuR3AAW4Gp6Ue-B-T2Z1FkKW99SPHwAce5Q_unUpwtm4' alt='Vegeta Base' width='500' height='400'/>
</li>
<li class='carousal__image'>
<img src='https://am21.mediaite.com/tms/cnt/uploads/2018/09/GohanSS2.jpg' alt='Gohan SS2' width='500' height='400'/>
</li>
</ul>
</section>
<div class='carousal__right'>
<button onclick='makeNextVisible()'>Right</button>
</div>
</div>
Updated codepen with feedback from the below answers and minor additional functionalities = https://codepen.io/lapstjup/pen/RwoRWVe
I think the trick is pretty simple. ;)
You should not move one or two images at the same time. Instead you should move ALL images at once.
Let's start with the CSS:
.carousal {
position: relative;
display: block;
}
.carousal__wrapper {
width: 500px;
height: 400px;
position: relative;
display: block;
overflow: hidden;
margin: 0;
padding: 0;
}
.carousal__wrapper,
.carousal__images {
transform: translate3d(0, 0, 0);
}
.carousal__images {
position: relative;
top: 0;
left: 0;
display: block;
margin-left: auto;
margin-right: auto;
}
.carousal__image {
float: left;
height: 100%;
min-height: 1px;
}
2nd step would be to calculate the maximum width for .carousal__images. For example in your case 4 * 500px makes 2000px. This value must be added to your carousal__images as part of the style attribute style="width: 2000px".
3rd step would be to calculate the next animation point and using transform: translate3d. We start at 0 and want the next slide which means that we have slide to the left. We also know the width of one slide. So the result would be -500px which also has to be added the style attribute of carousal__images => style="width: 2000px; transform: translate3d(-500px, 0px, 0px);"
That's it.
Link to my CodePen: Codepen for Basic Carousel with Autoplay
Try this. First stack all the images next to each other in a div and only show a single image at a time by setting overflow property to hidden for the div. Next, add event listeners to the buttons. When a bottom is clicked, the div containing the images is translated by -{size of an image} * {image number} on the x axis. For smooth animation, add transition: all 0.5s ease-in-out; to the div.
When someone clicks left arrow on the first image, the slide should display the last image. So for that counter is set to {number of images} - 1 and image is translated to left size * counter px.
For every click on the right arrow, the counter is incremented by 1 and slide is moved left. For every click on the left arrow, the counter is decremented by 1.
Slide.style.transform = "translateX(" + (-size * counter) + "px)"; this is the condition which is deciding how much the slide should be translated.
const PreviousButton = document.querySelector(".Previous-Button");
const NextButton = document.querySelector(".Next-Button");
const Images = document.querySelectorAll("img");
const Slide = document.querySelector(".Images");
const size = Slide.clientWidth;
var counter = 0;
// Arrow Click Events
PreviousButton.addEventListener("click", Previous);
NextButton.addEventListener("click", Next);
function Previous() {
counter--;
if (counter < 0) {
counter = Images.length - 1;
}
Slide.style.transform = "translateX(" + (-size * counter) + "px)";
}
function Next() {
counter++;
if (counter >= Images.length) {
counter = 0;
}
Slide.style.transform = "translateX(" + (-size * counter) + "px)";
}
* {
margin: 0px;
padding: 0px;
box-sizing: border-box;
}
.Container {
width: 60%;
margin: 0px auto;
margin-top: 90px;
overflow: hidden;
position: relative;
}
.Container .Images img {
width: 100%;
}
.Images {
transition: all 0.5s ease-in-out;
}
.Container .Previous-Button {
position: absolute;
background: transparent;
border: 0px;
outline: 0px;
top: 50%;
left: 20px;
transform: translateY(-50%);
filter: invert(80%);
z-index: 1;
}
.Container .Next-Button {
position: absolute;
background: transparent;
border: 0px;
outline: 0px;
top: 50%;
right: 20px;
transform: translateY(-50%);
filter: invert(80%);
z-index: 1;
}
.Container .Images {
display: flex;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="https://fonts.googleapis.com/css2?family=Cabin&family=Poppins&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<title>Carousel</title>
</head>
<body>
<div class="Container">
<button class="Previous-Button">
<svg style = "transform: rotate(180deg);" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M8.122 24l-4.122-4 8-8-8-8 4.122-4 11.878 12z"/></svg>
</button>
<button class="Next-Button">
<svg xmlns="http://www.w3.org/2000/svg" width = "24" height = "24" viewBox = "0 0 24 24"><path d="M8.122 24l-4.122-4 8-8-8-8 4.122-4 11.878 12z"/></svg>
</button>
<div class="Images">
<img src="https://source.unsplash.com/1280x720/?nature">
<img src="https://source.unsplash.com/1280x720/?water">
<img src="https://source.unsplash.com/1280x720/?rock">
<img src="https://source.unsplash.com/1280x720/?abstract">
<img src="https://source.unsplash.com/1280x720/?nature">
<img src="https://source.unsplash.com/1280x720/?trees">
<img src="https://source.unsplash.com/1280x720/?human">
<img src="https://source.unsplash.com/1280x720/?tech">
</div>
</div>
<script src="main.js"></script>
</body>
</html>

<br> mucking up .next() in JQuery

I am hacking together an experimental pagination interface called wigi(board) but have run into an issue.
The interface works by any l1 (subject) class or l2 (subheading) class running vertical down the left. Pages (l3 class nodes) are represented as points attached to the side of an l1 or l2.
Mousing over any node will move the selector to that node and call a db query to display a specific page's contents. This works fine. It moves like it should.
Right now I have buttons that will also move between the next and previous li in the navigation list. These are filler for future swiping and other interaction to demonstrate the issue.
Right now these buttons work to a point, until the jquery .next() hits a <br> node, which I am using in order to break the l3 lines and continue the menu vertical to the next l1 or l2. When the .next hits the last node before one of these, it stops dead and wont jump down to the next row. Why? What is the best strategy to fix it?
JS fiddle: http://jsfiddle.net/93g786jp/
The issue with next is in here. It is running over an li list (best to look at JSfiddle)
function nextAndBack(e) {
var cur = $('.dots .selected'),
next = cur.next('li'),
prev = cur.prev('li');
if (e.target.id == 'nextButton') {
if (next.length == 1) {
newSelected(next);
console.log("Next Node:")
console.log(next);
$(next).trigger("mouseover");
}
} else if (e.target.id == 'prevButton') {
if (prev.length == 1) {
newSelected(prev);
console.log("Previous Node:")
console.log(prev);
$(prev).trigger("mouseover");
}
}
}
Note this is based on the gooey interface by Lucas Bebber # https://codepen.io/lbebber/pen/lFdHu which was the closet match I could find for an interface like what I wanted. For the posted example, I stripped out any effects and other extras so some stubs exist.
As the <br /> gets in the way of selecting siblings you can instead use nextAll() or prevAll() and then get the first() of the selected items:
next = cur.nextAll('li').first(),
prev = cur.prevAll('li').first();
function wigiBoardMove() {
var cur = $(this);
var desty = cur.position().top;
var destx = cur.position().left;
var t = 0.6;
gsap.to($(".select"), t, {
y: desty,
ease: Back.easeOut
});
gsap.to($(".select"), t, {
x: destx,
ease: Back.easeOut
});
newSelected(cur);
}
function newSelected(newTarget) {
$('.selected').removeClass('selected');
newTarget.addClass('selected');
}
function nextAndBack(e) {
var cur = $('.dots .selected'),
next = cur.nextAll('li').first(),
prev = cur.prevAll('li').first();
if (e.target.id == 'nextButton') {
if (next.length == 1) {
newSelected(next);
$(next).trigger("mouseover");
}
} else if (e.target.id == 'prevButton') {
if (prev.length == 1) {
newSelected(prev);
$(prev).trigger("mouseover");
}
}
}
/* Modified from gooey pagnation code published by Lucas Bebber # https://codepen.io/lbebber/pen/lFdHu */
$(function() {
$(".dot").on("mouseenter", wigiBoardMove);
var lastPos = $(".select").position().top;
function updateScale() {
var pos = $(".select").position().top;
var speed = Math.abs(pos - lastPos);
var d = 44;
var offset = -20;
var hd = d / 2;
var scale = (offset + pos) % d;
if (scale > hd) {
scale = hd - (scale - hd);
}
scale = 1 - ((scale / hd) * 0.35);
gsap.to($(".select"), 0.1, {
scaleY: 1 + (speed * 0.06),
scaleX: scale
})
lastPos = pos;
requestAnimationFrame(updateScale);
}
requestAnimationFrame(updateScale);
$(".dot:eq(0)").trigger("mouseover");
// Back and Forward Node Logic
$('#nextButton, #prevButton').on('click', nextAndBack);
})
#container {}
.dots {
list-style-type: none;
padding: 0;
margin: 0;
padding-top: 20px;
padding-bottom: 20px;
padding-left: 20px;
margin-left: -10px;
padding-right: 10px;
position: absolute;
top: 0px;
width: 150px;
right: 0px;
}
.dot {
display: inline-block;
vertical-align: middle;
margin-left: 5px;
margin-right: 5px;
cursor: pointer;
color: white;
position: relative;
z-index: 2;
}
.l1 {
border-radius: 100%;
width: 10px;
height: 10px;
background: blue;
border: none;
}
.l3 {
border-radius: 100%;
width: 7px;
height: 7px;
border: none;
background: blue;
}
.select {
display: block;
border-radius: 100%;
width: 15px;
height: 15px;
background: #daa520;
position: absolute;
z-index: 3;
top: -4px;
left: 1px;
pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.5.1/gsap.min.js"></script>
<div id="container">
<ul class="dots">
<li class="select"></li>
<li class="dot l1"></li>
<li class="dot l3"></li>
<li class="dot l3"></li>
<li class="dot l3"></li><br>
<li class="dot l1"></li>
<li class="dot l3"></li>
<li class="dot l3"></li><br>
<li class="dot l1"></li>
<li class="dot l3"></li><br>
</ul>
<img id="nextButton" height="10" width="10" alt="Next Node" /><br>
<img id="prevButton" height="10" width="10" alt="Previous Node" />
</div>

CSS Scroll Snap Points with navigation (next, previous) buttons

I am building a carousel, very minimalist, using CSS snap points. It is important for me to have CSS only options, but I'm fine with enhancing a bit with javascript (no framework).
I am trying to add previous and next buttons to scroll programmatically to the next or previous element. If javascript is disabled, buttons will be hidden and carousel still functionnal.
My issue is about how to trigger the scroll to the next snap point ?
All items have different size, and most solution I found require pixel value (like scrollBy used in the exemple). A scrollBy 40px works for page 2, but not for others since they are too big (size based on viewport).
function goPrecious() {
document.getElementById('container').scrollBy({
top: -40,
behavior: 'smooth'
});
}
function goNext() {
document.getElementById('container').scrollBy({
top: 40,
behavior: 'smooth'
});
}
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goPrecious()">previous</button>
<button onClick="goNext()">next</button>
Nice question! I took this as a challenge.
So, I increased JavaScript for it to work dynamically. Follow my detailed solution (in the end the complete code):
First, add position: relative to the .container, because it need to be reference for scroll and height checkings inside .container.
Then, let's create 3 global auxiliary variables:
1) One to get items scroll positions (top and bottom) as arrays into an array. Example: [[0, 125], [125, 280], [280, 360]] (3 items in this case).
3) One that stores half of .container height (it will be useful later).
2) Another one to store the item index for scroll position
var carouselPositions;
var halfContainer;
var currentItem;
Now, a function called getCarouselPositions that creates the array with items positions (stored in carouselPositions) and calculates the half of .container (stored in halfContainer):
function getCarouselPositions() {
carouselPositions = [];
document.querySelectorAll('#container div').forEach(function(div) {
carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
})
halfContainer = document.querySelector('#container').offsetHeight/2;
}
getCarouselPositions(); // call it once
Let's replace the functions on buttons. Now, when you click on them, the same function will be called, but with "next" or "previous" argument:
<button onClick="goCarousel('previous')">previous</button>
<button onClick="goCarousel('next')">next</button>
Here is about the goCarousel function itself:
First, it creates 2 variables that store top scroll position and bottom scroll position of carousel.
Then, there are 2 conditionals to see if the current carousel position is on most top or most bottom.
If it's on top and clicked "next" button, it will go to the second item position. If it's on bottom and clicked "previous" button, it will go the previous one before the last item.
If both conditionals failed, it means the current item is not the first or the last one. So, it checks to see what is the current position, calculating using the half of the container in a loop with the array of positions to see what item is showing. Then, it combines with "previous" or "next" checking to set the correct next position for currentItem variable.
Finally, it goes to the correct position through scrollTo using currentItem new value.
Below, the complete code:
var carouselPositions;
var halfContainer;
var currentItem;
function getCarouselPositions() {
carouselPositions = [];
document.querySelectorAll('#container div').forEach(function(div) {
carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
})
halfContainer = document.querySelector('#container').offsetHeight/2;
}
getCarouselPositions(); // call it once
function goCarousel(direction) {
var currentScrollTop = document.querySelector('#container').scrollTop;
var currentScrollBottom = currentScrollTop + document.querySelector('#container').offsetHeight;
if (currentScrollTop === 0 && direction === 'next') {
currentItem = 1;
} else if (currentScrollBottom === document.querySelector('#container').scrollHeight && direction === 'previous') {
console.log('here')
currentItem = carouselPositions.length - 2;
} else {
var currentMiddlePosition = currentScrollTop + halfContainer;
for (var i = 0; i < carouselPositions.length; i++) {
if (currentMiddlePosition > carouselPositions[i][0] && currentMiddlePosition < carouselPositions[i][1]) {
currentItem = i;
if (direction === 'next') {
currentItem++;
} else if (direction === 'previous') {
currentItem--
}
}
}
}
document.getElementById('container').scrollTo({
top: carouselPositions[currentItem][0],
behavior: 'smooth'
});
}
window.addEventListener('resize', getCarouselPositions);
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
position: relative;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goCarousel('previous')">previous</button>
<button onClick="goCarousel('next')">next</button>
Another good detail to add is to call getCarouselPositions function again if the window resizes:
window.addEventListener('resize', getCarouselPositions);
That's it.
That was cool to do. I hope it can help somehow.
I've just done something similar recently. The idea is to use IntersectionObserver to keep track of which item is in view currently and then hook up the previous/next buttons to event handler calling Element.scrollIntoView().
Anyway, Safari does not currently support scroll behavior options. So you might want to polyfill it on demand with polyfill.app service.
let activeIndex = 0;
const container = document.querySelector("#container");
const elements = [...document.querySelectorAll("#container div")];
function handleIntersect(entries){
const entry = entries.find(e => e.isIntersecting);
if (entry) {
const index = elements.findIndex(
e => e === entry.target
);
activeIndex = index;
}
}
const observer = new IntersectionObserver(handleIntersect, {
root: container,
rootMargin: "0px",
threshold: 0.75
});
elements.forEach(el => {
observer.observe(el);
});
function goPrevious() {
if(activeIndex > 0) {
elements[activeIndex - 1].scrollIntoView({
behavior: 'smooth'
})
}
}
function goNext() {
if(activeIndex < elements.length - 1) {
elements[activeIndex + 1].scrollIntoView({
behavior: 'smooth'
})
}
}
#container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
border: 2px solid var(--gs0);
border-radius: 8px;
height: 60vh;
}
#container div {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
font-size: 4rem;
}
#container div:nth-child(1) {
background: hotpink;
color: white;
height: 50vh;
}
#container div:nth-child(2) {
background: azure;
height: 40vh;
}
#container div:nth-child(3) {
background: blanchedalmond;
height: 60vh;
}
#container div:nth-child(4) {
background: lightcoral;
color: white;
height: 40vh;
}
<div id="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<button onClick="goPrevious()">previous</button>
<button onClick="goNext()">next</button>
An easier approach done with react.
export const AppCarousel = props => {
const containerRef = useRef(null);
const carouselRef = useRef(null);
const [state, setState] = useState({
scroller: null,
itemWidth: 0,
isPrevHidden: true,
isNextHidden: false
})
const next = () => {
state.scroller.scrollBy({left: state.itemWidth * 3, top: 0, behavior: 'smooth'});
// Hide if is the last item
setState({...state, isNextHidden: true, isPrevHidden: false});
}
const prev = () => {
state.scroller.scrollBy({left: -state.itemWidth * 3, top: 0, behavior: 'smooth'});
setState({...state, isNextHidden: false, isPrevHidden: true});
// Hide if is the last item
// Show remaining
}
useEffect(() => {
const items = containerRef.current.childNodes;
const scroller = containerRef.current;
const itemWidth = containerRef.current.firstElementChild?.clientWidth;
setState({...state, scroller, itemWidth});
return () => {
}
},[props.items])
return (<div className="app-carousel" ref={carouselRef}>
<div className="carousel-items shop-products products-swiper" ref={containerRef}>
{props.children}
</div>
<div className="app-carousel--navigation">
<button className="btn prev" onClick={e => prev()} hidden={state.isPrevHidden}><</button>
<button className="btn next" onClick={e => next()} hidden={state.isNextHidden}>></button>
</div>
</div>)
}
I was struggling with the too while working with a react project and came up with this solution. Here's a super basic example of the code using react and styled-components.
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
const App = () => {
const ref = useRef();
const [scrollX, setScrollX] = useState(0);
const scrollSideways = (px) => {
ref.current.scrollTo({
top: 0,
left: scrollX + px,
behavior: 'smooth'
});
setScrollX(scrollX + px);
};
return (
<div>
<List ref={ref}>
<ListItem color="red">Card 1</ListItem>
<ListItem color="blue">Card 2</ListItem>
<ListItem color="green">Card 3</ListItem>
<ListItem color="yellow">Card 4</ListItem>
</List>
<button onClick={() => scrollSideways(-600)}> Left </button>
<button onClick={() => scrollSideways(600)}> Right </button>
</div>
);
};
const List = styled.ul`
display: flex;
overflow-x: auto;
padding-inline-start: 40px;
scroll-snap-type: x mandatory;
list-style: none;
padding: 40px;
width: 700px;
`;
const ListItem = styled.li`
display: flex;
flex-shrink: 0;
scroll-snap-align: start;
background: ${(p) => p.color};
width: 600px;
margin-left: 15px;
height: 200px;
`;

Categories