├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── demo ├── index.js └── style.css ├── package.json ├── react-number-editor.gif ├── src └── NumberEditor.jsx └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "latest", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "transform-object-rest-spread", 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | 8 | [*.{json,yml}] 9 | indent_size = 2 10 | 11 | [.*] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tleunen-react" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | lib/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | demo/ 3 | src/ 4 | webpack.config.js 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tommy Leunen 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-number-editor 2 | 3 | [![NPM](https://nodei.co/npm/react-number-editor.png)](https://nodei.co/npm/react-number-editor/) 4 | 5 | ![img](http://i.imgur.com/VIwMScb.gif) 6 | 7 | A [react](https://github.com/facebook/react) component to easily use number inputs. This one acts like those in After Effects or similar software. 8 | 9 | - Click and drag to slide the value. 10 | - Double-click to enter manually a new value. 11 | - Use your Up/Down keys to increment/decrement the value. 12 | - Hold shift key to step by bigger value. 13 | - Hold control/command key to step by smaller value. 14 | 15 | ## Example 16 | 17 | ```js 18 | var React = require('react'); 19 | var NumberEditor = require('react-number-editor'); 20 | 21 | React.render( 22 | , 23 | document.body 24 | ); 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### `` 30 | 31 | Here are the list of properties available for the component: 32 | 33 | - `min` (number) the minimum value. Default no minimum 34 | - `max` (number) the maximum value. Default no maximum 35 | - `step` (number) the step to increment when sliding and with up/down arrows. Default 1. 36 | - `stepModifier` (number) how much to multiply/divide with the modifier keys (shift and control/command). Default is 10. 37 | - `decimals` (number) the number of decimals to show. Default 0. 38 | - `initialValue` (number) the default value to show. Default 0. 39 | - `className` (string) the class name to apply to the DOM element. Default empty. 40 | - `onValueChange` (function) The callback when the value changes. The value is passed as the parameter. 41 | - onKeyDown (function) This callback is called when a key is pressed, after the control has processed the key press, and allows developers to implement their own shortcuts, etc. 42 | 43 | ## demo 44 | 45 | To run the demo, executes this command and go to `http://localhost:8080`: 46 | `npm run demo` 47 | 48 | ## License 49 | 50 | MIT, see [LICENSE.md](/LICENSE.md) for details. 51 | 52 | ## Thanks 53 | 54 | Thanks to [@mattdesl](https://github.com/mattdesl) for his work on [number-editor](https://github.com/mattdesl/number-editor). 55 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import NumberEditor from '../'; 6 | require("./style.css"); 7 | 8 | var KEYS = { 9 | K: 75, 10 | M: 77 11 | }; 12 | 13 | class Demo extends React.Component { 14 | constructor() { 15 | super(); 16 | this._onNumberChange = this._onNumberChange.bind(this); 17 | this._onKeyDown = this._onKeyDown.bind(this); 18 | 19 | this.state = { 20 | numberValue: 0 21 | }; 22 | } 23 | 24 | _onNumberChange(value) { 25 | this.setState({ 26 | numberValue: value 27 | }); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | 44 |
Value: {this.state.numberValue}
45 |

This control implements onKeyDown. Pressing 'k' will multiply the value by 1,000 and pressing 'm' will multiply the value by 1,000,000.

46 |
47 | ); 48 | } 49 | 50 | _onKeyDown(e) { 51 | var key = e.which; 52 | var value = this.state.numberValue; 53 | if(key === KEYS.K) { 54 | this._onNumberChange(value * 1000); 55 | } else if(key === KEYS.M) { 56 | this._onNumberChange(value * 1000000); 57 | } 58 | } 59 | } 60 | 61 | var container = document.createElement('div'); 62 | document.body.appendChild(container); 63 | render(, container); 64 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | html, body { height: 100%; } 2 | 3 | .spinner { 4 | width: 40px; 5 | border: 0; 6 | text-align: right; 7 | color: #393939; 8 | font-weight: bold; 9 | border-bottom: 1px dotted #393939; 10 | 11 | margin: 20px; 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-number-editor", 3 | "version": "4.0.3", 4 | "description": "Custom number editor (text field) react component", 5 | "main": "lib/NumberEditor.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/tleunen/react-number-editor.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/tleunen/react-number-editor/issues" 12 | }, 13 | "homepage": "https://github.com/tleunen/react-number-editor", 14 | "keywords": [ 15 | "react", 16 | "react-component", 17 | "number", 18 | "editor", 19 | "textfield", 20 | "dom", 21 | "element" 22 | ], 23 | "author": { 24 | "name": "Tommy Leunen", 25 | "email": "tommy.leunen@gmail.com", 26 | "url": "http://tommyleunen.com/" 27 | }, 28 | "license": "MIT", 29 | "dependencies": { 30 | "clamp": "^1.0.1", 31 | "prop-types": "^15.6.0", 32 | "react-clickdrag": "^3.0.2" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "^6.18.0", 36 | "babel-core": "^6.21.0", 37 | "babel-loader": "^6.2.10", 38 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 39 | "babel-preset-latest": "^6.16.0", 40 | "babel-preset-react": "^6.16.0", 41 | "css-loader": "^0.26.1", 42 | "eslint": "^3.12.2", 43 | "eslint-config-tleunen-react": "^1.0.3", 44 | "eslint-plugin-import": "^2.2.0", 45 | "eslint-plugin-jsx-a11y": "^2.2.3", 46 | "eslint-plugin-react": "^6.8.0", 47 | "html-webpack-plugin": "^2.24.1", 48 | "react": "^15.6.0", 49 | "react-dom": "^15.6.0", 50 | "style-loader": "^0.13.1", 51 | "webpack": "^1.14.0", 52 | "webpack-dev-server": "^1.16.2" 53 | }, 54 | "peerDependencies": { 55 | "react": "0.14.x || ^15.0.0-rc || ^16.0.0-rc", 56 | "react-dom": "0.14.x || ^15.0.0-rc || ^16.0.0-rc" 57 | }, 58 | "scripts": { 59 | "lint": "eslint src --ext .jsx", 60 | "demo": "webpack-dev-server", 61 | "compile": "babel src --out-dir lib", 62 | "prepublish": "npm run compile" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /react-number-editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tleunen/react-number-editor/f947e96d585c5e2320f4a7c6c29863a8bd17efd6/react-number-editor.gif -------------------------------------------------------------------------------- /src/NumberEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clickDrag from 'react-clickdrag'; 4 | import clamp from 'clamp'; 5 | 6 | const KEYS = { 7 | UP: 38, 8 | DOWN: 40, 9 | ENTER: 13, 10 | BACKSPACE: 8, 11 | }; 12 | 13 | const ALLOWED_KEYS = [ 14 | 8, // Backspace 15 | 9, // Tab 16 | 35, // End 17 | 36, // Home 18 | 37, // Left Arrow 19 | 39, // Right Arrow 20 | 46, // Delete 21 | 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, // 0 - 9 22 | 190, // (Dot) 23 | 189, 173, // (Minus) - [Multiple values across different browsers] 24 | 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, // Numpad 0-9 25 | 109, // Numpad - (Minus) 26 | 110, // Numpad . (Decimal point) 27 | ]; 28 | 29 | const propTypes = { 30 | className: PropTypes.string, 31 | decimals: PropTypes.number, 32 | max: PropTypes.number, 33 | min: PropTypes.number, 34 | onValueChange: PropTypes.func, 35 | step: PropTypes.number, 36 | stepModifier: PropTypes.number, 37 | style: PropTypes.object, 38 | value: PropTypes.oneOfType([ 39 | PropTypes.string, 40 | PropTypes.number, 41 | ]).isRequired, 42 | onKeyDown: PropTypes.func, 43 | }; 44 | 45 | const defaultProps = { 46 | className: '', 47 | decimals: 0, 48 | max: Number.MAX_VALUE, 49 | min: -Number.MAX_VALUE, 50 | onValueChange: () => { 51 | // do nothing 52 | }, 53 | step: 1, 54 | stepModifier: 10, 55 | style: {}, 56 | }; 57 | 58 | class NumberEditor extends React.Component { 59 | constructor(props) { 60 | super(props); 61 | 62 | this.onKeyDown = this.onKeyDown.bind(this); 63 | this.onDoubleClick = this.onDoubleClick.bind(this); 64 | this.onChange = this.onChange.bind(this); 65 | this.onBlur = this.onBlur.bind(this); 66 | 67 | this.state = { 68 | startEditing: false, 69 | wasUsingSpecialKeys: false, 70 | dragStartValue: Number(props.value), 71 | }; 72 | } 73 | 74 | componentWillReceiveProps(nextProps) { 75 | // start 76 | if (nextProps.dataDrag.isMouseDown && !nextProps.dataDrag.isMoving) { 77 | this.setState({ 78 | dragStartValue: Number(this.props.value), 79 | }); 80 | } 81 | 82 | if (nextProps.dataDrag.isMoving) { 83 | const step = this.getStepValue(nextProps.dataDrag, this.props.step); 84 | this.changeValue(this.state.dragStartValue + (nextProps.dataDrag.moveDeltaX * (step / 2))); 85 | } 86 | } 87 | 88 | onDoubleClick() { 89 | this.setState({ 90 | startEditing: true, 91 | }); 92 | } 93 | 94 | onChange(e) { 95 | this.props.onValueChange(e.target.value); 96 | } 97 | 98 | onBlur(e) { 99 | this.changeValue(Number(e.target.value)); 100 | this.setState({ 101 | startEditing: false, 102 | }); 103 | } 104 | 105 | onKeyDown(e) { 106 | const step = this.getStepValue(e, this.props.step); 107 | 108 | const value = Number(this.props.value); 109 | const key = e.which; 110 | 111 | if (key === KEYS.UP) { 112 | e.preventDefault(); 113 | this.changeValue(value + step); 114 | } else if (key === KEYS.DOWN) { 115 | e.preventDefault(); 116 | this.changeValue(value - step); 117 | } else if (key === KEYS.ENTER) { 118 | e.preventDefault(); 119 | if (this.state.startEditing) { 120 | // stop editing + save value 121 | this.onBlur(e); 122 | } else { 123 | this.setState({ 124 | startEditing: true, 125 | }); 126 | e.target.select(); 127 | } 128 | } else if (key === KEYS.BACKSPACE && !this.state.startEditing) { 129 | e.preventDefault(); 130 | } else if (ALLOWED_KEYS.indexOf(key) === -1) { 131 | // Suppress any key we are not allowing. 132 | e.preventDefault(); 133 | } 134 | 135 | if (this.props.onKeyDown) { 136 | this.props.onKeyDown(e); 137 | } 138 | } 139 | 140 | getStepValue(e, step) { 141 | let newStep = step; 142 | if (e.metaKey || e.ctrlKey) { 143 | newStep /= this.props.stepModifier; 144 | } else if (e.shiftKey) { 145 | newStep *= this.props.stepModifier; 146 | } 147 | 148 | return newStep; 149 | } 150 | 151 | changeValue(value) { 152 | const newVal = clamp(value.toFixed(this.props.decimals), this.props.min, this.props.max); 153 | 154 | if (Number(this.props.value) !== Number(newVal)) { 155 | this.props.onValueChange(newVal); 156 | } 157 | } 158 | 159 | render() { 160 | let cursor = 'ew-resize'; 161 | let readOnly = true; 162 | let value = this.props.value; 163 | if (this.state.startEditing) { 164 | cursor = 'auto'; 165 | readOnly = false; 166 | } 167 | 168 | if (!this.state.startEditing) { 169 | value = Number(value).toFixed(this.props.decimals); 170 | } 171 | 172 | return ( 173 | 184 | ); 185 | } 186 | } 187 | 188 | NumberEditor.propTypes = propTypes; 189 | NumberEditor.defaultProps = defaultProps; 190 | 191 | export default clickDrag(NumberEditor, { 192 | resetOnSpecialKeys: true, 193 | touch: true, 194 | getSpecificEventData: e => ({ 195 | metaKey: e.metaKey, 196 | ctrlKey: e.ctrlKey, 197 | shiftKey: e.shiftKey, 198 | }), 199 | onDragMove: (e) => { 200 | e.preventDefault(); 201 | }, 202 | }); 203 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlwebpackPlugin = require('html-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: './demo', 5 | output: { 6 | path: './build', 7 | filename: 'bundle.js' 8 | }, 9 | devtool: 'sourcemap', 10 | module: { 11 | loaders: [ 12 | { test: /\.js$/, loader: 'babel-loader' }, 13 | { test: /\.css$/, loader: 'style-loader!css-loader' } 14 | ] 15 | }, 16 | plugins: [ 17 | new HtmlwebpackPlugin({ 18 | title: 'React Number Editor Demo' 19 | }) 20 | ] 21 | }; 22 | --------------------------------------------------------------------------------