├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── app.js ├── app.less ├── components │ ├── Codeblock.js │ ├── Demo.js │ ├── Examples.js │ ├── Features.js │ ├── Install.js │ ├── Usage.js │ ├── footer.js │ ├── header.js │ └── sliders │ │ ├── float.js │ │ ├── horizontal.js │ │ ├── index.js │ │ ├── labels.js │ │ ├── negative.js │ │ └── orientation.js ├── favicon.ico ├── images │ ├── bg.png │ ├── code.svg │ ├── play.svg │ ├── rangeslider.png │ ├── rangeslider_dark.png │ └── slider.png ├── index.ejs ├── index.html ├── index.js ├── server.js └── webpack.config.js ├── package.json ├── src ├── Rangeslider.js ├── __tests__ │ ├── Rangeslider.spec.js │ ├── Sanity.spec.js │ └── __snapshots__ │ │ └── Rangeslider.spec.js.snap ├── index.js ├── rangeslider.less └── utils.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["standard", "standard-jsx"], 4 | "env": { 5 | "es6": true, 6 | "browser": true, 7 | "node": true, 8 | "jest": true 9 | }, 10 | "plugins": [ 11 | "react", 12 | "prettier" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories to be ignored 2 | .DS_Store 3 | npm-debug.log 4 | favicon.png 5 | node_modules 6 | coverage 7 | public 8 | lib 9 | umd 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | notifications: 4 | email: false 5 | node_js: 6 | - '6' 7 | - '4' 8 | script: 9 | - npm test 10 | before_script: 11 | - npm prune 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bhargav Anand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | react-rangeslider 4 | 5 |

6 | 7 |

8 | A fast & lightweight react component as a drop in replacement for HTML5 input range slider element. 9 |

10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 | ## Installation 30 | Using `npm` (use `--save` to include it in your package.json) 31 | 32 | ```bash 33 | $ npm install react-rangeslider --save 34 | ``` 35 | 36 | Using `yarn` (this command also adds react-rangeslider to your package.json dependencies) 37 | 38 | ```bash 39 | $ yarn add react-rangeslider 40 | ``` 41 | 42 | 43 | ## Getting Started 44 | React-Rangeslider is bundled with a slider component & default styles which can be overridden depending on your design requirements. 45 | 46 | With a module bundler like webpack that supports either CommonJS or ES2015 modules, use as you would anything else: 47 | 48 | ```js 49 | // Using an ES6 transpiler like Babel 50 | import Slider from 'react-rangeslider' 51 | 52 | // To include the default styles 53 | import 'react-rangeslider/lib/index.css' 54 | 55 | // Not using an ES6 transpiler 56 | var Slider = require('react-rangeslider') 57 | ``` 58 | 59 | The UMD build is also available on [unpkg][unpkg]: 60 | 61 | ```html 62 | 63 | ``` 64 | 65 | You can find the library on `window.ReactRangeslider`. Optionally you can drop in the default styles by adding the stylesheet. 66 | ```html 67 | 68 | ``` 69 | Check out [docs & examples](https://whoisandy.github.io/react-rangeslider). 70 | 71 | ## Basic Example 72 | 73 | ```jsx 74 | import React, { Component } from 'react' 75 | import Slider from 'react-rangeslider' 76 | 77 | class VolumeSlider extends Component { 78 | constructor(props, context) { 79 | super(props, context) 80 | this.state = { 81 | volume: 0 82 | } 83 | } 84 | 85 | handleOnChange = (value) => { 86 | this.setState({ 87 | volume: value 88 | }) 89 | } 90 | 91 | render() { 92 | let { volume } = this.state 93 | return ( 94 | 99 | ) 100 | } 101 | } 102 | ``` 103 | 104 | 105 | ## API 106 | Rangeslider is bundled as a single component, that accepts data and callbacks only as `props`. 107 | 108 | ### Component 109 | ```jsx 110 | import Slider from 'react-rangeslider' 111 | 112 | // inside render 113 | 128 | ``` 129 | 130 | ### Props 131 | Prop | Type | Default | Description 132 | --------- | ------- | ------- | ----------- 133 | `min` | number | 0 | minimum value the slider can hold 134 | `max` | number | 100 | maximum value the slider can hold 135 | `step` | number | 1 | step in which increments/decrements have to be made 136 | `value` | number | | current value of the slider 137 | `orientation` | string | horizontal | orientation of the slider 138 | `tooltip` | boolean | true | show or hide tooltip 139 | `reverse` | boolean | false | reverse direction of vertical slider (top-bottom) 140 | `labels` | object | {} | object containing key-value pairs. `{ 0: 'Low', 50: 'Medium', 100: 'High'}` 141 | `handleLabel` | string | '' | string label to appear inside slider handles 142 | `format` | function | | function to format and display the value in label or tooltip 143 | `onChangeStart` | function | | function gets called whenever the user starts dragging the slider handle 144 | `onChange` | function | | function gets called whenever the slider handle is being dragged or clicked 145 | `onChangeComplete` | function | | function gets called whenever the user stops dragging the slider handle. 146 | 147 | 148 | ## Development 149 | To work on the project locally, you need to pull its dependencies and run `npm start`. 150 | 151 | ```bash 152 | $ npm install 153 | $ npm start 154 | ``` 155 | 156 | ## Issues 157 | Feel free to contribute. Submit a Pull Request or open an issue for further discussion. 158 | 159 | ## License 160 | MIT 161 | 162 | 163 | [npm_img]: https://img.shields.io/npm/v/react-rangeslider.svg?style=flat-square 164 | [npm_site]: https://www.npmjs.org/package/react-rangeslider 165 | [license_img]: https://img.shields.io/github/license/whoisandy/react-rangeslider.svg 166 | [license_site]: https://github.com/whoisandy/react-rangeslider/blob/master/LICENSE 167 | [npm_dm_img]: http://img.shields.io/npm/dm/react-rangeslider.svg?style=flat-square 168 | [npm_dm_site]: https://www.npmjs.org/package/react-rangeslider 169 | [trav_img]: https://api.travis-ci.org/whoisandy/react-rangeslider.svg 170 | [trav_site]: https://travis-ci.org/whoisandy/react-rangeslider 171 | [std_img]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg 172 | [std_site]: http://standardjs.com 173 | [unpkg]: https://unpkg.com/react-rangeslider/umd/ReactRangeslider.min.js 174 | -------------------------------------------------------------------------------- /docs/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from './components/Header' 3 | import Features from './components/Features' 4 | import Usage from './components/Usage' 5 | import Install from './components/Install' 6 | import Examples from './components/Examples' 7 | import Footer from './components/Footer' 8 | import '../src/rangeslider.less' 9 | import './app.less' 10 | 11 | function App () { 12 | return ( 13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | ) 26 | } 27 | 28 | export default App 29 | -------------------------------------------------------------------------------- /docs/app.less: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after{ 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | background-color: #F5F5F5; 13 | font-family: "Roboto Mono", sans-serif; 14 | font-weight: 400; 15 | font-size: 16px; 16 | line-height: 1.4; 17 | overflow-x: hidden; 18 | color: #454545; 19 | } 20 | 21 | #mount{ 22 | position: relative; 23 | } 24 | 25 | hr { 26 | height: 1px; 27 | margin: 40px 0; 28 | background-color: #ccc; 29 | border: none; 30 | } 31 | h1, h2 { 32 | margin: 0 auto 20px; 33 | font-family: "Roboto Mono", sans-serif; 34 | font-weight: 700; 35 | } 36 | h1 { 37 | margin: 0; 38 | text-align: center; 39 | font-size: 36px; 40 | color: #2d2d2d; 41 | } 42 | h2 { 43 | padding-bottom: 14px; 44 | font-size: 24px; 45 | border-bottom: 1px #cacaca solid; 46 | color: #2d2d2d; 47 | } 48 | p { 49 | margin-bottom: 20px; 50 | } 51 | a { 52 | color: #00BCD4; 53 | } 54 | 55 | header, section { 56 | .block { 57 | width: 800px; 58 | margin: 0 auto; 59 | } 60 | } 61 | 62 | header { 63 | padding: 100px 0 60px; 64 | margin-bottom: 20px; 65 | background: #2d2d2d url(./images/bg.png) repeat; 66 | .block { 67 | > div { 68 | margin-top: 40px; 69 | } 70 | } 71 | a { 72 | color: #00BCD4; 73 | } 74 | h1 { 75 | margin-bottom: 40px; 76 | a { 77 | height: 130px; 78 | display: block; 79 | background: url(./images/rangeslider.png) no-repeat center center; 80 | text-indent: -9999px; 81 | } 82 | } 83 | p { 84 | text-align: center; 85 | color: #ffffff; 86 | } 87 | } 88 | 89 | .github-btn { 90 | font: bold 11px/14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 91 | height: 20px; 92 | margin-right: 20px; 93 | overflow: hidden; 94 | display: inline-block; 95 | } 96 | .gh-btn, 97 | .gh-count, 98 | .gh-ico { 99 | float: left; 100 | } 101 | .gh-btn, 102 | .gh-count { 103 | padding: 2px 5px 2px 4px; 104 | color: #333; 105 | text-decoration: none; 106 | white-space: nowrap; 107 | cursor: pointer; 108 | border-radius: 3px; 109 | } 110 | .gh-btn { 111 | background-color: #eee; 112 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fcfcfc), color-stop(100%, #eee)); 113 | background-image: -webkit-linear-gradient(top, #fcfcfc 0, #eee 100%); 114 | background-image: -moz-linear-gradient(top, #fcfcfc 0, #eee 100%); 115 | background-image: -ms-linear-gradient(top, #fcfcfc 0, #eee 100%); 116 | background-image: -o-linear-gradient(top, #fcfcfc 0, #eee 100%); 117 | background-image: linear-gradient(to bottom, #fcfcfc 0, #eee 100%); 118 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fcfcfc', endColorstr='#eeeeee', GradientType=0); 119 | background-repeat: no-repeat; 120 | border: 1px solid #d5d5d5; 121 | } 122 | .gh-btn:hover, 123 | .gh-btn:focus { 124 | text-decoration: none; 125 | background-color: #ddd; 126 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #eee), color-stop(100%, #ddd)); 127 | background-image: -webkit-linear-gradient(top, #eee 0, #ddd 100%); 128 | background-image: -moz-linear-gradient(top, #eee 0, #ddd 100%); 129 | background-image: -ms-linear-gradient(top, #eee 0, #ddd 100%); 130 | background-image: -o-linear-gradient(top, #eee 0, #ddd 100%); 131 | background-image: linear-gradient(to bottom, #eee 0, #ddd 100%); 132 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#dddddd', GradientType=0); 133 | border-color: #ccc; 134 | } 135 | .gh-btn:active { 136 | background-image: none; 137 | background-color: #dcdcdc; 138 | border-color: #b5b5b5; 139 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15); 140 | } 141 | .gh-ico { 142 | width: 14px; 143 | height: 14px; 144 | margin-right: 4px; 145 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjQwcHgiIGhlaWdodD0iNDBweCIgdmlld0JveD0iMTIgMTIgNDAgNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMTIgMTIgNDAgNDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGZpbGw9IiMzMzMzMzMiIGQ9Ik0zMiAxMy40Yy0xMC41IDAtMTkgOC41LTE5IDE5YzAgOC40IDUuNSAxNS41IDEzIDE4YzEgMC4yIDEuMy0wLjQgMS4zLTAuOWMwLTAuNSAwLTEuNyAwLTMuMiBjLTUuMyAxLjEtNi40LTIuNi02LjQtMi42QzIwIDQxLjYgMTguOCA0MSAxOC44IDQxYy0xLjctMS4yIDAuMS0xLjEgMC4xLTEuMWMxLjkgMC4xIDIuOSAyIDIuOSAyYzEuNyAyLjkgNC41IDIuMSA1LjUgMS42IGMwLjItMS4yIDAuNy0yLjEgMS4yLTIuNmMtNC4yLTAuNS04LjctMi4xLTguNy05LjRjMC0yLjEgMC43LTMuNyAyLTUuMWMtMC4yLTAuNS0wLjgtMi40IDAuMi01YzAgMCAxLjYtMC41IDUuMiAyIGMxLjUtMC40IDMuMS0wLjcgNC44LTAuN2MxLjYgMCAzLjMgMC4yIDQuNyAwLjdjMy42LTIuNCA1LjItMiA1LjItMmMxIDIuNiAwLjQgNC42IDAuMiA1YzEuMiAxLjMgMiAzIDIgNS4xYzAgNy4zLTQuNSA4LjktOC43IDkuNCBjMC43IDAuNiAxLjMgMS43IDEuMyAzLjVjMCAyLjYgMCA0LjYgMCA1LjJjMCAwLjUgMC40IDEuMSAxLjMgMC45YzcuNS0yLjYgMTMtOS43IDEzLTE4LjFDNTEgMjEuOSA0Mi41IDEzLjQgMzIgMTMuNHoiLz48L3N2Zz4='); 146 | background-size: 100% 100%; 147 | background-repeat: no-repeat; 148 | } 149 | .gh-count { 150 | position: relative; 151 | display: none; /* hidden to start */ 152 | margin-left: 4px; 153 | background-color: #fafafa; 154 | border: 1px solid #d4d4d4; 155 | } 156 | .gh-count:hover, 157 | .gh-count:focus { 158 | color: #4183C4; 159 | } 160 | .gh-count:before, 161 | .gh-count:after { 162 | content: ''; 163 | position: absolute; 164 | display: inline-block; 165 | width: 0; 166 | height: 0; 167 | border-color: transparent; 168 | border-style: solid; 169 | } 170 | .gh-count:before { 171 | top: 50%; 172 | left: -3px; 173 | margin-top: -4px; 174 | border-width: 4px 4px 4px 0; 175 | border-right-color: #fafafa; 176 | } 177 | .gh-count:after { 178 | top: 50%; 179 | left: -4px; 180 | z-index: -1; 181 | margin-top: -5px; 182 | border-width: 5px 5px 5px 0; 183 | border-right-color: #d4d4d4; 184 | } 185 | .github-btn-large { 186 | height: 30px; 187 | } 188 | .github-btn-large .gh-btn, 189 | .github-btn-large .gh-count { 190 | padding: 3px 10px 3px 8px; 191 | font-size: 16px; 192 | line-height: 22px; 193 | border-radius: 4px; 194 | } 195 | .github-btn-large .gh-ico { 196 | width: 20px; 197 | height: 20px; 198 | } 199 | .github-btn-large .gh-count { 200 | margin-left: 6px; 201 | } 202 | .github-btn-large .gh-count:before { 203 | left: -5px; 204 | margin-top: -6px; 205 | border-width: 6px 6px 6px 0; 206 | } 207 | .github-btn-large .gh-count:after { 208 | left: -6px; 209 | margin-top: -7px; 210 | border-width: 7px 7px 7px 0; 211 | } 212 | 213 | section { 214 | .block { 215 | margin-bottom: 40px; 216 | ul { 217 | margin: 0; 218 | padding: 0 0 0 40px; 219 | } 220 | 221 | .buttons { 222 | display: block; 223 | text-align: center; 224 | } 225 | 226 | .code { 227 | padding: 20px; 228 | margin-bottom: 20px; 229 | overflow-y: auto; 230 | } 231 | code { 232 | font-family: 'Fira Mono', monospace; 233 | font-size: 14px; 234 | } 235 | 236 | .value { 237 | text-align: center; 238 | padding-top: 20px; 239 | font-weight: 500; 240 | font-size: 24px; 241 | } 242 | } 243 | } 244 | 245 | 246 | .install { 247 | .code { 248 | margin-bottom: 40px; 249 | } 250 | } 251 | 252 | .demo-panel { 253 | margin-bottom: 80px; 254 | border: 1px #2d2d2d solid; 255 | .slider { 256 | padding: 40px; 257 | } 258 | } 259 | 260 | .demo-panel-title { 261 | padding: 10px 20px; 262 | background-color: #2d2d2d; 263 | background-color: smoke; 264 | border-bottom: 1px lighten(#37474F, 50%) solid; 265 | overflow: hidden; 266 | &:before, 267 | &:after { 268 | display: table; 269 | content: ' '; 270 | } 271 | &:after { 272 | clear: both; 273 | } 274 | h4, a { 275 | color: #ffffff; 276 | font-weight: 400; 277 | } 278 | h4 { 279 | float: left; 280 | font-size: 16px; 281 | } 282 | a { 283 | height: 18px; 284 | margin-top: 2px; 285 | margin-left: 20px; 286 | display: block; 287 | float: right; 288 | font-size: 12px; 289 | text-decoration: none; 290 | text-transform: uppercase; 291 | color: white; 292 | } 293 | #source { 294 | padding-left: 35px; 295 | background: url(./images/code.svg) no-repeat left center; 296 | background-size: contain; 297 | } 298 | #codesandbox { 299 | padding-left: 20px; 300 | background: url(./images/play.svg) no-repeat left center; 301 | background-size: contain; 302 | } 303 | } 304 | .demo-panel-content { 305 | padding-top: 20px; 306 | background-color: #ffffff; 307 | border-bottom-left-radius: 4px; 308 | border-bottom-right-radius: 4px; 309 | .code { 310 | display: none; 311 | } 312 | &.source { 313 | .code { 314 | display: block; 315 | margin-bottom: 0; 316 | } 317 | } 318 | } 319 | 320 | .custom-labels { 321 | .rangeslider-horizontal { 322 | .rangeslider__handle { 323 | width: 34px; 324 | height: 34px; 325 | border-radius: 30px; 326 | text-align: center; 327 | &:after { 328 | position: initial; 329 | } 330 | } 331 | .rangeslider__label { 332 | top: 18px; 333 | } 334 | .rangeslider__handle-tooltip { 335 | width: 60px; 336 | left: 50%; 337 | transform: translate3d(-50%, 0, 0); 338 | } 339 | .rangeslider__handle-label { 340 | position: absolute; 341 | top: 50%; 342 | left: 50%; 343 | transform: translate3d(-50%, -50%, 0); 344 | } 345 | } 346 | } 347 | 348 | .slider-group { 349 | display: flex; 350 | align-items: center; 351 | justify-content: center; 352 | } 353 | .slider-vertical, 354 | .slider-horizontal { 355 | flex: 1; 356 | } 357 | .slider-vertical { 358 | .rangeslider-vertical { 359 | width: 4px; 360 | } 361 | .rangeslider__fill { 362 | background-color: #EF5350; 363 | } 364 | .rangeslider__handle { 365 | width: 30px; 366 | height: 30px; 367 | left: -13px; 368 | border-radius: 50%; 369 | background-color: #fff; 370 | box-shadow: 0 1px 1px #333; 371 | } 372 | } 373 | .slider-horizontal { 374 | .rangeslider-horizontal, 375 | .rangeslider__fill, 376 | .rangeslider__handle { 377 | border-radius: 0; 378 | } 379 | .rangeslider-horizontal { 380 | height: 10px; 381 | } 382 | .rangeslider__fill { 383 | background-color: #1E88E5; 384 | } 385 | .rangeslider__handle { 386 | width: 10px; 387 | height: 30px; 388 | &:after { 389 | display: none; 390 | } 391 | } 392 | } 393 | 394 | .hearts { 395 | color: #EF5350; 396 | } 397 | 398 | footer { 399 | margin: 60px auto; 400 | p { 401 | margin-bottom: 4px; 402 | } 403 | .close { 404 | padding-top: 40px; 405 | text-align: center; 406 | > p { 407 | margin-bottom: 0; 408 | padding-bottom: 5px; 409 | line-height: 14px; 410 | font-size: 14px; 411 | color: #757575; 412 | } 413 | } 414 | } 415 | 416 | @media screen and (max-width: 1200px) { 417 | header, section { 418 | .block { 419 | width: 100%; 420 | margin-left: auto; 421 | margin-right: auto; 422 | padding: 0 20px; 423 | } 424 | } 425 | #source, 426 | #codesandbox { 427 | display: none; 428 | } 429 | footer.block { 430 | margin-bottom: 60px; 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /docs/components/Codeblock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import marked from 'marked' 3 | 4 | class Codeblock extends Component { 5 | componentWillMount () { 6 | marked.setOptions({ 7 | gfm: true, 8 | tables: true, 9 | breaks: false, 10 | pedantic: false, 11 | sanitize: false, 12 | smartLists: true, 13 | smartypants: false, 14 | highlight: function (code, lang) { 15 | return require('highlight.js').highlight(lang, code).value 16 | } 17 | }) 18 | } 19 | 20 | render () { 21 | const text = `\`\`\`js 22 | ${this.props.children} 23 | \`\`\`` 24 | 25 | return ( 26 |
30 | ) 31 | } 32 | } 33 | 34 | export default Codeblock 35 | -------------------------------------------------------------------------------- /docs/components/Demo.js: -------------------------------------------------------------------------------- 1 | import cx from 'classnames' 2 | import React, { Component } from 'react' 3 | 4 | class Demo extends Component { 5 | constructor (props, context) { 6 | super(props, context) 7 | this.state = { 8 | source: false 9 | } 10 | } 11 | 12 | handleToggle = e => { 13 | e.preventDefault() 14 | this.setState({ 15 | source: !this.state.source 16 | }) 17 | }; 18 | 19 | render () { 20 | const { source } = this.state 21 | const { title, children } = this.props 22 | const className = cx('demo-panel-content', { source: source }) 23 | return ( 24 |
25 |
26 |

{title}

27 | 28 | View Source 29 | 30 | 31 | Code Sandbox 32 | 33 |
34 |
35 | {children} 36 |
37 |
38 | ) 39 | } 40 | } 41 | 42 | export default Demo 43 | -------------------------------------------------------------------------------- /docs/components/Examples.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Horizontal, Negative, Float, Labels, Orientation } from './sliders' 3 | import Demo from './Demo' 4 | import Codeblock from './Codeblock' 5 | 6 | import horizontalExample from '!raw!./sliders/horizontal' 7 | import negativeExample from '!raw!./sliders/negative' 8 | import floatExample from '!raw!./sliders/float' 9 | import labelsExample from '!raw!./sliders/labels' 10 | import orientationExample from '!raw!./sliders/orientation' 11 | 12 | class Examples extends Component { 13 | render () { 14 | return ( 15 |
16 |

Examples

17 | 18 | 19 | 20 | {horizontalExample} 21 | 22 | 23 | 24 | 25 | 26 | {negativeExample} 27 | 28 | 29 | 30 | 31 | 32 | {floatExample} 33 | 34 | 35 | 36 | 37 | 38 | {labelsExample} 39 | 40 | 41 | 42 | 43 | 44 | {orientationExample} 45 | 46 | 47 |
48 | ) 49 | } 50 | } 51 | 52 | export default Examples 53 | -------------------------------------------------------------------------------- /docs/components/Features.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Features () { 4 | return ( 5 |
6 |

Features

7 |
    8 |
  • Touchscreen friendly
  • 9 |
  • Suitable for use within responsive designs
  • 10 |
  • Small and fast (8.1Kb Gzipped)
  • 11 |
12 |
13 | ) 14 | } 15 | 16 | export default Features 17 | -------------------------------------------------------------------------------- /docs/components/Install.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Codeblock from './Codeblock' 3 | 4 | function Install () { 5 | return ( 6 |
7 |

Installation

8 |

9 | Using npm (use --save to include it in your package.json) 10 |

11 | 12 | {`$ npm install react-rangeslider --save`} 13 | 14 |

15 | Using yarn (use --dev to include it in your package.json) 16 |

17 | 18 | {`$ yarn add react-rangeslider --save`} 19 | 20 |
21 | ) 22 | } 23 | 24 | export default Install 25 | -------------------------------------------------------------------------------- /docs/components/Usage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Codeblock from './Codeblock' 3 | 4 | class Usage extends Component { 5 | render () { 6 | const text = `// Using an ES6 transpiler like Babel 7 | import Slider from 'react-rangeslider' 8 | 9 | // To include the default styles 10 | import 'react-rangeslider/lib/index.css' 11 | 12 | // Not using an ES6 transpiler 13 | var Slider = require('react-rangeslider') 14 | ` 15 | 16 | const umdJs = `` 17 | const umdCss = `` 18 | 19 | return ( 20 |
21 |

Usage

22 |

23 | React-Rangeslider is bundled with a single slider component. By default, basic styles are applied, but can be overridden depending on your design requirements. 24 |

25 |

26 | With a module bundler like webpack that supports either CommonJS or ES2015 modules, use as you would anything else: 27 |

28 | 29 | {text} 30 | 31 |

32 | The UMD build is also available on 33 | {' '} 34 | 35 | unpkg: 36 | 37 |

38 | 39 | {umdJs} 40 | 41 |

42 | You can find the library on 43 | {' '} 44 | window.ReactRangeslider 45 | . Optionally you can drop in the default styles by adding the stylesheet. 46 |

47 | 48 | {umdCss} 49 | 50 |
51 | ) 52 | } 53 | } 54 | 55 | export default Usage 56 | -------------------------------------------------------------------------------- /docs/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Footer () { 4 | return ( 5 |
6 |
7 |

8 | The project is under Open Source 9 | {' '} 10 | 11 | MIT License 12 | 13 |

14 |

15 | Built with 16 | {' '} 17 | 18 | {' '} 19 | • Maintained by 20 | {' '} 21 | whoisandy 22 |

23 |

© 2015 Bhargav Anand

24 |
25 |
26 | ) 27 | } 28 | 29 | export default Footer 30 | -------------------------------------------------------------------------------- /docs/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import GitHubButton from 'react-github-button' 3 | 4 | function Header () { 5 | return ( 6 |
7 |

React Rangeslider

8 |

9 | A fast & lightweight react component as a drop in replacement for HTML5 input range slider element. 10 |

11 |

12 | Please refer to the source on 13 | {' '} 14 | Github 15 |

16 |
17 | 23 | 29 |
30 |
31 | ) 32 | } 33 | 34 | export default Header 35 | -------------------------------------------------------------------------------- /docs/components/sliders/float.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Slider from 'react-rangeslider' 3 | 4 | class Float extends Component { 5 | constructor (props, context) { 6 | super(props, context) 7 | this.state = { 8 | value: 12.5 9 | } 10 | } 11 | 12 | handleChange = (value) => { 13 | this.setState({ 14 | value: value 15 | }) 16 | } 17 | 18 | render () { 19 | const { value } = this.state 20 | return ( 21 |
22 | 29 |
{value}
30 |
31 | ) 32 | } 33 | } 34 | 35 | export default Float 36 | -------------------------------------------------------------------------------- /docs/components/sliders/horizontal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Slider from 'react-rangeslider' 3 | 4 | class Horizontal extends Component { 5 | constructor (props, context) { 6 | super(props, context) 7 | this.state = { 8 | value: 10 9 | } 10 | } 11 | 12 | handleChangeStart = () => { 13 | console.log('Change event started') 14 | }; 15 | 16 | handleChange = value => { 17 | this.setState({ 18 | value: value 19 | }) 20 | }; 21 | 22 | handleChangeComplete = () => { 23 | console.log('Change event completed') 24 | }; 25 | 26 | render () { 27 | const { value } = this.state 28 | return ( 29 |
30 | 38 |
{value}
39 |
40 | ) 41 | } 42 | } 43 | 44 | export default Horizontal 45 | -------------------------------------------------------------------------------- /docs/components/sliders/index.js: -------------------------------------------------------------------------------- 1 | export { default as Horizontal } from './horizontal' 2 | export { default as Float } from './float' 3 | export { default as Negative } from './negative' 4 | export { default as Labels } from './labels' 5 | export { default as Orientation } from './orientation' 6 | -------------------------------------------------------------------------------- /docs/components/sliders/labels.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Slider from 'react-rangeslider' 3 | 4 | class HorizontalCustomLabels extends Component { 5 | constructor (props, context) { 6 | super(props, context) 7 | this.state = { 8 | horizontal: 10, 9 | vertical: 50 10 | } 11 | } 12 | 13 | handleChangeHorizontal = value => { 14 | this.setState({ 15 | horizontal: value 16 | }) 17 | }; 18 | 19 | handleChangeVertical = value => { 20 | this.setState({ 21 | vertical: value 22 | }) 23 | }; 24 | 25 | render () { 26 | const { horizontal, vertical } = this.state 27 | const horizontalLabels = { 28 | 0: 'Low', 29 | 50: 'Medium', 30 | 100: 'High' 31 | } 32 | 33 | const verticalLabels = { 34 | 10: 'Getting started', 35 | 50: 'Half way', 36 | 90: 'Almost done', 37 | 100: 'Complete!' 38 | } 39 | 40 | const formatkg = value => value + ' kg' 41 | const formatPc = p => p + '%' 42 | 43 | return ( 44 |
45 | 54 |
{formatkg(horizontal)}
55 |
56 | 65 |
{formatPc(vertical)}
66 |
67 | ) 68 | } 69 | } 70 | 71 | export default HorizontalCustomLabels 72 | -------------------------------------------------------------------------------- /docs/components/sliders/negative.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Slider from 'react-rangeslider' 3 | 4 | class Negative extends Component { 5 | constructor (props, context) { 6 | super(props, context) 7 | this.state = { 8 | value: -10 9 | } 10 | } 11 | 12 | handleChange = (value) => { 13 | this.setState({ 14 | value: value 15 | }) 16 | } 17 | 18 | render () { 19 | const { value } = this.state 20 | return ( 21 |
22 | 29 |
{value}
30 |
31 | ) 32 | } 33 | } 34 | 35 | export default Negative 36 | -------------------------------------------------------------------------------- /docs/components/sliders/orientation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Slider from 'react-rangeslider' 3 | 4 | class Vertical extends Component { 5 | constructor (props, context) { 6 | super(props, context) 7 | this.state = { 8 | value: 25, 9 | reverseValue: 8 10 | } 11 | } 12 | 13 | handleChange = (value) => { 14 | this.setState({ 15 | value: value 16 | }) 17 | } 18 | 19 | handleChangeReverse = (value) => { 20 | this.setState({ 21 | reverseValue: value 22 | }) 23 | } 24 | 25 | render () { 26 | const { value, reverseValue } = this.state 27 | return ( 28 |
29 |
30 |
31 | 38 |
{value}
39 |
40 |
41 | 48 |
{reverseValue}
49 |
50 |
51 |
52 | ) 53 | } 54 | } 55 | 56 | export default Vertical 57 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisandy/react-rangeslider/28b957e41feb78624a52ecee204018e928873b6f/docs/favicon.ico -------------------------------------------------------------------------------- /docs/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisandy/react-rangeslider/28b957e41feb78624a52ecee204018e928873b6f/docs/images/bg.png -------------------------------------------------------------------------------- /docs/images/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | code 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/images/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | play 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/images/rangeslider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisandy/react-rangeslider/28b957e41feb78624a52ecee204018e928873b6f/docs/images/rangeslider.png -------------------------------------------------------------------------------- /docs/images/rangeslider_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisandy/react-rangeslider/28b957e41feb78624a52ecee204018e928873b6f/docs/images/rangeslider_dark.png -------------------------------------------------------------------------------- /docs/images/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whoisandy/react-rangeslider/28b957e41feb78624a52ecee204018e928873b6f/docs/images/slider.png -------------------------------------------------------------------------------- /docs/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Rangeslider 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReactGA from 'react-ga' 4 | import App from './app' 5 | 6 | function render () { 7 | const mount = document.getElementById('mount') 8 | ReactGA.initialize('UA-100351333-1') 9 | ReactGA.ga('send', 'pageview', '/') 10 | ReactDOM.render(, mount) 11 | } 12 | 13 | render() 14 | -------------------------------------------------------------------------------- /docs/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const webpackDevMiddleware = require('webpack-dev-middleware') 4 | const webpackHotMiddleware = require('webpack-hot-middleware') 5 | const config = require('./webpack.config') 6 | 7 | const app = new (require('express'))() 8 | const port = 3000 9 | 10 | const compiler = webpack(config) 11 | app.use(webpackDevMiddleware(compiler, { 12 | noInfo: true, 13 | publicPath: config.output.publicPath 14 | })) 15 | app.use(webpackHotMiddleware(compiler)) 16 | 17 | app.get('/', (req, res) => { 18 | res.sendFile(path.join(__dirname, 'index.html')) 19 | }) 20 | 21 | app.listen(port, (err) => { 22 | if (err) { 23 | console.log(err) 24 | } else { 25 | console.log(`Listening on ${port}`) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var webpack = require('webpack') 5 | var ExtractPlugin = require('extract-text-webpack-plugin') 6 | var HtmlPlugin = require('html-webpack-plugin') 7 | var config = { 8 | devtool: '#cheap-eval-source-map', 9 | 10 | entry: process.env.NODE_ENV === 'development' 11 | ? [ 12 | 'webpack-hot-middleware/client?http://localhost:3000', 13 | path.join(__dirname, 'index') 14 | ] 15 | : path.join(__dirname, 'index'), 16 | 17 | output: { 18 | path: process.env.NODE_ENV === 'development' ? __dirname : 'public', 19 | publicPath: process.env.NODE_ENV === 'development' ? '/static/' : '', 20 | filename: 'bundle.js' 21 | }, 22 | 23 | resolve: { 24 | extensions: ['', '.js', '.css', '.less'], 25 | alias: { 26 | 'react-rangeslider': path.join(__dirname, '../src/index.js') 27 | } 28 | }, 29 | 30 | module: { 31 | loaders: [ 32 | { 33 | test: /\.jsx?$/, 34 | exclude: /node_modules/, 35 | loader: 'babel' 36 | }, 37 | { 38 | test: /\.css$/, 39 | exclude: /node_modules/, 40 | loader: 'style!css' 41 | }, 42 | { 43 | test: /\.txt$/, 44 | exclude: /node_modules/, 45 | loader: 'raw' 46 | }, 47 | { 48 | test: /\.(jpg|png|svg)$/, 49 | loader: 'url-loader', 50 | options: { 51 | limit: 25000 52 | } 53 | } 54 | ] 55 | }, 56 | 57 | externals: { 58 | react: 'React', 59 | 'react-dom': 'ReactDOM', 60 | 'react-ga': 'ReactGA' 61 | }, 62 | 63 | plugins: [] 64 | } 65 | 66 | // Dev config 67 | if (process.env.NODE_ENV === 'development') { 68 | config.module.loaders.push({ 69 | test: /\.less$/, 70 | exclude: /node_modules/, 71 | loader: 'style!css!less' 72 | }) 73 | config.plugins.push( 74 | new webpack.DefinePlugin({ 75 | 'process.env': { 76 | NODE_ENV: JSON.stringify('development') 77 | } 78 | }), 79 | new webpack.NoErrorsPlugin(), 80 | new webpack.HotModuleReplacementPlugin() 81 | ) 82 | } 83 | 84 | // Build config 85 | if (process.env.NODE_ENV === 'production') { 86 | config.module.loaders.push([ 87 | { 88 | test: /\.less$/, 89 | exclude: /node_modules/, 90 | loader: ExtractPlugin.extract('style-loader', 'css-loader!less-loader') 91 | } 92 | ]) 93 | config.plugins.push( 94 | new webpack.DefinePlugin({ 95 | 'process.env': { 96 | NODE_ENV: JSON.stringify('production') 97 | } 98 | }), 99 | new webpack.optimize.UglifyJsPlugin({ 100 | minimize: true, 101 | compress: { 102 | unused: true, 103 | dead_code: true, 104 | warnings: false 105 | } 106 | }), 107 | new webpack.optimize.OccurenceOrderPlugin(), 108 | new ExtractPlugin('bundle.css'), 109 | new HtmlPlugin({ 110 | appMountId: 'mount', 111 | title: 'React RangeSlider', 112 | template: 'docs/index.ejs' 113 | }) 114 | ) 115 | } 116 | 117 | module.exports = config 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-rangeslider", 3 | "version": "2.2.0", 4 | "description": "A lightweight react component that acts as a HTML5 input range slider polyfill", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean:lib": "del lib umd", 8 | "clean:docs": "del public", 9 | "clean": "npm run clean:lib && npm run clean:docs", 10 | "lint": "eslint src docs", 11 | "test": "npm run lint && jest", 12 | "coverage": "npm test -- --coverage", 13 | "start": "cross-env NODE_ENV=development node -r babel-register docs/server.js", 14 | "build:less": "lessc ./src/Rangeslider.less ./lib/index.css", 15 | "build:less:umd": "lessc ./src/Rangeslider.less ./umd/rangeslider.css", 16 | "build:less:umd:min": "lessc --clean-css ./src/Rangeslider.less ./umd/rangeslider.min.css", 17 | "build:lib": "cross-env NODE_ENV=production babel ./src --stage 0 -d ./lib --ignore __tests__", 18 | "build:umd": "cross-env NODE_ENV=production webpack ./src/index.js ./umd/rangeslider.js", 19 | "build:min": "cross-env NODE_ENV=production webpack -p ./src/index.js ./umd/rangeslider.min.js", 20 | "build:docs": "cross-env NODE_ENV=production webpack -p --config=docs/webpack.config.js", 21 | "build": "npm run build:less && npm run build:lib && npm run build:umd && npm run build:min && npm run build:less:umd && npm run build:less:umd:min", 22 | "prebuild": "npm run clean:lib && npm test", 23 | "docs": "npm run clean:docs && npm run build:docs && cpy docs/favicon.ico public/", 24 | "deploy": "npm run docs && gh-pages -d public", 25 | "postpublish": "git push origin master --follow-tags", 26 | "minor": "npm version minor && npm publish", 27 | "major": "npm version major && npm publish", 28 | "patch": "npm version patch && npm publish" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/whoisandy/react-rangeslider.git" 33 | }, 34 | "files": [ 35 | "lib", 36 | "umd" 37 | ], 38 | "keywords": [ 39 | "rangeslider", 40 | "range-slider", 41 | "react-rangeslider", 42 | "input", 43 | "range", 44 | "react", 45 | "slider" 46 | ], 47 | "author": { 48 | "name": "Bhargav Anand", 49 | "email": "rjn143@gmail.com", 50 | "url": "github.com/whoisandy" 51 | }, 52 | "engines": { 53 | "node": ">=4" 54 | }, 55 | "license": "MIT", 56 | "bugs": { 57 | "url": "https://github.com/whoisandy/react-rangeslider/issues" 58 | }, 59 | "homepage": "https://github.com/whoisandy/react-rangeslider#readme", 60 | "devDependencies": { 61 | "babel": "^6.5.2", 62 | "babel-cli": "^6.18.0", 63 | "babel-core": "^6.14.0", 64 | "babel-eslint": "^6.1.2", 65 | "babel-jest": "^15.0.0", 66 | "babel-loader": "^6.2.5", 67 | "babel-preset-es2015": "^6.14.0", 68 | "babel-preset-react": "^6.11.1", 69 | "babel-preset-stage-0": "^6.5.0", 70 | "babel-register": "^6.14.0", 71 | "cpy-cli": "^1.0.1", 72 | "cross-env": "^2.0.1", 73 | "css-loader": "^0.25.0", 74 | "del-cli": "^0.2.1", 75 | "enzyme": "^2.4.1", 76 | "eslint": "^3.5.0", 77 | "eslint-config-standard": "^6.0.0", 78 | "eslint-config-standard-jsx": "^3.0.0", 79 | "eslint-config-standard-react": "^4.0.0", 80 | "eslint-loader": "^1.0.0", 81 | "eslint-plugin-import": "^1.14.0", 82 | "eslint-plugin-prettier": "^2.3.1", 83 | "eslint-plugin-promise": "^3.0.0", 84 | "eslint-plugin-react": "^6.2.0", 85 | "eslint-plugin-standard": "^2.0.0", 86 | "express": "^4.13.4", 87 | "extract-text-webpack-plugin": "^1.0.1", 88 | "file-loader": "^0.11.1", 89 | "gh-pages": "^0.11.0", 90 | "highlight.js": "^9.9.0", 91 | "html-webpack-plugin": "^2.22.0", 92 | "jest": "^15.1.1", 93 | "less": "^2.7.2", 94 | "less-loader": "^2.2.3", 95 | "less-plugin-clean-css": "^1.5.1", 96 | "marked": "^0.3.6", 97 | "opn-cli": "^3.1.0", 98 | "prop-types": "^15.5.9", 99 | "raw-loader": "^0.5.1", 100 | "react": "^15.3.1", 101 | "react-addons-test-utils": "^15.3.1", 102 | "react-dom": "^15.3.1", 103 | "react-ga": "^2.2.0", 104 | "react-github-button": "^0.1.11", 105 | "react-test-renderer": "^15.5.4", 106 | "style-loader": "^0.13.1", 107 | "url-loader": "^0.5.8", 108 | "webpack": "^1.13.0", 109 | "webpack-dev-middleware": "^1.6.1", 110 | "webpack-hot-middleware": "^2.10.0" 111 | }, 112 | "dependencies": { 113 | "classnames": "^2.2.3", 114 | "resize-observer-polyfill": "^1.4.2" 115 | }, 116 | "peerDependencies": { 117 | "react": "^0.14.0 || ^15.0.0" 118 | }, 119 | "jest": { 120 | "moduleNameMapper": { 121 | ".*\\.less$": "/src/" 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Rangeslider.js: -------------------------------------------------------------------------------- 1 | /* eslint no-debugger: "warn" */ 2 | import cx from 'classnames' 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | import ResizeObserver from 'resize-observer-polyfill' 6 | import { capitalize, clamp } from './utils' 7 | 8 | /** 9 | * Predefined constants 10 | * @type {Object} 11 | */ 12 | const constants = { 13 | orientation: { 14 | horizontal: { 15 | dimension: 'width', 16 | direction: 'left', 17 | reverseDirection: 'right', 18 | coordinate: 'x' 19 | }, 20 | vertical: { 21 | dimension: 'height', 22 | direction: 'top', 23 | reverseDirection: 'bottom', 24 | coordinate: 'y' 25 | } 26 | } 27 | } 28 | 29 | class Slider extends Component { 30 | static propTypes = { 31 | min: PropTypes.number, 32 | max: PropTypes.number, 33 | step: PropTypes.number, 34 | value: PropTypes.number, 35 | orientation: PropTypes.string, 36 | tooltip: PropTypes.bool, 37 | reverse: PropTypes.bool, 38 | labels: PropTypes.object, 39 | handleLabel: PropTypes.string, 40 | format: PropTypes.func, 41 | onChangeStart: PropTypes.func, 42 | onChange: PropTypes.func, 43 | onChangeComplete: PropTypes.func 44 | }; 45 | 46 | static defaultProps = { 47 | min: 0, 48 | max: 100, 49 | step: 1, 50 | value: 0, 51 | orientation: 'horizontal', 52 | tooltip: true, 53 | reverse: false, 54 | labels: {}, 55 | handleLabel: '' 56 | }; 57 | 58 | constructor (props, context) { 59 | super(props, context) 60 | 61 | this.state = { 62 | active: false, 63 | limit: 0, 64 | grab: 0 65 | } 66 | } 67 | 68 | componentDidMount () { 69 | this.handleUpdate() 70 | const resizeObserver = new ResizeObserver(this.handleUpdate) 71 | resizeObserver.observe(this.slider) 72 | } 73 | 74 | /** 75 | * Format label/tooltip value 76 | * @param {Number} - value 77 | * @return {Formatted Number} 78 | */ 79 | handleFormat = value => { 80 | const { format } = this.props 81 | return format ? format(value) : value 82 | }; 83 | 84 | /** 85 | * Update slider state on change 86 | * @return {void} 87 | */ 88 | handleUpdate = () => { 89 | if (!this.slider) { 90 | // for shallow rendering 91 | return 92 | } 93 | const { orientation } = this.props 94 | const dimension = capitalize(constants.orientation[orientation].dimension) 95 | const sliderPos = this.slider[`offset${dimension}`] 96 | const handlePos = this.handle[`offset${dimension}`] 97 | 98 | this.setState({ 99 | limit: sliderPos - handlePos, 100 | grab: handlePos / 2 101 | }) 102 | }; 103 | 104 | /** 105 | * Attach event listeners to mousemove/mouseup events 106 | * @return {void} 107 | */ 108 | handleStart = e => { 109 | const { onChangeStart } = this.props 110 | document.addEventListener('mousemove', this.handleDrag) 111 | document.addEventListener('mouseup', this.handleEnd) 112 | this.setState( 113 | { 114 | active: true 115 | }, 116 | () => { 117 | onChangeStart && onChangeStart(e) 118 | } 119 | ) 120 | }; 121 | 122 | /** 123 | * Handle drag/mousemove event 124 | * @param {Object} e - Event object 125 | * @return {void} 126 | */ 127 | handleDrag = e => { 128 | e.stopPropagation() 129 | const { onChange } = this.props 130 | const { target: { className, classList, dataset } } = e 131 | if (!onChange || className === 'rangeslider__labels') return 132 | 133 | let value = this.position(e) 134 | 135 | if ( 136 | classList && 137 | classList.contains('rangeslider__label-item') && 138 | dataset.value 139 | ) { 140 | value = parseFloat(dataset.value) 141 | } 142 | 143 | onChange && onChange(value, e) 144 | }; 145 | 146 | /** 147 | * Detach event listeners to mousemove/mouseup events 148 | * @return {void} 149 | */ 150 | handleEnd = e => { 151 | const { onChangeComplete } = this.props 152 | this.setState( 153 | { 154 | active: false 155 | }, 156 | () => { 157 | onChangeComplete && onChangeComplete(e) 158 | } 159 | ) 160 | document.removeEventListener('mousemove', this.handleDrag) 161 | document.removeEventListener('mouseup', this.handleEnd) 162 | }; 163 | 164 | /** 165 | * Support for key events on the slider handle 166 | * @param {Object} e - Event object 167 | * @return {void} 168 | */ 169 | handleKeyDown = e => { 170 | e.preventDefault() 171 | const { keyCode } = e 172 | const { value, min, max, step, onChange } = this.props 173 | let sliderValue 174 | 175 | switch (keyCode) { 176 | case 38: 177 | case 39: 178 | sliderValue = value + step > max ? max : value + step 179 | onChange && onChange(sliderValue, e) 180 | break 181 | case 37: 182 | case 40: 183 | sliderValue = value - step < min ? min : value - step 184 | onChange && onChange(sliderValue, e) 185 | break 186 | } 187 | }; 188 | 189 | /** 190 | * Calculate position of slider based on its value 191 | * @param {number} value - Current value of slider 192 | * @return {position} pos - Calculated position of slider based on value 193 | */ 194 | getPositionFromValue = value => { 195 | const { limit } = this.state 196 | const { min, max } = this.props 197 | const diffMaxMin = max - min 198 | const diffValMin = value - min 199 | const percentage = diffValMin / diffMaxMin 200 | const pos = Math.round(percentage * limit) 201 | 202 | return pos 203 | }; 204 | 205 | /** 206 | * Translate position of slider to slider value 207 | * @param {number} pos - Current position/coordinates of slider 208 | * @return {number} value - Slider value 209 | */ 210 | getValueFromPosition = pos => { 211 | const { limit } = this.state 212 | const { orientation, min, max, step } = this.props 213 | const percentage = clamp(pos, 0, limit) / (limit || 1) 214 | const baseVal = step * Math.round(percentage * (max - min) / step) 215 | const value = orientation === 'horizontal' ? baseVal + min : max - baseVal 216 | 217 | return clamp(value, min, max) 218 | }; 219 | 220 | /** 221 | * Calculate position of slider based on value 222 | * @param {Object} e - Event object 223 | * @return {number} value - Slider value 224 | */ 225 | position = e => { 226 | const { grab } = this.state 227 | const { orientation, reverse } = this.props 228 | 229 | const node = this.slider 230 | const coordinateStyle = constants.orientation[orientation].coordinate 231 | const directionStyle = reverse 232 | ? constants.orientation[orientation].reverseDirection 233 | : constants.orientation[orientation].direction 234 | const clientCoordinateStyle = `client${capitalize(coordinateStyle)}` 235 | const coordinate = !e.touches 236 | ? e[clientCoordinateStyle] 237 | : e.touches[0][clientCoordinateStyle] 238 | const direction = node.getBoundingClientRect()[directionStyle] 239 | const pos = reverse 240 | ? direction - coordinate - grab 241 | : coordinate - direction - grab 242 | const value = this.getValueFromPosition(pos) 243 | 244 | return value 245 | }; 246 | 247 | /** 248 | * Grab coordinates of slider 249 | * @param {Object} pos - Position object 250 | * @return {Object} - Slider fill/handle coordinates 251 | */ 252 | coordinates = pos => { 253 | const { limit, grab } = this.state 254 | const { orientation } = this.props 255 | const value = this.getValueFromPosition(pos) 256 | const position = this.getPositionFromValue(value) 257 | const handlePos = orientation === 'horizontal' ? position + grab : position 258 | const fillPos = orientation === 'horizontal' 259 | ? handlePos 260 | : limit - handlePos 261 | 262 | return { 263 | fill: fillPos, 264 | handle: handlePos, 265 | label: handlePos 266 | } 267 | }; 268 | 269 | renderLabels = labels => ( 270 |
    { 272 | this.labels = sl 273 | }} 274 | className={cx('rangeslider__labels')} 275 | > 276 | {labels} 277 |
278 | ); 279 | 280 | render () { 281 | const { 282 | value, 283 | orientation, 284 | className, 285 | tooltip, 286 | reverse, 287 | labels, 288 | min, 289 | max, 290 | handleLabel 291 | } = this.props 292 | const { active } = this.state 293 | const dimension = constants.orientation[orientation].dimension 294 | const direction = reverse 295 | ? constants.orientation[orientation].reverseDirection 296 | : constants.orientation[orientation].direction 297 | const position = this.getPositionFromValue(value) 298 | const coords = this.coordinates(position) 299 | const fillStyle = { [dimension]: `${coords.fill}px` } 300 | const handleStyle = { [direction]: `${coords.handle}px` } 301 | let showTooltip = tooltip && active 302 | 303 | let labelItems = [] 304 | let labelKeys = Object.keys(labels) 305 | 306 | if (labelKeys.length > 0) { 307 | labelKeys = labelKeys.sort((a, b) => (reverse ? a - b : b - a)) 308 | 309 | for (let key of labelKeys) { 310 | const labelPosition = this.getPositionFromValue(key) 311 | const labelCoords = this.coordinates(labelPosition) 312 | const labelStyle = { [direction]: `${labelCoords.label}px` } 313 | 314 | labelItems.push( 315 |
  • 324 | {this.props.labels[key]} 325 |
  • 326 | ) 327 | } 328 | } 329 | 330 | return ( 331 |
    { 333 | this.slider = s 334 | }} 335 | className={cx( 336 | 'rangeslider', 337 | `rangeslider-${orientation}`, 338 | { 'rangeslider-reverse': reverse }, 339 | className 340 | )} 341 | onMouseDown={this.handleDrag} 342 | onMouseUp={this.handleEnd} 343 | onTouchStart={this.handleStart} 344 | onTouchEnd={this.handleEnd} 345 | aria-valuemin={min} 346 | aria-valuemax={max} 347 | aria-valuenow={value} 348 | aria-orientation={orientation} 349 | > 350 |
    351 |
    { 353 | this.handle = sh 354 | }} 355 | className='rangeslider__handle' 356 | onMouseDown={this.handleStart} 357 | onTouchMove={this.handleDrag} 358 | onTouchEnd={this.handleEnd} 359 | onKeyDown={this.handleKeyDown} 360 | style={handleStyle} 361 | tabIndex={0} 362 | > 363 | {showTooltip 364 | ?
    { 366 | this.tooltip = st 367 | }} 368 | className='rangeslider__handle-tooltip' 369 | > 370 | {this.handleFormat(value)} 371 |
    372 | : null} 373 |
    {handleLabel}
    374 |
    375 | {labels ? this.renderLabels(labelItems) : null} 376 |
    377 | ) 378 | } 379 | } 380 | 381 | export default Slider 382 | -------------------------------------------------------------------------------- /src/__tests__/Rangeslider.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow, mount } from 'enzyme' 3 | import renderer from 'react-test-renderer' 4 | import Slider from '../Rangeslider' 5 | 6 | describe('Rangeslider specs', () => { 7 | it('should render properly', () => { 8 | const slider = shallow() 9 | expect(slider.hasClass('rangeslider')).toBeTruthy() 10 | expect(slider.children().length).toEqual(3) 11 | expect(slider.find('.rangeslider__fill').length).toEqual(1) 12 | expect(slider.find('.rangeslider__handle').length).toEqual(1) 13 | }) 14 | 15 | it('should have default props', () => { 16 | const slider = mount() 17 | expect(slider.prop('min')).toEqual(0) 18 | expect(slider.prop('max')).toEqual(100) 19 | expect(slider.prop('step')).toEqual(1) 20 | expect(slider.prop('value')).toEqual(0) 21 | expect(slider.prop('orientation')).toEqual('horizontal') 22 | expect(slider.prop('reverse')).toEqual(false) 23 | expect(slider.prop('handleLabel')).toEqual('') 24 | expect(slider.prop('labels')).toEqual({}) 25 | }) 26 | 27 | it('should render basic slider with defaults', () => { 28 | const tree = renderer.create().toJSON() 29 | expect(tree).toMatchSnapshot() 30 | }) 31 | 32 | it('should render slider when props passed in', () => { 33 | const tree = renderer 34 | .create() 35 | .toJSON() 36 | expect(tree).toMatchSnapshot() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/__tests__/Sanity.spec.js: -------------------------------------------------------------------------------- 1 | describe('Sanity Specs', () => { 2 | it('should evaluate true to be truthy', () => { 3 | expect(true).toBeTruthy() 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/Rangeslider.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Rangeslider specs should render basic slider with defaults 1`] = ` 2 |
    12 |
    19 |
    31 |
    33 | 34 |
    35 |
    36 |
      38 |
    39 | `; 40 | 41 | exports[`Rangeslider specs should render slider when props passed in 1`] = ` 42 |
    52 |
    59 |
    71 |
    73 | 74 |
    75 |
    76 |
      78 |
    79 | `; 80 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Rangeslider from './Rangeslider' 2 | export default Rangeslider 3 | -------------------------------------------------------------------------------- /src/rangeslider.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Rangeslider 3 | */ 4 | .rangeslider { 5 | margin: 20px 0; 6 | position: relative; 7 | background: #e6e6e6; 8 | -ms-touch-action: none; 9 | touch-action: none; 10 | 11 | &, 12 | .rangeslider__fill { 13 | display: block; 14 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4); 15 | } 16 | .rangeslider__handle { 17 | background: #fff; 18 | border: 1px solid #ccc; 19 | cursor: pointer; 20 | display: inline-block; 21 | position: absolute; 22 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 -1px 3px rgba(0, 0, 0, 0.4); 23 | .rangeslider__active { 24 | opacity: 1; 25 | } 26 | } 27 | 28 | .rangeslider__handle-tooltip { 29 | width: 40px; 30 | height: 40px; 31 | text-align: center; 32 | position: absolute; 33 | background-color: rgba(0, 0, 0, 0.8); 34 | font-weight: normal; 35 | font-size: 14px; 36 | transition: all 100ms ease-in; 37 | border-radius: 4px; 38 | display: inline-block; 39 | color: white; 40 | left: 50%; 41 | transform: translate3d(-50%, 0, 0); 42 | span { 43 | margin-top: 12px; 44 | display: inline-block; 45 | line-height: 100%; 46 | } 47 | &:after { 48 | content: ' '; 49 | position: absolute; 50 | width: 0; 51 | height: 0; 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Rangeslider - Horizontal slider 58 | */ 59 | .rangeslider-horizontal { 60 | height: 12px; 61 | border-radius: 10px; 62 | .rangeslider__fill { 63 | height: 100%; 64 | background-color: #7cb342; 65 | border-radius: 10px; 66 | top: 0; 67 | } 68 | .rangeslider__handle { 69 | width: 30px; 70 | height: 30px; 71 | border-radius: 30px; 72 | top: 50%; 73 | transform: translate3d(-50%, -50%, 0); 74 | &:after { 75 | content: ' '; 76 | position: absolute; 77 | width: 16px; 78 | height: 16px; 79 | top: 6px; 80 | left: 6px; 81 | border-radius: 50%; 82 | background-color: #dadada; 83 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4) inset, 84 | 0 -1px 3px rgba(0, 0, 0, 0.4) inset; 85 | } 86 | } 87 | .rangeslider__handle-tooltip { 88 | top: -55px; 89 | &:after { 90 | border-left: 8px solid transparent; 91 | border-right: 8px solid transparent; 92 | border-top: 8px solid rgba(0, 0, 0, 0.8); 93 | left: 50%; 94 | bottom: -8px; 95 | transform: translate3d(-50%, 0, 0); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Rangeslider - Vertical slider 102 | */ 103 | .rangeslider-vertical { 104 | margin: 20px auto; 105 | height: 150px; 106 | max-width: 10px; 107 | background-color: transparent; 108 | 109 | .rangeslider__fill, 110 | .rangeslider__handle { 111 | position: absolute; 112 | } 113 | 114 | .rangeslider__fill { 115 | width: 100%; 116 | background-color: #7cb342; 117 | box-shadow: none; 118 | bottom: 0; 119 | } 120 | .rangeslider__handle { 121 | width: 30px; 122 | height: 10px; 123 | left: -10px; 124 | box-shadow: none; 125 | } 126 | .rangeslider__handle-tooltip { 127 | left: -100%; 128 | top: 50%; 129 | transform: translate3d(-50%, -50%, 0); 130 | &:after { 131 | border-top: 8px solid transparent; 132 | border-bottom: 8px solid transparent; 133 | border-left: 8px solid rgba(0, 0, 0, 0.8); 134 | left: 100%; 135 | top: 12px; 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Rangeslider - Reverse 142 | */ 143 | 144 | .rangeslider-reverse { 145 | &.rangeslider-horizontal { 146 | .rangeslider__fill { 147 | right: 0; 148 | } 149 | } 150 | &.rangeslider-vertical { 151 | .rangeslider__fill { 152 | top: 0; 153 | bottom: inherit; 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * Rangeslider - Labels 160 | */ 161 | .rangeslider__labels { 162 | position: relative; 163 | .rangeslider-vertical & { 164 | position: relative; 165 | list-style-type: none; 166 | margin: 0 0 0 24px; 167 | padding: 0; 168 | text-align: left; 169 | width: 250px; 170 | height: 100%; 171 | left: 10px; 172 | 173 | .rangeslider__label-item { 174 | position: absolute; 175 | transform: translate3d(0, -50%, 0); 176 | 177 | &::before { 178 | content: ''; 179 | width: 10px; 180 | height: 2px; 181 | background: black; 182 | position: absolute; 183 | left: -14px; 184 | top: 50%; 185 | transform: translateY(-50%); 186 | z-index: -1; 187 | } 188 | } 189 | } 190 | 191 | .rangeslider__label-item { 192 | position: absolute; 193 | font-size: 14px; 194 | cursor: pointer; 195 | display: inline-block; 196 | top: 10px; 197 | transform: translate3d(-50%, 0, 0); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Capitalize first letter of string 3 | * @private 4 | * @param {string} - String 5 | * @return {string} - String with first letter capitalized 6 | */ 7 | export function capitalize (str) { 8 | return str.charAt(0).toUpperCase() + str.substr(1) 9 | } 10 | 11 | /** 12 | * Clamp position between a range 13 | * @param {number} - Value to be clamped 14 | * @param {number} - Minimum value in range 15 | * @param {number} - Maximum value in range 16 | * @return {number} - Clamped value 17 | */ 18 | export function clamp (value, min, max) { 19 | return Math.min(Math.max(value, min), max) 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var ExtractPlugin = require('extract-text-webpack-plugin') 5 | 6 | module.exports = { 7 | entry: path.join(__dirname, 'src', 'index'), 8 | 9 | output: { 10 | library: 'ReactRangeslider', 11 | libraryTarget: 'umd' 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js?$/, 17 | exclude: /node_modules/, 18 | loader: 'babel' 19 | }, 20 | { 21 | test: /\.less$/, 22 | exclude: /node_modules/, 23 | loader: ExtractPlugin.extract('style-loader', 'css-loader!less-loader') 24 | } 25 | ] 26 | }, 27 | 28 | plugins: [new ExtractPlugin('RangeSlider.css')], 29 | 30 | externals: [ 31 | { 32 | react: { 33 | root: 'React', 34 | commonjs2: 'react', 35 | commonjs: 'react', 36 | amd: 'react' 37 | } 38 | } 39 | ] 40 | } 41 | --------------------------------------------------------------------------------