slider.tsx 26 KB

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