react-alice-carousel
Version:
React image gallery, react slideshow carousel, react content rotator
661 lines (573 loc) • 20.3 kB
JavaScript
import React from 'react';
import Swipeable from 'react-swipeable';
import PropTypes from 'prop-types';
class AliceCarousel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
slides: [],
currentIndex: 1,
duration: props.duration || 250,
style: {
transition: 'transform 0ms ease-out'
}
};
this._slideNext = this._slideNext.bind(this);
this._slidePrev = this._slidePrev.bind(this);
this._onTouchEnd = this._onTouchEnd.bind(this);
this._onTouchMove = this._onTouchMove.bind(this);
this._keyUpHandler = this._keyUpHandler.bind(this);
this._disableAnimation = this._disableAnimation.bind(this);
this._allowAnimation = this._allowAnimation.bind(this);
this._getActiveSlideIndex = this._getActiveSlideIndex.bind(this);
this._resizeHandler = throttle(this._resizeHandler, 250).bind(this);
this._getStageComponentNode = this._getStageComponentNode.bind(this);
this._onMouseEnterAutoPlayHandler = this._onMouseEnterAutoPlayHandler.bind(this);
this._onMouseLeaveAutoPlayHandler = this._onMouseLeaveAutoPlayHandler.bind(this);
}
componentDidMount() {
this._allowAnimation();
this._setInitialState();
window.addEventListener('resize', this._resizeHandler);
if (!this.props.keysControlDisabled) {
window.addEventListener('keyup', this._keyUpHandler);
}
if (this.props.autoPlay) this._play();
}
componentWillReceiveProps(nextProps) {
const { currentIndex } = this.state;
const {
slideToIndex, duration, startIndex, keysControlDisabled,
autoPlayActionDisabled, autoPlayDirection, autoPlayInterval, autoPlay
} = nextProps;
if (this.props.duration !== duration) {
this.setState({ duration });
}
if (currentIndex !== slideToIndex && slideToIndex !== undefined) {
const slideNext = slideToIndex === currentIndex + 1;
const slidePrev = slideToIndex === currentIndex - 1;
slideNext ?
this._slideToItem(currentIndex + 1) : slidePrev ?
this._slideToItem(currentIndex - 1) : this._slideToItem(slideToIndex);
}
if (this.props.startIndex !== startIndex && !slideToIndex && slideToIndex !== 0) {
this._slideToItem(startIndex);
}
if (this.props.keysControlDisabled !== keysControlDisabled) {
keysControlDisabled === true
? window.removeEventListener('keyup', this._keyUpHandler)
: window.addEventListener('keyup', this._keyUpHandler);
}
if (this.props.autoPlayActionDisabled !== autoPlayActionDisabled ||
this.props.autoPlayDirection !== autoPlayDirection ||
this.props.autoPlayInterval !== autoPlayInterval ||
this.props.autoPlay !== autoPlay) {
this._pause();
}
}
componentDidUpdate(prevProps) {
if (this.props.autoPlayActionDisabled !== prevProps.autoPlayActionDisabled ||
this.props.autoPlayDirection !== prevProps.autoPlayDirection ||
this.props.autoPlayInterval !== prevProps.autoPlayInterval ||
this.props.autoPlay !== prevProps.autoPlay) {
if (this.props.autoPlay) {
this._play();
}
}
}
componentWillUnmount() {
window.removeEventListener('resize', this._resizeHandler);
if (!this.props.keysControlDisabled) {
window.removeEventListener('keyup', this._keyUpHandler);
}
}
/**
* create clones for carousel items
*
* @param children
* @param itemsInSlide
* @returns {object}
*/
_cloneSlides(children, itemsInSlide) {
const first = children.slice(0, itemsInSlide);
const last = children.slice(children.length - itemsInSlide);
return last.concat(children, first);
}
/**
* set initial carousel index
*
* @param {number} index
* @param {number} childrenLength
* @returns {number}
*/
_setStartIndex(childrenLength, index) {
const startIndex = index ? Math.abs(Math.ceil(index)) : 0;
return Math.min(startIndex, (childrenLength - 1));
}
/**
* calculate initial props
*
* @param {object} nextProps props which will be received
* @returns {object}
*/
_calculateInitialProps(nextProps) {
let totalItems;
let { startIndex, children } = this.props;
if (nextProps) {
totalItems = nextProps.responsive;
children = nextProps.children;
}
const items = this._setTotalItemsInSlide(totalItems, children.length);
const currentIndex = this._setStartIndex(children.length, startIndex);
const itemWidth = this.stageComponent.getBoundingClientRect().width / items;
return {
items,
itemWidth,
currentIndex,
slides: children,
clones: this._cloneSlides(children, items),
translate3d: -itemWidth * (items + currentIndex)
};
}
/**
* calculate total items in the slide
*
* @param {number} totalItems total items for the slide
* @param {number} childrenLength slides length
* @returns {number}
*/
_setTotalItemsInSlide(totalItems, childrenLength) {
let items = 1;
const width = window.innerWidth;
if (this.props.responsive) {
const responsive = totalItems || this.props.responsive || {};
if (Object.keys(responsive).length) {
Object.keys(responsive).forEach((key) => {
if (key < width) items = Math.min(responsive[key].items, childrenLength) || items;
});
}
}
return items;
}
/**
* set initial state
* */
_setInitialState() {
this.setState(this._calculateInitialProps());
}
/**
* window resize handler
* */
_resizeHandler() {
this._setInitialState();
}
/**
* refers to parent carousel node
*
* @param {HTMLElement} node
* */
_getStageComponentNode(node) { this.stageComponent = node; }
/**
* enable carousel animation
*/
_allowAnimation() { this.allowAnimation = true; }
/**
* disable carousel animation
*/
_disableAnimation() { this.allowAnimation = false; }
/**
* check MouseEnter event
* @returns {boolean}
*/
_isHovered() { return this.isHovered; }
/**
* check if need recalculate a slide position
* @param {bool} skip
*/
_checkRecalculation(skip) {
const { slides, currentIndex, duration } = this.state;
window.setTimeout(() => {
const recalculate = !skip && (currentIndex < 0 || currentIndex >= slides.length);
recalculate ? this._recalculateSlidePosition() : this._onSlideChanged();
}, duration);
}
/**
* set new props to the state
*/
_recalculateSlidePosition() {
let { items, itemWidth, slides, currentIndex } = this.state;
currentIndex = (currentIndex < 0) ? slides.length - 1 : 0;
this.setState({
currentIndex,
translate3d: -itemWidth * (currentIndex === 0 ? items : slides.length - 1 + items),
style: {transition: 'transform 0ms ease-out'}
}, () => this._onSlideChanged());
}
/**
* call callback from props
*/
_onSlideChange() {
if (this.props.onSlideChange) {
this.props.onSlideChange({
item: this.state.currentIndex,
slide: this._getActiveSlideIndex()
});
}
}
/**
* call callback from props
*/
_onSlideChanged() {
this._allowAnimation();
if (this.props.onSlideChanged) {
this.props.onSlideChanged({
item: this.state.currentIndex,
slide: this._getActiveSlideIndex()
});
}
}
/**
* create xml element
* @returns {XML}
*/
_prevButton() {
return(
<div className="alice-carousel__prev-btn">
<div className="alice-carousel__prev-btn-wrapper"
onMouseEnter={this._onMouseEnterAutoPlayHandler}
onMouseLeave={this._onMouseLeaveAutoPlayHandler}
>
<div className="alice-carousel__prev-btn-item"
onClick={this._slidePrev}
/>
</div>
</div>
);
}
/**
* create xml element
* @returns {XML}
*/
_nextButton() {
return(
<div className="alice-carousel__next-btn">
<div className="alice-carousel__next-btn-wrapper"
onMouseEnter={this._onMouseEnterAutoPlayHandler}
onMouseLeave={this._onMouseLeaveAutoPlayHandler}
>
<div className="alice-carousel__next-btn-item"
onClick={this._slideNext}
/>
</div>
</div>
);
}
/**
* calculate active dot navigation index
* @returns {number}
*/
_getActiveSlideIndex() {
let { slides, items, currentIndex } = this.state;
currentIndex = currentIndex + items;
const slidesLength = slides.length;
const dotsLength = slidesLength % items === 0
? Math.floor(slidesLength / items) - 1
: Math.floor(slidesLength / items);
if (items === 1) {
if (currentIndex < items) {
return slidesLength - items;
}
else if (currentIndex > slidesLength) {
return 0;
}
else {
return currentIndex - 1;
}
} else {
if (currentIndex === slidesLength + items) {
return 0;
}
else if (currentIndex < items && currentIndex !== 0) {
return dotsLength;
}
else if (currentIndex === 0) {
return slidesLength % items === 0 ? dotsLength : dotsLength - 1;
}
else {
return Math.floor(currentIndex / items) - 1;
}
}
}
/**
* create markdown for dots navigation
* @returns {XML}
*/
_renderDotsNavigation() {
const { slides, items } = this.state;
return(
<ul className="alice-carousel__dots">
{
slides.map((item, i) => {
if (i < slides.length / items) {
return <li
key={i}
onClick={() => this._slideToItem(i * items)}
onMouseEnter={this._onMouseEnterAutoPlayHandler}
onMouseLeave={this._onMouseLeaveAutoPlayHandler}
className={`alice-carousel__dots-item${ this._getActiveSlideIndex() === i ? ' __active' : '' }`}
/>;
}
})
}
</ul>
);
}
/**
* create markdown for play/pause button
* @returns {XML}
*/
_renderPlayPauseButton() {
return(
<div className="alice-carousel__play-btn" onClick={() => this._playPauseToggle()}>
<div className="alice-carousel__play-btn-wrapper">
<div className={`alice-carousel__play-btn-item${this.state.isPlaying ? ' __pause' : ''}`} />
</div>
</div>
);
}
/**
* set auto-play interval
*/
_play() {
const { autoPlayDirection, autoPlayInterval, duration } = this.props;
const playInterval = typeof autoPlayInterval === 'number'
? Math.max(autoPlayInterval, duration)
: duration;
this.setState({isPlaying: true});
if (!this._autoPlayIntervalId) {
this._autoPlayIntervalId = window.setInterval(() => {
if (!this._isHovered() && this._autoPlayIntervalId) { // check interval if force click
if (!this.allowAnimation) return;
autoPlayDirection === 'rtl' ? this._slidePrev(false) : this._slideNext(false);
}
}, playInterval);
}
}
/**
* clear auto-play interval
*/
_pause() {
if (this._autoPlayIntervalId) {
window.clearInterval(this._autoPlayIntervalId);
this._autoPlayIntervalId = null;
this.setState({isPlaying: false});
}
}
/**
* call play/pause methods
*/
_playPauseToggle () {
this.state.isPlaying ? this._pause() : this._play();
}
/**
* call external methods
* @param {Event} e
*/
_keyUpHandler(e) {
if (!this.allowAnimation) return;
switch(e.keyCode) {
case 32:
this._playPauseToggle();
break;
case 37:
this._slidePrev();
break;
case 39:
this._slideNext();
break;
}
}
/**
* call external methods
* @param {bool} action
*/
_slidePrev(action = true) {
if (action && this.props.autoPlayActionDisabled) {
this._pause();
}
this._slideToItem(this.state.currentIndex - 1);
}
/**
* call external methods
* @param {bool} action
*/
_slideNext(action = true) {
if (action && this.props.autoPlayActionDisabled) {
this._pause();
}
this._slideToItem(this.state.currentIndex + 1);
}
/**
* set new props to the state
*
* @param {number} index
* @param {bool} skip
* @param {number} duration
*/
_slideToItem(index, skip, duration = this.state.duration) {
if (!this.allowAnimation) return;
this._disableAnimation();
this._onSlideChange();
const { items, itemWidth } = this.state;
const translate = (index + items) * itemWidth;
this.setState({
currentIndex: index,
translate3d: -translate,
style: { transition: `transform ${duration}ms ease-out` }
}, () => this._checkRecalculation(skip));
}
/**
* calculate swipePosition on touchEvent start
*/
_onTouchMove() {
this._pause();
if (this.props.swipeDisabled) return;
const { slides, items, itemWidth, translate3d } = this.state;
const maxPosition = (slides.length + items) * itemWidth;
const direction = arguments[1] > 0 ? 'LEFT' : 'RIGHT';
let position = translate3d - arguments[1];
if (position >= 0 || Math.abs(position) >= maxPosition) {
recalculatePosition();
}
setTransformAnimation(this.stageComponent, position);
this.swipePosition = { position, direction };
function recalculatePosition() {
direction === 'RIGHT'
? position += -slides.length * itemWidth
: position += maxPosition - items * itemWidth;
if (position >= 0 || Math.abs(position) >= maxPosition) {
recalculatePosition();
}
}
}
/**
* set transformations before touchEvent end
*/
_beforeTouchEnd() {
const { itemWidth, slides, items, duration } = this.state;
const swipePosition = Math.abs(this.swipePosition.position);
let swipeIndex = this.swipePosition.direction === 'LEFT'
? Math.floor(swipePosition / itemWidth) + 1
: Math.floor(swipePosition / itemWidth);
const transformPosition = -swipeIndex * itemWidth;
setTransformAnimation(this.stageComponent, transformPosition, duration);
let currentIndex = swipeIndex - items;
if (currentIndex === slides.length) { currentIndex = 0; }
if (currentIndex < 0) { currentIndex = slides.length + currentIndex; }
const position = itemWidth * (currentIndex + items); //(index + items) * itemWidth;
setTimeout(() => {
setTransformAnimation(this.stageComponent, -position);
this._slideToItem(currentIndex, true, 0);
}, duration);
}
/**
* called on touchEvent end
*/
_onTouchEnd() {
if (this.props.swipeDisabled) return;
this._beforeTouchEnd();
}
/**
* called on mouseEnter event for parent carousel node
*/
_onMouseEnterAutoPlayHandler() {
this.isHovered = true;
}
/**
* called on mouseLeave event for parent carousel node
*/
_onMouseLeaveAutoPlayHandler() {
this.isHovered = false;
}
render() {
const style = Object.assign(
{},
this.state.style,
{ transform: `translate3d(${this.state.translate3d}px, 0, 0)` }
);
const slides = this.state.clones || this.state.slides;
return(
<div className="alice-carousel">
<Swipeable onSwiping={this._onTouchMove} onSwiped={this._onTouchEnd}>
<div className="alice-carousel__wrapper"
onMouseEnter={this._onMouseEnterAutoPlayHandler}
onMouseLeave={this._onMouseLeaveAutoPlayHandler}
>
<ul className="alice-carousel__stage" ref={this._getStageComponentNode} style={style} >
{
slides.map((item, i) => (
<li
className="alice-carousel__stage-item" key={i}
style={{width: `${this.state.itemWidth}px`}}
>
{ item }
</li>
))
}
</ul>
</div>
</Swipeable>
{ !this.props.buttonsDisabled ? this._prevButton() : null }
{ !this.props.buttonsDisabled ? this._nextButton() : null }
{ !this.props.dotsDisabled ? this._renderDotsNavigation() : null }
{ !this.props.playButtonDisabled ? this._renderPlayPauseButton() : null }
</div>
);
}
}
AliceCarousel.propTypes = {
children: PropTypes.array.isRequired,
onSlideChange: PropTypes.func,
onSlideChanged: PropTypes.func,
keysControlDisabled: PropTypes.bool,
playButtonDisabled: PropTypes.bool,
buttonsDisabled: PropTypes.bool,
dotsDisabled: PropTypes.bool,
swipeDisabled: PropTypes.bool,
responsive: PropTypes.object,
duration: PropTypes.number,
startIndex: PropTypes.number,
slideToIndex: PropTypes.number,
autoPlay: PropTypes.bool,
autoPlayInterval: PropTypes.number,
autoPlayDirection: PropTypes.string,
autoPlayActionDisabled: PropTypes.bool
};
export default AliceCarousel;
function setTransformAnimation(element, position, durationMs = 0) {
const prefixes = ['Webkit', 'Moz', 'ms', 'O', ''];
for (let value of prefixes) {
element.style[value + 'Transform'] = `translate3d(${position}px, 0, 0)`;
element.style[value + 'Transition'] = `transform ${durationMs}ms ease-out`;
}
}
function throttle(func, ms = 0) {
let savedArgs,savedThis, isThrottled = false;
function wrapper() {
if (isThrottled) {
savedArgs = arguments;
savedThis = this;
return;
}
func.apply(this, arguments);
isThrottled = true;
setTimeout(() => {
isThrottled = false;
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}