UNPKG

15.3 kBJavaScriptView Raw
1import React from "react";
2import PropTypes from "prop-types";
3
4import {
5 StyleSheet,
6 PanResponder,
7 View,
8 TouchableHighlight,
9 Platform,
10 I18nManager
11} from "react-native";
12
13import DefaultMarker from "./DefaultMarker";
14import { createArray, valueToPosition, positionToValue } from "./converters";
15
16const ViewPropTypes = require("react-native").ViewPropTypes || View.propTypes;
17
18export default class MultiSlider extends React.Component {
19 static defaultProps = {
20 values: [0],
21 onValuesChangeStart: () => {},
22 onValuesChange: values => {},
23 onValuesChangeFinish: values => {},
24 step: 1,
25 min: 0,
26 max: 10,
27 touchDimensions: {
28 borderRadius: 15,
29 slipDisplacement: 200
30 },
31 customMarker: DefaultMarker,
32 customMarkerLeft: DefaultMarker,
33 customMarkerRight: DefaultMarker,
34 markerOffsetX: 0,
35 markerOffsetY: 0,
36 sliderLength: 280,
37 onToggleOne: undefined,
38 onToggleTwo: undefined,
39 enabledOne: true,
40 enabledTwo: true,
41 allowOverlap: false,
42 snapped: false,
43 vertical: false,
44 minMarkerOverlapDistance: 0
45 };
46
47 constructor(props) {
48 super(props);
49
50 this.optionsArray =
51 this.props.optionsArray ||
52 createArray(this.props.min, this.props.max, this.props.step);
53 this.stepLength = this.props.sliderLength / this.optionsArray.length;
54
55 var initialValues = this.props.values.map(value =>
56 valueToPosition(value, this.optionsArray, this.props.sliderLength)
57 );
58
59 this.state = {
60 pressedOne: true,
61 valueOne: this.props.values[0],
62 valueTwo: this.props.values[1],
63 pastOne: initialValues[0],
64 pastTwo: initialValues[1],
65 positionOne: initialValues[0],
66 positionTwo: initialValues[1]
67 };
68 }
69
70 componentWillMount() {
71 var customPanResponder = (start, move, end) => {
72 return PanResponder.create({
73 onStartShouldSetPanResponder: (evt, gestureState) => true,
74 onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
75 onMoveShouldSetPanResponder: (evt, gestureState) => true,
76 onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
77 onPanResponderGrant: (evt, gestureState) => start(),
78 onPanResponderMove: (evt, gestureState) => move(gestureState),
79 onPanResponderTerminationRequest: (evt, gestureState) => false,
80 onPanResponderRelease: (evt, gestureState) => end(gestureState),
81 onPanResponderTerminate: (evt, gestureState) => end(gestureState),
82 onShouldBlockNativeResponder: (evt, gestureState) => true
83 });
84 };
85
86 this._panResponderOne = customPanResponder(
87 this.startOne,
88 this.moveOne,
89 this.endOne
90 );
91 this._panResponderTwo = customPanResponder(
92 this.startTwo,
93 this.moveTwo,
94 this.endTwo
95 );
96 }
97
98 componentWillReceiveProps(nextProps) {
99 if (this.state.onePressed || this.state.twoPressed) {
100 return;
101 }
102
103 let nextState = {};
104 if (
105 nextProps.min !== this.props.min ||
106 nextProps.max !== this.props.max ||
107 nextProps.step !== this.props.step ||
108 nextProps.values[0] !== this.state.valueOne ||
109 nextProps.sliderLength !== this.props.sliderLength ||
110 nextProps.values[1] !== this.state.valueTwo ||
111 (nextProps.sliderLength !== this.props.sliderLength &&
112 nextProps.values[1])
113 ) {
114 this.optionsArray =
115 this.props.optionsArray ||
116 createArray(nextProps.min, nextProps.max, nextProps.step);
117
118 this.stepLength = this.props.sliderLength / this.optionsArray.length;
119
120 var positionOne = valueToPosition(
121 nextProps.values[0],
122 this.optionsArray,
123 nextProps.sliderLength
124 );
125 nextState.valueOne = nextProps.values[0];
126 nextState.pastOne = positionOne;
127 nextState.positionOne = positionOne;
128
129 var positionTwo = valueToPosition(
130 nextProps.values[1],
131 this.optionsArray,
132 nextProps.sliderLength
133 );
134 nextState.valueTwo = nextProps.values[1];
135 nextState.pastTwo = positionTwo;
136 nextState.positionTwo = positionTwo;
137 }
138
139 if (nextState != {}) {
140 this.setState(nextState);
141 }
142 }
143
144 startOne = () => {
145 if (this.props.enabledOne) {
146 this.props.onValuesChangeStart();
147 this.setState({
148 onePressed: !this.state.onePressed
149 });
150 }
151 };
152
153 startTwo = () => {
154 if (this.props.enabledTwo) {
155 this.props.onValuesChangeStart();
156 this.setState({
157 twoPressed: !this.state.twoPressed
158 });
159 }
160 };
161
162 moveOne = gestureState => {
163 if (!this.props.enabledOne) {
164 return;
165 }
166
167 const accumDistance = this.props.vertical
168 ? -gestureState.dy
169 : gestureState.dx;
170 const accumDistanceDisplacement = this.props.vertical
171 ? gestureState.dx
172 : gestureState.dy;
173
174 const unconfined = I18nManager.isRTL
175 ? this.state.pastOne - accumDistance
176 : accumDistance + this.state.pastOne;
177 var bottom = 0;
178 var trueTop =
179 this.state.positionTwo -
180 (this.props.allowOverlap
181 ? 0
182 : this.props.minMarkerOverlapDistance > 0
183 ? this.props.minMarkerOverlapDistance
184 : this.stepLength);
185 var top = trueTop === 0 ? 0 : trueTop || this.props.sliderLength;
186 var confined =
187 unconfined < bottom ? bottom : unconfined > top ? top : unconfined;
188 var slipDisplacement = this.props.touchDimensions.slipDisplacement;
189
190 if (
191 Math.abs(accumDistanceDisplacement) < slipDisplacement ||
192 !slipDisplacement
193 ) {
194 var value = positionToValue(
195 confined,
196 this.optionsArray,
197 this.props.sliderLength
198 );
199 var snapped = valueToPosition(
200 value,
201 this.optionsArray,
202 this.props.sliderLength
203 );
204 this.setState({
205 positionOne: this.props.snapped ? snapped : confined
206 });
207
208 if (value !== this.state.valueOne) {
209 this.setState(
210 {
211 valueOne: value
212 },
213 () => {
214 var change = [this.state.valueOne];
215 if (this.state.valueTwo) {
216 change.push(this.state.valueTwo);
217 }
218 this.props.onValuesChange(change);
219 }
220 );
221 }
222 }
223 };
224
225 moveTwo = gestureState => {
226 if (!this.props.enabledTwo) {
227 return;
228 }
229
230 const accumDistance = this.props.vertical
231 ? -gestureState.dy
232 : gestureState.dx;
233 const accumDistanceDisplacement = this.props.vertical
234 ? gestureState.dx
235 : gestureState.dy;
236
237 const unconfined = I18nManager.isRTL
238 ? this.state.pastTwo - accumDistance
239 : accumDistance + this.state.pastTwo;
240 var bottom =
241 this.state.positionOne +
242 (this.props.allowOverlap
243 ? 0
244 : this.props.minMarkerOverlapDistance > 0
245 ? this.props.minMarkerOverlapDistance
246 : this.stepLength);
247 var top = this.props.sliderLength;
248 var confined =
249 unconfined < bottom ? bottom : unconfined > top ? top : unconfined;
250 var slipDisplacement = this.props.touchDimensions.slipDisplacement;
251
252 if (
253 Math.abs(accumDistanceDisplacement) < slipDisplacement ||
254 !slipDisplacement
255 ) {
256 var value = positionToValue(
257 confined,
258 this.optionsArray,
259 this.props.sliderLength
260 );
261 var snapped = valueToPosition(
262 value,
263 this.optionsArray,
264 this.props.sliderLength
265 );
266
267 this.setState({
268 positionTwo: this.props.snapped ? snapped : confined
269 });
270
271 if (value !== this.state.valueTwo) {
272 this.setState(
273 {
274 valueTwo: value
275 },
276 () => {
277 this.props.onValuesChange([
278 this.state.valueOne,
279 this.state.valueTwo
280 ]);
281 }
282 );
283 }
284 }
285 };
286
287 endOne = gestureState => {
288 if (gestureState.moveX === 0 && this.props.onToggleOne) {
289 this.props.onToggleOne();
290 return;
291 }
292
293 this.setState(
294 {
295 pastOne: this.state.positionOne,
296 onePressed: !this.state.onePressed
297 },
298 () => {
299 var change = [this.state.valueOne];
300 if (this.state.valueTwo) {
301 change.push(this.state.valueTwo);
302 }
303 this.props.onValuesChangeFinish(change);
304 }
305 );
306 };
307
308 endTwo = gestureState => {
309 if (gestureState.moveX === 0 && this.props.onToggleTwo) {
310 this.props.onToggleTwo();
311 return;
312 }
313
314 this.setState(
315 {
316 twoPressed: !this.state.twoPressed,
317 pastTwo: this.state.positionTwo
318 },
319 () => {
320 this.props.onValuesChangeFinish([
321 this.state.valueOne,
322 this.state.valueTwo
323 ]);
324 }
325 );
326 };
327
328 render() {
329 const { positionOne, positionTwo } = this.state;
330 const {
331 selectedStyle,
332 unselectedStyle,
333 sliderLength,
334 markerOffsetX,
335 markerOffsetY
336 } = this.props;
337 const twoMarkers = this.props.values.length == 2; // when allowOverlap, positionTwo could be 0, identified as string '0' and throwing 'RawText 0 needs to be wrapped in <Text>' error
338
339 const trackOneLength = positionOne;
340 const trackOneStyle = twoMarkers
341 ? unselectedStyle
342 : selectedStyle || styles.selectedTrack;
343 const trackThreeLength = twoMarkers ? sliderLength - positionTwo : 0;
344 const trackThreeStyle = unselectedStyle;
345 const trackTwoLength = sliderLength - trackOneLength - trackThreeLength;
346 const trackTwoStyle = twoMarkers
347 ? selectedStyle || styles.selectedTrack
348 : unselectedStyle;
349 const Marker = this.props.customMarker;
350
351 const MarkerLeft = this.props.customMarkerLeft;
352 const MarkerRight = this.props.customMarkerRight;
353 const isMarkersSeparated = this.props.isMarkersSeparated || false;
354
355 const {
356 slipDisplacement,
357 height,
358 width,
359 borderRadius
360 } = this.props.touchDimensions;
361 const touchStyle = {
362 borderRadius: borderRadius || 0
363 };
364
365 const markerContainerOne = {
366 top: markerOffsetY - 24,
367 left: trackOneLength + markerOffsetX - 24
368 };
369
370 const markerContainerTwo = {
371 top: markerOffsetY - 24,
372 right: trackThreeLength - markerOffsetX - 24
373 };
374
375 const containerStyle = [styles.container, this.props.containerStyle];
376
377 if (this.props.vertical) {
378 containerStyle.push({
379 transform: [{ rotate: "-90deg" }]
380 });
381 }
382
383 return (
384 <View style={containerStyle}>
385 <View style={[styles.fullTrack, { width: sliderLength }]}>
386 <View
387 style={[
388 styles.track,
389 this.props.trackStyle,
390 trackOneStyle,
391 { width: trackOneLength }
392 ]}
393 />
394 <View
395 style={[
396 styles.track,
397 this.props.trackStyle,
398 trackTwoStyle,
399 { width: trackTwoLength }
400 ]}
401 />
402 {twoMarkers && (
403 <View
404 style={[
405 styles.track,
406 this.props.trackStyle,
407 trackThreeStyle,
408 { width: trackThreeLength }
409 ]}
410 />
411 )}
412 <View
413 style={[
414 styles.markerContainer,
415 markerContainerOne,
416 this.props.markerContainerStyle,
417 positionOne > sliderLength / 2 && styles.topMarkerContainer
418 ]}
419 >
420 <View
421 style={[styles.touch, touchStyle]}
422 ref={component => (this._markerOne = component)}
423 {...this._panResponderOne.panHandlers}
424 >
425 {isMarkersSeparated === false ? (
426 <Marker
427 enabled={this.props.enabledOne}
428 pressed={this.state.onePressed}
429 markerStyle={[styles.marker, this.props.markerStyle]}
430 pressedMarkerStyle={this.props.pressedMarkerStyle}
431 currentValue={this.state.valueOne}
432 valuePrefix={this.props.valuePrefix}
433 valueSuffix={this.props.valueSuffix}
434 />
435 ) : (
436 <MarkerLeft
437 enabled={this.props.enabledOne}
438 pressed={this.state.onePressed}
439 markerStyle={[styles.marker, this.props.markerStyle]}
440 pressedMarkerStyle={this.props.pressedMarkerStyle}
441 currentValue={this.state.valueOne}
442 valuePrefix={this.props.valuePrefix}
443 valueSuffix={this.props.valueSuffix}
444 />
445 )}
446 </View>
447 </View>
448 {twoMarkers && positionOne !== this.props.sliderLength && (
449 <View
450 style={[
451 styles.markerContainer,
452 markerContainerTwo,
453 this.props.markerContainerStyle
454 ]}
455 >
456 <View
457 style={[styles.touch, touchStyle]}
458 ref={component => (this._markerTwo = component)}
459 {...this._panResponderTwo.panHandlers}
460 >
461 {isMarkersSeparated === false ? (
462 <Marker
463 pressed={this.state.twoPressed}
464 markerStyle={this.props.markerStyle}
465 pressedMarkerStyle={this.props.pressedMarkerStyle}
466 currentValue={this.state.valueTwo}
467 enabled={this.props.enabledTwo}
468 valuePrefix={this.props.valuePrefix}
469 valueSuffix={this.props.valueSuffix}
470 />
471 ) : (
472 <MarkerRight
473 pressed={this.state.twoPressed}
474 markerStyle={this.props.markerStyle}
475 pressedMarkerStyle={this.props.pressedMarkerStyle}
476 currentValue={this.state.valueTwo}
477 enabled={this.props.enabledTwo}
478 valuePrefix={this.props.valuePrefix}
479 valueSuffix={this.props.valueSuffix}
480 />
481 )}
482 </View>
483 </View>
484 )}
485 </View>
486 </View>
487 );
488 }
489}
490
491const styles = StyleSheet.create({
492 container: {
493 position: "relative",
494 height: 50,
495 justifyContent: "center"
496 },
497 fullTrack: {
498 flexDirection: "row"
499 },
500 track: {
501 ...Platform.select({
502 ios: {
503 height: 2,
504 borderRadius: 2,
505 backgroundColor: "#A7A7A7"
506 },
507 android: {
508 height: 2,
509 backgroundColor: "#CECECE"
510 },
511 web: {
512 height: 2,
513 borderRadius: 2,
514 backgroundColor: "#A7A7A7"
515 }
516 })
517 },
518 selectedTrack: {
519 ...Platform.select({
520 ios: {
521 backgroundColor: "#095FFF"
522 },
523 android: {
524 backgroundColor: "#0D8675"
525 },
526 web: {
527 backgroundColor: "#095FFF"
528 }
529 })
530 },
531 markerContainer: {
532 position: "absolute",
533 width: 48,
534 height: 48,
535 backgroundColor: "transparent",
536 justifyContent: "center",
537 alignItems: "center"
538 },
539 topMarkerContainer: {
540 zIndex: 1
541 },
542 touch: {
543 backgroundColor: "transparent",
544 justifyContent: "center",
545 alignItems: "center",
546 alignSelf: "stretch"
547 }
548});