Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import React, { Component, PropTypes } from 'react';
- import {generateRandomId} from '../helpers/generators';
- export default class CircleSlider extends Component {
- constructor(props){
- super(props);
- this.state={
- width: props.width,
- fill: props.fill,
- dragging: false,
- min: props.min && this._convertToDegrees(props.min),
- max: props.max && this._convertToDegrees(props.max)
- };
- // generate unique ids for the elements in case there's more than one instace of component in dom
- this.ringId = generateRandomId('r');
- this.scrubberId = generateRandomId('s');
- this.degrees = this._getDerivedDegrees();
- this._dragEnd = this._dragEnd.bind(this);
- }
- componentWillReceiveProps(nextProps){
- this.setState({
- width: nextProps.width,
- fill: nextProps.fill
- });
- // if there's a new value for degrees from the parent, process it
- this.degrees = nextProps.degreees && nextProps.degrees !== this.degrees
- ? this._getDerivedDegrees()
- : this.degrees;
- }
- componentDidMount(){
- const scrubber = document.getElementById(this.scrubberId);
- window.addEventListener('mouseup', this._dragEnd, true);
- if (this.degrees !== 0){
- // if starting value is specifed, rotate the scrubber by those degrees
- scrubber.style.transform = "rotate(" + this.degrees + "deg)";
- }
- }
- componentWillUnmount(){
- window.removeEventListener('mouseup', this._dragEnd);
- }
- _convertToDegrees(val){
- // convert the min or max percentage to degrees of a circle
- const calc_degrees = Math.round(val * 360);
- return calc_degrees
- }
- // if we're dragging, initiate moveScrubber
- _dragCalc(e){
- const {dragging} = this.state;
- if (dragging) {
- this._moveScrubber(e);
- }
- }
- _dragEnd(e){
- e.preventDefault();
- this.setState({
- dragging: false
- })
- }
- _dragStart(e){
- e.preventDefault();
- this.setState({
- dragging: true
- })
- }
- // if the user has passed a % value, use it. Otherwise use px
- _getDimension(attr){
- !attr.includes('%')
- ? attr +='px'
- : null
- return attr;
- }
- _getFill(){
- // if the fill is a string that we support in the CSS, use it. Otherwise, use pink
- const {fill} = this.state,
- colors = ['green', 'purple', 'blue', 'pink', 'red'];
- if (colors.includes(fill)){
- return fill;
- } else {
- console.error('CircleSlider color not found. substituting pink');
- return 'pink';
- }
- }
- // when we initialize or get new vals from the parent
- _getDerivedDegrees(){
- let derived_degrees;
- const degrees = this.props.degrees;
- const {min, max} = this.state;
- // if we have a value
- if (degrees){
- // check to see if the val is in range of the optional min/max
- const max_satisfied = !max || degrees <= max;
- const min_satisfied = !min || degrees >= min;
- // if it's in range, then cool
- if (max_satisfied && min_satisfied){
- derived_degrees = degrees;
- } else {
- // if min is fine, it means that degrees exceeds max
- // if max is fine, it means that the value is too low
- // so set it to min (if defined) or 0 if not
- // in practice, the parent should be watching this and we shouldn't get here anyway
- derived_degrees = min || 0;
- }
- } else {
- // no degrees specified, but we know degrees has minimum val
- if (min){
- derived_degrees = min;
- // nothing stated, just do 0
- } else {
- derived_degrees = 0
- }
- }
- return derived_degrees;
- }
- _moveScrubber(e){
- e.preventDefault();
- // main circle
- const {min, max} = this.state;
- const scrubber = document.getElementById(this.scrubberId),
- // the div based at the midpoint that has the visible scrubber at the end
- ring = document.getElementById(this.ringId),
- // instantiate these vars so they're not defined in the conditional
- // get the actual position of ring, including any scroll but minus css additions (borders etc)
- offSetLeft = ring.getBoundingClientRect().left + document.documentElement.scrollLeft - document.documentElement.clientLeft,
- offSetTop = ring.getBoundingClientRect().top + document.documentElement.scrollTop - document.documentElement.clientTop,
- // find the x and y of the circle center. Essentially the width/height/2 (circle radius) plus the offset we calculated
- center = [(ring.offsetWidth / 2) + offSetLeft, (ring.offsetHeight / 2) + offSetTop],
- // current cursor positions
- x_pos = e.pageX,
- y_pos = e.pageY,
- // the difference between the circle midpoint x or y and current cursor x or y
- x_delta = center[0] - x_pos,
- y_delta = center[1] - y_pos,
- // the arctangent of the deltas to a plane
- radians = Math.atan2(y_delta, x_delta);
- // formula for finding degrees based on radians is "degrees = (radians * pi) / 180". I had to look this up.
- // we're going to do some additional adjustments, so this can't be grouped wit hthe consts above.
- let degrees = radians * (180 / Math.PI),
- // at the end of this, we're going to calculate what percentage of the circle was selected.
- returned_percentage;
- // we did the arctangnet calculation from a horizontal plane. Subtract 90 degrees to make it vertical
- // because our scrubber starts out vertical
- degrees -= 90;
- // if that puts us in the negative, we're in the back half of the circle. We only want to count in one direction
- if (degrees < 0) {
- degrees = 360 + degrees;
- }
- //make it a nice whole number
- degrees = Math.round(degrees);
- if (max && degrees > max) {
- degrees = max
- }
- if (min && degrees < min) {
- degrees = min
- }
- // rotate the scrubber by those degrees
- scrubber.style.transform = "rotate(" + degrees + "deg)";
- // update the top-level scope value of degrees
- this.degrees = degrees;
- this._reportToParent();
- }
- _reportToParent(){
- // returns a percentage expressed as a decimal.
- const percentage = this.degrees/360;
- this.props.defaultAction(percentage);
- }
- render(){
- const height = this._getDimension(this.state.width),
- width = this._getDimension(this.state.width),
- fill = this._getFill();
- const {ringId, scrubberId} = this;
- return (
- <div className="position-relative" style={{height, width}}>
- <div className="middle">
- {this.props.children
- ? this.props.children
- : null
- }
- </div>
- <div className="ring"
- id={ringId}
- onMouseDown={this._dragStart.bind(this)}
- onMouseMove={this._dragCalc.bind(this)}
- onClick={this._moveScrubber.bind(this)}
- >
- {this._renderSvg()}
- <div className={`scrubber ${fill}`} id={scrubberId}>
- </div>
- </div>
- </div>
- )
- }
- _getCoordinatesForDegrees(percent){
- // sine and cosine are ratios of triangle sides to triangle hypotenuse.
- // get x and y. 2pi equals 1:1, we need the sin/cos to be a percent of that to get the radians.
- const x = Math.cos(2 * Math.PI * percent),
- y = Math.sin(2 * Math.PI * percent);
- return [x, y];
- }
- _renderSvg(){
- // if the fill prop was passed, get a value;
- const fill = this.state.fill ? this._getFill() : '',
- // the degrees we established in _dragCalc
- degrees = this.degrees,
- // what percentage around the circle are we?
- percent = degrees/360,
- // get sin and cosin to draw the arc
- [end_x, end_y] = this._getCoordinatesForDegrees(percent),
- // if the slice is less than 50%, use the shorter arc. If it's more, go the longer way.
- arc_direction = percent > .5 ? 1 : 0,
- // create an array and join for transparency
- path_coords = [
- `M 1 0`, // 'move' coords. SVG always starts in same place, so static
- `A 1 1 0 ${arc_direction} 1 ${end_x} ${end_y}`, // arc coords, arc degrees static, arc direction and the endpoints dynamic
- `L 0 0`, //line coords. Always terminates in circle midpoint
- ].join(' ');
- // create an svg that sits with the ring, create one path that is an arc based on the path_coords
- // same "off by 90" issue as the scrubber-- calculations are for horizontal plane, we rotate 90
- return (
- <svg viewBox="-1 -1 2 2" style={{"transform": "rotate(-90deg)"}}>
- <path d={path_coords} className={fill}></path>
- </svg>
- )
- }
- }
- CircleSlider.propTypes = {
- degrees : PropTypes.number,
- min : PropTypes.number,
- max : PropTypes.number,
- width : PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number
- ]),
- fill : PropTypes.string
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement