Instantly share code, notes, and snippets.

Embed
What would you like to do?
Very simple (and not-quite-complete) slider
export default class SimpleSlider {
constructor(container, options) {
this.options = options;
this.autoInterval = options.auto;
this.visible = options.visible;
this.classes = options.classes;
this.selectors = options.selectors;
this.container = container;
this.list = this.container.querySelector(this.selectors.list);
this.items = Array.from(
this.container.querySelectorAll(this.selectors.item),
);
// triplicate items
this.itemsPrev = this.items.map(item => {
const clone = item.cloneNode(true);
clone.classList.add(this.classes.clonePrev);
this.list.insertBefore(clone, this.items[0]);
return clone;
});
this.itemsNext = this.items.map(item => {
const clone = item.cloneNode(true);
clone.classList.add(this.classes.cloneNext);
return this.list.appendChild(clone);
});
this.allItems = [].concat(this.itemsPrev, this.items, this.itemsNext);
this.container.classList.add(this.classes.enabled);
// bind later used event handlers to this
this.goTo = this.goTo.bind(this);
this.next = this.next.bind(this);
this.prev = this.prev.bind(this);
this.resize = this.resize.bind(this);
this.pause = this.pause.bind(this);
this.resume = this.resume.bind(this);
this.onEnd = this.onEnd.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
this.onFirstTouch = this.onFirstTouch.bind(this);
this.container.addEventListener('goTo', this.goTo);
this.container.addEventListener('next', this.next);
this.container.addEventListener('prev', this.prev);
this.container.addEventListener('pause', this.pause);
this.container.addEventListener('resume', this.resume);
window.addEventListener('resize', this.resize, {
passive: true,
});
// go to first slide
this.container.dispatchEvent(
new CustomEvent('goTo', {
detail: {
index: 0,
},
}),
);
// run slider
this.resume();
// are we on touch-capable device? because:
// https://codeburst.io/the-only-way-to-detect-touch-with-javascript-7791a3346685 ;)
window.addEventListener(
'touchstart',
this.onFirstTouch,
{
passive: true,
},
false,
);
}
// private
onFirstTouch() {
// setup touch handling
this.list.addEventListener(
'touchstart',
this.onTouchStart,
{
passive: true,
},
false,
);
this.list.addEventListener(
'touchend',
this.onTouchEnd,
{
passive: true,
},
false,
);
window.removeEventListener('touchstart', this.onFirstTouch, false);
}
onTouchStart(ev) {
[this.lastTouchStart] = ev.changedTouches;
this.pause();
}
onTouchEnd(ev) {
if (this.lastTouchStart) {
const [touchEnd] = ev.changedTouches;
let { minSwipeLength } = this.options;
minSwipeLength = minSwipeLength || 10;
if (touchEnd.screenX < this.lastTouchStart.screenX - minSwipeLength) {
this.next();
} else if (
touchEnd.screenX >
this.lastTouchStart.screenX + minSwipeLength
) {
this.prev();
}
this.resume();
this.lastTouchStart = null;
}
}
onEnd(ev) {
if (ev.target === this.list) {
this.container.classList.add(this.classes.disableTransition);
requestAnimationFrame(() => {
const offset =
-this.items[0].offsetLeft +
this.itemsPrev[this.itemsPrev.length - 1].clientWidth;
this.setTransformOnList(offset);
this.setItemsClasses(this.itemsPrev);
requestAnimationFrame(() => {
this.container.classList.remove(this.classes.disableTransition);
});
});
this.container.removeEventListener('transitionend', this.onEnd);
}
}
setTransformOnList(value) {
this.list.style.transform = `translate3d(${value}px, 0, 0)`;
}
setItemsClasses(items) {
this.allItems.forEach(item => {
item.classList.remove(this.classes.active);
item.classList.remove(this.classes.past);
item.classList.remove(this.classes.future);
});
const currentAllIndex = this.allItems.indexOf(items[this.currentIndex]);
this.allItems
.slice(currentAllIndex, currentAllIndex + this.visible)
.forEach(item => item.classList.add(this.classes.active));
if (currentAllIndex > 0) {
this.allItems[currentAllIndex - 1].classList.add(this.classes.past);
}
if (currentAllIndex + this.visible < this.allItems.length) {
this.allItems[currentAllIndex + this.visible].classList.add(
this.classes.future,
);
}
}
// public
goTo(ev) {
let { index, offset } = ev.detail;
if (this.visible === undefined || this.visible === 'auto') {
this.visible = Math.floor(
this.container.clientWidth / (this.items[0].clientWidth - 1),
);
}
if (offset === undefined) {
offset = 0;
}
if (index === undefined) {
const firstItem = this.allItems.filter(item =>
item.classList.contains(this.classes.active),
);
if (firstItem) {
index =
this.allItems.indexOf(firstItem[0]) + offset - this.itemsPrev.length;
} else {
index = 0;
}
}
const max = this.items.length - 1;
this.currentIndex = (index + max + 1) % (max + 1);
this.setItemsClasses(this.items);
if (this.currentIndex === max) {
this.container.addEventListener('transitionend', this.onEnd);
}
this.setTransformOnList(-this.items[this.currentIndex].offsetLeft);
}
next() {
this.goTo({
detail: {
offset: 1,
},
});
}
prev() {
this.goTo({
detail: {
offset: -1,
},
});
}
pause() {
clearTimeout(this.autoTimeout);
}
resume() {
if (this.autoInterval && this.visible < this.items.length) {
this.autoTimeout = setTimeout(() => {
requestAnimationFrame(() => {
this.goTo({
detail: {
offset: 1,
},
});
this.resume();
});
}, this.autoInterval);
}
}
resize() {
this.visible = this.options.visible;
this.goTo({
detail: {
offset: 0,
},
});
this.pause();
this.resume();
}
}
<div class="c-slider">
<div class="c-slider__frame c-slider__frame--uncropped">
<ul class="c-slider__list">
<li class="c-slider__item">
<div class="c-slider__content">
A
</div>
</li>
<li class="c-slider__item">
<div class="c-slider__content">
B
</div>
</li>
<li class="c-slider__item">
<div class="c-slider__content">
C
</div>
</li>
<li class="c-slider__item">
<div class="c-slider__content">
D
</div>
</li>
<li class="c-slider__item">
<div class="c-slider__content">
E
</div>
</li>
</ul>
</div>
</div>
$content-width: 1140px;
$bp-large-item-width: ceil($content-width / 3);
$global-partial-transition: 0.3s ease-in-out;
$global-transition: all $global-partial-transition;
$color-bg: #a7b9ff;
.c-slider {
margin: 0;
width: 100%;
}
.c-slider__frame {
position: relative;
width: 100%;
overflow: hidden;
}
.c-slider__list {
position: relative;
width: 100%;
display: flex;
list-style: none;
padding: 0;
margin: 0;
transition: transform $global-partial-transition;
flex-wrap: wrap;
justify-content: center;
.is-transition-disabled & {
transition-duration: 0ms;
}
.is-enabled & {
justify-content: flex-start;
flex-wrap: nowrap;
}
}
.c-slider__item {
position: relative;
flex: 0 0 auto;
margin: 0;
padding: 10px;
width: 100%;
pointer-events: none;
&.is-active {
pointer-events: auto;
}
@include bp-small {
width: 50%;
}
@include bp-medium {
width: 33.33%;
}
@include bp-large {
padding: 20px;
}
}
.c-slider__content {
width: 100%;
pointer-events: none;
transition: $global-transition;
background: $color-bg;
.is-past &,
.is-future & {
background: rgba($color-bg, 0.5);
}
.is-transition-disabled & {
transition-duration: 0ms;
}
@include bp-full {
.is-enabled & {
backface-visibility: hidden;
margin-left: -100vw;
opacity: 0;
}
.is-past & {
margin-left: -50vw;
transform: translateX(($content-width+$bp-large-item-width) / 2);
opacity: 1;
}
.is-active & {
margin-left: 0vw; /* stylelint-disable-line length-zero-no-unit */ /* Fix IE11 flickering bug */
transform: translateX(0);
opacity: 1;
}
.is-future & {
margin-left: 50vw;
transform: translateX(-($content-width+$bp-large-item-width) / 2);
opacity: 1;
}
.is-future + .c-slider__item & {
margin-left: 100vw;
}
}
}
.c-slider__frame--uncropped {
overflow: visible;
}
const slider = new SimpleSlider(document.querySelector('.c-slider'), {
auto: 3000,
selectors: {
list: '.c-slider__list',
item: '.c-slider__item',
},
classes: {
active: 'is-active',
past: 'is-past',
future: 'is-future',
enabled: 'is-enabled',
disableTransition: 'is-transition-disabled',
cloneNext: 'js-clone-next',
clonePrev: 'js-clone-prev',
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment