diff --git a/assets/index.less b/assets/index.less index a736195..66717e3 100644 --- a/assets/index.less +++ b/assets/index.less @@ -131,55 +131,6 @@ cursor: not-allowed!important; } } - - // slider tooltip style - &-tooltip { - position: absolute; - left: -9999px; - top: -9999px; - z-index: 4; - visibility: visible; - - .borderBox(); - - &-hidden { - display: none; - } - - &-placement-top { - padding: @tooltip-arrow-width 0 @tooltip-distance 0; - } - - &-inner { - padding: 6px 2px; - min-width: 24px; - height: 24px; - font-size: 12px; - line-height: 1; - color: @tooltip-color; - text-align: center; - text-decoration: none; - background-color: @tooltip-bg; - border-radius: @border-radius-base; - box-shadow: 0 0 4px #d9d9d9; - } - - &-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - } - - &-placement-top &-arrow { - bottom: @tooltip-distance - @tooltip-arrow-width; - left: 50%; - margin-left: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width 0; - border-top-color: @tooltip-arrow-color; - } - } } .motion-common() { @@ -241,3 +192,51 @@ transform: scale(0, 0); } } + +.rc-tooltip { + position: absolute; + left: -9999px; + top: -9999px; + z-index: 4; + visibility: visible; + + .borderBox(); + + &-hidden { + display: none; + } + + &-placement-top { + padding: @tooltip-arrow-width 0 @tooltip-distance 0; + } + + &-inner { + padding: 6px 2px; + min-width: 24px; + height: 24px; + font-size: 12px; + line-height: 1; + color: @tooltip-color; + text-align: center; + text-decoration: none; + background-color: @tooltip-bg; + border-radius: @border-radius-base; + box-shadow: 0 0 4px #d9d9d9; + } + + &-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } + + &-placement-top &-arrow { + bottom: @tooltip-distance - @tooltip-arrow-width; + left: 50%; + margin-left: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } +} \ No newline at end of file diff --git a/examples/marks.js b/examples/marks.js index a487bc0..7592dca 100644 --- a/examples/marks.js +++ b/examples/marks.js @@ -7,7 +7,13 @@ var ReactDOM = require('react-dom'); var Slider = require('rc-slider'); var style = {width: 400, margin: 50}; -var marks = ['A','B','C','D', 'E', 'F']; +var marks = { + 0: '0°C', + 26: '26°C', + 37: '37°C', + 100: '100°C' +}; + var log = function(value) { console.log(value); }; @@ -16,29 +22,29 @@ ReactDOM.render(

Slider with marks, `included=true`

- +

Slider with marks and steps, `included=true`

- +

Slider with marks, `included=false`

- +

Slider with marks and steps, `included=false`

- +

Range with marks

- +

Range with marks and steps

- +
, document.getElementById('__react-content')); diff --git a/package.json b/package.json index 6ea1296..fd1ec6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-slider", - "version": "2.4.0", + "version": "3.0.0", "description": "slider ui component for react", "keywords": [ "react", diff --git a/src/Handle.jsx b/src/Handle.jsx index 80df82e..929d330 100644 --- a/src/Handle.jsx +++ b/src/Handle.jsx @@ -39,7 +39,7 @@ export default class Handle extends React.Component { const isTooltipVisible = dragging || this.state.isTooltipVisible; return ({tipFormatter ? tipFormatter(value) : value}} diff --git a/src/Marks.jsx b/src/Marks.jsx index 7a03023..db7ace7 100644 --- a/src/Marks.jsx +++ b/src/Marks.jsx @@ -1,35 +1,36 @@ import React from 'react'; import rcUtil from 'rc-util'; -const Marks = ({className, marks, index, included}) => { - const marksLen = marks.length; - const unit = 100 / (marksLen - 1); +const Marks = ({className, marks, included, upperBound, lowerBound, max, min}) => { + const marksKeys = Object.keys(marks); + const marksCount = marksKeys.length; + const unit = 100 / (marksCount - 1); const markWidth = unit / 2 + '%'; - const elements = []; - for (let i = 0; i < marksLen; i++) { - const isActived = (included && i <= index) || (!included && i === index); + const range = max - min; + const elements = marksKeys.map(parseFloat).map((point) => { + const isActived = (!included && point === upperBound) || + (included && point <= upperBound && point >= lowerBound); const markClassName = rcUtil.classSet({ [className + '-text']: true, [className + '-text-active']: isActived, }); const style = { width: markWidth }; - const offset = unit * i; - if (i === marksLen - 1) { + if (point === marksCount - 1) { style.right = -unit / 4 + '%'; + } else if (point === 0) { + style.left = -unit / 4 + '%'; } else { - style.left = (i > 0 ? offset - unit / 4 : -unit / 4) + '%'; + style.left = (point - min) / range * 100 - unit / 4 + '%'; } - elements.push( - {marks[i]} - ); - } + return ( + {marks[point]} + ); + }); - return (
- {elements} -
); + return
{elements}
; }; export default Marks; diff --git a/src/Slider.jsx b/src/Slider.jsx index 4dde98a..5cecc7c 100644 --- a/src/Slider.jsx +++ b/src/Slider.jsx @@ -25,40 +25,34 @@ function pauseEvent(e) { e.preventDefault(); } -// This is an utility method, tries to get property, then defaultPropery with -// special check using 'in', because propery can be '0' -function propOrDefault(props, name, fallback) { - const defaultName = 'default' + name.charAt(0).toUpperCase() + name.substring(1); - const defaultValue = (defaultName in props ? props[defaultName] : fallback); - return (name in props ? props[name] : defaultValue); +function isEmpty(collection) { + return Object.keys(collection).length === 0; } class Slider extends React.Component { constructor(props) { super(props); + const {range, min, max} = props; + const initialValue = range ? [min, min] : min; + const defaultValue = ('defaultValue' in props ? props.defaultValue : initialValue); + const value = ('value' in props ? props.value : defaultValue); + let upperBound; let lowerBound; - const initialValue = props.range ? [0, 0] : 0; - if (props.marks.length > 0) { - const index = propOrDefault(props, 'index', initialValue); - ({lowerBound, upperBound} = this.getBoundsFromIndex(index, props)); + if (props.range) { + lowerBound = this.trimAlignValue(value[0]); + upperBound = this.trimAlignValue(value[1]); } else { - const value = propOrDefault(props, 'value', initialValue); - if (props.range) { - lowerBound = this.trimAlignValue(value[0]); - upperBound = this.trimAlignValue(value[1]); - } else { - upperBound = this.trimAlignValue(value); - } + upperBound = this.trimAlignValue(value); } let recent; if (props.range && upperBound === lowerBound) { - if (lowerBound === props.max) { + if (lowerBound === max) { recent = 'lowerBound'; } - if (upperBound === props.min) { + if (upperBound === min) { recent = 'upperBound'; } } else { @@ -70,7 +64,7 @@ class Slider extends React.Component { recent: recent, upperBound: upperBound, // If Slider is not range, set `lowerBound` equal to `min`. - lowerBound: (lowerBound || props.min), + lowerBound: (lowerBound || min), }; } @@ -87,12 +81,26 @@ class Slider extends React.Component { this.setState({ upperBound: nextProps.value, }); - } else if ('index' in nextProps) { - const index = ('index' in nextProps ? nextProps.index : nextProps.defaultIndex); - this.setState(this.getBoundsFromIndex(index, nextProps)); } } + onChange(handle, value) { + const props = this.props; + const isNotControlled = !('value' in props); + if (isNotControlled) { + this.setState({[handle]: value}); + } + + const state = this.state; + const data = { + upperBound: state.upperBound, + lowerBound: state.lowerBound, + }; + data[handle] = value; + const changedValue = props.range ? [data.lowerBound, data.upperBound] : data.upperBound; + props.onChange(changedValue); + } + onMouseMove(e) { const position = getMousePosition(e); this.onMove(e, position); @@ -120,14 +128,7 @@ class Slider extends React.Component { const oldValue = state[state.handle]; if (value === oldValue) return; - // If it is not controlled component - if (!('value' in props) && !('index' in props)) { - this.setState({[state.handle]: value}, () => { - this.triggerEvents('onChange', this.getValue()); - }); - } else { - this.triggerEvents('onChange', this.getChangedValue(state.handle, value)); - } + this.onChange(state.handle, value); } onTouchStart(e) { @@ -139,7 +140,7 @@ class Slider extends React.Component { pauseEvent(e); } - onSliderMouseDown(e) { + onMouseDown(e) { const position = getMousePosition(e); this.onStart(position); this.addDocumentEvents('mouse'); @@ -147,7 +148,8 @@ class Slider extends React.Component { } onStart(position) { - this.triggerEvents('onBeforeChange', this.getValue()); + const props = this.props; + props.onBeforeChange(this.getValue()); const value = this.calcValueByPos(position); this.startValue = value; @@ -178,17 +180,10 @@ class Slider extends React.Component { recent: valueNeedChanging, }); - const props = this.props; - // If it is not controlled component - if (!('value' in props) && !('index' in props)) { - this.setState({ - [valueNeedChanging]: value, - }, () => { - this.triggerEvents('onChange', this.getValue()); - }); - } else { - this.triggerEvents('onChange', this.getChangedValue(valueNeedChanging, value)); - } + const oldValue = state[valueNeedChanging]; + if (value === oldValue) return; + + this.onChange(valueNeedChanging, value); } getValue() { @@ -196,36 +191,15 @@ class Slider extends React.Component { return this.props.range ? [lowerBound, upperBound] : upperBound; } - getChangedValue(valueNeedChanging, value) { - const state = this.state; - const data = { - upperBound: state.upperBound, - lowerBound: state.lowerBound, - }; - data[valueNeedChanging] = value; - return this.props.range ? [data.lowerBound, data.upperBound] : data.upperBound; - } - - getIndex(value) { - const {marks, min, max, step} = this.props; - - if (marks.length === 0) { - return Math.floor((value - min) / step); + getPoints() { + const {marks, step, min, max} = this.props; + const points = Object.keys(marks); + if (isEmpty(marks) || step > 1) { + for (let i = min; i <= max; i = i + step) { + points.push(i); + } } - const unit = ((max - min) / (marks.length - 1)).toFixed(5); - return Math.round(value / unit); - } - - getBoundsFromIndex(indexes, props) { - if (props.range) { - return { - lowerBound: this.calcValueFromIndex(indexes[0], props), - upperBound: this.calcValueFromIndex(indexes[1], props), - }; - } - return { - upperBound: this.calcValueFromIndex(indexes, props), - }; + return points; } getSliderLength() { @@ -247,10 +221,7 @@ class Slider extends React.Component { trimAlignValue(v) { const state = this.state || {}; const {handle, lowerBound, upperBound} = state; - const props = this.props; - const {marks, min, max} = props; - const marksLen = marks.length; - const step = (marksLen > 0) ? (max - min) / (marksLen - 1) : props.step; + const {min, max} = this.props; let val = v; if (val <= min) { @@ -266,14 +237,11 @@ class Slider extends React.Component { val = upperBound; } - const valModStep = (val - min) % step; + const points = this.getPoints().map(parseFloat); + const diffs = points.map((point) => Math.abs(val - point)); + const closestPoint = points[diffs.indexOf(Math.min.apply(Math, diffs))]; - let alignValue = val - valModStep; - if (Math.abs(valModStep) * 2 >= step) { - alignValue += (valModStep > 0) ? step : (-step); - } - - return parseFloat(alignValue.toFixed(5)); + return closestPoint; } calcOffset(value) { @@ -294,35 +262,6 @@ class Slider extends React.Component { return nextValue; } - calcValueFromIndex(index, props) { - const marksLen = props.marks.length; - if (marksLen > 0) { - const value = ((props.max - props.min) / (marksLen - 1)) * (index); - return parseFloat(value.toFixed(5)); - } - return ('value' in props ? props.value : props.defaultValue); - } - - triggerEvents(event, v) { - const props = this.props; - const hasMarks = (props.marks.length > 0); - if (props[event]) { - let data; - if (hasMarks) { - if (props.range) { - data = v.map(bound => this.getIndex(bound)); - } else { - data = this.getIndex(v); - } - } else if (v === undefined) { - data = this.state.value; - } else { - data = v; - } - props[event](data); - } - } - addDocumentEvents(type) { if (type === 'touch') { // just work for chrome iOS Safari and Android Browser @@ -346,15 +285,15 @@ class Slider extends React.Component { end(type) { this.removeEventons(type); - this.triggerEvents('onAfterChange', this.getValue()); + this.props.onAfterChange(this.getValue()); this.setState({handle: null}); } render() { const {handle, upperBound, lowerBound} = this.state; - const {className, prefixCls, disabled, included, isIncluded, dots, range, - marks, step, max, min, tipTransitionName, tipFormatter, children} = this.props; - const marksLen = marks.length; + const {className, prefixCls, disabled, dots, included, range, + marks, max, min, tipTransitionName, tipFormatter, children} = this.props; + const marksCount = Object.keys(marks).length; const sliderClassName = classSet({ [prefixCls]: true, @@ -366,13 +305,13 @@ class Slider extends React.Component { const lowerOffset = this.calcOffset(lowerBound); let track = null; - if ((included && isIncluded) || range) { + if (included || range) { const trackClassName = prefixCls + '-track'; track = ; } const handleClassName = prefixCls + '-handle'; - const isNoTip = (marksLen > 0) && !tipFormatter; + const isNoTip = (marksCount > 0) && !tipFormatter; const upper = (); @@ -382,33 +321,20 @@ class Slider extends React.Component { offset={lowerOffset} value={lowerBound} dragging={handle === 'lowerBound'} />); } - const upperIndex = this.getIndex(upperBound); - - let steps = null; - if (marksLen > 0 || (step > 1 && dots)) { - const stepsClassName = prefixCls + '-step'; - const stepNum = marksLen > 0 ? marksLen : Math.floor((max - min) / step) + 1; - steps = (); - } - - let mark = null; - if (marksLen > 0) { - const markClassName = prefixCls + '-mark'; - mark = (); - } - + const isIncluded = included || range; return (
+ onMouseDown={disabled ? noop : this.onMouseDown.bind(this)}> {track} {upper} {lower} - {steps} - {mark} + + {children}
); @@ -423,20 +349,11 @@ Slider.propTypes = { React.PropTypes.number, React.PropTypes.arrayOf(React.PropTypes.number), ]), - defaultIndex: React.PropTypes.oneOfType([ - React.PropTypes.number, - React.PropTypes.arrayOf(React.PropTypes.number), - ]), value: React.PropTypes.oneOfType([ React.PropTypes.number, React.PropTypes.arrayOf(React.PropTypes.number), ]), - index: React.PropTypes.oneOfType([ - React.PropTypes.number, - React.PropTypes.arrayOf(React.PropTypes.number), - ]), - marks: React.PropTypes.array, - isIncluded: React.PropTypes.bool, // @Deprecated + marks: React.PropTypes.object, included: React.PropTypes.bool, className: React.PropTypes.string, prefixCls: React.PropTypes.string, @@ -452,17 +369,18 @@ Slider.propTypes = { }; Slider.defaultProps = { + prefixCls: 'rc-slider', + className: '', + tipTransitionName: '', min: 0, max: 100, step: 1, - defaultIndex: 0, - marks: [], - isIncluded: true, // @Deprecated + marks: {}, + onBeforeChange: noop, + onChange: noop, + onAfterChange: noop, included: true, - className: '', - prefixCls: 'rc-slider', disabled: false, - tipTransitionName: '', dots: false, range: false, }; diff --git a/src/Steps.jsx b/src/Steps.jsx index da2cdb0..38e3394 100644 --- a/src/Steps.jsx +++ b/src/Steps.jsx @@ -1,28 +1,24 @@ import React from 'react'; -import rcUtil from 'rc-util'; +import { classSet } from 'rc-util'; -const Steps = ({className, stepNum, included, lowerIndex, upperIndex}) => { - const dotClassName = className.replace('step', 'dot'); - const unit = 100 / (stepNum - 1); +const Steps = ({prefixCls, points, dots, included, lowerBound, upperBound, max, min}) => { + const range = max - min; + const elements = points.filter((point) => typeof point === 'string' || dots).map(parseFloat) + .map((point) => { + const offset = (point - min) / range * 100 + '%'; + const style = { left: offset }; - const elements = []; - for (let i = 0; i < stepNum; i++) { - const offset = unit * i + '%'; - const style = { left: offset }; + const isActived = (!included && point === upperBound) || + (included && point <= upperBound && point >= lowerBound); + const pointClassName = classSet({ + [prefixCls + '-dot']: true, + [prefixCls + '-dot-active']: isActived, + }); - const isActived = (included && i <= upperIndex && i >= lowerIndex ) || - (!included && i === upperIndex); - const stepClassName = rcUtil.classSet({ - [dotClassName]: true, - [dotClassName + '-active']: isActived, - }); + return ; + }); - elements.push(); - } - - return (
- {elements} -
); + return
{elements}
; }; export default Steps; diff --git a/tests/index.spec.js b/tests/index.spec.js index 07c55ec..68acbef 100644 --- a/tests/index.spec.js +++ b/tests/index.spec.js @@ -52,17 +52,17 @@ describe('rc-slider', function () { it('should render a slider with marks correctly!', function () { var slider = ReactDOM.render( - , + , div ); var node = $(div); expect(node.find('.rc-slider').length).to.be(1); expect(node.find('.rc-slider-handle').length).to.be(1); expect(node.find('.rc-slider-track').length).to.be(1); - expect(node.find('.rc-slider-dot').length).to.be(slider.props.marks.length); + expect(node.find('.rc-slider-dot').length).to.be(Object.keys(slider.props.marks).length); expect(node.find('.rc-slider-mark').length).to.be(1); - expect(node.find('.rc-slider-mark-text').length).to.be(slider.props.marks.length); - expect(slider.getIndex(slider.state.upperBound)).to.be(3); + expect(node.find('.rc-slider-mark-text').length).to.be(Object.keys(slider.props.marks).length); + expect(slider.state.upperBound).to.be(40); }); // it('should mouseDown works!', function (done) {