nw-react-slider--bki
Version:
Slider Component
293 lines (256 loc) • 8.78 kB
JSX
var React = require('react')
var ReactDOM = require('react-dom')
var isFunction = require('lodash/isFunction')
var Draggable = require('react-draggable')
var isUndefined = require('lodash/isUndefined')
var throttle = require('lodash/throttle')
module.exports = React.createClass({
displayName: 'core-slider',
propTypes: {
value: React.PropTypes.number,
min: React.PropTypes.number,
max: React.PropTypes.number,
ticks: React.PropTypes.bool,
tickShape: React.PropTypes.string,
onChange: React.PropTypes.func,
onDragStart: React.PropTypes.func,
onDragEnd: React.PropTypes.func,
markerLabel: React.PropTypes.array,
markerLabelPosition: React.PropTypes.string,
fill: React.PropTypes.bool,
},
getDefaultProps: function () {
return {
value: 0,
min: 0,
max: 10,
ticks: false
}
},
getInitialState: function () {
return {
position: undefined,
value: this.props.value,
dragging: false
}
},
componentWillReceiveProps: function (nextProps, nextState) {
var newValue
// keep state up to date with passed in props
if (this.state.value !== nextProps.value) {
newValue = this.getBoundValue(nextProps, nextProps.value)
this.setState({value: newValue})
this.setHandlePosition(nextProps, newValue)
}
// if min or max changes, have to reposition the handle
if (this.props.min !== nextProps.min || this.props.max !== nextProps.max) {
newValue = this.getBoundValue(nextProps, newValue || this.state.value)
this.setState({value: newValue})
this.setHandlePosition(nextProps, newValue)
}
},
componentWillUpdate: function (nextProps, nextState) {
if (isUndefined(this.state.position) && !isUndefined(nextState.position)) {
this.props.onChange(nextState.value, nextState.position)
}
},
shouldComponentUpdate: function (nextProps, nextState) {
// Don't alter the component while dragging is occurring
return (!nextState.dragging)
},
componentDidMount: function () {
this.updateTrackWidth()
this.updateTrackWidth = throttle(this.updateTrackWidth, 100, {leading: false})
window.addEventListener('resize', this.updateTrackWidth)
},
componentWillUnmount: function () {
window.removeEventListener('resize', this.updateTrackWidth)
},
getBoundValue: function (props, value) {
var newValue = value
if (newValue < props.min) {
newValue = props.min
} else if (newValue > props.max) {
newValue = props.max
}
return newValue
},
updateTrackWidth: function () {
var track = ReactDOM.findDOMNode(this.refs.track)
if (!track) {
return
}
var trackWidth = track.offsetWidth
this.setState({trackWidth}, this.setHandlePosition)
},
componentDidUpdate: function () {
// after a render, ensure that draggable is in correct position
this.refs.drag && this.refs.drag.setState({clientX: this.state.position})
},
setHandlePosition: function (props = this.props, value = this.state.value) {
var position = this.state.trackWidth / (props.max - props.min) * (value - props.min)
this.setState({position})
if (isFunction(this.props.onChange)) {
this.props.onChange(value, position)
}
},
updateValueFromPosition: function (newPosition) {
var currentPosition = newPosition
var value, position
if (this.props.max === this.props.min) {
value = this.props.min
position = this.state.trackWidth / 2
} else {
// find the two closest ticks to the current position
var currentPercent = currentPosition / this.state.trackWidth * 100
var percentStep = 100 / (this.props.max - this.props.min)
var closestSmallerValue = Math.floor(currentPercent / percentStep)
var closestLargerValue = closestSmallerValue + 1
var bestMatchPercent, bestMatchTick
// determine which of the two values is closest
if (currentPercent - (closestSmallerValue * percentStep) <= (closestLargerValue * percentStep) - currentPercent) {
bestMatchTick = closestSmallerValue
bestMatchPercent = bestMatchTick * percentStep
} else {
bestMatchTick = closestLargerValue
bestMatchPercent = bestMatchTick * percentStep
}
// update the value and position
value = this.props.min + bestMatchTick
position = this.state.trackWidth * (bestMatchPercent / 100)
}
// fire change event if callback exists
if (isFunction(this.props.onChange)) {
var rtposition = position
if (this.state.dragging) {
rtposition = currentPosition
}
this.props.onChange(value, rtposition)
}
// Although set state is async, pushing its invocation as late as possible
this.setState({value, position})
return position
},
cumulativeOffset: function (element) {
// determine the overall offset of the element by crawling up the DOM, borrowed from Prototype.js
var top = 0
var left = 0
do {
top += element.offsetTop || 0
left += element.offsetLeft || 0
element = element.offsetParent
} while (element)
return {
top: top,
left: left
}
},
clickOnTrack: function (event) {
var clickFromLeft = event.clientX - this.cumulativeOffset(event.target).left
this.updateValueFromPosition(clickFromLeft)
},
handleUp: function (event, ui) {
const position = this.state.position
// Do we have a drag end hook ?
if (isFunction(this.props.onDragEnd)) {
this.props.onDragEnd(position)
}
this.setState({dragging: false})
this.updateValueFromPosition(position)
},
handleDown: function (event, ui) {
// Do we have a drag start hook ?
if (isFunction(this.props.onDragStart)) {
this.props.onDragStart(this.state.position)
}
this.setState({dragging: true})
},
dragging: function (event, ui) {
var pos = this.refs.drag.state.clientX || 0
this.updateValueFromPosition(pos)
event.preventDefault()
},
renderTicks: function () {
if (!this.props.ticks) return (<span/>)
var elements = []
var min = this.props.min
var max = this.props.max
var percentStep = 100 / (max - min)
// Don't render ticks if it is too high. Will crash the browser and the ticks become useless
if ((max - min) < 200) {
for (var i = min + 1; i < max; i++) {
var style = {
left: (percentStep * (i - min)) + '%'
}
elements.push(<span key={ 'tick' + i } className='slider__tick' style={ style } />)
}
}
return (
<div key='ticks' className={ 'slider__ticks slider__ticks--' + this.props.tickShape } onClick={ this.clickOnTrack }>{ elements }</div>
)
},
renderMarkers: function () {
if (!this.props.markerLabel) return (<span/>)
var elements = []
var { min, max, markerLabel: markers } = this.props
var percentStep = 100 / (max - min)
for (var i in markers) {
var style = {
left: (percentStep * (markers[i].value - min)) + '%'
}
if (markers[i].value <= max && markers[i].value >= min && max - min < 200) {
// don't render a tick for this marker if ticks are already being rendered
elements.push(
<div key={ 'marker' + i } className='slider__marker marker' style={ style }>
<p className='marker__label'>{ markers[i].label }</p>
</div>
);
}
}
return (
<div key='markers' className={ 'slider__markers slider__markers--' + this.props.markerLabelPosition } onClick={ this.clickOnTrack }>{ elements }</div>
)
},
renderFill: function() {
if (!this.props.fill) {
return
}
return (
<div
className='slider__fill'
style={{
right: (this.state.trackWidth - this.state.position) + 'px',
}}>
</div>
)
},
render: function () {
var draggableProps, draggable
if (!isUndefined(this.state.position)) {
draggableProps = {
axis: 'x',
handle: '.slider__handle',
bounds: {left: 0, right: this.state.trackWidth},
start: {x: this.state.position, y: 0},
onStop: this.handleUp,
onStart: this.handleDown,
onDrag: this.dragging
}
draggable = (
<Draggable ref='drag' key='draggable' { ...draggableProps }>
<span ref='handle' className='slider__handle'/>
</Draggable>
)
}
return (
<div ref='slider' className='slider'>
{ draggable }
<div ref='track' className='slider__track' onClick={ this.clickOnTrack }>
{ this.renderTicks() }
{ this.renderMarkers() }
{ this.renderFill() }
</div>
</div>
)
}
})