steevehook
9/12/2018 - 2:03 PM

Refactored vanilla carousel

Refactored vanilla carousel

@import "../../core/fonts";
@import "../../core/colors";
@import "../../core/mixins";

$number-of-slides-carousel: 4;
$number-of-slides-content-images: 3;

.refresh-content-images {
  margin-bottom: 20px;

  @include breakpoint(xp) {
    margin-bottom: 80px;
  }

  &__carousel-wrapper, &__list-wrapper {
    position: relative;
  }

  &__arrow {
    position: absolute;
    top: 45%;
    transform: translateY(-100%);
    width: 35px;
    height: 35px;
    background: $white-dark;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 18px;
    cursor: pointer;
    z-index: 10;

    &--left {
      left: 0;
    }

    &--right {
      right: 0;
    }

    &--disabled {
      display: none;
    }

    @at-root .touchevents & {
      display: none;
    }
  }

  &__title {
    font-family: $avantgarde-gothic;
    font-size: 46px;
    font-weight: 300;
    text-transform: uppercase;
    text-align: center;
    margin-bottom: 40px;

    @include breakpoint(xp) {
      font-size: 82px;
    }
  }

  &__list, &__carousel {
    position: relative;
    display: flex;
    transition: all .3s ease-in-out;
  }

  &__list-item {
    display: flex;
    flex-direction: column;
    box-sizing: border-box;

    &:hover {
      .refresh-content-images__info-cta {
        @at-root .no-touchevents & {
          a {
            color: $gold-refresh;
          }
        }
      }

      img {
        @include breakpoint(xp) {
          opacity: 0.8;
        }
      }
    }

    img {
      display: block;
      height: auto;
      pointer-events: none;
    }
  }

  &__info {
    font-family: $avantgarde-gothic;
    text-transform: uppercase;
    color: $gray-refresh;
    padding: 25px;
    box-sizing: border-box;

    @include breakpoint(xp) {
      padding: 35px;
    }
  }

  &__info-title {
    font-size: 16px;

    @include breakpoint(xp) {
      line-height: 24px;
      font-size: 24px;
    }
  }

  &__info-cta {
    font-size: 12px;
    margin-top: 20px;

    @include breakpoint(xp) {
      font-size: 13px;
      margin-top: 25px;
    }

    a {
      color: lighten($gray-refresh, 20%);

      &,
      &:hover {
        @include breakpoint(xp) {
          text-decoration: underline;
        }
      }
    }

    &--active {
      a {
        color: $gold-refresh;
      }
    }
  }

  &__list {
    .refresh-content-images__list-item {
      .refresh-content-images__info,
      img {
        width: 1024px/$number-of-slides-carousel;

        @include breakpoint(sm) {
          width: 1024px/$number-of-slides-content-images;
        }

        @include breakpoint(xp) {
          width: (100vw/$number-of-slides-content-images);
        }
      }
    }
  }

  &__list {
    text-align: center;
  }

  &__carousel {
    .refresh-content-images__list-item {
      &:nth-child(odd) {
        background-color: $white-dark;
      }

      .refresh-content-images__info,
      img {
        width: 1024px/$number-of-slides-carousel;

        @include breakpoint(xp) {
          width: (100vw/$number-of-slides-carousel);
        }
      }

      .refresh-content-images__info-title {
        font-size: 13px;

        @include breakpoint(xp) {
          font-size: 16px;
        }
      }

      .refresh-content-images__info-copy {
        margin-top: 20px;
        font-family: $helvetica-light;
        font-size: 12px;
        text-transform: none;
        font-weight: 300;

        @include breakpoint(xp) {
          font-size: 14px;
          margin-top: 25px;
        }
      }
    }
  }
}
import lazyLoad from './lazyLoad';

export const enableArrows = (com) => {
    com.rightArrow.classList.remove('refresh-content-images__arrow--disabled');
    com.leftArrow.classList.remove('refresh-content-images__arrow--disabled');
};

export const disableArrow = arrow => arrow.classList.add('refresh-content-images__arrow--disabled');

export const slide = ({com, position}) => {
    com.carousel.style.transform = 'translateX(' + position + 'px)'
};

export const next = ({com, position, remainingSlidesWidth}) => {
    if (-position <= remainingSlidesWidth) {
        enableArrows(com);
    }
    if (Math.ceil(-position) >= Math.ceil(remainingSlidesWidth)) {
        disableArrow(com.rightArrow);
    }
};

export const previous = ({com, position}) => {
    if (position <= 0) {
        enableArrows(com);
    }
    if (position === 0) {
        disableArrow(com.leftArrow);
    }
};

export const nextHandler = metadata => {
    const {
        com,
        rightmostSlide,
        gallery,
        slideWidth,
        position,
        numberOfSlides,
        remainingSlidesWidth
    } = metadata;

    if (window.innerWidth <= 1024) {
        metadata.slideWidth = gallery[0].children[0].children[0].width;
        metadata.remainingSlidesWidth = gallery.length * slideWidth - window.innerWidth;
    }

    const rightMostSlidePos = rightmostSlide * slideWidth;
    const positionPlus = position - numberOfSlides * slideWidth;
    if (-position < remainingSlidesWidth) {
        lazyLoad(rightmostSlide, metadata);
        if (-rightMostSlidePos > positionPlus) {
            lazyLoad(rightmostSlide + 1, metadata);
        }
        if (position - slideWidth > -remainingSlidesWidth) {
            metadata.position -= slideWidth;
        } else {
            metadata.position = -remainingSlidesWidth;
        }

        metadata.rightmostSlide++;
        slide({com, position: metadata.position});
    }

    next(metadata);
};

export const previousHandler = metadata => {
    const {com, position, slideWidth} = metadata;
    if (position <= 0) {
        metadata.rightmostSlide--;
        metadata.position += slideWidth;
        if (metadata.position >= 0) {
            metadata.position = 0;
        }
        slide({com, position: metadata.position});
    }

    previous(metadata);
};
export default element => {
    const wrapper = element.parentElement;
    const position = 0;
    const numberOfSlides = parseInt(element.dataset.numberOfSlides);
    const slideWidth = window.innerWidth / numberOfSlides;
    const rightmostSlide = numberOfSlides;
    const loadedSlides = [numberOfSlides];
    const com = {
        carousel: element,
        titleList: element.querySelectorAll('.refresh-content-images__info-title'),
        leftArrow: wrapper.querySelector('.refresh-content-images__arrow--left'),
        rightArrow: wrapper.querySelector('.refresh-content-images__arrow--right')
    };
    const gallery = Array.prototype
        .slice
        .call(com.carousel.children)
        .filter(e => e.classList.contains('refresh-content-images__list-item'));
    const remainingSlidesWidth = gallery.length * slideWidth - numberOfSlides * slideWidth;

    return {
        wrapper,
        position,
        numberOfSlides,
        slideWidth,
        remainingSlidesWidth,
        rightmostSlide,
        loadedSlides,
        com,
        gallery
    };
};
const getSrcSet = image => {
    if (!image || !image.srcset) {
        return [];
    }

    return image.srcset.split(',').map(link => {
        const [url, ratio] = link.trim().split` `;
        const splitLink = url.split`/`;

        return {
            ratio,
            width: splitLink[splitLink.length - 1]
        };
    });
};

export default (slide, metadata) => {
    const {gallery} = metadata;
    const lastLoadedSlide = metadata.loadedSlides[metadata.loadedSlides.length - 1];
    const [gallerySample] = gallery[0].children[0].children;

    if (slide > gallery.length - 1) {
        return;
    }

    for (let i = lastLoadedSlide; i <= slide; i++) {
        const [slideElement] = gallery[i].children,
            [slideImage] = slideElement.children,
            kalturaEntry = slideImage.dataset.kalturaEntry;
        const sampleSrc = gallerySample.src.trim().split`/`,
            sampleSrcWidth = sampleSrc[sampleSrc.length - 1];

        if (!slideImage.src) {
            slideImage.src = `${window.GHD.config.kalturaThumbnailURL}/${kalturaEntry}/width/${sampleSrcWidth}`;
            slideImage.srcset = getSrcSet(gallerySample).map(v =>
                `${window.GHD.config.kalturaThumbnailURL}/${kalturaEntry}/quality/75/width/${v.width}  ${v.ratio}`
            );
            slideImage.sizes = gallerySample.sizes;

            metadata.loadedSlides.push(i);
        }
    }
};
import hammer from './hammer';
import {disableArrow, enableArrows, nextHandler, previousHandler} from './moves';
import {onResizeThrottler} from '../../tools/throttler';

const resizeTitleHeights = titleList => {
    const titleArray = Array.from(titleList);
    const titleHeights = titleArray.map(title => title.offsetHeight);
    const greatestTitleHeight = titleHeights.sort()[0];

    titleArray.forEach(title => title.style.height = greatestTitleHeight + 'px');
};

const addEventListeners = metadata => {
    const {com} = metadata;

    com.leftArrow.addEventListener('click', () => previousHandler(metadata));
    com.rightArrow.addEventListener('click', () => nextHandler(metadata));
};

export default metadata => {
    const {gallery, numberOfSlides, com} = metadata;

    if (gallery.length > numberOfSlides) {
        enableArrows(com);
    }
    disableArrow(com.leftArrow);
    addEventListeners(metadata);
    onResizeThrottler(() => resizeTitleHeights(com.titleList), 500);
    hammer(metadata);
    resizeTitleHeights(com.titleList);
}
import Hammer from 'hammerjs';
import lazyLoad from './lazyLoad';
import {next, previous, slide} from './moves';
import {throttle} from '../../tools/throttler';


const markActiveSlide = (gallery, slide) => {
    gallery.forEach(elem => {
        const slideElemCTA = elem.children[1].children[1];
        slideElemCTA.classList.remove('refresh-content-images__info-cta--active');
    });

    const slideElemCTA = gallery[slide].children[1].children[1];
    slideElemCTA.classList.add('refresh-content-images__info-cta--active');
};

export default metadata => {
    const {gallery, com, numberOfSlides, wrapper} = metadata;
    const sliderManager = new Hammer.Manager(wrapper);
    const reposition = e => {
        const slideWidth = gallery[0].children[0].children[0].width;
        const remainingSlidesWidth = -(gallery.length * slideWidth - com.carousel.offsetWidth);
        let carouselPosition = (metadata.position + e.deltaX);

        if (carouselPosition >= 0) {
            carouselPosition = 0;
        }
        if (carouselPosition < remainingSlidesWidth) {
            carouselPosition = remainingSlidesWidth;
        }

        return carouselPosition;
    };

    sliderManager.add(new Hammer.Pan({
        threshold: 10,
        pointers: 1,
        direction: Hammer.DIRECTION_HORIZONTAL
    }));

    sliderManager.on('panleft', e => {
        const slide = numberOfSlides - Math.ceil((metadata.position + e.deltaX) / gallery[0].offsetWidth);
        if (e.pointerType !== 'touch') {
            return;
        }

        metadata.rightmostSlide = slide;
        lazyLoad(slide, metadata);
        next(metadata);
    });

    sliderManager.on('panright', e => {
        if (e.pointerType !== 'touch') {
            return;
        }

        previous(metadata);
    });

    sliderManager.on('panleft panright', e => {
        const slideWidth = gallery[0].children[0].children[0].width,
            offset = slideWidth / 2,
            carouselPos = reposition(e),
            activeSlide = Math.floor(Math.abs(carouselPos - offset) / slideWidth);

        if (e.pointerType !== 'touch' || Math.abs(e.deltaX) < Math.abs(e.deltaY)) {
            return;
        }

        throttle(() => markActiveSlide(gallery, activeSlide));
        slide({com, position: carouselPos});
    });

    sliderManager.on('panstart', e => {
        if (e.pointerType !== 'touch') {
            return;
        }

        com.carousel.style.transition = 'none';
    });

    sliderManager.on('panend', e => {
        if (e.pointerType !== 'touch') {
            return;
        }

        com.carousel.style.transition = null;
        metadata.position = reposition(e);
    });
}
import carousel from './carousel';

export default (function () {
    document.querySelectorAll('.refresh-carousel').forEach(carousel);
})();
import metadata from './utils/metadata';
import init from './utils/init';

export default element => {
    const carouselData = metadata(element);
    if (!carouselData.com.carousel) {
        return;
    }

    init(carouselData);
};