Слайдер на чистом JS (конкурс)
Я реализовал слайдер на JavaScript. Работает управление с телефона, работает перетаскивание слайдов, только не реализовано прокручивание до определённого слайда в зависимости от скорости перетаскивания слайдов, что важно для скролла слайдов с телефона, например.
У меня получилась очень сложная логика авто слайда при перетаскивании (как мне кажется)
Поэтому я бы хотел увидеть как люди реализуют слайдер на чистом JavaScript с этой функциональностью, потому что у меня могут быть мысли, которые заводят меня в тупик.
Каким должен быть слайдер:
- Кнопки влево/вправо плавно переключают слайды
- Работает перетаскивание слайдов, авто докручивание до слайда происходит плавно
- Перетаскивание слайдов работает также от скорости перетаскивания. Если быстро запустили перетаскивание - докрутится до слайда подальше. (я это не реализовал у себя)
- Слайдер правильно работает на мобильных устройствах
- Должно корректно работать в браузере Chrome последней версии
Имеет смысл какие то ещё свои доп плюшки реализовать, если у вас будут какие-то мысли. Мне было бы очень интересно понять, как мыслят другие разработчики.
class Slider {
nodes = {
sliderNode: null,
sliderItemsNode: null,
sliderItemNodes: [],
sliderArrowLeftNode: null,
sliderArrowRightNode: null
};
cssSelectors = {
items: '.slider__items',
item: '.slider__item',
wrapper: '.slider__wrapper',
arrowLeft: '.slider__arrow_left',
arrowRight: '.slider__arrow_right'
};
moveSlideShiftX = 0;
shiftX = 0;
lastShifX = 0;
objShiftX = {};
startMovePos = {
x: 0,
y: 0
};
activeItemIndex = 0;
isDragging = false;
countDragging = 0;
teamsTitleNode = document.querySelector('.teams__title');
constructor(sliderSelector) {
this.initNodes(sliderSelector);
this.initEventListeners();
this.setObjShiftX();
this.addTransition();
this.setShiftX();
}
initNodes(sliderSelector) {
this.nodes.sliderNode = document.querySelector(sliderSelector);
if (this.nodes.sliderNode === null) {
throw new Error(`Slider: по селектору ${sliderSelector} не найден элемент в DOM дереве`);
}
this.nodes.sliderItemsNode = this.nodes.sliderNode.querySelector(this.cssSelectors.items);
if (this.nodes.sliderItemsNode === null) {
throw new Error(`Slider: по селектору ${this.cssSelectors.items} не найден элемент в DOM дереве`);
}
this.nodes.sliderItemNodes = Array.from(this.nodes.sliderNode.querySelectorAll(this.cssSelectors.item));
if (this.nodes.sliderItemNodes.length === 0) {
throw new Error(`Slider: по селектору ${this.cssSelectors.item} не найдены элементы слайдера в DOM дереве`);
}
this.nodes.sliderArrowLeftNode = this.nodes.sliderNode.querySelector(this.cssSelectors.arrowLeft);
if (this.nodes.sliderArrowLeftNode === null) {
throw new Error(`Slider: по селектору ${this.cssSelectors.arrowLeft} не найден элемент в DOM дереве`);
}
this.nodes.sliderArrowRightNode = this.nodes.sliderNode.querySelector(this.cssSelectors.arrowRight);
if (this.nodes.sliderArrowLeftNode === null) {
throw new Error(`Slider: по селектору ${this.cssSelectors.arrowRight} не найден элемент в DOM дереве`);
}
}
initEventListeners() {
window.addEventListener('resize', this.debounce(this.resizeEvent, 50));
this.nodes.sliderArrowLeftNode.addEventListener('click', () => {
const nextIndex = this.getNextIndex(-1);
this.changeSlide(nextIndex);
});
this.nodes.sliderArrowRightNode.addEventListener('click', () => {
const nextIndex = this.getNextIndex(1);
this.changeSlide(nextIndex);
});
for (const sliderNode of this.nodes.sliderItemNodes) {
sliderNode.addEventListener('pointerdown', this.dragStart);
}
document.addEventListener('pointermove', this.dragging);
document.addEventListener('pointerout', (event) => {
if (event.relatedTarget === null) {
console.log("Курсор мыши ушёл из браузера");
this.dragStop(event);
}
});
document.addEventListener('pointerup', this.dragStop);
document.addEventListener('pointerleave', this.dragStop);
}
resizeEvent = () => {
this.setObjShiftX();
this.removeTransition();
this.setShiftX();
this.addTransition();
}
dragStart = (e) => {
const x = e.clientX;
const y = e.clientY;
if (e.button === 2 || e.button === 1) return false; // Если это правая или средняя кнопка мыши, это не тот клик
this.isDragging = true;
this.startMovePos = {
x: x,
y: y
};
this.lastShifX = this.shiftX;
this.removeTransition();
console.log('dragStart');
}
dragStop = (e) => {
this.isDragging = false;
this.addTransition();
if (this.moveSlideShiftX !== 0) this.autoSlide();
this.moveSlideShiftX = 0;
console.log('dragStop');
}
dragging = (e) => {
const x = e.clientX;
const y = e.clientY;
if (this.isDragging === false) return;
const nextMovePos = {
x: x,
y: y
};
const diffMovePos = {
x: nextMovePos.x - this.startMovePos.x,
y: nextMovePos.y - this.startMovePos.y
}
this.moveSlideShiftX = diffMovePos.x;
this.calcMoveSlideShiftX();
this.countDragging++;
this.teamsTitleNode.innerText = `${this.countDragging} dragging`;
console.log('dragging');
}
autoSlide() {
const dir = this.moveSlideShiftX < 0 ? 'left' : 'right';
let nextIndex = this.activeItemIndex + (dir === 'left' ? 1 : -1);
let remainingShiftX = Math.abs(this.moveSlideShiftX);
const loopBool = dir === 'left' ? nextIndex < this.nodes.sliderItemNodes.length : nextIndex >= 0;
for (let step = 0; loopBool; step++) {
const lastIndex = nextIndex + (dir === 'left' ? -1 : 1);
const currentShiftX = step === 0 ? this.lastShifX : this.objShiftX[lastIndex];
const nextShiftX = this.objShiftX[nextIndex];
const distanceBetweenSlides = Math.abs(currentShiftX - nextShiftX);
const percentMoved = remainingShiftX / distanceBetweenSlides;
if (percentMoved >= 0.5 && percentMoved <= 1) {
this.activeItemIndex = nextIndex;
break;
} else if (percentMoved < 0.5 && step === 0) {
break;
} else if (percentMoved < 0.5 && step !== 0) {
this.activeItemIndex = lastIndex;
break;
} else if (percentMoved > 1) {
const isLastSlide = dir === 'left' ? nextIndex + 1 >= this.nodes.sliderItemNodes.length : nextIndex - 1 < 0;
if (isLastSlide) {
this.activeItemIndex = nextIndex;
break;
} else {
nextIndex += dir === 'left' ? 1 : -1;
remainingShiftX -= distanceBetweenSlides;
continue;
}
}
}
this.setShiftX(this.activeItemIndex);
}
addTransition() {
const noParseTransitionDuration = parseFloat(getComputedStyle(this.nodes.sliderItemsNode).getPropertyValue('--transitionDurationSeconds'));
const transitionDuration = isNaN(noParseTransitionDuration) ? 0.3 : noParseTransitionDuration;
this.nodes.sliderItemsNode.style.transitionDuration = `${transitionDuration}s`;
}
removeTransition() {
this.nodes.sliderItemsNode.style.transitionDuration = '0s';
}
setObjShiftX() {
this.objShiftX = {};
for (let i = 0; i < this.nodes.sliderItemNodes.length; i++) {
this.objShiftX[i] = this.getShiftX(i);
}
console.log(this.objShiftX);
}
getNextIndex(dir) {
const quantityItems = this.nodes.sliderItemNodes.length;
return (this.activeItemIndex + dir + quantityItems) % quantityItems;
}
getCenteringShiftX = (activeItemIndex = this.activeItemIndex) => {
const firstSliderItemNode = this.nodes.sliderItemNodes[activeItemIndex];
const sliderItemsNode = this.nodes.sliderItemsNode;
const widthSlider = sliderItemsNode.offsetWidth;
const widthFirstSliderItem = firstSliderItemNode.offsetWidth;
const centeringShiftX = Math.max((widthSlider - widthFirstSliderItem) / 2, 0);
// this.centeringShiftX = Math.max((widthSlider - widthFirstSliderItem) / 2, 0);
return centeringShiftX;
}
getChangeSlideShiftX(activeItemIndex = this.activeItemIndex) {
const firstRect = this.nodes.sliderItemNodes[0].getBoundingClientRect();
const nextRect = this.nodes.sliderItemNodes[activeItemIndex].getBoundingClientRect();
const diffLeftNextSlide = nextRect.left - firstRect.left;
return diffLeftNextSlide;
// this.changeSlideShiftX = diffLeftNextSlide;
}
getShiftX(activeItemIndex = this.activeItemIndex) {
return this.getCenteringShiftX(activeItemIndex) - this.getChangeSlideShiftX(activeItemIndex);
}
setShiftX = (activeItemIndex = this.activeItemIndex) => {
this.shiftX = this.objShiftX[activeItemIndex];
this.nodes.sliderItemsNode.style.transform = `translateX(${this.shiftX}px)`;
}
calcMoveSlideShiftX() {
this.shiftX = this.lastShifX + this.moveSlideShiftX;
this.nodes.sliderItemsNode.style.transform = `translateX(${this.shiftX}px)`;
}
changeSlide(nextIndex) {
this.activeItemIndex = nextIndex;
this.setShiftX();
}
debounce(func, delay) {
let timeoutId;
return function(...args) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
}
const slider = new Slider('.teams__slider');
.slider {
display: flex;
flex-direction: column;
gap: 20px;
user-select: none;
}
.slider__arrows {
display: flex;
gap: 30px;
margin: 0 auto;
user-select: none;
}
.slider__items {
--transitionDurationSeconds: 0.3;
display: flex;
align-items: center;
gap: 50px;
user-select: none;
transition-property: transform;
transition-timing-function: linear;
transition-duration: 0s;
touch-action: pan-y;
}
.slider__wrapper img {
pointer-events: none;
}
.slider__item {
cursor: grab;
}
.slider__arrow {
cursor: pointer;
padding: 5px 12px;
transition: background-color 0.2s ease;
font-size: 2em;
color: white;
}
.slider__arrow:hover {
background-color: rgba(255, 255, 255, 0.1);
}
*,
*::before,
*::after {
box-sizing: inherit;
}
html {
box-sizing: border-box;
}
body {
margin: 0;
font-size: 16px;
font-family: 'Roboto', sans-serif;
}
img {
display: block;
max-width: 100%;
max-height: 100%;
}
.container {
max-width: 1094px;
display: block;
margin: 0 auto;
padding: 0 1em;
}
.link {
white-space: nowrap;
text-decoration: none;
color: inherit;
transition-property: color;
transition-timing-function: ease;
transition-duration: 0.2s;
}
.link:hover {
color: #90ee90;
}
.btn {
display: inline-block;
font-size: inherit;
border: none;
outline: none;
color: white;
padding: 1em 1.625em;
background-color: inherit;
transition-property: color, background-color;
transition-timing-function: ease;
transition-duration: 0.2s;
font-weight: bold;
}
.btn_green {
background-color: rgb(66, 174, 96);
border-radius: 6px;
}
.btn_green:hover {
background-color: rgb(135, 189, 29);
color: white;
}
.teams {
background-color: rgb(39, 55, 32);
padding: 120px 0;
overflow-x: hidden;
}
.teams__title {
color: white;
margin: 0;
font-size: 2em;
text-align: center;
margin: 0;
}
.teams__wrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
.teams__slider-item {
padding: 2.75em 5.125em;
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
border-radius: 30px;
--width: max(75%, 500px);
min-width: var(--width);
max-width: var(--width);
}
.teams__slider-item:nth-child(2n + 1) {
/* нечётные */
background-color: white;
}
.teams__slider-item:nth-child(2n) {
/* чётные */
background-color: #D2EBD5;
}
.teams__slider-item-img {
--width: 128px;
min-width: var(--width);
max-width: var(--width);
}
.teams__slider-text {
margin: 0;
font-size: 2em;
}
.teams__slider-review {
display: flex;
align-items: center;
gap: 24px;
}
.teams__slider-review-img {
--width: 64px;
min-width: var(--width);
max-width: var(--width);
}
.teams__slider-review-text {
font-size: 1.125em;
}
@media (max-width: 720px) {
.teams__slider-item {
--width: max(75%, 300px);
padding: 1em 2em;
}
.teams__slider-text {
font-size: 1em;
}
.teams__slider-review-text {
font-size: 0.8em;
}
}
.as-console {
display: none;
visibility: hidden;
opacity: 0;
position: fixed;
z-index: -1;
}
<section class="teams">
<div class="container">
<div class="teams__wrapper">
<h2 class="teams__title">Trusted by top teams</h2>
<div class="teams__slider slider">
<div class="slider__items teams__slider-items">
<div class="slider__item teams__slider-item">
<div class="teams__slider-item-img-wrapper">
<img src="./img/teams-slider-logo.png" alt="" class="teams__slider-item-img">
</div>
<p class="teams__slider-text">
1 “HyperComply has enabled us to complete questionnaires quickly and accurately, and has had a hugely positive effect on speeding up our sales cycle”
</p>
<div class="teams__slider-review">
<div class="teams__slider-review-img-wrapper">
<img src="./img/teams-slider-review.jpg" alt="" class="teams__slider-review-img">
</div>
<div class="teams__slider-review-text">
Desiree R, Sr. Director of Information Security
</div>
</div>
</div>
<!-- .slider__item.teams__slider-item -->
<div class="slider__item teams__slider-item">
<div class="teams__slider-item-img-wrapper">
<img src="./img/teams-slider-logo.png" alt="" class="teams__slider-item-img">
</div>
<p class="teams__slider-text">
2 “With HyperComply, we can now complete questionnaires with ease and accuracy, resulting in a faster sales cycle. This innovative tool has truly transformed the way we do business”
</p>
<div class="teams__slider-review">
<div class="teams__slider-review-img-wrapper">
<img src="./img/teams-slider-review.jpg" alt="" class="teams__slider-review-img">
</div>
<div class="teams__slider-review-text">
Desiree R, Sr. Director of Information Security
</div>
</div>
</div>
<!-- .slider__item.teams__slider-item -->
<div class="slider__item teams__slider-item">
<div class="teams__slider-item-img-wrapper">
<img src="./img/teams-slider-logo.png" alt="" class="teams__slider-item-img">
</div>
<p class="teams__slider-text">
3 “Thanks to HyperComply, we are able to breeze through questionnaires and speed up our sales cycle. This game-changing technology has made a significant impact on our business operations.”
</p>
<div class="teams__slider-review">
<div class="teams__slider-review-img-wrapper">
<img src="./img/teams-slider-review.jpg" alt="" class="teams__slider-review-img">
</div>
<div class="teams__slider-review-text">
Desiree R, Sr. Director of Information Security
</div>
</div>
</div>
<!-- .slider__item.teams__slider-item -->
<div class="slider__item teams__slider-item">
<div class="teams__slider-item-img-wrapper">
<img src="./img/teams-slider-logo.png" alt="" class="teams__slider-item-img">
</div>
<p class="teams__slider-text">
4 “Thanks to HyperComply, we are able to breeze through questionnaires and speed up our sales cycle. This game-changing technology has made a significant impact on our business operations.”
</p>
<div class="teams__slider-review">
<div class="teams__slider-review-img-wrapper">
<img src="./img/teams-slider-review.jpg" alt="" class="teams__slider-review-img">
</div>
<div class="teams__slider-review-text">
Desiree R, Sr. Director of Information Security
</div>
</div>
</div>
<!-- .slider__item.teams__slider-item -->
<div class="slider__item teams__slider-item">
<div class="teams__slider-item-img-wrapper">
<img src="./img/teams-slider-logo.png" alt="" class="teams__slider-item-img">
</div>
<p class="teams__slider-text">
5 “Thanks to HyperComply, we are able to breeze through questionnaires and speed up our sales cycle. This game-changing technology has made a significant impact on our business operations.”
</p>
<div class="teams__slider-review">
<div class="teams__slider-review-img-wrapper">
<img src="./img/teams-slider-review.jpg" alt="" class="teams__slider-review-img">
</div>
<div class="teams__slider-review-text">
Desiree R, Sr. Director of Information Security
</div>
</div>
</div>
<!-- .slider__item.teams__slider-item -->
<div class="slider__item teams__slider-item">
<div class="teams__slider-item-img-wrapper">
<img src="./img/teams-slider-logo.png" alt="" class="teams__slider-item-img">
</div>
<p class="teams__slider-text">
6 “Thanks to HyperComply, we are able to breeze through questionnaires and speed up our sales cycle. This game-changing technology has made a significant impact on our business operations.”
</p>
<div class="teams__slider-review">
<div class="teams__slider-review-img-wrapper">
<img src="./img/teams-slider-review.jpg" alt="" class="teams__slider-review-img">
</div>
<div class="teams__slider-review-text">
Desiree R, Sr. Director of Information Security
</div>
</div>
</div>
<!-- .slider__item.teams__slider-item -->
</div>
<!-- .slider__items teams__slider-items -->
<div class="slider__arrows">
<div class="slider__arrow slider__arrow_left">
<
</div>
<div class="slider__arrow slider__arrow_right">
>
</div>
</div>
</div>
<!-- .teams__slider slider -->
</div>
<!-- .teams__wrapper -->
</section>
Источник: Stack Overflow на русском