UNPKG

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