1 | import React from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import classNames from 'classnames';
|
4 | import CarouselItem from './CarouselItem';
|
5 | import { mapToCssModules } from './utils';
|
6 |
|
7 | const SWIPE_THRESHOLD = 40;
|
8 |
|
9 | class 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 |
|
33 | if (this.props.ride === 'carousel') {
|
34 | this.setInterval();
|
35 | }
|
36 |
|
37 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
233 | Carousel.propTypes = {
|
234 |
|
235 | activeIndex: PropTypes.number,
|
236 |
|
237 | next: PropTypes.func.isRequired,
|
238 |
|
239 | previous: PropTypes.func.isRequired,
|
240 |
|
241 | keyboard: PropTypes.bool,
|
242 | |
243 |
|
244 |
|
245 | pause: PropTypes.oneOf(['hover', false]),
|
246 |
|
247 |
|
248 | ride: PropTypes.oneOf(['carousel']),
|
249 |
|
250 |
|
251 | interval: PropTypes.oneOfType([
|
252 | PropTypes.number,
|
253 | PropTypes.string,
|
254 | PropTypes.bool,
|
255 | ]),
|
256 | children: PropTypes.array,
|
257 |
|
258 | mouseEnter: PropTypes.func,
|
259 |
|
260 | mouseLeave: PropTypes.func,
|
261 |
|
262 | slide: PropTypes.bool,
|
263 | cssModule: PropTypes.object,
|
264 | className: PropTypes.string,
|
265 | enableTouch: PropTypes.bool,
|
266 | };
|
267 |
|
268 | Carousel.defaultProps = {
|
269 | interval: 5000,
|
270 | pause: 'hover',
|
271 | keyboard: true,
|
272 | slide: true,
|
273 | enableTouch: true,
|
274 | };
|
275 |
|
276 | Carousel.childContextTypes = {
|
277 | direction: PropTypes.string
|
278 | };
|
279 |
|
280 | export default Carousel;
|