UNPKG

8.18 kBJavaScriptView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import classNames from 'classnames';
4import CarouselItem from './CarouselItem';
5import { mapToCssModules } from './utils';
6
7const SWIPE_THRESHOLD = 40;
8
9class Carousel extends React.Component {
10 constructor(props) {
11 super(props);
12 this.handleKeyPress = this.handleKeyPress.bind(this);
13 this.renderItems = this.renderItems.bind(this);
14 this.hoverStart = this.hoverStart.bind(this);
15 this.hoverEnd = this.hoverEnd.bind(this);
16 this.handleTouchStart = this.handleTouchStart.bind(this);
17 this.handleTouchEnd = this.handleTouchEnd.bind(this);
18 this.touchStartX = 0;
19 this.touchStartY = 0;
20 this.state = {
21 activeIndex: this.props.activeIndex,
22 direction: 'right',
23 indicatorClicked: false,
24 };
25 }
26
27 getChildContext() {
28 return { direction: this.state.direction };
29 }
30
31 componentDidMount() {
32 // Set up the cycle
33 if (this.props.ride === 'carousel') {
34 this.setInterval();
35 }
36
37 // TODO: move this to the specific carousel like bootstrap. Currently it will trigger ALL carousels on the page.
38 document.addEventListener('keyup', this.handleKeyPress);
39 }
40
41 static getDerivedStateFromProps(nextProps, prevState) {
42 let newState = null;
43 let { activeIndex, direction, indicatorClicked } = prevState;
44
45 if (nextProps.activeIndex !== activeIndex) {
46 // Calculate the direction to turn
47 if (nextProps.activeIndex === activeIndex + 1) {
48 direction = 'right';
49 } else if (nextProps.activeIndex === activeIndex -1) {
50 direction = 'left';
51 } else if (nextProps.activeIndex < activeIndex) {
52 direction = indicatorClicked ? 'left' : 'right';
53 } else if (nextProps.activeIndex !== activeIndex) {
54 direction = indicatorClicked ? 'right' : 'left';
55 }
56
57 newState = {
58 activeIndex: nextProps.activeIndex,
59 direction,
60 indicatorClicked: false,
61 }
62 }
63
64 return newState;
65 }
66
67 componentDidUpdate(prevProps, prevState) {
68 if (prevState.activeIndex === this.state.activeIndex) return;
69 this.setInterval(this.props);
70 }
71
72 componentWillUnmount() {
73 this.clearInterval();
74 document.removeEventListener('keyup', this.handleKeyPress);
75 }
76
77 setInterval(props = this.props) {
78 // make sure not to have multiple intervals going...
79 this.clearInterval();
80 if (props.interval) {
81 this.cycleInterval = setInterval(() => {
82 props.next();
83 }, parseInt(props.interval, 10));
84 }
85 }
86
87 clearInterval() {
88 clearInterval(this.cycleInterval);
89 }
90
91 hoverStart(...args) {
92 if (this.props.pause === 'hover') {
93 this.clearInterval();
94 }
95 if (this.props.mouseEnter) {
96 this.props.mouseEnter(...args);
97 }
98 }
99
100 hoverEnd(...args) {
101 if (this.props.pause === 'hover') {
102 this.setInterval();
103 }
104 if (this.props.mouseLeave) {
105 this.props.mouseLeave(...args);
106 }
107 }
108
109 handleKeyPress(evt) {
110 if (this.props.keyboard) {
111 if (evt.keyCode === 37) {
112 this.props.previous();
113 } else if (evt.keyCode === 39) {
114 this.props.next();
115 }
116 }
117 }
118
119 handleTouchStart(e) {
120 if(!this.props.enableTouch) {
121 return;
122 }
123 this.touchStartX = e.changedTouches[0].screenX;
124 this.touchStartY = e.changedTouches[0].screenY;
125 }
126
127 handleTouchEnd(e) {
128 if(!this.props.enableTouch) {
129 return;
130 }
131
132 const currentX = e.changedTouches[0].screenX;
133 const currentY = e.changedTouches[0].screenY;
134 const diffX = Math.abs(this.touchStartX - currentX);
135 const diffY = Math.abs(this.touchStartY - currentY);
136
137 // Don't swipe if Y-movement is bigger than X-movement
138 if(diffX < diffY) {
139 return;
140 }
141
142 if(diffX < SWIPE_THRESHOLD) {
143 return;
144 }
145
146 if(currentX < this.touchStartX) {
147 this.props.next();
148 } else {
149 this.props.previous();
150 }
151 }
152
153 renderItems(carouselItems, className) {
154 const { slide } = this.props;
155 return (
156 <div className={className}>
157 {carouselItems.map((item, index) => {
158 const isIn = (index === this.state.activeIndex);
159 return React.cloneElement(item, {
160 in: isIn,
161 slide: slide,
162 });
163 })}
164 </div>
165 );
166 }
167
168 render() {
169 const { cssModule, slide, className } = this.props;
170 const outerClasses = mapToCssModules(classNames(
171 className,
172 'carousel',
173 slide && 'slide'
174 ), cssModule);
175
176 const innerClasses = mapToCssModules(classNames(
177 'carousel-inner'
178 ), cssModule);
179
180 // filter out booleans, null, or undefined
181 const children = this.props.children.filter(child => child !== null && child !== undefined && typeof child !== 'boolean');
182
183 const slidesOnly = children.every(child => child.type === CarouselItem);
184
185 // Rendering only slides
186 if (slidesOnly) {
187 return (
188 <div className={outerClasses} onMouseEnter={this.hoverStart} onMouseLeave={this.hoverEnd}>
189 {this.renderItems(children, innerClasses)}
190 </div>
191 );
192 }
193
194 // Rendering slides and controls
195 if (children[0] instanceof Array) {
196 const carouselItems = children[0];
197 const controlLeft = children[1];
198 const controlRight = children[2];
199
200 return (
201 <div className={outerClasses} onMouseEnter={this.hoverStart} onMouseLeave={this.hoverEnd}>
202 {this.renderItems(carouselItems, innerClasses)}
203 {controlLeft}
204 {controlRight}
205 </div>
206 );
207 }
208
209 // Rendering indicators, slides and controls
210 const indicators = children[0];
211 const wrappedOnClick = (e) => {
212 if (typeof indicators.props.onClickHandler === 'function') {
213 this.setState({ indicatorClicked: true }, () => indicators.props.onClickHandler(e));
214 }
215 };
216 const wrappedIndicators = React.cloneElement(indicators, { onClickHandler: wrappedOnClick });
217 const carouselItems = children[1];
218 const controlLeft = children[2];
219 const controlRight = children[3];
220
221 return (
222 <div className={outerClasses} onMouseEnter={this.hoverStart} onMouseLeave={this.hoverEnd}
223 onTouchStart={this.handleTouchStart} onTouchEnd={this.handleTouchEnd}>
224 {wrappedIndicators}
225 {this.renderItems(carouselItems, innerClasses)}
226 {controlLeft}
227 {controlRight}
228 </div>
229 );
230 }
231}
232
233Carousel.propTypes = {
234 // the current active slide of the carousel
235 activeIndex: PropTypes.number,
236 // a function which should advance the carousel to the next slide (via activeIndex)
237 next: PropTypes.func.isRequired,
238 // a function which should advance the carousel to the previous slide (via activeIndex)
239 previous: PropTypes.func.isRequired,
240 // controls if the left and right arrow keys should control the carousel
241 keyboard: PropTypes.bool,
242 /* If set to "hover", pauses the cycling of the carousel on mouseenter and resumes the cycling of the carousel on
243 * mouseleave. If set to false, hovering over the carousel won't pause it. (default: "hover")
244 */
245 pause: PropTypes.oneOf(['hover', false]),
246 // Autoplays the carousel after the user manually cycles the first item. If "carousel", autoplays the carousel on load.
247 // This is how bootstrap defines it... I would prefer a bool named autoplay or something...
248 ride: PropTypes.oneOf(['carousel']),
249 // the interval at which the carousel automatically cycles (default: 5000)
250 // eslint-disable-next-line react/no-unused-prop-types
251 interval: PropTypes.oneOfType([
252 PropTypes.number,
253 PropTypes.string,
254 PropTypes.bool,
255 ]),
256 children: PropTypes.array,
257 // called when the mouse enters the Carousel
258 mouseEnter: PropTypes.func,
259 // called when the mouse exits the Carousel
260 mouseLeave: PropTypes.func,
261 // controls whether the slide animation on the Carousel works or not
262 slide: PropTypes.bool,
263 cssModule: PropTypes.object,
264 className: PropTypes.string,
265 enableTouch: PropTypes.bool,
266};
267
268Carousel.defaultProps = {
269 interval: 5000,
270 pause: 'hover',
271 keyboard: true,
272 slide: true,
273 enableTouch: true,
274};
275
276Carousel.childContextTypes = {
277 direction: PropTypes.string
278};
279
280export default Carousel;