Advertisement
Guest User

Untitled

a guest
Oct 15th, 2019
93
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.79 KB | None | 0 0
  1. import React, { Component, PropTypes } from 'react';
  2. import {generateRandomId} from '../helpers/generators';
  3.  
  4. export default class CircleSlider extends Component {
  5.  
  6. constructor(props){
  7. super(props);
  8. this.state={
  9. width: props.width,
  10. fill: props.fill,
  11. dragging: false,
  12. min: props.min && this._convertToDegrees(props.min),
  13. max: props.max && this._convertToDegrees(props.max)
  14. };
  15. // generate unique ids for the elements in case there's more than one instace of component in dom
  16. this.ringId = generateRandomId('r');
  17. this.scrubberId = generateRandomId('s');
  18. this.degrees = this._getDerivedDegrees();
  19. this._dragEnd = this._dragEnd.bind(this);
  20. }
  21.  
  22. componentWillReceiveProps(nextProps){
  23. this.setState({
  24. width: nextProps.width,
  25. fill: nextProps.fill
  26. });
  27. // if there's a new value for degrees from the parent, process it
  28. this.degrees = nextProps.degreees && nextProps.degrees !== this.degrees
  29. ? this._getDerivedDegrees()
  30. : this.degrees;
  31. }
  32.  
  33. componentDidMount(){
  34. const scrubber = document.getElementById(this.scrubberId);
  35. window.addEventListener('mouseup', this._dragEnd, true);
  36. if (this.degrees !== 0){
  37. // if starting value is specifed, rotate the scrubber by those degrees
  38. scrubber.style.transform = "rotate(" + this.degrees + "deg)";
  39. }
  40. }
  41.  
  42. componentWillUnmount(){
  43. window.removeEventListener('mouseup', this._dragEnd);
  44. }
  45.  
  46. _convertToDegrees(val){
  47. // convert the min or max percentage to degrees of a circle
  48. const calc_degrees = Math.round(val * 360);
  49. return calc_degrees
  50. }
  51.  
  52. // if we're dragging, initiate moveScrubber
  53. _dragCalc(e){
  54. const {dragging} = this.state;
  55. if (dragging) {
  56. this._moveScrubber(e);
  57. }
  58. }
  59.  
  60. _dragEnd(e){
  61. e.preventDefault();
  62. this.setState({
  63. dragging: false
  64. })
  65. }
  66.  
  67. _dragStart(e){
  68. e.preventDefault();
  69. this.setState({
  70. dragging: true
  71. })
  72. }
  73.  
  74. // if the user has passed a % value, use it. Otherwise use px
  75. _getDimension(attr){
  76. !attr.includes('%')
  77. ? attr +='px'
  78. : null
  79. return attr;
  80. }
  81.  
  82. _getFill(){
  83. // if the fill is a string that we support in the CSS, use it. Otherwise, use pink
  84. const {fill} = this.state,
  85. colors = ['green', 'purple', 'blue', 'pink', 'red'];
  86. if (colors.includes(fill)){
  87. return fill;
  88. } else {
  89. console.error('CircleSlider color not found. substituting pink');
  90. return 'pink';
  91. }
  92. }
  93.  
  94. // when we initialize or get new vals from the parent
  95. _getDerivedDegrees(){
  96. let derived_degrees;
  97. const degrees = this.props.degrees;
  98. const {min, max} = this.state;
  99. // if we have a value
  100. if (degrees){
  101. // check to see if the val is in range of the optional min/max
  102. const max_satisfied = !max || degrees <= max;
  103. const min_satisfied = !min || degrees >= min;
  104.  
  105. // if it's in range, then cool
  106. if (max_satisfied && min_satisfied){
  107. derived_degrees = degrees;
  108. } else {
  109. // if min is fine, it means that degrees exceeds max
  110. // if max is fine, it means that the value is too low
  111. // so set it to min (if defined) or 0 if not
  112. // in practice, the parent should be watching this and we shouldn't get here anyway
  113. derived_degrees = min || 0;
  114. }
  115. } else {
  116. // no degrees specified, but we know degrees has minimum val
  117. if (min){
  118. derived_degrees = min;
  119. // nothing stated, just do 0
  120. } else {
  121. derived_degrees = 0
  122. }
  123. }
  124. return derived_degrees;
  125. }
  126.  
  127. _moveScrubber(e){
  128. e.preventDefault();
  129. // main circle
  130. const {min, max} = this.state;
  131. const scrubber = document.getElementById(this.scrubberId),
  132. // the div based at the midpoint that has the visible scrubber at the end
  133. ring = document.getElementById(this.ringId),
  134. // instantiate these vars so they're not defined in the conditional
  135. // get the actual position of ring, including any scroll but minus css additions (borders etc)
  136. offSetLeft = ring.getBoundingClientRect().left + document.documentElement.scrollLeft - document.documentElement.clientLeft,
  137. offSetTop = ring.getBoundingClientRect().top + document.documentElement.scrollTop - document.documentElement.clientTop,
  138. // find the x and y of the circle center. Essentially the width/height/2 (circle radius) plus the offset we calculated
  139. center = [(ring.offsetWidth / 2) + offSetLeft, (ring.offsetHeight / 2) + offSetTop],
  140. // current cursor positions
  141. x_pos = e.pageX,
  142. y_pos = e.pageY,
  143. // the difference between the circle midpoint x or y and current cursor x or y
  144. x_delta = center[0] - x_pos,
  145. y_delta = center[1] - y_pos,
  146. // the arctangent of the deltas to a plane
  147. radians = Math.atan2(y_delta, x_delta);
  148. // formula for finding degrees based on radians is "degrees = (radians * pi) / 180". I had to look this up.
  149. // we're going to do some additional adjustments, so this can't be grouped wit hthe consts above.
  150. let degrees = radians * (180 / Math.PI),
  151. // at the end of this, we're going to calculate what percentage of the circle was selected.
  152. returned_percentage;
  153. // we did the arctangnet calculation from a horizontal plane. Subtract 90 degrees to make it vertical
  154. // because our scrubber starts out vertical
  155. degrees -= 90;
  156. // if that puts us in the negative, we're in the back half of the circle. We only want to count in one direction
  157. if (degrees < 0) {
  158. degrees = 360 + degrees;
  159. }
  160. //make it a nice whole number
  161. degrees = Math.round(degrees);
  162.  
  163. if (max && degrees > max) {
  164. degrees = max
  165. }
  166.  
  167. if (min && degrees < min) {
  168. degrees = min
  169. }
  170. // rotate the scrubber by those degrees
  171. scrubber.style.transform = "rotate(" + degrees + "deg)";
  172. // update the top-level scope value of degrees
  173. this.degrees = degrees;
  174. this._reportToParent();
  175. }
  176.  
  177. _reportToParent(){
  178. // returns a percentage expressed as a decimal.
  179. const percentage = this.degrees/360;
  180. this.props.defaultAction(percentage);
  181. }
  182.  
  183. render(){
  184. const height = this._getDimension(this.state.width),
  185. width = this._getDimension(this.state.width),
  186. fill = this._getFill();
  187. const {ringId, scrubberId} = this;
  188. return (
  189. <div className="position-relative" style={{height, width}}>
  190. <div className="middle">
  191. {this.props.children
  192. ? this.props.children
  193. : null
  194. }
  195. </div>
  196. <div className="ring"
  197. id={ringId}
  198. onMouseDown={this._dragStart.bind(this)}
  199. onMouseMove={this._dragCalc.bind(this)}
  200. onClick={this._moveScrubber.bind(this)}
  201. >
  202. {this._renderSvg()}
  203. <div className={`scrubber ${fill}`} id={scrubberId}>
  204. </div>
  205. </div>
  206. </div>
  207. )
  208. }
  209.  
  210. _getCoordinatesForDegrees(percent){
  211. // sine and cosine are ratios of triangle sides to triangle hypotenuse.
  212. // get x and y. 2pi equals 1:1, we need the sin/cos to be a percent of that to get the radians.
  213. const x = Math.cos(2 * Math.PI * percent),
  214. y = Math.sin(2 * Math.PI * percent);
  215. return [x, y];
  216. }
  217.  
  218. _renderSvg(){
  219. // if the fill prop was passed, get a value;
  220. const fill = this.state.fill ? this._getFill() : '',
  221. // the degrees we established in _dragCalc
  222. degrees = this.degrees,
  223. // what percentage around the circle are we?
  224. percent = degrees/360,
  225. // get sin and cosin to draw the arc
  226. [end_x, end_y] = this._getCoordinatesForDegrees(percent),
  227. // if the slice is less than 50%, use the shorter arc. If it's more, go the longer way.
  228. arc_direction = percent > .5 ? 1 : 0,
  229. // create an array and join for transparency
  230. path_coords = [
  231. `M 1 0`, // 'move' coords. SVG always starts in same place, so static
  232. `A 1 1 0 ${arc_direction} 1 ${end_x} ${end_y}`, // arc coords, arc degrees static, arc direction and the endpoints dynamic
  233. `L 0 0`, //line coords. Always terminates in circle midpoint
  234. ].join(' ');
  235. // create an svg that sits with the ring, create one path that is an arc based on the path_coords
  236. // same "off by 90" issue as the scrubber-- calculations are for horizontal plane, we rotate 90
  237. return (
  238. <svg viewBox="-1 -1 2 2" style={{"transform": "rotate(-90deg)"}}>
  239. <path d={path_coords} className={fill}></path>
  240. </svg>
  241. )
  242. }
  243. }
  244.  
  245. CircleSlider.propTypes = {
  246. degrees : PropTypes.number,
  247. min : PropTypes.number,
  248. max : PropTypes.number,
  249. width : PropTypes.oneOfType([
  250. PropTypes.string,
  251. PropTypes.number
  252. ]),
  253. fill : PropTypes.string
  254. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement