slider.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import * as React from 'react'
  7. export class Slider extends React.Component<{
  8. min: number,
  9. max: number,
  10. value: number,
  11. step?: number,
  12. onChange: (v: number) => void,
  13. disabled?: boolean
  14. }, { isChanging: boolean, current: number }> {
  15. state = { isChanging: false, current: 0 }
  16. static getDerivedStateFromProps(props: { value: number }, state: { isChanging: boolean, current: number }) {
  17. if (state.isChanging || props.value === state.current) return null;
  18. return { current: props.value };
  19. }
  20. begin = () => {
  21. this.setState({ isChanging: true });
  22. }
  23. end = (v: number) => {
  24. this.setState({ isChanging: false });
  25. this.props.onChange(v);
  26. }
  27. updateCurrent = (current: number) => {
  28. this.setState({ current });
  29. }
  30. render() {
  31. let step = this.props.step;
  32. if (step === void 0) step = 1;
  33. return <div className='msp-slider'>
  34. <div>
  35. <div>
  36. <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
  37. onBeforeChange={this.begin}
  38. onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
  39. </div></div>
  40. <div>
  41. {`${Math.round(100 * this.state.current) / 100}`}
  42. </div>
  43. </div>;
  44. }
  45. }
  46. /**
  47. * The following code was adapted from react-components/slider library.
  48. *
  49. * The MIT License (MIT)
  50. * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
  51. *
  52. * Permission is hereby granted, free of charge, to any person obtaining a copy
  53. * of this software and associated documentation files (the "Software"), to deal
  54. * in the Software without restriction, including without limitation the rights
  55. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  56. * copies of the Software, and to permit persons to whom the Software is
  57. * furnished to do so, subject to the following conditions:
  58. * The above copyright notice and this permission notice shall be included in
  59. * all copies or substantial portions of the Software.
  60. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  61. * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  62. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  63. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  64. * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  65. * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  66. * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  67. */
  68. function classNames(_classes: { [name: string]: boolean | number }) {
  69. let classes = [];
  70. let hasOwn = {}.hasOwnProperty;
  71. for (let i = 0; i < arguments.length; i++) {
  72. let arg = arguments[i];
  73. if (!arg) continue;
  74. let argType = typeof arg;
  75. if (argType === 'string' || argType === 'number') {
  76. classes.push(arg);
  77. } else if (Array.isArray(arg)) {
  78. classes.push(classNames.apply(null, arg));
  79. } else if (argType === 'object') {
  80. for (let key in arg) {
  81. if (hasOwn.call(arg, key) && arg[key]) {
  82. classes.push(key);
  83. }
  84. }
  85. }
  86. }
  87. return classes.join(' ');
  88. }
  89. function noop() {
  90. }
  91. function isNotTouchEvent(e: TouchEvent) {
  92. return e.touches.length > 1 || (e.type.toLowerCase() === 'touchend' && e.touches.length > 0);
  93. }
  94. function getTouchPosition(vertical: boolean, e: TouchEvent) {
  95. return vertical ? e.touches[0].clientY : e.touches[0].pageX;
  96. }
  97. function getMousePosition(vertical: boolean, e: MouseEvent) {
  98. return vertical ? e.clientY : e.pageX;
  99. }
  100. function getHandleCenterPosition(vertical: boolean, handle: HTMLElement) {
  101. const coords = handle.getBoundingClientRect();
  102. return vertical ?
  103. coords.top + (coords.height * 0.5) :
  104. coords.left + (coords.width * 0.5);
  105. }
  106. function pauseEvent(e: Event) {
  107. e.stopPropagation();
  108. e.preventDefault();
  109. }
  110. export class Handle extends React.Component<Partial<HandleProps>, {}> {
  111. render() {
  112. const {
  113. className,
  114. tipFormatter,
  115. vertical,
  116. offset,
  117. value,
  118. index,
  119. } = this.props as HandleProps;
  120. const style = vertical ? { bottom: `${offset}%` } : { left: `${offset}%` };
  121. return (
  122. <div className={className} style={style} title={tipFormatter(value, index)}
  123. />
  124. );
  125. }
  126. }
  127. export interface SliderBaseProps {
  128. min: number,
  129. max: number,
  130. step?: number,
  131. defaultValue?: number | number[],
  132. value?: number | number[],
  133. marks?: any,
  134. included?: boolean,
  135. className?: string,
  136. prefixCls?: string,
  137. disabled?: boolean,
  138. children?: any,
  139. onBeforeChange?: (value: number | number[]) => void,
  140. onChange?: (value: number | number[]) => void,
  141. onAfterChange?: (value: number | number[]) => void,
  142. handle?: JSX.Element,
  143. tipFormatter?: (value: number, index: number) => any,
  144. dots?: boolean,
  145. range?: boolean | number,
  146. vertical?: boolean,
  147. allowCross?: boolean,
  148. pushable?: boolean | number,
  149. }
  150. export interface SliderBaseState {
  151. handle: number | null,
  152. recent: number,
  153. bounds: number[]
  154. }
  155. export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState> {
  156. private sliderElement: HTMLElement | undefined = void 0;
  157. private handleElements: (HTMLElement | undefined)[] = [];
  158. constructor(props: SliderBaseProps) {
  159. super(props);
  160. const { range, min, max } = props;
  161. const initialValue = range ? Array.apply(null, Array(+range + 1)).map(() => min) : min;
  162. const defaultValue = ('defaultValue' in props ? props.defaultValue : initialValue);
  163. const value = (props.value !== undefined ? props.value : defaultValue);
  164. const bounds = (range ? value : [min, value]).map((v: number) => this.trimAlignValue(v));
  165. let recent;
  166. if (range && bounds[0] === bounds[bounds.length - 1] && bounds[0] === max) {
  167. recent = 0;
  168. } else {
  169. recent = bounds.length - 1;
  170. }
  171. this.state = {
  172. handle: null,
  173. recent,
  174. bounds,
  175. };
  176. }
  177. public static defaultProps: SliderBaseProps = {
  178. prefixCls: 'msp-slider-base',
  179. className: '',
  180. min: 0,
  181. max: 100,
  182. step: 1,
  183. marks: {},
  184. handle: <Handle className='' vertical={false} offset={0} tipFormatter={v => v} value={0} index={0} />,
  185. onBeforeChange: noop,
  186. onChange: noop,
  187. onAfterChange: noop,
  188. tipFormatter: (value, index) => value,
  189. included: true,
  190. disabled: false,
  191. dots: false,
  192. range: false,
  193. vertical: false,
  194. allowCross: true,
  195. pushable: false,
  196. };
  197. private dragOffset = 0;
  198. private startPosition = 0;
  199. private startValue = 0;
  200. private _getPointsCache: any = void 0;
  201. componentWillReceiveProps(nextProps: SliderBaseProps) {
  202. if (!('value' in nextProps || 'min' in nextProps || 'max' in nextProps)) return;
  203. const { bounds } = this.state;
  204. if (nextProps.range) {
  205. const value = nextProps.value || bounds;
  206. const nextBounds = (value as number[]).map((v: number) => this.trimAlignValue(v, nextProps));
  207. if (nextBounds.every((v: number, i: number) => v === bounds[i])) return;
  208. this.setState({ bounds: nextBounds } as SliderBaseState);
  209. if (bounds.some(v => this.isValueOutOfBounds(v, nextProps))) {
  210. this.props.onChange!(nextBounds);
  211. }
  212. } else {
  213. const value = nextProps.value !== undefined ? nextProps.value : bounds[1];
  214. const nextValue = this.trimAlignValue(value as number, nextProps);
  215. if (nextValue === bounds[1] && bounds[0] === nextProps.min) return;
  216. this.setState({ bounds: [nextProps.min, nextValue] } as SliderBaseState);
  217. if (this.isValueOutOfBounds(bounds[1], nextProps)) {
  218. this.props.onChange!(nextValue);
  219. }
  220. }
  221. }
  222. onChange(state: this['state']) {
  223. const props = this.props;
  224. const isNotControlled = !('value' in props);
  225. if (isNotControlled) {
  226. this.setState(state);
  227. } else if (state.handle !== undefined) {
  228. this.setState({ handle: state.handle } as SliderBaseState);
  229. }
  230. const data = { ...this.state, ...(state as any) };
  231. const changedValue = props.range ? data.bounds : data.bounds[1];
  232. props.onChange!(changedValue);
  233. }
  234. onMouseDown(e: MouseEvent) {
  235. if (e.button !== 0) { return; }
  236. let position = getMousePosition(this.props.vertical!, e);
  237. if (!this.isEventFromHandle(e)) {
  238. this.dragOffset = 0;
  239. } else {
  240. const handlePosition = getHandleCenterPosition(this.props.vertical!, e.target as HTMLElement);
  241. this.dragOffset = position - handlePosition;
  242. position = handlePosition;
  243. }
  244. this.onStart(position);
  245. this.addDocumentEvents('mouse');
  246. pauseEvent(e);
  247. }
  248. onMouseMove(e: MouseEvent) {
  249. const position = getMousePosition(this.props.vertical!, e);
  250. this.onMove(e, position - this.dragOffset);
  251. }
  252. onMove(e: MouseEvent | TouchEvent, position: number) {
  253. pauseEvent(e);
  254. const props = this.props;
  255. const state = this.state;
  256. let diffPosition = position - this.startPosition;
  257. diffPosition = this.props.vertical ? -diffPosition : diffPosition;
  258. const diffValue = diffPosition / this.getSliderLength() * (props.max - props.min);
  259. const value = this.trimAlignValue(this.startValue + diffValue);
  260. const oldValue = state.bounds[state.handle!];
  261. if (value === oldValue) return;
  262. const nextBounds = [...state.bounds];
  263. nextBounds[state.handle!] = value;
  264. let nextHandle = state.handle!;
  265. if (props.pushable !== false) {
  266. const originalValue = state.bounds[nextHandle];
  267. this.pushSurroundingHandles(nextBounds, nextHandle, originalValue);
  268. } else if (props.allowCross) {
  269. nextBounds.sort((a, b) => a - b);
  270. nextHandle = nextBounds.indexOf(value);
  271. }
  272. this.onChange({
  273. handle: nextHandle,
  274. bounds: nextBounds,
  275. } as SliderBaseState);
  276. }
  277. onStart(position: number) {
  278. const props = this.props;
  279. props.onBeforeChange!(this.getValue());
  280. const value = this.calcValueByPos(position);
  281. this.startValue = value;
  282. this.startPosition = position;
  283. const state = this.state;
  284. const { bounds } = state;
  285. let valueNeedChanging = 1;
  286. if (this.props.range) {
  287. let closestBound = 0;
  288. for (let i = 1; i < bounds.length - 1; ++i) {
  289. if (value > bounds[i]) { closestBound = i; }
  290. }
  291. if (Math.abs(bounds[closestBound + 1] - value) < Math.abs(bounds[closestBound] - value)) {
  292. closestBound = closestBound + 1;
  293. }
  294. valueNeedChanging = closestBound;
  295. const isAtTheSamePoint = (bounds[closestBound + 1] === bounds[closestBound]);
  296. if (isAtTheSamePoint) {
  297. valueNeedChanging = state.recent;
  298. }
  299. if (isAtTheSamePoint && (value !== bounds[closestBound + 1])) {
  300. valueNeedChanging = value < bounds[closestBound + 1] ? closestBound : closestBound + 1;
  301. }
  302. }
  303. this.setState({
  304. handle: valueNeedChanging,
  305. recent: valueNeedChanging,
  306. } as SliderBaseState);
  307. const oldValue = state.bounds[valueNeedChanging];
  308. if (value === oldValue) return;
  309. const nextBounds = [...state.bounds];
  310. nextBounds[valueNeedChanging] = value;
  311. this.onChange({ bounds: nextBounds } as SliderBaseState);
  312. }
  313. onTouchMove(e: TouchEvent) {
  314. if (isNotTouchEvent(e)) {
  315. this.end('touch');
  316. return;
  317. }
  318. const position = getTouchPosition(this.props.vertical!, e);
  319. this.onMove(e, position - this.dragOffset);
  320. }
  321. onTouchStart(e: TouchEvent) {
  322. if (isNotTouchEvent(e)) return;
  323. let position = getTouchPosition(this.props.vertical!, e);
  324. if (!this.isEventFromHandle(e)) {
  325. this.dragOffset = 0;
  326. } else {
  327. const handlePosition = getHandleCenterPosition(this.props.vertical!, e.target as HTMLElement);
  328. this.dragOffset = position - handlePosition;
  329. position = handlePosition;
  330. }
  331. this.onStart(position);
  332. this.addDocumentEvents('touch');
  333. pauseEvent(e);
  334. }
  335. /**
  336. * Returns an array of possible slider points, taking into account both
  337. * `marks` and `step`. The result is cached.
  338. */
  339. getPoints() {
  340. const { marks, step, min, max } = this.props;
  341. const cache = this._getPointsCache;
  342. if (!cache || cache.marks !== marks || cache.step !== step) {
  343. const pointsObject = { ...marks };
  344. if (step !== null) {
  345. for (let point = min; point <= max; point += step!) {
  346. pointsObject[point] = point;
  347. }
  348. }
  349. const points = Object.keys(pointsObject).map(parseFloat);
  350. points.sort((a, b) => a - b);
  351. this._getPointsCache = { marks, step, points };
  352. }
  353. return this._getPointsCache.points;
  354. }
  355. getPrecision(step: number) {
  356. const stepString = step.toString();
  357. let precision = 0;
  358. if (stepString.indexOf('.') >= 0) {
  359. precision = stepString.length - stepString.indexOf('.') - 1;
  360. }
  361. return precision;
  362. }
  363. getSliderLength() {
  364. const slider = this.sliderElement;
  365. if (!slider) {
  366. return 0;
  367. }
  368. return this.props.vertical ? slider.clientHeight : slider.clientWidth;
  369. }
  370. getSliderStart() {
  371. const slider = this.sliderElement as HTMLElement;
  372. const rect = slider.getBoundingClientRect();
  373. return this.props.vertical ? rect.top : rect.left;
  374. }
  375. getValue(): number {
  376. const { bounds } = this.state;
  377. return (this.props.range ? bounds : bounds[1]) as number;
  378. }
  379. private eventHandlers = {
  380. 'touchmove': (e: TouchEvent) => this.onTouchMove(e),
  381. 'touchend': (e: TouchEvent) => this.end('touch'),
  382. 'mousemove': (e: MouseEvent) => this.onMouseMove(e),
  383. 'mouseup': (e: MouseEvent) => this.end('mouse'),
  384. }
  385. addDocumentEvents(type: 'touch' | 'mouse') {
  386. if (type === 'touch') {
  387. document.addEventListener('touchmove', this.eventHandlers.touchmove);
  388. document.addEventListener('touchend', this.eventHandlers.touchend);
  389. } else if (type === 'mouse') {
  390. document.addEventListener('mousemove', this.eventHandlers.mousemove);
  391. document.addEventListener('mouseup', this.eventHandlers.mouseup);
  392. }
  393. }
  394. calcOffset(value: number) {
  395. const { min, max } = this.props;
  396. const ratio = (value - min) / (max - min);
  397. return ratio * 100;
  398. }
  399. calcValue(offset: number) {
  400. const { vertical, min, max } = this.props;
  401. const ratio = Math.abs(offset / this.getSliderLength());
  402. const value = vertical ? (1 - ratio) * (max - min) + min : ratio * (max - min) + min;
  403. return value;
  404. }
  405. calcValueByPos(position: number) {
  406. const pixelOffset = position - this.getSliderStart();
  407. const nextValue = this.trimAlignValue(this.calcValue(pixelOffset));
  408. return nextValue;
  409. }
  410. end(type: 'mouse' | 'touch') {
  411. this.removeEvents(type);
  412. this.props.onAfterChange!(this.getValue());
  413. this.setState({ handle: null } as SliderBaseState);
  414. }
  415. isEventFromHandle(e: Event) {
  416. for (const h of this.handleElements) {
  417. if (h === e.target) return true;
  418. }
  419. return false;
  420. // return this.state.bounds.some((x, i) => e.target
  421. // (
  422. // //this.handleElements[i] && e.target === ReactDOM.findDOMNode(this.handleElements[i])
  423. // ));
  424. }
  425. isValueOutOfBounds(value: number, props: SliderBaseProps) {
  426. return value < props.min || value > props.max;
  427. }
  428. pushHandle(bounds: number[], handle: number, direction: number, amount: number) {
  429. const originalValue = bounds[handle];
  430. let currentValue = bounds[handle];
  431. while (direction * (currentValue - originalValue) < amount) {
  432. if (!this.pushHandleOnePoint(bounds, handle, direction)) {
  433. // can't push handle enough to create the needed `amount` gap, so we
  434. // revert its position to the original value
  435. bounds[handle] = originalValue;
  436. return false;
  437. }
  438. currentValue = bounds[handle];
  439. }
  440. // the handle was pushed enough to create the needed `amount` gap
  441. return true;
  442. }
  443. pushHandleOnePoint(bounds: number[], handle: number, direction: number) {
  444. const points = this.getPoints();
  445. const pointIndex = points.indexOf(bounds[handle]);
  446. const nextPointIndex = pointIndex + direction;
  447. if (nextPointIndex >= points.length || nextPointIndex < 0) {
  448. // reached the minimum or maximum available point, can't push anymore
  449. return false;
  450. }
  451. const nextHandle = handle + direction;
  452. const nextValue = points[nextPointIndex];
  453. const { pushable: threshold } = this.props;
  454. const diffToNext = direction * (bounds[nextHandle] - nextValue);
  455. if (!this.pushHandle(bounds, nextHandle, direction, +threshold! - diffToNext)) {
  456. // couldn't push next handle, so we won't push this one either
  457. return false;
  458. }
  459. // push the handle
  460. bounds[handle] = nextValue;
  461. return true;
  462. }
  463. pushSurroundingHandles(bounds: number[], handle: number, originalValue: number) {
  464. const { pushable: threshold } = this.props;
  465. const value = bounds[handle];
  466. let direction = 0;
  467. if (bounds[handle + 1] - value < threshold!) {
  468. direction = +1;
  469. } else if (value - bounds[handle - 1] < threshold!) {
  470. direction = -1;
  471. }
  472. if (direction === 0) { return; }
  473. const nextHandle = handle + direction;
  474. const diffToNext = direction * (bounds[nextHandle] - value);
  475. if (!this.pushHandle(bounds, nextHandle, direction, +threshold! - diffToNext)) {
  476. // revert to original value if pushing is impossible
  477. bounds[handle] = originalValue;
  478. }
  479. }
  480. removeEvents(type: 'touch' | 'mouse') {
  481. if (type === 'touch') {
  482. document.removeEventListener('touchmove', this.eventHandlers.touchmove);
  483. document.removeEventListener('touchend', this.eventHandlers.touchend);
  484. } else if (type === 'mouse') {
  485. document.removeEventListener('mousemove', this.eventHandlers.mousemove);
  486. document.removeEventListener('mouseup', this.eventHandlers.mouseup);
  487. }
  488. }
  489. trimAlignValue(v: number, nextProps?: SliderBaseProps) {
  490. const { handle, bounds } = (this.state || {}) as this['state'];
  491. const { marks, step, min, max, allowCross } = { ...this.props, ...(nextProps || {}) } as SliderBaseProps;
  492. let val = v;
  493. if (val <= min) {
  494. val = min;
  495. }
  496. if (val >= max) {
  497. val = max;
  498. }
  499. /* eslint-disable eqeqeq */
  500. if (!allowCross && handle != null && handle > 0 && val <= bounds[handle - 1]) {
  501. val = bounds[handle - 1];
  502. }
  503. if (!allowCross && handle != null && handle < bounds.length - 1 && val >= bounds[handle + 1]) {
  504. val = bounds[handle + 1];
  505. }
  506. /* eslint-enable eqeqeq */
  507. const points = Object.keys(marks).map(parseFloat);
  508. if (step !== null) {
  509. const closestStep = (Math.round((val - min) / step!) * step!) + min;
  510. points.push(closestStep);
  511. }
  512. const diffs = points.map((point) => Math.abs(val - point));
  513. const closestPoint = points[diffs.indexOf(Math.min.apply(Math, diffs))];
  514. return step !== null ? parseFloat(closestPoint.toFixed(this.getPrecision(step!))) : closestPoint;
  515. }
  516. render() {
  517. const {
  518. handle,
  519. bounds,
  520. } = this.state;
  521. const {
  522. className,
  523. prefixCls,
  524. disabled,
  525. vertical,
  526. dots,
  527. included,
  528. range,
  529. step,
  530. marks,
  531. max, min,
  532. tipFormatter,
  533. children,
  534. } = this.props;
  535. const customHandle = this.props.handle;
  536. const offsets = bounds.map(v => this.calcOffset(v));
  537. const handleClassName = `${prefixCls}-handle`;
  538. const handlesClassNames = bounds.map((v, i) => classNames({
  539. [handleClassName]: true,
  540. [`${handleClassName}-${i + 1}`]: true,
  541. [`${handleClassName}-lower`]: i === 0,
  542. [`${handleClassName}-upper`]: i === bounds.length - 1,
  543. }));
  544. const isNoTip = (step === null) || (tipFormatter === null);
  545. const commonHandleProps = {
  546. prefixCls,
  547. noTip: isNoTip,
  548. tipFormatter,
  549. vertical,
  550. };
  551. this.handleElements = [];
  552. const handles = bounds.map((v, i) => React.cloneElement(customHandle!, {
  553. ...commonHandleProps,
  554. className: handlesClassNames[i],
  555. value: v,
  556. offset: offsets[i],
  557. dragging: handle === i,
  558. index: i,
  559. key: i,
  560. ref: (h: any) => this.handleElements.push(h) //`handle-${i}`,
  561. }));
  562. if (!range) { handles.shift(); }
  563. const isIncluded = included || range;
  564. const tracks: JSX.Element[] = [];
  565. // for (let i = 1; i < bounds.length; ++i) {
  566. // const trackClassName = classNames({
  567. // [`${prefixCls}-track`]: true,
  568. // [`${prefixCls}-track-${i}`]: true,
  569. // });
  570. // tracks.push(
  571. // <Track className={trackClassName} vertical={vertical} included={isIncluded}
  572. // offset={offsets[i - 1]} length={offsets[i] - offsets[i - 1]} key={i}
  573. // />
  574. // );
  575. // }
  576. const sliderClassName = classNames({
  577. [prefixCls!]: true,
  578. [`${prefixCls}-with-marks`]: Object.keys(marks).length,
  579. [`${prefixCls}-disabled`]: disabled!,
  580. [`${prefixCls}-vertical`]: this.props.vertical!,
  581. [className!]: !!className,
  582. });
  583. return (
  584. <div ref={e => this.sliderElement = e!} className={sliderClassName}
  585. onTouchStart={disabled ? noop : this.onTouchStart.bind(this)}
  586. onMouseDown={disabled ? noop : this.onMouseDown.bind(this)}
  587. >
  588. <div className={`${prefixCls}-rail`} />
  589. {tracks}
  590. <Steps prefixCls={prefixCls} vertical={vertical} marks={marks} dots={dots} step={step}
  591. included={isIncluded} lowerBound={bounds[0]}
  592. upperBound={bounds[bounds.length - 1]} max={max} min={min}
  593. />
  594. {handles}
  595. <Marks className={`${prefixCls}-mark`} vertical={vertical!} marks={marks}
  596. included={isIncluded!} lowerBound={bounds[0]}
  597. upperBound={bounds[bounds.length - 1]} max={max} min={min}
  598. />
  599. {children}
  600. </div>
  601. );
  602. }
  603. }
  604. export interface HandleProps {
  605. className: string,
  606. vertical: boolean,
  607. offset: number,
  608. tipFormatter: (v: number, index: number) => any,
  609. value: number,
  610. index: number,
  611. }
  612. interface MarksProps {
  613. className: string,
  614. vertical: boolean,
  615. marks: any,
  616. included: boolean | number,
  617. upperBound: number,
  618. lowerBound: number,
  619. max: number,
  620. min: number
  621. }
  622. const Marks = ({ className, vertical, marks, included, upperBound, lowerBound, max, min }: MarksProps) => {
  623. const marksKeys = Object.keys(marks);
  624. const marksCount = marksKeys.length;
  625. const unit = 100 / (marksCount - 1);
  626. const markWidth = unit * 0.9;
  627. const range = max - min;
  628. const elements = marksKeys.map(parseFloat).sort((a, b) => a - b).map((point) => {
  629. const isActived = (!included && point === upperBound) ||
  630. (included && point <= upperBound && point >= lowerBound);
  631. const markClassName = classNames({
  632. [`${className}-text`]: true,
  633. [`${className}-text-active`]: isActived,
  634. });
  635. const bottomStyle = {
  636. // height: markWidth + '%',
  637. marginBottom: '-50%',
  638. bottom: `${(point - min) / range * 100}%`,
  639. };
  640. const leftStyle = {
  641. width: `${markWidth}%`,
  642. marginLeft: `${-markWidth / 2}%`,
  643. left: `${(point - min) / range * 100}%`,
  644. };
  645. const style = vertical ? bottomStyle : leftStyle;
  646. const markPoint = marks[point];
  647. const markPointIsObject = typeof markPoint === 'object' && !React.isValidElement(markPoint);
  648. const markLabel = markPointIsObject ? markPoint.label : markPoint;
  649. const markStyle = markPointIsObject ? { ...style, ...markPoint.style } : style;
  650. return (<span className={markClassName} style={markStyle} key={point}>
  651. {markLabel}
  652. </span>);
  653. });
  654. return <div className={className}>{elements}</div>;
  655. };
  656. function calcPoints(vertical: boolean, marks: any, dots: boolean, step: number, min: number, max: number) {
  657. const points = Object.keys(marks).map(parseFloat);
  658. if (dots) {
  659. for (let i = min; i <= max; i = i + step) {
  660. if (points.indexOf(i) >= 0) continue;
  661. points.push(i);
  662. }
  663. }
  664. return points;
  665. }
  666. const Steps = ({ prefixCls, vertical, marks, dots, step, included,
  667. lowerBound, upperBound, max, min }: any) => {
  668. const range = max - min;
  669. const elements = calcPoints(vertical, marks, dots, step, min, max).map((point) => {
  670. const offset = `${Math.abs(point - min) / range * 100}%`;
  671. const style = vertical ? { bottom: offset } : { left: offset };
  672. const isActived = (!included && point === upperBound) ||
  673. (included && point <= upperBound && point >= lowerBound);
  674. const pointClassName = classNames({
  675. [`${prefixCls}-dot`]: true,
  676. [`${prefixCls}-dot-active`]: isActived,
  677. });
  678. return <span className={pointClassName} style={style} key={point} />;
  679. });
  680. return <div className={`${prefixCls}-step`}>{elements}</div>;
  681. };
  682. // const Track = ({ className, included, vertical, offset, length }: any) => {
  683. // const style: any = {
  684. // visibility: included ? 'visible' : 'hidden'
  685. // };
  686. // if (vertical) {
  687. // style.bottom = `${offset}%`;
  688. // style.height = `${length}%`;
  689. // } else {
  690. // style.left = `${offset}%`;
  691. // style.width = `${length}%`;
  692. // }
  693. // return <div className={className} style={style} />;
  694. // };