react-slider
Advanced tools
Comparing version 0.1.2 to 0.2.1
{ | ||
"name": "react-slider", | ||
"version": "0.1.2", | ||
"description": "Slider component for React.js", | ||
"version": "0.2.1", | ||
"description": "Slider component for React", | ||
"main": "react-slider.js", | ||
@@ -20,6 +20,10 @@ "scripts": { | ||
"author": "Michał Powaga <michalpowaga13@gmail.com>", | ||
"contributors": [{ | ||
"name": "Florian Klampfer", | ||
"email": "f.klampfer@gmail.com" | ||
}], | ||
"license": "MIT", | ||
"dependencies": { | ||
"react": "^0.11.1" | ||
"react": "^0.12.1" | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
(function(root, factory) { | ||
(function (root, factory) { | ||
if (typeof define === 'function' && define.amd) { | ||
@@ -9,29 +9,47 @@ define(['react'], factory); | ||
} | ||
}(this, function(React) { | ||
}(this, function (React) { | ||
var ReactSlider = React.createClass({ displayName: 'ReactSlider', | ||
var ReactSlider = React.createClass({ | ||
displayName: 'ReactSlider', | ||
propTypes: { | ||
minValue: React.PropTypes.number, | ||
maxValue: React.PropTypes.number, | ||
min: React.PropTypes.number, | ||
max: React.PropTypes.number, | ||
step: React.PropTypes.number, | ||
defaultValue: 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) | ||
]), | ||
orientation: React.PropTypes.oneOf(['horizontal', 'vertical']), | ||
className: React.PropTypes.string, | ||
handleClassName: React.PropTypes.string, | ||
barClassName: React.PropTypes.string, | ||
withBars: React.PropTypes.bool, | ||
disabled: React.PropTypes.bool, | ||
onChange: React.PropTypes.func, | ||
valuePropName: React.PropTypes.string | ||
onChanged: React.PropTypes.func | ||
}, | ||
getDefaultProps: function() { | ||
getDefaultProps: function () { | ||
return { | ||
minValue: 0, | ||
maxValue: 100, | ||
min: 0, | ||
max: 100, | ||
step: 1, | ||
defaultValue: 0, | ||
orientation: 'horizontal', | ||
valuePropName: 'sliderValue' | ||
className: 'slider', | ||
handleClassName: 'handle', | ||
barClassName: 'bar', | ||
disabled: false | ||
}; | ||
}, | ||
getInitialState: function() { | ||
getInitialState: function () { | ||
return { | ||
offset: 0, | ||
lowerBound: 0, | ||
_dragMoveCache: {}, // TODO: find better solution | ||
_touchMoveCache: {}, // TODO: find better solution | ||
upperBound: 0, | ||
@@ -41,9 +59,42 @@ handleWidth: 0, | ||
sliderMax: 0, | ||
value: this.props.minValue | ||
value: this._or(this.props.value, this.props.defaultValue) | ||
}; | ||
}, | ||
componentDidMount: function() { | ||
_or: function (v1, v2) { | ||
var count = React.Children.count(this.props.children); | ||
switch (count) { | ||
case 0: | ||
return or(v1, v2); | ||
case size(v1): | ||
return v1; | ||
case size(v2): | ||
return v2; | ||
default: | ||
if (size(v1) !== count || size(v2) !== count) { | ||
console.warn("ReactSlider: Number of values does not match number of children."); | ||
} | ||
return linspace(this.props.min, this.props.max, count); | ||
} | ||
}, | ||
componentDidMount: function () { | ||
window.addEventListener('resize', this._handleResize); | ||
this._handleResize(); | ||
var value = map(this.state.value, this._trimAlignValue); | ||
this.setState({value: value}); | ||
}, | ||
componentWillUnmount: function () { | ||
window.removeEventListener('resize', this._handleResize); | ||
}, | ||
getValue: function () { | ||
return this.state.value | ||
}, | ||
_handleResize: function () { | ||
var slider = this.refs.slider.getDOMNode(); | ||
var handle = this.refs.handle.getDOMNode(); | ||
var handle = this.refs.handle0.getDOMNode(); | ||
var rect = slider.getBoundingClientRect(); | ||
@@ -56,82 +107,155 @@ | ||
var position = { | ||
horizontal: { min: 'left', max: 'right' }, | ||
vertical: { min: 'top', max: 'bottom' } | ||
}[this.props.orientation]; | ||
this.setState({ | ||
upperBound: slider[size] - handle[size], | ||
handleWidth: handle[size], | ||
sliderMin: rect[position.min], | ||
sliderMax: rect[position.max] - handle[size], | ||
sliderMin: rect[this._min()], | ||
sliderMax: rect[this._max()] - handle[size] | ||
}); | ||
}, | ||
render: function() { | ||
var handleStyle = { | ||
transform: 'translate' + this._axis() + '(' + this.state.offset + 'px)', | ||
// let this element be the same size as its children. | ||
display: 'inline-block' | ||
_calcOffset: function (value) { | ||
var ratio = (value - this.props.min) / (this.props.max - this.props.min); | ||
return ratio * this.state.upperBound; | ||
}, | ||
_buildHandleStyle: function (offset) { | ||
var transform = 'translate' + this._axis() + '(' + offset + 'px)'; | ||
return { | ||
WebkitTransform: transform, | ||
MozTransform: transform, | ||
msTransform: transform, | ||
OTransform: transform, | ||
transform: transform, | ||
position: 'absolute' | ||
} | ||
}, | ||
_buildBarStyle: function (minMax) { | ||
var obj = { | ||
position: 'absolute' | ||
}; | ||
obj[this._min()] = minMax.min; | ||
obj[this._max()] = minMax.max; | ||
return obj; | ||
}, | ||
var userHandle = this.props.children; | ||
userHandle.props[this.props.valuePropName] = this.state.value; | ||
_getClosestIndex: function (clickOffset) { | ||
var self = this; | ||
return ( | ||
React.DOM.div({ ref: 'slider', className: this.props.className, onClick: this._onClick }, | ||
React.DOM.div({ ref: 'handle', style: handleStyle, onMouseDown: this._dragStart, onTouchMove: this._touchMove }, | ||
userHandle | ||
))); | ||
// TODO: No need to iterate all | ||
return reduce(this.state.value, function (min, value, i) { | ||
var minDist = min[1]; | ||
var offset = self._calcOffset(value); | ||
var dist = Math.abs(clickOffset - offset); | ||
return (dist < minDist) ? [i, dist] : min; | ||
}, [-1, Number.MAX_VALUE])[0]; | ||
}, | ||
_onClick: function(e) { | ||
_onClick: function (e) { | ||
var position = e['page' + this._axis()]; | ||
// make center of handle appear under the cursor position | ||
this._moveHandle(position - (this.state.handleWidth / 2)); | ||
var clickOffset = position - this.state.sliderMin; | ||
var closestIndex = this._getClosestIndex(clickOffset); | ||
this._moveHandle(closestIndex, position); | ||
if (this.props.onChanged) { | ||
this.props.onChanged(this.state.value); | ||
} | ||
}, | ||
_dragStart: function() { | ||
document.addEventListener('mousemove', this._dragMove, false); | ||
document.addEventListener('mouseup', this._dragEnd, false); | ||
_dragStart: function (i) { | ||
var self = this; | ||
return function (e) { | ||
document.addEventListener('mousemove', self._dragMove(i), false); | ||
document.addEventListener('mouseup', self._dragEnd(i), false); | ||
pauseEvent(e); | ||
} | ||
}, | ||
_dragMove: function(e) { | ||
var position = e['page' + this._axis()]; | ||
this._moveHandle(position); | ||
_dragMove: function (i) { | ||
var self = this; | ||
if (!this.state._dragMoveCache[i]) { | ||
this.state._dragMoveCache[i] = | ||
function (e) { | ||
var position = e['page' + self._axis()]; | ||
self._moveHandle(i, position); | ||
} | ||
} | ||
return this.state._dragMoveCache[i]; | ||
}, | ||
_dragEnd: function() { | ||
document.removeEventListener('mousemove', this._dragMove, false); | ||
document.removeEventListener('mouseup', this._dragEnd, false); | ||
_dragEnd: function (i) { | ||
var self = this; | ||
return function () { | ||
document.removeEventListener('mousemove', self._dragMove(i), false); | ||
document.removeEventListener('mouseup', self._dragEnd(i), false); | ||
if (self.props.onChanged) { | ||
self.props.onChanged(this.state.value); | ||
} | ||
} | ||
}, | ||
_touchMove: function(e) { | ||
var last = e.changedTouches[e.changedTouches.length - 1]; | ||
var position = last['page' + this._axis()]; | ||
this._moveHandle(position); | ||
e.preventDefault(); | ||
_onTouchEnd: function () { | ||
if (this.props.onChanged) { | ||
this.props.onChanged(this.state.value); | ||
} | ||
}, | ||
_moveHandle: function(position) { | ||
_touchMove: function (i) { | ||
var self = this; | ||
if (!this.state._touchMoveCache[i]) { | ||
this.state._touchMoveCache[i] = | ||
function (e) { | ||
var last = e.changedTouches[e.changedTouches.length - 1]; | ||
var position = last['page' + self._axis()]; | ||
self._moveHandle(i, position); | ||
e.preventDefault(); | ||
} | ||
} | ||
return this.state._touchMoveCache[i]; | ||
}, | ||
_moveHandle: function (i, position) { | ||
if (this.props.disabled) return; | ||
position = position - (this.state.handleWidth / 2); | ||
var lastValue = this.state.value; | ||
var ratio = (position - this.state.sliderMin) / (this.state.sliderMax - this.state.sliderMin); | ||
var value = ratio * (this.props.maxValue - this.props.minValue) + this.props.minValue; | ||
var nextValue = map(this.state.value, function (value, j) { | ||
if (i !== j) return value; | ||
var nextValue = this._trimAlignValue(value); | ||
var nextRatio = (nextValue - this.props.minValue) / (this.props.maxValue - this.props.minValue); | ||
var nextOffset = nextRatio * this.state.upperBound; | ||
var ratio = (position - this.state.sliderMin) / (this.state.sliderMax - this.state.sliderMin); | ||
var nextValue = this._trimAlignValue(ratio * (this.props.max - this.props.min) + this.props.min); | ||
this.setState({ | ||
value: nextValue, | ||
offset: nextOffset | ||
}); | ||
// TODO: DRY? | ||
if (i > 0) { | ||
var valueBefore = at(this.state.value, i - 1); | ||
if (nextValue <= valueBefore) { | ||
nextValue = this._trimAlignValue(valueBefore + this.props.step); | ||
} | ||
} | ||
var changed = nextValue !== lastValue; | ||
if (changed && this.props.onChange) { | ||
this.props.onChange(nextValue); | ||
if (i < size(this.state.value) - 1) { | ||
var valueAfter = at(this.state.value, i + 1); | ||
if (nextValue >= valueAfter) { | ||
nextValue = this._trimAlignValue(valueAfter - this.props.step); | ||
} | ||
} | ||
return nextValue; | ||
}, this); | ||
var changed = !is(nextValue, lastValue); | ||
if (changed) { | ||
this.setState({value: nextValue}); | ||
if (this.props.onChange) this.props.onChange(nextValue); | ||
} | ||
}, | ||
_axis: function() { | ||
_axis: function () { | ||
return { | ||
@@ -143,20 +267,173 @@ 'horizontal': 'X', | ||
_trimAlignValue: function(val) { | ||
if (val <= this.props.minValue) val = this.props.minValue; | ||
if (val >= this.props.maxValue) val = this.props.maxValue; | ||
_min: function () { | ||
return { | ||
'horizontal': 'left', | ||
'vertical': 'top' | ||
}[this.props.orientation]; | ||
}, | ||
var valModStep = (val - this.props.minValue) % this.props.step; | ||
_max: function () { | ||
return { | ||
'horizontal': 'right', | ||
'vertical': 'bottom' | ||
}[this.props.orientation]; | ||
}, | ||
_trimAlignValue: function (val) { | ||
if (val <= this.props.min) val = this.props.min; | ||
if (val >= this.props.max) val = this.props.max; | ||
var valModStep = (val - this.props.min) % this.props.step; | ||
var alignValue = val - valModStep; | ||
if (Math.abs(valModStep) * 2 >= this.props.step) { | ||
alignValue += (valModStep > 0) ? this.props.step : (- this.props.step); | ||
alignValue += (valModStep > 0) ? this.props.step : (-this.props.step); | ||
} | ||
return parseFloat(alignValue.toFixed(5)); | ||
}, | ||
_renderHandle: function (styles) { | ||
var self = this; | ||
return function (child, i) { | ||
return ( | ||
React.createElement('div', { | ||
ref: 'handle' + i, | ||
key: 'handle' + i, | ||
className: self.props.handleClassName + ' ' + self.props.handleClassName + '-' + i, | ||
style: at(styles, i), | ||
onMouseDown: self._dragStart(i), | ||
onTouchMove: self._touchMove(i), | ||
onTouchEnd: self._onTouchEnd, | ||
onClick: self._pauseEvent | ||
}, | ||
child | ||
) | ||
); | ||
} | ||
}, | ||
_pauseEvent: function (e) { | ||
pauseEvent(e); | ||
}, | ||
_renderHandles: function (offset) { | ||
var styles = map(offset, this._buildHandleStyle, this); | ||
if (React.Children.count(this.props.children) > 0) { | ||
return React.Children.map(this.props.children, this._renderHandle(styles), this); | ||
} else { | ||
return map(offset, function (offset, i) { | ||
return this._renderHandle(styles)(null, i); | ||
}, this); | ||
} | ||
}, | ||
_renderBar: function (i, offsetFrom, offsetTo) { | ||
return ( | ||
React.createElement('div', { | ||
key: 'bar' + i, | ||
ref: 'bar' + i, | ||
className: this.props.barClassName + ' ' + this.props.barClassName + '-' + i, | ||
style: this._buildBarStyle({ | ||
min: offsetFrom, | ||
max: this.state.upperBound - offsetTo | ||
}) | ||
}) | ||
); | ||
}, | ||
_renderBars: function (offset) { | ||
var bars = []; | ||
var lastIndex = size(offset) - 1; | ||
bars.push(this._renderBar(0, 0, at(offset, 0))); | ||
for (var i = 0; i < lastIndex; i++) { | ||
bars.push(this._renderBar(i + 1, offset[i], offset[i + 1])); | ||
} | ||
bars.push(this._renderBar(lastIndex + 1, at(offset, lastIndex), this.state.upperBound)); | ||
return bars; | ||
}, | ||
render: function () { | ||
var value = this._or(this.props.value, this.state.value); | ||
var offset = map(value, this._calcOffset, this); | ||
var bars = this.props.withBars ? this._renderBars(offset) : null; | ||
var handles = this._renderHandles(offset); | ||
return ( | ||
React.createElement('div', { | ||
ref: 'slider', | ||
style: {position: 'relative'}, | ||
className: this.props.className, | ||
onClick: this._onClick | ||
}, | ||
bars, | ||
handles | ||
) | ||
); | ||
} | ||
}); | ||
/** | ||
* Prevent text selection while dragging. | ||
* http://stackoverflow.com/questions/5429827/how-can-i-prevent-text-element-selection-with-cursor-drag | ||
*/ | ||
function pauseEvent(e) { | ||
if (e.stopPropagation) e.stopPropagation(); | ||
if (e.preventDefault) e.preventDefault(); | ||
e.cancelBubble = true; | ||
e.returnValue = false; | ||
return false; | ||
} | ||
/** | ||
* A little "hack" to treat a value and an array of values equally. | ||
*/ | ||
function map(v, f, context) { | ||
return (v && v.map) ? v.map(f, context) : f.call(context, v, 0); | ||
} | ||
function reduce(v, f, init) { | ||
return (v && v.reduce) ? v.reduce(f, init) : f(init, v, 0); | ||
} | ||
function size(v) { | ||
return exists(v) ? (v.length ? v.length : 1) : 0; | ||
} | ||
function at(v, i) { | ||
return (v && v.map) ? v[i] : v; | ||
} | ||
function is(a, b) { | ||
return size(a) === size(b) && | ||
reduce(a, function (res, v, i) { | ||
return res && v === at(b, i) | ||
}, true); | ||
} | ||
function or(maybe, other) { | ||
return exists(maybe) ? maybe : other; | ||
} | ||
function exists(maybe) { | ||
return typeof maybe !== 'undefined' | ||
} | ||
function linspace(min, max, count) { | ||
var range = (max - min) / (count - 1); | ||
var res = []; | ||
for (var i = 0; i < count; i++) { | ||
res.push(range * i); | ||
} | ||
return res; | ||
} | ||
return ReactSlider; | ||
})); | ||
})); |
114
README.md
@@ -0,10 +1,14 @@ | ||
# React Slider | ||
CSS agnostic slider component for React. | ||
See demo: [https://cell303.github.io/react-slider](https://cell303.github.io/react-slider/) | ||
### Important Note | ||
This is alpha release. Use with caution and hope. | ||
This is an alpha release. Use with caution and hope. | ||
### Installation | ||
``` | ||
```sh | ||
npm install react-slider | ||
@@ -15,28 +19,35 @@ ``` | ||
#### Single slider: | ||
Similar to `<input type="range" defaultValue={50} />` | ||
```javascript | ||
var Handle = React,createClass({ | ||
render: function() { | ||
return <div className="handle">{this.props.sliderValue}</div>; | ||
} | ||
}); | ||
React.render(<ReactSlider defaultValue={50} />, document.body); | ||
``` | ||
var slider = ( | ||
<ReactSlider className="slider"> | ||
<Handle /> | ||
</ReactSlider> | ||
); | ||
#### Double slider (with bars between the handles): | ||
React.renderComponent(slider, document.body); | ||
```javascript | ||
React.render(<ReactSlider defaultValue={[0, 100]} withBars />, document.body); | ||
``` | ||
Outputs following html: | ||
#### Multi slider: | ||
```html | ||
<div class="slider"> | ||
<div style="/* some weird stuff */"> | ||
<!-- yes, this is your handle component --> | ||
<div className="handle">0</div> | ||
</div> | ||
```javascript | ||
React.render(<ReactSlider defaultValue={[0, 33, 67, 100]} withBars />, document.body); | ||
``` | ||
#### Provide custom handles: | ||
```javascript | ||
React.render( | ||
<ReactSlider withBars> | ||
<div className="my-handle">1</div> | ||
<div className="my-handle">2</div> | ||
<div className="my-handle">3</div> | ||
</ReactSlider>, | ||
document.body | ||
); | ||
``` | ||
Now you can style it as you want. Checkout the ```examples``` directory to see how. | ||
@@ -46,7 +57,7 @@ | ||
##### minValue {number} default: 0 | ||
##### min {number} default: 0 | ||
The minimum value of the slider. | ||
##### maxValue {number} default: 100 | ||
##### max {number} default: 100 | ||
@@ -57,4 +68,18 @@ The maximum value of the slider | ||
Value to be added or subtracted on each step the slider makes. Must be greater than zero. ```maxValue - minValue``` should be evenly divisible by the step value. | ||
Value to be added or subtracted on each step the slider makes. Must be greater than zero. | ||
```max - min``` should be evenly divisible by the step value. | ||
##### defaultValue {number|array\<number\>} default: 0 | ||
Determines the initial positions of the handles. | ||
Also determines the number of handles. | ||
If a number is passed a slider with one handle will be rendered. | ||
If an array is passed each value will determine the position of one handle. | ||
The values in the array must be sorted. | ||
##### value {number|array\<number\>} | ||
Like `defaultValue` but for [controlled components](http://facebook.github.io/react/docs/forms.html#controlled-components). | ||
##### orientation {string} default: 'horizontal' | ||
@@ -64,2 +89,26 @@ | ||
##### className {string} default: 'slider' | ||
The css class set on the slider node. | ||
##### handleClassName {string} default: 'handle' | ||
The css class set on each handle node. | ||
In addition each handle will receive a numbered css class of the form `${handleClassName}-${i}`, | ||
e.g. `handle-0`, `handle-1`, ... | ||
##### barClassName {string} default: 'bar' | ||
The css class set on the bars between the handles. | ||
In addition bar fragment will receive a numbered css class of the form `${barClassName}-${i}`, | ||
e.g. `bar-0`, `bar-1`, ... | ||
##### withBars {boolean} default: false | ||
If `true` bars between the handles will be rendered. | ||
##### disabled {boolean} default: false | ||
If `true` the handles can't be moved. | ||
##### onChange {function} | ||
@@ -70,9 +119,20 @@ | ||
```javascript | ||
function onChange(value) { | ||
console.log('New slider value: ' + value); | ||
function onChange(value) { | ||
console.log('New slider value: ' + value); | ||
} | ||
``` | ||
##### valuePropName {string} default: 'sliderValue' | ||
##### onChanged {function} | ||
Name of property which is passed to children component and contains current slider value. | ||
Callback called only after dragging/touching has ended or when a new value is set by clicking on the slider. | ||
### Methods | ||
##### getValue | ||
Returns the current value of the slider, which is a number in the case of a single slider, | ||
or an array of numbers in case of a multi slider. | ||
### License | ||
See the [License](LICENSE) file. |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
19992
6
365
134
1
+ Addedacorn@5.7.4(transitive)
+ Addedast-types@0.9.6(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbase62@1.2.8(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedcommander@2.20.3(transitive)
+ Addedcommoner@0.10.8(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addeddefined@1.0.1(transitive)
+ Addeddetective@4.7.1(transitive)
+ Addedenvify@3.4.1(transitive)
+ Addedesprima@3.1.3(transitive)
+ Addedesprima-fb@15001.1.0-dev-harmony-fb(transitive)
+ Addedglob@5.0.15(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addediconv-lite@0.4.24(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedjstransform@11.0.3(transitive)
+ Addedminimatch@3.1.2(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedobject-assign@2.1.1(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedprivate@0.1.8(transitive)
+ Addedq@1.5.1(transitive)
+ Addedreact@0.12.2(transitive)
+ Addedrecast@0.11.23(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsource-map@0.4.40.5.7(transitive)
+ Addedwrappy@1.0.2(transitive)
- Removedbase62@0.1.1(transitive)
- Removedenvify@2.0.1(transitive)
- Removedesprima-fb@3001.1.0-dev-harmony-fb(transitive)
- Removedjstransform@3.0.0(transitive)
- Removedobject-keys@0.4.0(transitive)
- Removedreact@0.11.2(transitive)
- Removedsource-map@0.1.31(transitive)
- Removedxtend@2.1.2(transitive)
Updatedreact@^0.12.1