@wix/design-system
Version:
@wix/design-system
440 lines • 19.6 kB
JavaScript
import React, { Children, PureComponent } from 'react';
import PropTypes from 'prop-types';
import { st, classes, vars } from './CarouselWIP.st.css.js';
import { ChevronLeftSmall, ChevronRightSmall } from '@wix/wix-ui-icons-common';
import Loader from '../Loader';
import Control from './Control';
import Slide from './Slide';
import { CONTROLS_START_END, SLIDING_TYPE, ALIGNMENT, DATA_HOOKS, AUTOPLAY_SPEED, SWIPE_THRESHOLD, } from './constants';
import { isWhollyInView, animate, nop, normalizeIndex } from './utils';
/** The carousel component creates a slideshow for cycling through a series of content. */
class CarouselWIP extends PureComponent {
constructor(props) {
super(props);
this.loadingImagesCount = 0;
this.childCount = 0;
this.autoplayTimer = -1;
this.dragStartX = null;
this.initialScrollLeft = 0;
this.capturedPointerId = null;
this.hasSetPointerCapture = false;
// Need to wait for images to load so we know which images are visible
// Adding onLoad and onError callbacks to all images under the component
this._setImagesOnLoadHandlers = () => {
Array.from(this.carousel.children).forEach(child => {
const childImages = Array.from(child.getElementsByTagName('img'));
childImages.forEach(img => {
if (img.complete) {
return;
}
this.setState({ isLoading: true });
this.loadingImagesCount++;
img.onload = this._onImageLoad;
img.onerror = this._onImageLoad;
});
});
};
this._updateChildCount = () => {
const { images } = this.props;
this.childCount = this.carousel?.children?.length ?? images?.length ?? 0;
};
this._onImageLoad = () => {
const { initialSlideIndex } = this.props;
this.loadingImagesCount--;
if (!this.loadingImagesCount) {
this.setState({ isLoading: false });
this._slideTo({
index: initialSlideIndex,
immediate: true,
}).catch(nop);
}
};
this._setAutoplayTimer = (active) => {
clearInterval(this.autoplayTimer);
if (active) {
this.autoplayTimer = window.setInterval(this._next, AUTOPLAY_SPEED);
}
};
this._setVisibleSlides = () => {
const { props, carousel, childCount } = this;
const { infinite } = props;
const firstVisibleChild = Math.max(Array.from(carousel.children).findIndex(child => isWhollyInView(carousel)(child)), 0);
const lastVisibleChild = Math.max(Array.from(carousel.children).findIndex((child, i, children) => isWhollyInView(carousel)(child) &&
(i === children.length - 1 ||
!isWhollyInView(carousel)(children[i + 1]))), 0);
this.setState({
visibleSlides: [firstVisibleChild, lastVisibleChild],
isLeftArrowDisabled: !infinite && firstVisibleChild === 0,
isRightArrowDisabled: !infinite && lastVisibleChild === childCount - 1,
isShowStartGradient: firstVisibleChild > 0,
isShowEndGradient: lastVisibleChild < childCount - 1,
});
};
this._slideTo = ({ index = 0, alignTo = ALIGNMENT.LEFT, immediate } = {
index: 0,
alignTo: ALIGNMENT.LEFT,
immediate: false,
}) => {
if (this.childCount === 0) {
return Promise.reject('No children to slide to');
}
if (!this.carousel) {
return Promise.reject('The Carousel is not mounted');
}
const { afterChange, beforeChange, easing, animationDuration: duration, infinite, startEndOffset = 0, } = this.props;
const { children, scrollLeft, offsetWidth } = this.carousel;
const slideIndex = normalizeIndex(index, this.childCount, infinite);
const { visibleSlides } = this.state;
const [firstVisibleSlide] = visibleSlides;
let delta;
const child = children[slideIndex];
if (alignTo === ALIGNMENT.RIGHT) {
delta =
child.offsetWidth -
(offsetWidth - child.offsetLeft) -
scrollLeft +
startEndOffset;
}
else {
delta = child.offsetLeft - scrollLeft - startEndOffset;
}
if (firstVisibleSlide !== slideIndex && beforeChange) {
beforeChange(firstVisibleSlide, index);
}
this.setState({ isAnimating: true });
return new Promise((res, _) => {
if (immediate) {
this.carousel.scrollLeft = child.offsetLeft;
return res();
}
else {
const prop = 'scrollLeft';
return res(animate(this.carousel, {
prop,
delta,
easing,
duration,
}));
}
})
.then(() => {
this.setState({ isAnimating: false });
this._setVisibleSlides();
if (firstVisibleSlide !== slideIndex && afterChange) {
return afterChange(slideIndex);
}
})
.catch(_ => {
this._setVisibleSlides();
this.setState({ isAnimating: false });
});
};
this._next = () => {
const { slidingType, infinite } = this.props;
const { visibleSlides } = this.state;
const [firstVisibleSlide, lastVisibleSlide] = visibleSlides;
let nextSlide, alignTo;
if ([SLIDING_TYPE.REVEAL_CHUNK, SLIDING_TYPE.REVEAL_ONE].includes(slidingType)) {
if (lastVisibleSlide === this.childCount - 1) {
nextSlide = infinite ? 0 : lastVisibleSlide;
}
else {
nextSlide = lastVisibleSlide + 1;
}
alignTo =
slidingType === SLIDING_TYPE.REVEAL_CHUNK
? ALIGNMENT.LEFT
: ALIGNMENT.RIGHT;
}
else {
if (firstVisibleSlide === this.childCount - 1) {
nextSlide = infinite ? 0 : firstVisibleSlide;
}
else {
nextSlide = firstVisibleSlide + 1;
}
alignTo = ALIGNMENT.LEFT;
}
if (nextSlide === this.childCount - 1) {
this.setState({ isRightArrowDisabled: true, isShowEndGradient: false });
}
if (firstVisibleSlide === 0) {
this.setState({ isLeftArrowDisabled: false, isShowStartGradient: true });
}
return this._slideTo({ index: nextSlide, alignTo });
};
this._prev = () => {
const { slidingType, infinite } = this.props;
const { visibleSlides } = this.state;
const [firstVisibleSlide, lastVisibleSlide] = visibleSlides;
let prevSlide, alignTo;
if ([SLIDING_TYPE.REVEAL_CHUNK, SLIDING_TYPE.REVEAL_ONE].includes(slidingType)) {
if (firstVisibleSlide === 0) {
prevSlide = infinite ? this.childCount - 1 : firstVisibleSlide;
}
else {
prevSlide = firstVisibleSlide - 1;
}
alignTo =
slidingType === SLIDING_TYPE.REVEAL_CHUNK
? ALIGNMENT.RIGHT
: ALIGNMENT.LEFT;
}
else {
if (firstVisibleSlide === 0) {
prevSlide = infinite ? this.childCount - 1 : 0;
}
else {
prevSlide = firstVisibleSlide - 1;
}
alignTo = ALIGNMENT.LEFT;
}
if (prevSlide === 0) {
this.setState({ isLeftArrowDisabled: true, isShowStartGradient: false });
}
if (lastVisibleSlide === this.childCount - 1) {
this.setState({ isRightArrowDisabled: false, isShowEndGradient: true });
}
return this._slideTo({ index: prevSlide, alignTo });
};
this._setRef = (r) => {
this.carousel = r;
};
this._renderLeftControl = () => {
const { isLeftArrowDisabled } = this.state;
const { controlsPosition, controlsStartEnd, controlsSize, controlsSkin } = this.props;
return (controlsPosition !== 'none' &&
(!isLeftArrowDisabled ||
controlsStartEnd === CONTROLS_START_END.DISABLED) && (React.createElement(Control, { dataHook: DATA_HOOKS.prevButton, onClick: this._prev, icon: React.createElement(ChevronLeftSmall, null), size: controlsSize, skin: controlsSkin, disabled: isLeftArrowDisabled, className: st(classes.control, classes.prev) })));
};
this._renderRightControl = () => {
const { isRightArrowDisabled } = this.state;
const { controlsPosition, controlsStartEnd, controlsSize, controlsSkin } = this.props;
return (controlsPosition !== 'none' &&
(!isRightArrowDisabled ||
controlsStartEnd === CONTROLS_START_END.DISABLED) && (React.createElement(Control, { dataHook: DATA_HOOKS.nextButton, onClick: this._next, icon: React.createElement(ChevronRightSmall, null), size: controlsSize, skin: controlsSkin, disabled: isRightArrowDisabled, className: st(classes.control, classes.next) })));
};
this._handlePointerDown = (e) => {
if (!e.isPrimary)
return;
e.preventDefault();
this.dragStartX = e.clientX;
this.initialScrollLeft = this.carousel.scrollLeft;
this.capturedPointerId = e.pointerId;
this.setState({
isDragging: true,
});
};
this.requestAnimationFrameId = null;
this._handlePointerMove = (e) => {
const { isDragging } = this.state;
if (!isDragging ||
!this.dragStartX ||
e.pointerId !== this.capturedPointerId) {
return;
}
e.preventDefault();
if (!this.hasSetPointerCapture) {
e.currentTarget.setPointerCapture(e.pointerId);
this.hasSetPointerCapture = true;
}
if (this.requestAnimationFrameId) {
cancelAnimationFrame(this.requestAnimationFrameId);
}
const swipeDistance = e.clientX - this.dragStartX;
this.requestAnimationFrameId = requestAnimationFrame(() => {
this.carousel.scrollLeft = this.initialScrollLeft - swipeDistance;
});
};
this._handlePointerUp = (e) => {
const { isDragging } = this.state;
if (!isDragging ||
!this.dragStartX ||
e.pointerId !== this.capturedPointerId)
return;
this._handleSwipeEnd(e);
this._releasePointerCapture(e.pointerId);
this._resetDragState();
};
this._handlePointerCancel = (e) => {
if (e.pointerId !== this.capturedPointerId || !this.dragStartX) {
return;
}
this._handleSwipeEnd(e);
this._releasePointerCapture(e.pointerId);
this._resetDragState();
};
this._handleSwipeEnd = (e) => {
if (!this.dragStartX)
return;
const swipeDistance = e.clientX - this.dragStartX;
if (Math.abs(swipeDistance) > SWIPE_THRESHOLD) {
if (swipeDistance > 0) {
// Swiped right - go to previous slide
this._prev();
}
else {
// Swiped left - go to next slide
this._next();
}
}
else {
const { easing, animationDuration: duration } = this.props;
// sliding back to the original position
animate(this.carousel, {
prop: 'scrollLeft',
delta: swipeDistance,
easing,
duration,
});
}
};
this._releasePointerCapture = (capturedPointerId) => {
if (this.carousel?.hasPointerCapture(capturedPointerId)) {
this.carousel?.releasePointerCapture(capturedPointerId);
}
};
this._resetDragState = () => {
this.dragStartX = null;
this.initialScrollLeft = 0;
this.capturedPointerId = null;
this.hasSetPointerCapture = false;
this.setState({
isDragging: false,
});
};
this._renderSlides = () => {
const { images = [], children, gutter, imagesPosition, imagesFit, } = this.props;
const slide = ({ i, image, child, }) => (React.createElement(Slide, { dataHook: DATA_HOOKS.child, className: classes.slide, key: `slide-${i}`, width: "auto", gutter: i > 0 && gutter != null ? `${gutter}px` : undefined, image: image, children: child, imagePosition: imagesPosition, imageFit: imagesFit }));
return (React.createElement("div", { className: st(classes.carousel, { dragging: this.state.isDragging }), role: "list", ref: this._setRef, "data-hook": DATA_HOOKS.carousel, onPointerDown: this._handlePointerDown, onPointerMove: this._handlePointerMove, onPointerUp: this._handlePointerUp, onPointerCancel: this._handlePointerCancel }, Children.count(children)
? Children.map(children, (child, i) => slide({ i, child }))
: images.map((image, i) => slide({ i, image }))));
};
this._renderLoader = () => (React.createElement("div", { className: classes.loader },
React.createElement(Loader, { dataHook: DATA_HOOKS.loader, size: "small" })));
this._renderDots = () => {
const { children, images } = this.props;
const { visibleSlides } = this.state;
const [firstVisibleSlide, lastVisibleSlide] = visibleSlides;
const slidesCount = Children.count(children) || images?.length || 0;
return (React.createElement("div", { className: classes.dots }, Array(slidesCount)
.fill(0)
.map((_, index) => (React.createElement("div", { "data-hook": DATA_HOOKS.pageNavigation(index), key: index, className: st(classes.dot, {
active: index >= firstVisibleSlide && index <= lastVisibleSlide,
}), onClick: () => {
if (index < firstVisibleSlide || index > lastVisibleSlide)
this._slideTo({
index,
alignTo: index > firstVisibleSlide
? ALIGNMENT.RIGHT
: ALIGNMENT.LEFT,
});
} })))));
};
this._renderStartGradient = () => React.createElement("div", { className: classes.start });
this._renderEndGradient = () => React.createElement("div", { className: classes.end });
this.loadingImagesCount = 0;
this.state = {
visibleSlides: [],
isAnimating: false,
isLoading: false,
isLeftArrowDisabled: true,
isRightArrowDisabled: true,
isShowStartGradient: false,
isShowEndGradient: false,
isDragging: false,
};
}
componentDidMount() {
const { initialSlideIndex = 0, autoplay } = this.props;
this._updateChildCount();
this._setImagesOnLoadHandlers();
if (!this.loadingImagesCount) {
this._slideTo({ index: initialSlideIndex, immediate: true }).catch(nop);
this._setVisibleSlides();
}
this._setAutoplayTimer(autoplay);
}
componentDidUpdate(prevProps) {
const { autoplay } = this.props;
if (prevProps.autoplay !== autoplay)
this._setAutoplayTimer(autoplay);
const lastCount = this.childCount;
this._updateChildCount();
if (this.childCount && lastCount !== this.childCount) {
this._setVisibleSlides();
}
}
render() {
const { dataHook, className, controlsPosition, controlsSize, showControlsShadow, sidesGradientColor, hideDots, } = this.props;
const { isShowStartGradient, isShowEndGradient, isLoading } = this.state;
const showSidesGradients = !!sidesGradientColor;
return isLoading ? (this._renderLoader()) : (React.createElement("div", { "data-hook": dataHook, className: st(classes.root, {
controlsPosition,
controlsSize,
showControlsShadow,
showSidesGradients,
hideDots,
}, className), style: { [vars.sidesGradientColor]: sidesGradientColor } },
React.createElement("div", { style: { position: 'relative' } },
showSidesGradients &&
isShowStartGradient &&
this._renderStartGradient(),
this._renderLeftControl(),
this._renderSlides(),
this._renderRightControl(),
showSidesGradients && isShowEndGradient && this._renderEndGradient()),
!hideDots && this._renderDots()));
}
componentWillUnmount() {
if (this.capturedPointerId) {
this._releasePointerCapture(this.capturedPointerId);
}
this._setAutoplayTimer(false);
}
}
CarouselWIP.displayName = 'CarouselWIP';
CarouselWIP.propTypes = {
dataHook: PropTypes.any,
className: PropTypes.any,
children: PropTypes.any,
images: PropTypes.any,
controlsSkin: PropTypes.any,
showControlsShadow: PropTypes.any,
infinite: PropTypes.any,
initialSlideIndex: PropTypes.any,
afterChange: PropTypes.any,
beforeChange: PropTypes.any,
controlsPosition: PropTypes.any,
controlsSize: PropTypes.any,
controlsStartEnd: PropTypes.any,
slidingType: PropTypes.any,
startEndOffset: PropTypes.any,
gutter: PropTypes.any,
sidesGradientColor: PropTypes.any,
imagesPosition: PropTypes.any,
imagesFit: PropTypes.any,
autoplay: PropTypes.any,
hideDots: PropTypes.any,
variableWidth: PropTypes.any,
animationDuration: PropTypes.any,
easing: PropTypes.any,
};
CarouselWIP.defaultProps = {
children: [],
infinite: true,
controlsSkin: 'standard',
controlsStartEnd: 'disabled',
showControlsShadow: false,
images: [],
initialSlideIndex: 0,
controlsPosition: 'sides',
controlsSize: 'medium',
slidingType: 'align-to-start',
startEndOffset: 0,
gutter: undefined,
hideDots: false,
autoplay: false,
};
export default CarouselWIP;
//# sourceMappingURL=CarouselWIP.js.map