1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. import Component from '../utils/Component'
  4. import classnames from 'classnames'
  5. import Logger from '../utils/logger'
  6.  
  7. import Drag from '../drag/'
  8.  
  9. import '../style'
  10. import 'phoenix-styles/less/modules/slider.less'
  11.  
  12. /**
  13. * 滑动输入条组件<br/>
  14. * - 滑动进度条确定当前进度的百分比。
  15. * - 可通过设置process确定初始进度百分比, 范围从0-100。
  16. * - 可通过tipMode选择当前查看进度的方式,可选default和tooltip。
  17. * - 可通过placement设置当前进度提示框的位置, 可选top/bottoom(tipMode为tooltip时生效)。
  18. * - 可通过tipStay设置初始和松开按钮时提示是否消失,默认false不显示(tipMode为tooltip时生效)。
  19. * - 可通过range制定范围,默认0-100,必需是长度为2的数组,第一个数字表示初始,第二个数字表示终点。
  20. * - 可通过showRange判断是否在进度条前后显示范围,默认不显示。
  21. * - 可通过duration设置固定移动的距离,默认1。
  22. * - 可通过slideCallback设置拖拽进度条松开时的回调函数。
  23. * - 可通过disabled设置进度条只读。
  24. * - 使用Slider前确保父级是有宽度的元素;使用flex需要加一层宽度100%的外壳。
  25. *
  26. * 主要属性和接口:
  27. * - process:初始进度百分比, 默认0 <br/>
  28. * 如: `<Slider progress={10}/>`
  29. * - placement:进度提示框的位置, 默认top <br/>
  30. * 如: `<Slider placement='bottom' />`
  31. * - tipStay:初始和松开按钮时提示是否消失,默认false <br/>
  32. * 如: `<Slider tipStay />`
  33. * - range:范围,默认[0,100]。 <br/>
  34. * 如: `<Slider range={[20,50]} />`
  35. * - showRange:是否在进度条前后显示范围,默认不显示。 <br/>
  36. * 如: `<Slider showRange />`
  37. * - duration:固定移动的距离,默认1。 <br/>
  38. * 如: `<Slider duration={20} />`
  39. * - slideCallback:拖拽进度条松开时的回调函数 <br/>
  40. * 如: `<Slider slideCallback={(progress)=>{console.log(progress);} />`
  41. * - disabled:进度条只读, 不可操作 <br/>
  42. * 如: `<Slider disabled/>`
  43. *
  44. * @class Slider
  45. * @module 操作类组件
  46. * @extends Component
  47. * @constructor
  48. * @since 1.0.0
  49. * @demo slider|slider.js {展示}
  50. * @show true
  51. * */
  52.  
  53. export default class Slider extends Component{
  54. static propTypes = {
  55. /**
  56. * 样式前缀
  57. * @property classPrefix
  58. * @type String
  59. * @default 'slider'
  60. * */
  61. classPrefix: PropTypes.string,
  62. /**
  63. * 标签tagName
  64. * @property componentTag
  65. * @type String
  66. * */
  67. componentTag:PropTypes.string,
  68. /**
  69. * 初始进程,默认0
  70. * @property progress
  71. * @type String
  72. * */
  73. progress:PropTypes.number,
  74. /**
  75. * 进程提示的位置,默认top
  76. * @property placement
  77. * @type String
  78. * @default 'top'
  79. * */
  80. placement: PropTypes.string,
  81. /**
  82. * 范围,默认0-100,可传固定范围的数组如:[25,50]
  83. * @property range
  84. * @type Array
  85. * @default [0,100]
  86. * */
  87. range: PropTypes.array,
  88. /**
  89. * 是否在进度条前后显示范围
  90. * @property showRange
  91. * @type Boolean
  92. * @default false
  93. * */
  94. showRange: PropTypes.bool,
  95. /**
  96. * 显示提示的模式,可选[default,tooltip]
  97. * @property tipMode
  98. * @type String
  99. * @default 'default'
  100. * */
  101. tipMode: PropTypes.string,
  102. /**
  103. * 每次移动的固定距离,默认1
  104. * @property duration
  105. * @type Number
  106. * @default 1
  107. * */
  108. duration: PropTypes.number,
  109. /**
  110. * 初始及松开按钮时是否显示tooltip
  111. * @property tipStay
  112. * @type Boolean
  113. * @default false
  114. * */
  115. tipStay: PropTypes.bool,
  116. /**
  117. * 改变进程时的回调函数
  118. * @method slideCallback
  119. * @param {number} progress 进度
  120. * @type Function
  121. * */
  122. slideCallback: PropTypes.func
  123. };
  124.  
  125. static defaultProps = {
  126. placement: 'top',
  127. progress: 0,
  128. range: [0,100],
  129. showRange: false,
  130. duration: 1,
  131. tipMode: 'default',
  132. tipStay: false,
  133. classPrefix:'slider',
  134. componentTag:'div',
  135. classMapping : {
  136. 'disabled': 'disabled',
  137. 'top': 'tip-top',
  138. 'bottom': 'tip-bottom'
  139. }
  140. };
  141.  
  142. constructor(props, context) {
  143. super(props, context)
  144. new Logger('Slider')
  145. this.init(props);
  146. this.state = {
  147. realProgress: props.progress || this.range[0],
  148. tipVisible: props.tipStay || false
  149. }
  150. }
  151.  
  152. init(props){
  153. this.range = this.validateRange(props);
  154. this.rangeDiff = this.range[1]-this.range[0];
  155.  
  156. this.duration = this.validateDuration(props);
  157. this.eachDur = (this.range[1]-this.range[0])/this.duration;
  158. }
  159.  
  160. validateRange(props){
  161. let {range, progress} = props,
  162. defaultRange = [0,100];
  163. if(!range instanceof Array) return defaultRange;
  164. if(range.length != 2){
  165. console.error('Invalid prop `range` of length not equal to 2.');
  166. return defaultRange;
  167. }
  168. if(range[0] >= range[1]){
  169. console.error('Invalid prop `range[0]` must be less than or equal to `range[1]`.');
  170. return defaultRange;
  171. }
  172. if(progress)
  173. if(progress<range[0] || progress>range[1]){
  174. console.error('`Progress prop` have to between `range[0]` and `range[1]`.');
  175. return range;
  176. }
  177. return range;
  178. }
  179.  
  180. validateDuration(props){
  181. let {duration} = props,
  182. defaultDuration = 1;
  183. if(duration<=0) {
  184. console.error('Invalid prop `duration` have to be Positive.');
  185. return defaultDuration;
  186. }
  187. if((this.range[1] - this.range[0])%duration != 0){ // 不能整除的情况
  188. console.error('Prop `duration` can not be divided by `range`.');
  189. return defaultDuration;
  190. }
  191. return duration;
  192. }
  193.  
  194. componentDidMount(){
  195. this.sliderLength = parseInt(this.sliderLine.offsetWidth);
  196. this.eachSection = this.sliderLength/this.rangeDiff*this.duration;
  197. // if(this.eachSection<1) this.eachSection = 1; // 最小1px
  198.  
  199. this.newProgressWidth = this.getNewProgressWidth(this.state.realProgress);
  200. this.setSliderPosition(this.newProgressWidth + 'px');
  201. }
  202.  
  203. componentWillReceiveProps(nextProps){
  204. if(nextProps.progress!=undefined && this.state.realProgress != nextProps.progress){
  205. this.setState({
  206. realProgress: nextProps.progress
  207. });
  208. this.setNewProgress(nextProps)
  209. }
  210. if(nextProps.range!=undefined && this.range != nextProps.range){
  211. this.init(nextProps)
  212. this.setNewProgress(nextProps)
  213. }
  214. }
  215.  
  216. setNewProgress(props){
  217. this.newProgressWidth = this.getNewProgressWidth(props.progress);
  218. this.setSliderPosition(this.newProgressWidth + 'px');
  219. }
  220.  
  221. getNewProgressWidth(realProgress){ // 保留2位小数
  222. let per = Math.round((realProgress-this.range[0])/this.rangeDiff*100)/100
  223. if(per>=1) per = 1
  224. if(per<=0) per = 0
  225. return this.sliderLength * per
  226. }
  227.  
  228. setSliderPosition(distance){
  229. this.sliderProgress.style.width = distance;
  230. this.sliderBtn.style.left = distance;
  231. }
  232.  
  233. dragCallback(event, position){
  234. let newProgress, nowSec;
  235.  
  236. this.preX = position.start.x;
  237. this.X = position.move.x;
  238. this.distance = this.X - this.preX;
  239.  
  240. this.prevProgressWidth = this.newProgressWidth + this.distance;
  241.  
  242. if(this.prevProgressWidth <= 0) this.prevProgressWidth = 0;
  243. if(this.prevProgressWidth >= this.sliderLength) this.prevProgressWidth = this.sliderLength;
  244.  
  245. nowSec = Math.round(this.prevProgressWidth/this.eachSection, 0);
  246. this.prevProgressWidth = this.eachSection*nowSec;
  247.  
  248. newProgress = this.prevProgressWidth/this.sliderLength * this.rangeDiff + this.range[0];
  249.  
  250. this.setSliderPosition(this.prevProgressWidth + 'px');
  251.  
  252. this.setState({
  253. tipVisible: true,
  254. realProgress: parseInt(newProgress)
  255. });
  256. }
  257.  
  258. dropCallback(event, position){
  259. let {tipStay, slideCallback} = this.props
  260. if(!tipStay){
  261. this.setState({
  262. tipVisible: false
  263. });
  264. }
  265.  
  266. this.newProgressWidth = this.prevProgressWidth;
  267.  
  268. if(slideCallback) slideCallback(this.state.realProgress);
  269. }
  270.  
  271. renderSliderText(showTipMode){
  272. if(showTipMode){
  273. return <div className={this.setPhPrefix('text')}>{this.state.realProgress}</div>
  274. }
  275. }
  276.  
  277. renderSliderRange(){
  278. if(this.props.showRange){
  279. return (
  280. <div className={this.setPhPrefix('range')}>
  281. <strong className={this.setPhPrefix('range-start')}>{this.range[0]}</strong>
  282. <strong className={this.setPhPrefix('range-end')}>{this.range[1]}</strong>
  283. </div>
  284. );
  285. }else{
  286. return '';
  287. }
  288. }
  289.  
  290. renderSlider(){
  291. let {componentTag:Component, className, showRange, tipMode} = this.props,
  292. showTipMode = tipMode=='default';
  293.  
  294. return (
  295. <Component {...this.otherProps} className={classnames(
  296. this.getProperty(true),
  297. className,
  298. showRange? this.setPhPrefix('keep-range',true):''
  299. )}>
  300. {this.renderSliderText(showTipMode)}
  301. {this.renderSliderRange()}
  302. <div className={this.setPhPrefix('line')} ref={(sliderLine)=>{this.sliderLine=sliderLine}}>
  303. <div className={this.setPhPrefix('progress')} ref={(sliderProgress)=>{this.sliderProgress=sliderProgress}}></div>
  304. <div className={this.setPhPrefix('content')} ref={(sliderBtn)=>{this.sliderBtn=sliderBtn}}>
  305. <div className={classnames(this.setPhPrefix('tip'), this.state.tipVisible && !showTipMode?'show':'hide')}>{this.state.realProgress}</div>
  306. <Drag className={classnames(this.setPhPrefix('btn'),'hardware')} dragCallback={this.dragCallback.bind(this)} dropCallback={this.dropCallback.bind(this)}></Drag>
  307. </div>
  308. </div>
  309. </Component>
  310. );
  311. }
  312.  
  313. render(){
  314. return this.renderSlider()
  315. }
  316.  
  317. }