| Jeremy Mordkoff | e29efc3 | 2016-09-07 18:59:17 -0400 | [diff] [blame] | 1 | |
| 2 | /* |
| 3 | * |
| 4 | * Copyright 2016 RIFT.IO Inc |
| 5 | * |
| 6 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | * you may not use this file except in compliance with the License. |
| 8 | * You may obtain a copy of the License at |
| 9 | * |
| 10 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | * |
| 12 | * Unless required by applicable law or agreed to in writing, software |
| 13 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | * See the License for the specific language governing permissions and |
| 16 | * limitations under the License. |
| 17 | * |
| 18 | */ |
| 19 | /** |
| 20 | * https://github.com/larsmaultsby/react-rangeslider |
| 21 | * |
| 22 | * |
| 23 | * Forked from: https://github.com/whoisandie/react-rangeslider |
| 24 | * |
| 25 | * |
| 26 | The MIT License (MIT) |
| 27 | |
| 28 | Copyright (c) 2015 Bhargav Anand |
| 29 | |
| 30 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 31 | of this software and associated documentation files (the "Software"), to deal |
| 32 | in the Software without restriction, including without limitation the rights |
| 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 34 | copies of the Software, and to permit persons to whom the Software is |
| 35 | furnished to do so, subject to the following conditions: |
| 36 | |
| 37 | The above copyright notice and this permission notice shall be included in all |
| 38 | copies or substantial portions of the Software. |
| 39 | |
| 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 46 | SOFTWARE. |
| 47 | * |
| 48 | */ |
| 49 | |
| 50 | import React, { PropTypes, Component, findDOMNode } from 'react'; |
| 51 | import joinClasses from 'react/lib/joinClasses'; |
| 52 | |
| 53 | function capitalize(str) { |
| 54 | return str.charAt(0).toUpperCase() + str.substr(1); |
| 55 | } |
| 56 | |
| 57 | function maxmin(pos, min, max) { |
| 58 | if (pos < min) { return min; } |
| 59 | if (pos > max) { return max; } |
| 60 | return pos; |
| 61 | } |
| 62 | |
| 63 | const constants = { |
| 64 | orientation: { |
| 65 | horizontal: { |
| 66 | dimension: 'width', |
| 67 | direction: 'left', |
| 68 | coordinate: 'x', |
| 69 | }, |
| 70 | vertical: { |
| 71 | dimension: 'height', |
| 72 | direction: 'top', |
| 73 | coordinate: 'y', |
| 74 | } |
| 75 | } |
| 76 | }; |
| 77 | |
| 78 | class Slider extends Component { |
| 79 | static propTypes = { |
| 80 | min: PropTypes.number, |
| 81 | max: PropTypes.number, |
| 82 | step: PropTypes.number, |
| 83 | value: PropTypes.number, |
| 84 | orientation: PropTypes.string, |
| 85 | onChange: PropTypes.func, |
| 86 | className: PropTypes.string, |
| 87 | } |
| 88 | |
| 89 | static defaultProps = { |
| 90 | min: 0, |
| 91 | max: 100, |
| 92 | step: 1, |
| 93 | value: 0, |
| 94 | orientation: 'horizontal', |
| 95 | } |
| 96 | |
| 97 | state = { |
| 98 | limit: 0, |
| 99 | grab: 0 |
| 100 | } |
| 101 | |
| 102 | // Add window resize event listener here |
| 103 | componentDidMount() { |
| 104 | this.calculateDimensions(); |
| 105 | window.addEventListener('resize', this.calculateDimensions); |
| 106 | } |
| 107 | |
| 108 | componentWillUnmount() { |
| 109 | window.removeEventListener('resize', this.calculateDimensions); |
| 110 | } |
| 111 | |
| 112 | handleSliderMouseDown = (e) => { |
| 113 | let value, { onChange } = this.props; |
| 114 | if (!onChange) return; |
| 115 | value = this.position(e); |
| 116 | onChange && onChange(value); |
| 117 | } |
| 118 | |
| 119 | handleKnobMouseDown = () => { |
| 120 | document.addEventListener('mousemove', this.handleDrag); |
| 121 | document.addEventListener('mouseup', this.handleDragEnd); |
| 122 | } |
| 123 | |
| 124 | handleDrag = (e) => { |
| 125 | let value, { onChange } = this.props; |
| 126 | if (!onChange) return; |
| 127 | value = this.position(e); |
| 128 | onChange && onChange(value); |
| 129 | } |
| 130 | |
| 131 | handleDragEnd = () => { |
| 132 | document.removeEventListener('mousemove', this.handleDrag); |
| 133 | document.removeEventListener('mouseup', this.handleDragEnd); |
| 134 | } |
| 135 | |
| 136 | handleNoop = (e) => { |
| 137 | e.stopPropagation(); |
| 138 | e.preventDefault(); |
| 139 | } |
| 140 | |
| 141 | calculateDimensions = () => { |
| 142 | let { orientation } = this.props; |
| 143 | let dimension = capitalize(constants.orientation[orientation].dimension); |
| 144 | const sliderPos = findDOMNode(this.refs.slider)['offset' + dimension]; |
| 145 | const handlePos = findDOMNode(this.refs.handle)['offset' + dimension] |
| 146 | this.setState({ |
| 147 | limit: sliderPos - handlePos, |
| 148 | grab: handlePos / 2, |
| 149 | }); |
| 150 | } |
| 151 | getPositionFromValue = (value) => { |
| 152 | let percentage, pos; |
| 153 | let { limit } = this.state; |
| 154 | let { min, max } = this.props; |
| 155 | percentage = (value - min) / (max - min); |
| 156 | pos = Math.round(percentage * limit); |
| 157 | |
| 158 | return pos; |
| 159 | } |
| 160 | |
| 161 | getValueFromPosition = (pos) => { |
| 162 | let percentage, value; |
| 163 | let { limit } = this.state; |
| 164 | let { orientation, min, max, step } = this.props; |
| 165 | percentage = (maxmin(pos, 0, limit) / (limit || 1)); |
| 166 | |
| 167 | if (orientation === 'horizontal') { |
| 168 | value = step * Math.round(percentage * (max - min) / step) + min; |
| 169 | } else { |
| 170 | value = max - (step * Math.round(percentage * (max - min) / step) + min); |
| 171 | } |
| 172 | |
| 173 | return value; |
| 174 | } |
| 175 | |
| 176 | position = (e) => { |
| 177 | let pos, value, { grab } = this.state; |
| 178 | let { orientation } = this.props; |
| 179 | const node = findDOMNode(this.refs.slider); |
| 180 | const coordinateStyle = constants.orientation[orientation].coordinate; |
| 181 | const directionStyle = constants.orientation[orientation].direction; |
| 182 | const coordinate = e['client' + capitalize(coordinateStyle)]; |
| 183 | const direction = node.getBoundingClientRect()[directionStyle]; |
| 184 | |
| 185 | pos = coordinate - direction - grab; |
| 186 | value = this.getValueFromPosition(pos); |
| 187 | |
| 188 | return value; |
| 189 | } |
| 190 | |
| 191 | coordinates = (pos) => { |
| 192 | let value, fillPos, handlePos; |
| 193 | let { limit, grab } = this.state; |
| 194 | let { orientation } = this.props; |
| 195 | value = this.getValueFromPosition(pos); |
| 196 | handlePos = this.getPositionFromValue(value); |
| 197 | |
| 198 | if (orientation === 'horizontal') { |
| 199 | fillPos = handlePos + grab; |
| 200 | } else { |
| 201 | fillPos = limit - handlePos + grab; |
| 202 | } |
| 203 | |
| 204 | return { |
| 205 | fill: fillPos, |
| 206 | handle: handlePos, |
| 207 | }; |
| 208 | } |
| 209 | |
| 210 | render() { |
| 211 | let dimension, direction, position, coords, fillStyle, handleStyle, displayValue; |
| 212 | let { value, orientation, className } = this.props; |
| 213 | |
| 214 | dimension = constants.orientation[orientation].dimension; |
| 215 | direction = constants.orientation[orientation].direction; |
| 216 | |
| 217 | position = this.getPositionFromValue(value); |
| 218 | coords = this.coordinates(position); |
| 219 | |
| 220 | fillStyle = {[dimension]: `${coords.fill}px`}; |
| 221 | handleStyle = {[direction]: `${coords.handle}px`}; |
| 222 | |
| 223 | if(this.props.displayValue) { |
| 224 | displayValue = <div>{this.props.value}</div>; |
| 225 | } |
| 226 | |
| 227 | return ( |
| 228 | <div |
| 229 | ref="slider" |
| 230 | className={joinClasses('rangeslider ', 'rangeslider-' + orientation, className)} |
| 231 | onMouseDown={this.handleSliderMouseDown} |
| 232 | onClick={this.handleNoop} |
| 233 | style={{display:'flex'}}> |
| 234 | <div |
| 235 | ref="fill" |
| 236 | className="rangeslider__fill" |
| 237 | style={fillStyle} /> |
| 238 | <div |
| 239 | ref="handle" |
| 240 | className="rangeslider__handle" |
| 241 | onMouseDown={this.handleKnobMouseDown} |
| 242 | onClick={this.handleNoop} |
| 243 | style={handleStyle}> |
| 244 | {displayValue} |
| 245 | </div> |
| 246 | </div> |
| 247 | ); |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | export default Slider; |