diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..604c94ef4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*.{js,css}] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f8d8a67bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +*.iml +*.log +.idea/ +.ipr +.iws +*~ +~* +*.diff +*.patch +*.bak +.DS_Store +Thumbs.db +.project +.*proj +.svn/ +*.swp +*.swo +*.pyc +*.pyo +.build +node_modules +_site +sea-modules +spm_modules +.cache +dist +assets/**/*.css +build/ \ No newline at end of file diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 000000000..20b0f4dc0 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,27 @@ +{ + "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideParentheses": true, + "disallowQuotedKeysInObjects": "allButReserved", + "disallowSpaceAfterObjectKeys": true, + "requireSpaceBeforeBinaryOperators": ["-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=" ], + "requireSpacesInConditionalExpression": { + "afterTest": true, + "beforeConsequent": true, + "afterConsequent": true, + "beforeAlternate": true + }, + "requireSpaceAfterBinaryOperators": ["/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + "disallowKeywords": [ "with" ], + "disallowSpaceAfterPrefixUnaryOperators": [ "!" , "++", "--", "+", "-", "~"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--", ","], + "disallowMultipleLineBreaks": true, + "disallowKeywordsOnNewLine": ["else"], + "safeContextKeyword": "self", + "excludeFiles": ["lib/**/parser.js"] +} \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..ef5e85e5e --- /dev/null +++ b/.jshintrc @@ -0,0 +1,28 @@ +{ + "camelcase": true, + "curly": true, + "eqeqeq": true, + "freeze": true, + "indent": 4, + "latedef": "nofunc", + "quotmark": "false", + "nonew": true, + "newcap": false, + "immed": true, + "noarg": true, + "eqnull": true, + "trailing": true, + "undef": true, + "unused": true, + "browser": true, + "node": true, + "esnext": true, + "globals": { + "describe": false, + "expect": false, + "beforeEach": false, + "afterEach": false, + "modulex": false, + "it": false + } +} \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..e43e0f4e4 --- /dev/null +++ b/.npmignore @@ -0,0 +1,29 @@ +bower_components/ +build/ +*.cfg +node_modules/ +nohup.out +*.iml +.idea/ +.ipr +.iws +*~ +~* +*.diff +*.log +*.patch +*.bak +.DS_Store +Thumbs.db +.project +.*proj +.svn/ +*.swp +out/ +.build +node_modules +_site +sea-modules +spm_modules +.cache +dist \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..2fde59c31 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: node_js +notifications: + email: + - yiminghe@gmail.com +node_js: +- 0.12 +before_script: +- npm start & +- npm install mocha-phantomjs -g +- phantomjs --version +script: +- npm test +- npm run-script browser-test +- npm run-script browser-test-cover \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 000000000..e69de29bb diff --git a/README.md b/README.md index 57f09a88e..7334a994e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,147 @@ -# slider -react slider component +# rc-slider +--- + +slider ui component for react + +[![NPM version][npm-image]][npm-url] +[![SPM version](http://spmjs.io/badge/rc-slider)](http://spmjs.io/package/rc-slider) +[![build status][travis-image]][travis-url] +[![Test coverage][coveralls-image]][coveralls-url] +[![gemnasium deps][gemnasium-image]][gemnasium-url] +[![node version][node-image]][node-url] +[![npm download][download-image]][download-url] +[![Sauce Test Status](https://saucelabs.com/buildstatus/rc-slider)](https://saucelabs.com/u/rc-slider) + +[![Sauce Test Status](https://saucelabs.com/browser-matrix/rc-slider.svg)](https://saucelabs.com/u/rc-slider) + +[npm-image]: http://img.shields.io/npm/v/rc-slider.svg?style=flat-square +[npm-url]: http://npmjs.org/package/rc-slider +[travis-image]: https://img.shields.io/travis/react-component/slider.svg?style=flat-square +[travis-url]: https://travis-ci.org/react-component/slider +[coveralls-image]: https://img.shields.io/coveralls/react-component/slider.svg?style=flat-square +[coveralls-url]: https://coveralls.io/r/react-component/slider?branch=master +[gemnasium-image]: http://img.shields.io/gemnasium/react-component/slider.svg?style=flat-square +[gemnasium-url]: https://gemnasium.com/react-component/slider +[node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square +[node-url]: http://nodejs.org/download/ +[download-image]: https://img.shields.io/npm/dm/rc-slider.svg?style=flat-square +[download-url]: https://npmjs.org/package/rc-slider + +## Screenshots + + + + + + + + +## Feature + +* support ie8,ie8+,chrome,firefox,safari + +### Keyboard + + + +## install + +[![rc-slider](https://nodei.co/npm/rc-slider.png)](https://npmjs.org/package/rc-slider) + +## Usage + +```js +var Rcslider = require('rc-slider'); +var React = require('react'); +React.render(, container); +``` + +## API + +### props + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
classNameStringrc-slideradditional css class of root dom node
minnumber0The minimum value of the slider
maxnumber100The maximum value of the slider
stepnumber1Value 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.
valuenumber0Determines the initial positions of the handles.
marksarray[]mark every step for the slider, it will ignore the `step` parameter if it has been defined
indexnumber0For step or marks slider, determine the initial positions of the handles.
disabledbooleanfalseIf true the handles can't be moved.
+ +## Development + +``` +npm install +npm start +``` + +## Example + +http://localhost:8000/examples/ + +online example: http://react-component.github.io/slider/build/examples/ + +## Test Case + +http://localhost:8000/tests/runner.html?coverage + +## Coverage + +http://localhost:8000/node_modules/rc-server/node_modules/node-jscover/lib/front-end/jscoverage.html?w=http://localhost:8088/tests/runner.html?coverage + +## License + +rc-slider is released under the MIT license. diff --git a/assets/index.less b/assets/index.less new file mode 100644 index 000000000..60b2d6156 --- /dev/null +++ b/assets/index.less @@ -0,0 +1,117 @@ +@prefixClass: rc-slider; + +// color +@sliderColor: #999; +@trackColor: #b2e9fd; +@normalHandleColor: #d9d9d9; +@activeHandleColor: #2db7f5; +@disabledColor: #e9e9e9; + +.@{prefixClass} { + position: relative; + height: 4px; + width: 100%; + border-radius: 2px; + background-color: @sliderColor; + + &-track { + position: absolute; + height: 4px; + border-radius: 2px; + background-color: @trackColor; + z-index: 1; + } + + &-handle { + position: absolute; + margin-left: -7px; + margin-top: -5px; + width: 10px; + height: 10px; + cursor: default; + border-radius: 50%; + border: solid 2px @normalHandleColor; + background-color: #fff; + z-index: 2; + &:hover, &:active, &.active { + border-color: @activeHandleColor; + } + &:active { + background-color: @activeHandleColor; + box-shadow: 0 0 3px 0 rgb(45, 183, 245, .75); + } + } + + &-mark { + position: absolute; + top: 10px; + left: 0px; + width: 100%; + height: 20px; + font-size: 12px; + z-index: 3; + } + + &-mark-text { + position: absolute; + display: inline-block; + line-height: 1.5; + height: 20px; + vertical-align: middle; + text-align: center; + cursor: pointer; + &:first-child { + text-align: left; + } + } + + &-step { + position: absolute; + width: 100%; + height: 4px; + background: transparent; + z-index: 1; + .dot { + position: absolute; + top: -2px; + margin-left: -4px; + width: 4px; + height: 4px; + border: 2px solid #fff; + cursor: pointer; + border-radius: 50%; + vertical-align: middle; + &:first-child { + margin-left: -2px; + } + &:last-child { + margin-left: -6px; + } + } + } + + &-disabled { + color: @disabledColor; + background-color: @disabledColor; + + .@{prefixClass}-track { + background-color: @disabledColor; + } + + .@{prefixClass}-handle { + border-color: @disabledColor; + background-color: @disabledColor; + cursor: not-allowed; + + &:hover, &:active { + border-color: @disabledColor; + background-color: @disabledColor; + box-shadow: none; + } + } + + .@{prefixClass}-mark-text, .dot { + cursor: not-allowed!important; + } + } +} \ No newline at end of file diff --git a/examples/simple.html b/examples/simple.html new file mode 100644 index 000000000..b3a425249 --- /dev/null +++ b/examples/simple.html @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/examples/simple.js b/examples/simple.js new file mode 100644 index 000000000..29b1b4e44 --- /dev/null +++ b/examples/simple.js @@ -0,0 +1,8 @@ +/** @jsx React.DOM */ +// use jsx to render html, do not modify simple.html +require('rc-slider/assets/index.css'); +var Slider = require('rc-slider'); +var React = require('react'); +// React.render(, document.getElementById('__react-content')); +// React.render(, document.getElementById('__react-content')); +React.render(, document.getElementById('__react-content')); diff --git a/index.js b/index.js new file mode 100644 index 000000000..de274eb06 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/Slider'); diff --git a/lib/Slider.js b/lib/Slider.js new file mode 100644 index 000000000..44f323748 --- /dev/null +++ b/lib/Slider.js @@ -0,0 +1,350 @@ +/** @jsx React.DOM */ +var React = require('react'); + +function pauseEvent(e) { + if (e.stopPropagation) { + e.stopPropagation(); + } + if (e.preventDefault) { + e.preventDefault(); + } + e.cancelBubble = true; + e.returnValue = false; + return false; +} + +var Slider = React.createClass({ + propTypes: { + min: React.PropTypes.number, + max: React.PropTypes.number, + step: React.PropTypes.number, + value: React.PropTypes.number, + index: React.PropTypes.number, + marks: React.PropTypes.array, + className: React.PropTypes.string, + disabled: React.PropTypes.bool, + onBeforeChange: React.PropTypes.func, + onChange: React.PropTypes.func, + onAfterChange: React.PropTypes.func + }, + + getDefaultProps: function() { + return { + min: 0, + max: 100, + step: 1, + value: 0, + marks: [], + className: 'rc-slider', + disabled: false, + index: 0 + }; + }, + + getInitialState: function() { + var props = this.props; + var value = props.value; + var marksLen = props.marks.length; + if (marksLen > 0) { + value = ((props.max - props.min) / (marksLen - 1)) * (props.index); + value = value.toFixed(5); + } + + return { + upperBound: 0, + sliderLength: 0, + value: value, + active: props.disabled ? '' : ((value > 0 || props.index > 0) ? 'active' : '') + }; + }, + + componentWillReceiveProps: function(newProps) { + var value = newProps.value; + this.state.value = this._trimAlignValue(value, newProps); + }, + + componentDidMount: function() { + window.addEventListener('resize', this.handleResize); + this.handleResize(); + }, + + componentWillUnmount: function() { + window.removeEventListener('resize', this.handleResize); + }, + + getValue: function() { + return this.state.value; + }, + + getIndex: function() { + var props = this.props; + if (props.marks.length === 0) { + return; + } + var value = this.state.value; + var unit = (props.max - props.min) / (props.marks.length - 1); + return Math.floor(value / unit); + }, + + _trimAlignValue: function(val, props) { + props = props || this.props; + + var step = props.marks.length > 0 ? (props.max - props.min) / (props.marks.length - 1) : props.step; + + if (val <= props.min) { + val = props.min; + } + if (val >= props.max) { + val = props.max; + } + + var valModStep = (val - props.min) % step; + var alignValue = val - valModStep; + + if (Math.abs(valModStep) * 2 >= step) { + alignValue += (valModStep > 0) ? step : (-step); + } + + return parseFloat(alignValue.toFixed(5)); + }, + + _calcOffset: function(value) { + var ratio = (value - this.props.min) / (this.props.max - this.props.min); + return ratio * this.state.upperBound; + }, + + _calcValue: function(offset) { + var ratio = offset / this.state.upperBound; + return ratio * (this.props.max - this.props.min) + this.props.min; + }, + + _calValueByPos: function (position, callback) { + var pixelOffset = position - this.state.sliderStart; + // pixelOffset -= (this.state.handleSize / 2); + + var nextValue = this._trimAlignValue(this._calcValue(pixelOffset)); + + this.setState({value: nextValue, active: 'active'}, callback.bind(this)); + }, + + _triggerEvents: function(event) { + if (this.props[event]) { + this.props[event](this.state.value); + } + }, + + _addEventHandles: function(eventMap) { + for (var key in eventMap) { + document.addEventListener(key, eventMap[key], false); + } + }, + + _removeEventHandles: function (eventMap) { + for (var key in eventMap) { + document.removeEventListener(key, eventMap[key], false); + } + }, + + _getMouseEventMap: function() { + return { + mousemove: this._onMouseMove, + mouseup: this._onMouseUp + }; + }, + + _start: function(position) { + if (document.activeElement) { + document.activeElement.blur(); + } + + this._triggerEvents('onBeforeChange'); + + this.setState({ + startValue: this.state.value, + startPosition: position + }); + }, + + _end: function(eventMap) { + this._removeEventHandles(eventMap); + this.setState(this._triggerEvents.bind(this, 'onAfterChange')); + }, + + _onMouseUp: function() { + this._end(this._getMouseEventMap()); + }, + + _onMouseMove: function(e) { + var position = e.pageX; + + var props = this.props; + var state = this.state; + + var value = state.value; + var oldValue = value; + + var diffPosition = position - state.startPosition; + + var diffValue = diffPosition / (state.sliderLength) * (props.max - props.min); + var newValue = this._trimAlignValue(state.startValue + diffValue); + + value = newValue; + + if (newValue !== oldValue) { + this.setState({value: value, active: 'active'} ,this._triggerEvents.bind(this, 'onChange')); + } + }, + + handleResize: function() { + var slider = this.refs.slider.getDOMNode(); + var rect = slider.getBoundingClientRect(); + + var sliderMin = rect.left; + var sliderMax = rect.right; + this.setState({ + upperBound: slider.clientWidth, + sliderLength: Math.abs(sliderMax - sliderMin), + sliderStart: sliderMin + }); + }, + + handleMouseDown: function() { + return function(e) { + if (this.props.disabled) { + return; + } + var position = e.pageX; + this._start(position); + this._addEventHandles(this._getMouseEventMap()); + pauseEvent(e); + }.bind(this); + }, + + handleSliderMouseDown: function(e) { + if (this.props.disabled) { + return; + } + var position = e.pageX; + this._calValueByPos(position, function() { + this._triggerEvents('onChange'); + this._start(position); + this._addEventHandles(this._getMouseEventMap()); + }.bind(this)); + pauseEvent(e); + }, + + renderSteps: function() { + var props = this.props; + var marksLen = props.marks.length; + var stepNum = marksLen > 0 ? marksLen : Math.floor((props.max - props.min) / props.step) + 1; + var unit = this.state.sliderLength / (stepNum - 1); + + var stepClassName = props.className + '-step'; + + var elements = []; + for (var i = 0; i < stepNum; i++) { + var style = { + left: (unit * i).toFixed(5) + 'px' + }; + elements[i] = ( + + ); + } + + return ( +
+ {elements} +
+ ); + }, + + renderMark: function(i) { + var marks = this.props.marks; + var marksLen = marks.length; + var unit = this.state.sliderLength / (marksLen - 1); + var offset = unit * i; + + var style = { + width: (unit / 2).toFixed(5) + 'px' + }; + + if (i === (marksLen - 1)) { + style.right = '0'; + style.width = 'auto'; + }else { + style.left = (i > 0 ? (offset - (unit / 4)).toFixed(5) : offset) + 'px'; + } + var className = this.props.className + '-mark-text '; + + return ( + {this.props.marks[i]} + ); + }, + + renderMarks: function() { + var marks = this.props.marks; + var marksLen = marks.length; + var elements = []; + for (var i = 0; i < marksLen; i++) { + elements[i] = this.renderMark(i); + } + + return ( +
+ {elements} +
+ ); + }, + + renderHandle: function(offset) { + var handleStyle = { + left: offset + 'px' + }; + var className = this.props.className + '-handle ' + this.state.active; + + return ( + + ); + }, + + renderTrack: function(offset) { + var style = { + left: 0, + width: offset + }; + var trackClassName = this.props.className + '-track'; + return ( +
+ ); + }, + + render: function() { + var state = this.state; + var props = this.props; + + var value = state.value; + var offset = this._calcOffset(value); + + var track = this.renderTrack(offset); + var handles = this.renderHandle(offset); + var steps = (props.step > 1 || props.marks.length > 0) ? this.renderSteps() : null; + var sliderMarks = (props.marks.length > 0) ? this.renderMarks() : null; + + var sliderClassName = props.className + (props.disabled ? ' '+props.className+ '-disabled' : ''); + + return ( +
+ {track} + {handles} + {steps} + {sliderMarks} +
+ ); + } +}); + +module.exports = Slider; diff --git a/package.json b/package.json new file mode 100644 index 000000000..1e819f147 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "rc-slider", + "version": "1.0.0", + "description": "slider ui component for react", + "keywords": [ + "react", + "react-component", + "react-slider", + "slider" + ], + "homepage": "http://github.com/react-component/slider", + "author": "sima.zhang1990@gmail.com", + "repository": { + "type": "git", + "url": "git@github.com:react-component/slider.git" + }, + "bugs": { + "url": "http://github.com/react-component/slider/issues" + }, + "licenses": "MIT", + "spm": { + "dependencies": { + "react": "*" + } + }, + "config":{ + "port": 8000 + }, + "scripts": { + "build": "rc-tools run build", + "less": "rc-tools run less", + "gh-pages": "rc-tools run gh-pages", + "history": "rc-tools run history", + "start": "node-dev --harmony node_modules/.bin/rc-server", + "publish": "spm publish && rc-tools run tag", + "lint": "rc-tools run lint", + "test": "", + "saucelabs": "rc-tools run saucelabs", + "browser-test": "rc-tools run browser-test", + "browser-test-cover": "rc-tools run browser-test-cover" + }, + "devDependencies": { + "expect.js": "0.3.x", + "precommit-hook": "1.x", + "rc-server": "2.x", + "rc-tools": "2.x", + "react": "0.13.x", + "node-dev":"2.x", + "jquery": "^1.11.2", + "css-loader": "^0.9.1" + }, + "precommit": [ + "lint", + "less" + ] +} diff --git a/tests/index.spec.js b/tests/index.spec.js new file mode 100644 index 000000000..525794f13 --- /dev/null +++ b/tests/index.spec.js @@ -0,0 +1,75 @@ +/** @jsx React.DOM */ +var expect = require('expect.js'); +var Slider = require('../index.js'); +var React = require('react'); +var TestUtils = React.addons.TestUtils; +var Simulate = TestUtils.Simulate; +var $ = require('jquery'); + +describe('rc-slider', function () { + this.timeout(5000); + var div = document.createElement('div'); + document.body.appendChild(div); + + afterEach(function () { + React.unmountComponentAtNode(div); + }); + + it('should render a simple slider with value correctly!', function () { + var slider = React.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(slider.getValue()).to.be(40); + var trackWidth = node.find('.rc-slider-track')[0].style.width; + expect(trackWidth).to.eql(node.find('.rc-slider-handle')[0].style.left); + }); + + it('should render a slider with correct numbers of step!', function () { + var slider = React.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('.dot').length).to.be(6); + expect(slider.getValue()).to.be(0); + }); + + it('should render a slider with marks correctly!', function () { + var slider = React.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('.dot').length).to.be(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()).to.be(3); + }); + + it('should mouseDown works!', function (done) { + var slider = React.render( + , + div + ); + var selectedStep = slider.refs.step3.getDOMNode(); + + Simulate.mouseDown(selectedStep); + + setTimeout( function() { + expect(slider.state.active).to.be('active'); + done(); + }, 200); + }); + +}); \ No newline at end of file diff --git a/tests/runner.html b/tests/runner.html new file mode 100644 index 000000000..7bd069c80 --- /dev/null +++ b/tests/runner.html @@ -0,0 +1 @@ +stub \ No newline at end of file