/* eslint-disable no-param-reassign,getter-return */
// noinspection DuplicatedCode
/**
* SimpleAdaptiveSlider by itchief (https://github.com/itchief/ui-components/tree/master/simple-adaptive-slider)
* Copyright 2020 - 2023 Alexander Maltsev
* Licensed under MIT (https://github.com/itchief/ui-components/blob/master/LICENSE)
*/
class ItcSimpleSlider {
// базовые классы и селекторы
static PREFIX = 'itcss';
static EL_WRAPPER = `${ItcSimpleSlider.PREFIX}__wrapper`;
static EL_ITEM = `${ItcSimpleSlider.PREFIX}__item`;
static EL_ITEM_ACTIVE = `${ItcSimpleSlider.PREFIX}__item_active`;
static EL_ITEMS = `${ItcSimpleSlider.PREFIX}__items`;
static EL_INDICATOR = `${ItcSimpleSlider.PREFIX}__indicator`;
static EL_INDICATOR_ACTIVE = `${ItcSimpleSlider.PREFIX}__indicator_active`;
static EL_INDICATORS = `${ItcSimpleSlider.PREFIX}__indicators`;
static EL_CONTROL = `${ItcSimpleSlider.PREFIX}__btn`;
// порог для переключения слайда (20%)
static SWIPE_THRESHOLD = 20;
// класс для отключения transition
static TRANSITION_NONE = 'transition-none';
// Определите, поддерживает ли текущий клиент пассивные события
static checkSupportPassiveEvents() {
let passiveSupported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
passiveSupported = true;
},
});
window.addEventListener('testPassiveListener', null, options);
window.removeEventListener('testPassiveListener', null, options);
} catch (error) {
passiveSupported = false;
}
return passiveSupported;
}
constructor(target, config) {
this._el = typeof target === 'string' ? document.querySelector(target) : target;
this._elWrapper = this._el.querySelector(`.${this.constructor.EL_WRAPPER}`);
this._elItems = this._el.querySelector(`.${this.constructor.EL_ITEMS}`);
this._elListItem = this._el.querySelectorAll(`.${this.constructor.EL_ITEM}`);
// экстремальные значения слайдов
this._exOrderMin = 0;
this._exOrderMax = 0;
this._exItemMin = null;
this._exItemMax = null;
this._exTranslateMin = 0;
this._exTranslateMax = 0;
this._states = [];
this._isBalancing = false;
// направление смены слайдов (по умолчанию)
this._direction = 'next';
// текущее значение трансформации
this._transform = 0;
this._clientRect = this._elWrapper.getBoundingClientRect();
this._supportResizeObserver = typeof window.ResizeObserver !== 'undefined';
const styleElItems = window.getComputedStyle(this._elItems);
this._delay = Math.round(parseFloat(styleElItems.transitionDuration) * 50);
// swipe параметры
this._hasSwipeState = false;
this._swipeStartPosX = 0;
// id таймера
this._intervalId = null;
this._config = {
loop: true,
autoplay: false,
interval: 5000,
indicators: true,
swipe: true,
...config
};
this._elItems.dataset.translate = '0';
// добавляем к слайдам data-атрибуты
this._elListItem.forEach((item, index) => {
item.dataset.order = `${index}`;
item.dataset.index = `${index}`;
item.dataset.translate = '0';
this._states.push(index === 0 ? 1 : 0);
});
// перемещаем последний слайд перед первым
if (this._config.loop) {
const count = this._elListItem.length - 1;
const translate = -this._elListItem.length;
this._elListItem[count].dataset.order = '-1';
this._elListItem[count].dataset.translate = `${-this._elListItem.length}`;
const valueX = translate * this._clientRect.width;
this._elListItem[count].style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
}
// добавляем индикаторы к слайдеру
this._addIndicators();
this._elListIndicator = this._el.querySelectorAll(`.${this.constructor.EL_INDICATOR}`);
// обновляем экстремальные значения переменных
this._updateExProperties();
// помечаем активные элементы
this._changeActiveItems();
this._config.onInit ? this._config.onInit(this) : null;
// назначаем обработчики
this._addEventListener();
// запускаем автоматическую смену слайдов
this._autoplay();
}
_changeActiveItems() {
this._states.forEach((item, index) => {
if (item) {
this._elListItem[index].classList.add(this.constructor.EL_ITEM_ACTIVE);
} else {
this._elListItem[index].classList.remove(this.constructor.EL_ITEM_ACTIVE);
}
if (this._elListIndicator.length && item) {
this._elListIndicator[index].classList.add(this.constructor.EL_INDICATOR_ACTIVE);
} else if (this._elListIndicator.length && !item) {
this._elListIndicator[index].classList.remove(this.constructor.EL_INDICATOR_ACTIVE);
}
});
if (this._states.length) {
const btnPrev = this._el.querySelector('.itcss__btn_prev');
const btnNext = this._el.querySelector('.itcss__btn_next');
if (btnPrev) {
this._states[0] === 1 ? btnPrev.classList.add('d-none') : btnPrev.classList.remove('d-none');
}
if (btnNext) {
this._states[this._states.length - 1] === 1 ? btnNext.classList.add('d-none') : btnNext.classList.remove('d-none');
}
}
this._el.dispatchEvent(new CustomEvent('change.itc.slider', { bubbles: true }));
}
// смена слайдов
_move() {
this._elItems.classList.remove(this.constructor.TRANSITION_NONE);
if (this._direction === 'none') {
const valueX = this._transform * this._clientRect.width;
this._elItems.style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
return;
}
if (!this._config.loop) {
const isNotMovePrev = this._states[0] && this._direction === 'prev';
const isNotMoveNext = this._states[this._states.length - 1] && this._direction === 'next';
if (isNotMovePrev || isNotMoveNext) {
this._autoplay('stop');
return;
}
}
this._transform += this._direction === 'next' ? -1 : 1;
if (this._direction === 'next') {
this._states = [...this._states.slice(-1), ...this._states.slice(0, -1)];
} else if (this._direction === 'prev') {
this._states = [...this._states.slice(1), ...this._states.slice(0, 1)];
}
this._elItems.dataset.translate = this._transform;
const valueX = this._transform * this._clientRect.width;
this._elItems.style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
this._elItems.dispatchEvent(new CustomEvent('moving.itc.slider', { bubbles: true }));
this._changeActiveItems();
if (!this._isBalancing) {
this._isBalancing = true;
window.requestAnimationFrame(this._balanceItems.bind(this));
}
}
// функция для перемещения к слайду по индексу
_moveTo(index) {
const currIndex = this._states.indexOf(1);
this._direction = index > currIndex ? 'next' : 'prev';
for (let i = 0; i < Math.abs(index - currIndex); i++) {
this._move();
}
}
// метод для автоматической смены слайдов
_autoplay(action) {
if (!this._config.autoplay) {
return;
}
if (action === 'stop') {
clearInterval(this._intervalId);
this._intervalId = null;
return;
}
if (this._intervalId === null) {
this._intervalId = setInterval(() => {
this._direction = 'next';
this._move();
}, this._config.interval);
}
}
// добавление индикаторов
_addIndicators() {
const el = this._el.querySelector(`.${this.constructor.EL_INDICATORS}`);
if (el || !this._config.indicators) {
return;
}
let rows = '';
for (let i = 0, { length } = this._elListItem; i < length; i++) {
rows += `
`;
}
const html = `${rows}
`;
this._el.insertAdjacentHTML('beforeend', html);
}
// refresh extreme values
_updateExProperties() {
const els = Object.values(this._elListItem).map((el) => el);
const orders = els.map((item) => Number(item.dataset.order));
this._exOrderMin = Math.min(...orders);
this._exOrderMax = Math.max(...orders);
const min = orders.indexOf(this._exOrderMin);
const max = orders.indexOf(this._exOrderMax);
this._exItemMin = els[min];
this._exItemMax = els[max];
this._exTranslateMin = Number(this._exItemMin.dataset.translate);
this._exTranslateMax = Number(this._exItemMax.dataset.translate);
}
_balanceItems() {
if (!this._isBalancing) {
return;
}
if (this._direction === 'next') {
const exItemRight = this._exItemMin.getBoundingClientRect().right;
if (exItemRight < this._clientRect.left - this._clientRect.width / 2) {
this._exItemMin.dataset.order = `${this._exOrderMin + this._elListItem.length}`;
this._exItemMin.dataset.translate = `${this._exTranslateMin + this._elListItem.length}`;
const valueX = (this._exTranslateMin + this._elListItem.length) * this._clientRect.width;
this._exItemMin.style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
this._updateExProperties();
}
} else {
const exItemLeft = this._exItemMax.getBoundingClientRect().left;
if (exItemLeft > this._clientRect.right + this._clientRect.width / 2) {
this._exItemMax.dataset.order = `${this._exOrderMax - this._elListItem.length}`;
this._exItemMax.dataset.translate = `${this._exTranslateMax - this._elListItem.length}`;
const valueX = (this._exTranslateMax - this._elListItem.length) * this._clientRect.width;
this._exItemMax.style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
this._updateExProperties();
}
}
window.setTimeout(() => {
window.requestAnimationFrame(this._balanceItems.bind(this));
}, this._delay);
}
// adding listeners
_addEventListener() {
const onSwipeStart = (e) => {
this._autoplay('stop');
if (e.target.closest(`.${this.constructor.EL_CONTROL}`)) {
return;
}
const event = e.type.search('touch') === 0 ? e.touches[0] : e;
this._swipeStartPosX = event.clientX;
this._swipeStartPosY = event.clientY;
this._hasSwipeState = true;
this._hasSwiping = false;
};
const onSwipeMove = (e) => {
if (!this._hasSwipeState) {
return;
}
const event = e.type.search('touch') === 0 ? e.touches[0] : e;
let diffPosX = this._swipeStartPosX - event.clientX;
const diffPosY = this._swipeStartPosY - event.clientY;
if (!this._hasSwiping) {
if (Math.abs(diffPosY) > Math.abs(diffPosX) || Math.abs(diffPosX) === 0) {
this._hasSwipeState = false;
return;
}
this._hasSwiping = true;
}
e.preventDefault();
if (!this._config.loop) {
const isNotMoveFirst = this._states[0] && diffPosX <= 0;
const isNotMoveLast = this._states[this._states.length - 1] && diffPosX >= 0;
if (isNotMoveFirst || isNotMoveLast) {
diffPosX /= 4;
}
}
this._elItems.classList.add(this.constructor.TRANSITION_NONE);
const valueX = this._transform * this._clientRect.width - diffPosX;
this._elItems.style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
};
const onSwipeEnd = (e) => {
if (!this._hasSwipeState) {
return;
}
const event = e.type.search('touch') === 0 ? e.changedTouches[0] : e;
let diffPosX = this._swipeStartPosX - event.clientX;
if (diffPosX === 0) {
this._hasSwipeState = false;
return;
}
if (!this._config.loop) {
const isNotMoveFirst = this._states[0] && diffPosX <= 0;
const isNotMoveLast = this._states[this._states.length - 1] && diffPosX >= 0;
if (isNotMoveFirst || isNotMoveLast) {
diffPosX = 0;
}
}
const value = (diffPosX / this._clientRect.width) * 100;
this._elItems.classList.remove(this.constructor.TRANSITION_NONE);
if (value > this.constructor.SWIPE_THRESHOLD) {
this._direction = 'next';
this._move();
} else if (value < -this.constructor.SWIPE_THRESHOLD) {
this._direction = 'prev';
this._move();
} else {
this._direction = 'none';
this._move();
}
this._hasSwipeState = false;
this._autoplay();
};
// click
this._el.addEventListener('click', (e) => {
const $target = e.target;
this._autoplay('stop');
if ($target.classList.contains(this.constructor.EL_CONTROL)) {
e.preventDefault();
this._direction = $target.dataset.slide;
this._move();
} else if ($target.dataset.slideTo) {
e.preventDefault();
const index = parseInt($target.dataset.slideTo, 10);
this._moveTo(index);
}
this._autoplay();
});
// transitionstart and transitionend
if (this._config.loop) {
this._elItems.addEventListener('transitionend', () => {
this._isBalancing = false;
});
}
// mouseenter and mouseleave
this._el.addEventListener('mouseenter', () => {
this._autoplay('stop');
});
this._el.addEventListener('mouseleave', () => {
this._autoplay();
});
// swipe
if (this._config.swipe) {
const options = this.constructor.checkSupportPassiveEvents() ? { passive: false } : false;
this._el.addEventListener('touchstart', onSwipeStart, options);
this._el.addEventListener('touchmove', onSwipeMove, options);
this._el.addEventListener('mousedown', onSwipeStart);
this._el.addEventListener('mousemove', onSwipeMove);
document.addEventListener('touchend', onSwipeEnd);
document.addEventListener('mouseup', onSwipeEnd);
document.addEventListener('mouseout', onSwipeEnd);
}
this._el.addEventListener('dragstart', (e) => {
e.preventDefault();
});
// при изменении активности вкладки
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && this._config.loop) {
this._autoplay();
} else {
this._autoplay('stop');
}
});
if (this._supportResizeObserver) {
const resizeObserver = new ResizeObserver((entries) => {
const { contentRect } = entries[0];
if (Math.round(this._clientRect.width * 10) === Math.round(contentRect.width * 10)) {
return;
}
this._clientRect = contentRect;
const newValueX = contentRect.width * Number(this._elItems.dataset.translate);
this.reset(newValueX, true);
this._autoplay();
});
resizeObserver.observe(this._elWrapper);
}
}
reset(newValueX = 0, recalc = false) {
this._autoplay('stop');
this._elItems.classList.add(this.constructor.TRANSITION_NONE);
this._elItems.style.transform = `translate3D(${newValueX}px, 0px, 0.1px)`;
this._elListItem.forEach((el) => {
const valueX = recalc ? Number(el.dataset.translate) * this._clientRect.width : 0;
el.style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
});
if (!recalc) {
this._transform = 0;
this._states = [];
this._elItems.dataset.translate = '0';
this._elListItem = this._el.querySelectorAll(`.${this.constructor.EL_ITEM}`);
// добавляем к слайдам data-атрибуты
this._elListItem.forEach((item, index) => {
item.dataset.order = `${index}`;
item.dataset.index = `${index}`;
item.dataset.translate = '0';
this._states.push(index === 0 ? 1 : 0);
});
// перемещаем последний слайд перед первым
if (this._config.loop) {
const count = this._elListItem.length - 1;
const translate = -this._elListItem.length;
this._elListItem[count].dataset.order = '-1';
this._elListItem[count].dataset.translate = `${-this._elListItem.length}`;
const valueX = translate * this._clientRect.width;
this._elListItem[count].style.transform = `translate3D(${valueX}px, 0px, 0.1px)`;
}
this._el.querySelector(`.${this.constructor.EL_INDICATORS}`).remove();
// добавляем индикаторы к слайдеру
this._addIndicators();
this._elListIndicator = document.querySelectorAll(`.${this.constructor.EL_INDICATOR}`);
// обновляем экстремальные значения переменных
this._updateExProperties();
// помечаем активные элементы
this._changeActiveItems();
}
this._autoplay();
}
// перейти к следующему слайду
next() {
this._direction = 'next';
this._move();
}
// перейти к предыдущему слайду
prev() {
this._direction = 'prev';
this._move();
}
// управление автоматической сменой слайдов
autoplay() {
this._autoplay('stop');
}
moveTo(index) {
this._moveTo(index);
}
}