├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── .eslintrc ├── Autocomplete.test.js ├── Autosize.test.js ├── Combobox.test.js ├── DatePicker.test.js ├── Dropdown.test.js ├── Mask.test.js ├── __snapshots__ │ ├── Autocomplete.test.js.snap │ ├── Autosize.test.js.snap │ ├── Combobox.test.js.snap │ ├── DatePicker.test.js.snap │ ├── Dropdown.test.js.snap │ └── Mask.test.js.snap ├── jest.jsdom.config.json ├── jest.jsdom.setup.js ├── jest.node.config.json └── jest.node.setup.js ├── demo ├── dist │ ├── index.html │ └── js │ │ └── bundle.js └── src │ ├── .noderequirer.json │ ├── index.html │ └── js │ ├── DemoApp.jsx │ ├── bootstrap-input-inline.css │ ├── countries.js │ ├── ie.css │ ├── index.js │ └── pure.js ├── index.html ├── package.json ├── postcss.config.js ├── src ├── .noderequirer.json ├── Autocomplete.jsx ├── Autosize.jsx ├── Combobox.jsx ├── DatePicker.jsx ├── Dropdown.jsx ├── DropdownOption.jsx ├── InputPopup.jsx ├── Mask.jsx ├── applyMaskToString.js ├── createStyling.js ├── filters │ ├── filterByMatchingTextWithThreshold.js │ ├── filterRedudantSeparators.js │ ├── index.js │ ├── limitBy.js │ ├── notFoundMessage.js │ └── sortByMatchingText.js ├── index.js ├── shapes.js ├── themes │ └── default.js └── utils │ ├── deprecated.js │ ├── findMatchingTextIndex.js │ ├── getComputedStyle.js │ ├── getInput.js │ ├── getOptionLabel.js │ ├── getOptionText.js │ ├── getOptionValue.js │ ├── isStatic.js │ ├── registerInput.js │ └── renderChild.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-0", "react"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "no-console": "warn" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | lib 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | static 2 | src 3 | demo 4 | .* 5 | webpack.config.js 6 | postcss.config.js 7 | index.html 8 | __tests__ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.5.2 / 2016-09-18 2 | ------------------ 3 | * `DatePicker` value can be patched now with `onValuePreUpdate` callback. 4 | 5 | 0.5.1 / 2016-09-18 6 | ------------------ 7 | * Fix server rendering, tests added 8 | 9 | 0.5.0 / 2016-09-18 10 | ------------------ 11 | * IE11 fixes 12 | 13 | 0.5.0-beta2 / 2016-09-17 14 | ------------------------ 15 | * Just some refactoring 16 | * API changed - `registerInput` goes as a third parameter now: 17 | ```jsx 18 | onChange(e.target.value)} 22 | > 23 | {(inputProps, otherProps, registerInput) => 24 | registerInput(ReactDOM.findDOMNode(c))} 26 | type='text' 27 | {...inputProps} 28 | /> 29 | } 30 | 31 | ``` 32 | 33 | 0.5.0-beta1 / 2016-09-11 34 | ------------------------ 35 | 36 | * Auto-resolving `input` node is deprecated - now you have to provide it yourself, i.e.: 37 | ```jsx 38 | onChange(e.target.value)} 42 | > 43 | {(inputProps, { registerInput }) => 44 | registerInput(ReactDOM.findDOMNode(c))} 46 | type='text' 47 | {...inputProps} 48 | /> 49 | } 50 | 51 | ``` 52 | * `onValueChange` is deprecated - use `onSelect` instead 53 | * Components do not proxy props anymore (unless these props are used in component) 54 | * Not-so-themeable [react-date-picker](https://github.com/Hacker0x01/react-date-picker) is replaced with [react-day-picker-themeable](https://github.com/alexkuz/react-day-picker-themeable), this library no longer depends on `jss` 55 | * `DatePicker` and `Dropdown` are now themeable (via [react-base16-styling](https://github.com/alexkuz/react-base16-styling)) 56 | 57 | 0.4.12 / 2016-06-20 58 | ------------------- 59 | 60 | * 0.4.12 61 | * upgrade moment for DatePicker 62 | 63 | 0.4.11 / 2016-05-16 64 | ------------------- 65 | 66 | * 0.4.11 67 | * fix managing null value in dropdown 68 | 69 | 0.4.10 / 2016-05-01 70 | ------------------- 71 | 72 | * 0.4.10 73 | * fix bluring Dropdown 74 | * fix disabled option 75 | * 0.4.9 76 | * remove a hack (proposed in [#13](https://github.com/alexkuz/react-input-enhancements/issues/13)) 77 | 78 | 0.4.8 / 2016-03-18 79 | ------------------ 80 | 81 | * 0.4.8 82 | * better async dropdown update 83 | 84 | 0.4.7 / 2016-03-10 85 | ------------------ 86 | 87 | * 0.4.7 88 | * fix dependencies 89 | 90 | 0.4.6 / 2016-03-10 91 | ------------------ 92 | 93 | * 0.4.6 94 | * sync options changes with input text 95 | 96 | 0.4.5 / 2016-01-29 97 | ------------------ 98 | 99 | * 0.4.5 100 | * fix weird react error on dropdown options traversing 101 | 102 | 0.4.4 / 2016-01-13 103 | ------------------ 104 | 105 | * 0.4.4 106 | * allow function as option label 107 | 108 | 0.4.3 / 2016-01-12 109 | ------------------ 110 | 111 | * Merge branch 'master' of https://github.com/alexkuz/react-input-enhancements 112 | * 0.4.3 113 | * some refactoring 114 | * Merge pull request [#4](https://github.com/alexkuz/react-input-enhancements/issues/4) from nadav-dav/master 115 | moved react-pure-renderer from devDependencies to dependencies 116 | * moved react-pure-renderer from devDependencies to dependencies 117 | 118 | 0.4.2 / 2015-12-29 119 | ------------------ 120 | 121 | * 0.4.2 122 | * consistent Autocomplete/Dropdown options parsing 123 | 124 | 0.4.1 / 2015-12-28 125 | ------------------ 126 | 127 | * 0.4.1 128 | * fix: proper autocomplete for emptyish values 129 | * Update README.md 130 | * update demo 131 | 132 | 0.4.0 / 2015-12-26 133 | ------------------ 134 | 135 | * 0.4.0 136 | * datepicker 137 | 138 | 0.3.11 / 2015-12-24 139 | ------------------- 140 | 141 | * 0.3.11 142 | * proxy onRenderCaret in dropdown 143 | 144 | 0.3.10 / 2015-12-24 145 | ------------------- 146 | 147 | * 0.3.10 148 | * merge branches 149 | * build demo 150 | * DatePicker (WIP) 151 | 152 | 0.3.9 / 2015-12-21 153 | ------------------ 154 | 155 | * 0.3.9 156 | * fix sorting and autocompleting 157 | * build demo 158 | * reset Dropdown state value on losing focus 159 | * controlled mode for Dropdown 160 | * DatePicker (WIP) 161 | * refactor Dropdown - extract InputPopup 162 | 163 | 0.3.8 / 2015-12-18 164 | ------------------ 165 | 166 | * 0.3.8 167 | * call Mask.onChange with updated value 168 | 169 | 0.3.7 / 2015-12-18 170 | ------------------ 171 | 172 | * 0.3.7 173 | * remove hmr from production version 174 | 175 | 0.3.6 / 2015-12-17 176 | ------------------ 177 | 178 | * 0.3.6 179 | * fix dead loop in Dropdown 180 | 181 | 0.3.5 / 2015-12-15 182 | ------------------ 183 | 184 | * 0.3.5 185 | * fix dropdown initializing 186 | 187 | 0.3.4 / 2015-12-15 188 | ------------------ 189 | 190 | * Merge branch 'master' of https://github.com/alexkuz/react-input-enhancements 191 | * 0.3.4 192 | * fix dropdown value reset 193 | * Update README.md 194 | 195 | 0.3.3 / 2015-12-14 196 | ------------------ 197 | 198 | * 0.3.3 199 | * make dropdown more stable 200 | 201 | 0.3.2 / 2015-12-14 202 | ------------------ 203 | 204 | * 0.3.2 205 | * fix dropdown onValueChange event 206 | 207 | 0.3.1 / 2015-12-14 208 | ------------------ 209 | 210 | * 0.3.1 211 | * add keywords 212 | * 0.3.0 213 | * rearrange files 214 | * add Mask; Dropdown fixes 215 | 216 | 0.2.0 / 2015-12-09 217 | ------------------ 218 | 219 | * 0.2.0 220 | * rebuild 221 | * oups!; back to first version 222 | 223 | 0.1.4 / 2015-12-09 224 | ------------------ 225 | 226 | * 0.1.4 227 | * tune dropdown header style 228 | 229 | 0.1.3 / 2015-12-09 230 | ------------------ 231 | 232 | * 0.1.3 233 | * fix dropdown options key 234 | 235 | 0.1.2 / 2015-12-09 236 | ------------------ 237 | 238 | * 0.1.2 239 | * dropdown fixes 240 | * update build 241 | * 0.1.1 242 | * dropdown caret styling 243 | 244 | 0.1.0 / 2015-12-08 245 | ------------------ 246 | 247 | * build demo 248 | * update demo code 249 | * first version 250 | * some fixes, dropdown layout (WIP) 251 | * update build 252 | * fix demo build 253 | * update readme 254 | * init 255 | * Initial commit 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexander Kuznetsov 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 | **This project was originally thought to be an experiment and currently is unmaintained (and buggy)** 4 | 5 | **Use it at your own risk** 6 | 7 | Also, consider using more modern, WAI-ARIA compliant approach like [downshift](https://github.com/downshift-js/downshift) 8 | 9 |
10 | 11 | # react-input-enhancements [![Gitter chat](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/react-input-enhancements) 12 | 13 | Set of enhancements for input control 14 | 15 | The intention of creating this library was to bring `input` component out of the dropdown/autocomplete/whatever code, so it could be easily replaced with your custom component, and also to split independent functionality into different components, which could be combined with each other (still not quite sure it was worth it, though). 16 | 17 | There are currently five components: 18 | 19 | 1. [``](#autosize) 20 | 2. [``](#autocomplete) 21 | 3. [``](#dropdown) 22 | 4. [``](#mask) 23 | 5. [``](#datepicker) 24 | 25 | [``](#combobox) is a combination of `Dropdown`, `Autosize` and/or `Autocomplete` components. 26 | 27 | ## Demo 28 | 29 | http://alexkuz.github.io/react-input-enhancements/ 30 | 31 | ## How it works 32 | 33 | * Each component is responsible for a corresponding behaviour (`` resizes `` according to it's content length, `` adds popup with options, and so on). 34 | * All components accept `function` as a child, providing props as a first argument, which you should pass to your `input` component. If there is nothing else except `input`, it could be passed as a child directly (for simplicity). 35 | * If you need to have combined behaviour in your component, let's say `` with `` just pass `` as a child to `` (see `` source code for reference) 36 | 37 | #### Registering `` 38 | 39 | All components needs an access to `` DOM element. To provide it, use `getInputComponent` prop: 40 | 41 | ```jsx 42 | let input; 43 | 44 | getInput() { 45 | return input; 46 | } 47 | 48 | 52 | {props => 53 | input = c} 55 | {...props} 56 | /> 57 | } 58 | 59 | ``` 60 | Or, if you don't want to store the node in your component: 61 | 62 | ```jsx 63 | 66 | {(props, otherProps, registerInput) => 67 | registerInput(c)} 69 | {...props} 70 | /> 71 | } 72 | 73 | ``` 74 | The first option also allows you to use shorter form with implicit parameters passing: 75 | ```jsx 76 | let input; 77 | 78 | getInput() { 79 | return input; 80 | } 81 | 82 | 86 | input = c} 88 | /> 89 | 90 | ``` 91 | However, this is not preferable as there is too much magic happening. 92 | 93 | If `` element wasn't provided, component tries to find node automatically, however this behaviour is deprecated and will be removed in future versions. 94 | 95 | ## Autosize 96 | 97 | `Autosize` resizes component to fit it's content. 98 | 99 | ```jsx 100 | 102 | {(inputProps, { width, registerInput }) => 103 | registerInput(c)} /> 104 | } 105 | 106 | ``` 107 | 108 | ### Autosize Props 109 | 110 | * **`value`** *string* - Input value (for a controlled component) 111 | * **`defaultValue`** *string* - Initial value (for a uncontrolled component) 112 | * **`getInputElement`** *function()* - Optional callback that provides `` DOM element 113 | * **`registerInput`** *function* - Registers `` DOM element 114 | * **`defaultWidth`** *number* - Minimum input width 115 | 116 | ## Autocomplete 117 | 118 | `Autocomplete` prompts a value based on provided `options` (see also [react-autocomplete](https://github.com/reactjs/react-autocomplete) for the same behaviour) 119 | 120 | ```jsx 121 | 123 | {(inputProps, { matchingText, value, registerInput }) => 124 | registerInput(c)} /> 125 | } 126 | 127 | ``` 128 | 129 | ### Autocomplete Props 130 | 131 | * **`value`** *string* - Input value (for a controlled component) 132 | * **`defaultValue`** *string* - Initial value (for a uncontrolled component) 133 | * **`getInputElement`** *function* - Optional callback that provides `` DOM element 134 | * **`registerInput`** *function* - Registers `` DOM element 135 | * **`options`** *array* - Array of options that are used to predict a value 136 | 137 | `options` is an array of strings or objects with a `text` or `value` string properties. 138 | 139 | ## Dropdown 140 | 141 | `Dropdown` shows a dropdown with a (optionally filtered) list of suitable options. 142 | 143 | ```jsx 144 | 146 | {(inputProps, { textValue }) => 147 | 148 | } 149 | 150 | ``` 151 | 152 | ### Dropdown Props 153 | 154 | * **`value`** *string* - Input value (for a controlled component) 155 | * **`defaultValue`** *string* - Initial value (for a uncontrolled component) 156 | * **`options`** *array* - Array of shown options 157 | * **`onRenderOption`** *function(className, style, option)* - Renders option in list 158 | * **`onRenderCaret`** *function(className, style, isActive, children)* - Renders a caret 159 | * **`onRenderList`** *function(className, style, isActive, listShown, children, header)* - Renders list of options 160 | * **`onRenderListHeader`** *function(allCount, shownCount, staticCount)* - Renders list header 161 | * **`dropdownProps`** *object* - Custom props passed to dropdown root element 162 | * **`optionFilters`** *array* - List of option filters 163 | * **`getInputElement`** *function* - Optional callback that provides `` DOM element 164 | * **`registerInput`** *function* - Registers `` DOM element 165 | 166 | `options` is an array of strings or objects with a shape: 167 | 168 | * **`value`** - "real" value of on option 169 | * **`text`** - text used as input value when option is selected 170 | * **`label`** - text or component rendered in list 171 | * **`static`** - option is never filtered out or sorted 172 | * **`disabled`** - option is not selectable 173 | 174 | `null` option is rendered as a separator 175 | 176 | `optionFilters` is an array of filters for options (for convenience). By default, these filters are used: 177 | 178 | * `filters.filterByMatchingTextWithThreshold(20)` - filters options by matching value, if options length is more than 20 179 | * `filters.sortByMatchingText` - sorting by matching value 180 | * `filters.limitBy(100)` - cuts options longer than 100 181 | * `filters.notFoundMessage('No matches found')` - shows option with 'No matches found' label if all options are filtered out 182 | * `filters.filterRedudantSeparators` - removes redudant separators (duplicated or at the begin/end of the list) 183 | 184 | ## Mask 185 | 186 | `Mask` formats input value. 187 | 188 | ```jsx 189 | 191 | {(inputProps, { value }) => 192 | 193 | } 194 | 195 | ``` 196 | 197 | ### Mask Props 198 | 199 | * **`value`** *string* - Input value (for a controlled component) 200 | * **`defaultValue`** *string* - Initial value (for a uncontrolled component) 201 | * **`getInputElement`** *function* - Optional callback that provides `` DOM element 202 | * **`registerInput`** *function* - Registers `` DOM element 203 | * **`pattern`** *string* - String formatting pattern. Only '0' (digit) or 'a' (letter) pattern chars are currently supported. 204 | * **`emptyChar`** *string* - Character used as an empty symbol (`' '` by default) 205 | * **`placeholder`** *string* - If set, it is shown when `unmaskedValue` is empty 206 | * **`onUnmaskedValueChange`** *function(text)* - Fires when value is changed, providing unmasked value 207 | * **`onValuePreUpdate`** *function* - Optional callback to update value before it is parsed by `Mask` 208 | 209 | ## DatePicker 210 | 211 | `DatePicker` uses `Mask` to format date and shows calendar ([react-date-picker](https://github.com/zippyui/react-date-picker) by default) in popup. 212 | 213 | ```jsx 214 | 218 | {(inputProps, { value }) => 219 | 220 | } 221 | 222 | ``` 223 | 224 | ### DatePicker Props 225 | 226 | * **`value`** *string* - Input value (for a controlled component) 227 | * **`defaultValue`** *string* - Initial value (for a uncontrolled component) 228 | * **`pattern`** *string* - Date formatting pattern. For now, only these tokens are supported: 229 | * `DD` - day of month 230 | * `MM` - month 231 | * `YYYY` - year 232 | * `ddd` - day of week *(not editable)* 233 | * **`placeholder`** *string* - If set, it is shown when `unmaskedValue` is empty 234 | * **`locale`** *string* - Date locale 235 | * **`todayButtonText`** *string* - Text for 'Go to Today' button label 236 | * **`onRenderCalendar`** *function({ styling, style, date, isActive, popupShown, onSelect, locale, todayButtonText })* - Returns calendar component shown in popup ([react-day-picker-themeable](https://github.com/alexkuz/react-day-picker-themeable) by default) 237 | * **`onChange`** *function(date)* - Fires when date is selected, providing [moment.js](http://momentjs.com/) object 238 | * **`getInputElement`** *function* - Optional callback that provides `` DOM element 239 | * **`registerInput`** *function* - Registers `` DOM element 240 | * **`onValuePreUpdate`** *function* - Optional callback to update value before it is parsed by `DatePicker`. In this example, it parses inserted timestamp: 241 | ```js 242 | onValuePreUpdate={v => parseInt(v, 10) > 1e8 ? 243 | moment(parseInt(v, 10)).format('ddd DD/MM/YYYY') : v 244 | } 245 | ``` 246 | 247 | ## Combobox 248 | 249 | `Combobox` combines `Dropdown`, `Autosize` and/or `Autocomplete` components. 250 | 251 | ```jsx 252 | 256 | {(inputProps, { matchingText, width }) => 257 | 258 | } 259 | 260 | ``` 261 | 262 | `Autosize` and `Autocomlete` are enabled with corresponding bool props, other properties are proxied to `Dropdown` component. 263 | 264 | See [demo](http://alexkuz.github.io/react-input-enhancements/) for code examples. 265 | 266 | ## Some other (probably better) implementations 267 | 268 | * [react-autocomplete](https://github.com/rackt/react-autocomplete) - Dropdown with autocompletion by Ryan Florence (that led me to create this library) 269 | * [react-maskedinput](https://github.com/insin/react-maskedinput) - More advanced masked input by Jonny Buchanan 270 | * [react-autosuggest](https://github.com/moroshko/react-autosuggest) - Beautifully crafted input with dropdown suggestions 271 | -------------------------------------------------------------------------------- /__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "jest": true 5 | } 6 | } -------------------------------------------------------------------------------- /__tests__/Autocomplete.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Autocomplete from '../src/Autocomplete'; 4 | 5 | describe('Autocomplete', () => { 6 | it('renders correctly with function child', () => { 7 | const tree = renderer.create( 8 | 9 | {props => } 10 | 11 | ).toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | 15 | it('renders correctly with element child', () => { 16 | const tree = renderer.create( 17 | 18 | 19 | 20 | ).toJSON(); 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/Autosize.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Autosize from '../src/Autosize'; 4 | 5 | describe('Autosize', () => { 6 | it('renders correctly with function child', () => { 7 | const tree = renderer.create( 8 | 9 | {props => } 10 | 11 | ).toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | 15 | it('renders correctly with element child', () => { 16 | const tree = renderer.create( 17 | 18 | 19 | 20 | ).toJSON(); 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/Combobox.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Combobox from '../src/Combobox'; 4 | 5 | describe('Combobox', () => { 6 | it('renders correctly with function child', () => { 7 | const tree = renderer.create( 8 | 9 | {props => } 10 | 11 | ).toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | 15 | it('renders correctly with element child', () => { 16 | const tree = renderer.create( 17 | 18 | 19 | 20 | ).toJSON(); 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/DatePicker.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import DatePicker from '../src/DatePicker'; 4 | import moment from 'moment'; 5 | 6 | const now = moment(0); 7 | 8 | describe('DatePicker', () => { 9 | it('renders correctly with function child', () => { 10 | const tree = renderer.create( 11 | 15 | {props => } 16 | 17 | ).toJSON(); 18 | expect(tree).toMatchSnapshot(); 19 | }); 20 | 21 | it('renders correctly with element child', () => { 22 | const tree = renderer.create( 23 | 27 | 28 | 29 | ).toJSON(); 30 | expect(tree).toMatchSnapshot(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/Dropdown.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Dropdown from '../src/Dropdown'; 4 | 5 | describe('Dropdown', () => { 6 | it('renders correctly with function child', () => { 7 | const tree = renderer.create( 8 | 9 | {props => } 10 | 11 | ).toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | 15 | it('renders correctly with element child', () => { 16 | const tree = renderer.create( 17 | 18 | 19 | 20 | ).toJSON(); 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/Mask.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Mask from '../src/Mask'; 4 | 5 | describe('Mask', () => { 6 | it('renders correctly with function child', () => { 7 | const tree = renderer.create( 8 | 9 | {props => } 10 | 11 | ).toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | }); 14 | 15 | it('renders correctly with element child', () => { 16 | const tree = renderer.create( 17 | 18 | 19 | 20 | ).toJSON(); 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/Autocomplete.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Autocomplete renders correctly with element child 1`] = ` 2 | 6 | `; 7 | 8 | exports[`Autocomplete renders correctly with function child 1`] = ` 9 | 13 | `; 14 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/Autosize.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Autosize renders correctly with element child 1`] = ` 2 | 7 | `; 8 | 9 | exports[`Autosize renders correctly with function child 1`] = ` 10 | 15 | `; 16 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/Combobox.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Combobox renders correctly with element child 1`] = ` 2 |
11 | 22 |
34 | 46 | 48 | 49 |
50 |
51 | `; 52 | 53 | exports[`Combobox renders correctly with function child 1`] = ` 54 |
63 | 74 |
86 | 98 | 100 | 101 |
102 |
103 | `; 104 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/DatePicker.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`DatePicker renders correctly with element child 1`] = ` 2 |
11 | 26 |
38 | 50 | 52 | 53 |
54 |
55 | `; 56 | 57 | exports[`DatePicker renders correctly with function child 1`] = ` 58 |
67 | 82 |
94 | 106 | 108 | 109 |
110 |
111 | `; 112 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/Dropdown.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Dropdown renders correctly with element child 1`] = ` 2 |
11 | 26 |
38 | 50 | 52 | 53 |
54 |
55 | `; 56 | 57 | exports[`Dropdown renders correctly with function child 1`] = ` 58 |
67 | 82 |
94 | 106 | 108 | 109 |
110 |
111 | `; 112 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/Mask.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Mask renders correctly with element child 1`] = ` 2 | 7 | `; 8 | 9 | exports[`Mask renders correctly with function child 1`] = ` 10 | 15 | `; 16 | -------------------------------------------------------------------------------- /__tests__/jest.jsdom.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "jsdom", 3 | "moduleFileExtensions": ["js", "jsx"], 4 | "testRegex": "/__tests__/.*\\.test\\.js$", 5 | "setupTestFrameworkScriptFile": "__tests__/jest.jsdom.setup.js" 6 | } -------------------------------------------------------------------------------- /__tests__/jest.jsdom.setup.js: -------------------------------------------------------------------------------- 1 | import 'raf/polyfill'; 2 | 3 | jest.mock('react-dom'); 4 | 5 | jest.mock('../src/utils/getInput', () => { 6 | return jest.fn(() => ({ 7 | style: {}, 8 | ownerDocument: { 9 | styleSheets: [] 10 | } 11 | })); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/jest.node.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "moduleFileExtensions": ["js", "jsx"], 4 | "testRegex": "/__tests__/.*\\.test\\.js$", 5 | "setupTestFrameworkScriptFile": "__tests__/jest.node.setup.js" 6 | } -------------------------------------------------------------------------------- /__tests__/jest.node.setup.js: -------------------------------------------------------------------------------- 1 | jest.mock('react-dom'); 2 | -------------------------------------------------------------------------------- /demo/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react-input-enhancements v0.7.6 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/src/.noderequirer.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": true 3 | } 4 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.env.npm_package_name || '[[Package Name]]' %> v<%= htmlWebpackPlugin.options.env.npm_package_version || '0.0.0' %> 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/src/js/DemoApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 4 | import FormGroup from 'react-bootstrap/lib/FormGroup'; 5 | import InputGroup from 'react-bootstrap/lib/InputGroup'; 6 | import FormControl from 'react-bootstrap/lib/FormControl'; 7 | import ControlLabel from 'react-bootstrap/lib/ControlLabel'; 8 | import Col from 'react-bootstrap/lib/Col'; 9 | import Glyphicon from 'react-bootstrap/lib/Glyphicon'; 10 | import Collapse from 'react-bootstrap/lib/Collapse'; 11 | import Button from 'react-bootstrap/lib/Button'; 12 | import moment from 'moment'; 13 | import countries from './countries'; 14 | import pure from './pure'; 15 | 16 | import Autosize from 'Autosize'; 17 | import Autocomplete from 'Autocomplete'; 18 | import Combobox from 'Combobox'; 19 | import Mask from 'Mask'; 20 | import DatePicker from 'DatePicker'; 21 | 22 | import './bootstrap-input-inline.css'; 23 | import './ie.css'; 24 | 25 | import Prefixer from 'inline-style-prefixer'; 26 | const prefixerInstance = new Prefixer(window.navigator); 27 | const prefixer = prefixerInstance.prefix.bind(prefixerInstance); 28 | 29 | const ValueInput1 = pure(({ value, onChange }) => ( 30 |
31 | 32 | onChange(e.target.value)}> 33 | {(inputProps, otherProps, registerInput) => ( 34 | registerInput(ReactDOM.findDOMNode(c))} 37 | {...inputProps} 38 | /> 39 | )} 40 | 41 | 42 | 43 | 44 | 45 |
46 | )); 47 | 48 | const ValueInput2 = pure(({ value, onChange }) => ( 49 |
50 | 51 | onChange(e.target.value)} 55 | > 56 | {(inputProps, otherProps, registerInput) => ( 57 | registerInput(ReactDOM.findDOMNode(c))} 60 | {...inputProps} 61 | /> 62 | )} 63 | 64 | 65 | 66 | 67 | 68 |
69 | )); 70 | 71 | const code1and2 = ` 72 | This text has no default width:{' '} 73 |
74 | 75 | onChange(e.target.value)} 78 | > 79 | {inputProps => 80 | 84 | } 85 | 86 | 87 | 88 | 89 | 90 |
91 | and this has 100px default width:{' '} 92 |
93 | 94 | onChange(e.target.value)} 98 | > 99 | {inputProps => 100 | 104 | } 105 | 106 | 107 | 108 | 109 | 110 |
111 | `; 112 | 113 | const ValueInput3 = pure(({ value, onChange }) => ( 114 | 115 | 116 | Autocomplete: 117 | 118 | 119 | onChange(e.target.value)} 123 | > 124 | {(inputProps, otherProps, registerInput) => ( 125 | registerInput(ReactDOM.findDOMNode(c))} 127 | type="text" 128 | {...inputProps} 129 | /> 130 | )} 131 | 132 | 133 | 134 | )); 135 | 136 | const code3 = ` 137 | 138 | 139 | Autocomplete: 140 | 141 | 142 | onChange(e.target.value)} 146 | > 147 | {(inputProps, { registerInput }) => 148 | registerInput(ReactDOM.findDOMNode(c))} 150 | type='text' 151 | {...inputProps} 152 | /> 153 | } 154 | 155 | 156 | 157 | `; 158 | 159 | const ValueInput4 = pure(({ value, onChange }) => ( 160 | 161 | 162 | Combobox (Dropdown + Autocomplete): 163 | 164 | 165 | 172 | {(inputProps, otherProps, registerInput) => ( 173 | registerInput(ReactDOM.findDOMNode(c))} 176 | type="text" 177 | placeholder="No Country" 178 | /> 179 | )} 180 | 181 | 182 | 183 | )); 184 | 185 | const code4 = ` 186 | 187 | 188 | Combobox (Dropdown + Autocomplete): 189 | 190 | 191 | 198 | {(inputProps, { registerInput }) => 199 | registerInput(ReactDOM.findDOMNode(c))} 202 | type='text' 203 | placeholder='No Country' 204 | /> 205 | } 206 | 207 | 208 | 209 | `; 210 | 211 | const ValueInput5 = pure(({ value, onChange }) => ( 212 | 213 | 214 | Combobox (Dropdown + Autosize): 215 | 216 | 217 | 223 | {(inputProps, otherProps, registerInput) => ( 224 | registerInput(ReactDOM.findDOMNode(c))} 227 | type="text" 228 | placeholder="No Country" 229 | /> 230 | )} 231 | 232 | 233 | 234 | )); 235 | 236 | const code5 = ` 237 | 238 | 239 | Combobox (Dropdown + Autosize): 240 | 241 | 242 | 248 | {inputProps => 249 | 254 | } 255 | 256 | 257 | 258 | `; 259 | 260 | const ValueInput6 = pure(({ value, onChange, addonAfter, options }) => ( 261 | 262 | 263 | Combobox (Dropdown + Autosize + Autocomplete, defaultWidth=100): 264 | 265 | 266 | 267 | 275 | {(inputProps, otherProps, registerInput) => ( 276 | registerInput(ReactDOM.findDOMNode(c))} 279 | type="text" 280 | placeholder="No Country" 281 | /> 282 | )} 283 | 284 | {addonAfter} 285 | 286 | 287 | 288 | )); 289 | 290 | const code6 = ` 291 | 292 | 293 | Combobox (Dropdown + Autosize + Autocomplete, defaultWidth=100): 294 | 295 | 296 | 297 | 305 | {(inputProps, { registerInput }) => 306 | registerInput(ReactDOM.findDOMNode(c))} 309 | type='text' 310 | placeholder='No Country' 311 | /> 312 | } 313 | 314 | 315 | {addonAfter} 316 | 317 | 318 | 319 | 320 | `; 321 | 322 | const ValueInput7 = pure(({ value, onChange, onUnmaskedValueChange }) => ( 323 | 324 | 325 | Mask + Autosize (credit card): 326 | 327 | 328 | 335 | onChange(e.target.value)} 340 | onUnmaskedValueChange={onUnmaskedValueChange} 341 | > 342 | {(inputProps, otherProps, registerInput) => ( 343 | 348 | {(autosizeInputProps, otherProps, registerInput) => ( 349 | registerInput(ReactDOM.findDOMNode(c))} 354 | /> 355 | )} 356 | 357 | )} 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | )); 366 | 367 | const code7 = ` 368 | 369 | 370 | Mask + Autosize (credit card): 371 | 372 | 373 | 376 | onChange(e.target.value)} 381 | onUnmaskedValueChange={onUnmaskedValueChange} 382 | > 383 | {(inputProps, { registerInput }) => 384 | 388 | {(autosizeInputProps) => 389 | registerInput(ReactDOM.findDOMNode(c))} 394 | /> 395 | } 396 | 397 | } 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | `; 406 | 407 | const ValueInput8 = pure(({ value, onChange, onUnmaskedValueChange }) => ( 408 | 409 | 410 | Mask + Autosize (phone number): 411 | 412 | 413 | 420 | onChange(e.target.value)} 425 | onUnmaskedValueChange={onUnmaskedValueChange} 426 | > 427 | {(inputProps, otherProps, registerInput) => ( 428 | 433 | {(autosizeInputProps, otherProps, registerInput) => ( 434 | registerInput(ReactDOM.findDOMNode(c))} 439 | /> 440 | )} 441 | 442 | )} 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | )); 451 | 452 | const code8 = ` 453 | 454 | 455 | Mask + Autosize (phone number): 456 | 457 | 458 | 461 | onChange(e.target.value)} 466 | onUnmaskedValueChange={onUnmaskedValueChange} 467 | > 468 | {(inputProps, { registerInput }) => 469 | 473 | {(autosizeInputProps) => 474 | registerInput(ReactDOM.findDOMNode(c))} 479 | /> 480 | } 481 | 482 | } 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | `; 491 | 492 | const ValueInput9 = pure(({ value, onChange }) => ( 493 | 494 | 495 | DatePicker: 496 | 497 | 498 | 502 | parseInt(v, 10) > 1e6 503 | ? moment(parseInt(v, 10)).format('ddd DD/MM/YYYY') 504 | : v 505 | } 506 | > 507 | {(inputProps, otherProps, registerInput) => ( 508 | registerInput(ReactDOM.findDOMNode(c))} 512 | type="text" 513 | /> 514 | )} 515 | 516 | 517 | 518 | )); 519 | 520 | const code9 = ` 521 | 522 | 523 | DatePicker: 524 | 525 | 526 | parseInt(v, 10) > 1e8 ? 531 | moment(parseInt(v, 10)).format('ddd DD/MM/YYYY') : v 532 | } 533 | > 534 | {(inputProps, { registerInput }) => 535 | registerInput(ReactDOM.findDOMNode(c))} 539 | type='text' 540 | /> 541 | } 542 | 543 | 544 | 545 | `; 546 | 547 | let frDatePicker; 548 | 549 | const ValueInput10 = pure(({ value, onChange }) => { 550 | const frCurrent = moment(value || undefined); 551 | frCurrent.locale('fr'); 552 | const frNow = moment(); 553 | frNow.locale('fr'); 554 | 555 | return ( 556 | 557 | 558 | DatePicker (FR): 559 | 560 | 561 | frDatePicker} 568 | todayButtonText="aller à aujourd’hui" 569 | > 570 | (frDatePicker = ReactDOM.findDOMNode(c))} 573 | type="text" 574 | /> 575 | 576 | 577 | 578 | ); 579 | }); 580 | 581 | const code10 = ` 582 | const frCurrent = moment(value || undefined); 583 | frCurrent.locale('fr'); 584 | const frNow = moment(); 585 | frNow.locale('fr'); 586 | 587 | // more compact and more magic form 588 | 589 | 590 | 591 | DatePicker (FR): 592 | 593 | 594 | frDatePicker} 601 | todayButtonText='aller à aujourd’hui' 602 | > 603 | frDatePicker = ReactDOM.findDOMNode(c)} 606 | type='text' 607 | /> 608 | 609 | 610 | 611 | `; 612 | 613 | export default class DemoApp extends React.Component { 614 | constructor(props) { 615 | super(props); 616 | this.state = { 617 | countries, 618 | value1: '', 619 | value2: '', 620 | value3: '', 621 | value4: '', 622 | value5: '', 623 | value6: 'value--Fiji', 624 | value7: '', 625 | unmaskedValue7: '', 626 | value8: '', 627 | unmaskedValue8: '', 628 | value9: '', 629 | value10: '', 630 | code1and2open: false, 631 | code3open: false, 632 | code4open: false, 633 | code5open: false, 634 | code6open: false, 635 | code7open: false, 636 | code8open: false, 637 | code9open: false, 638 | code10open: false, 639 | asyncAlterCountries: false 640 | }; 641 | } 642 | 643 | componentDidMount() { 644 | window.setTimeout(this.setDelayedState, 200); 645 | } 646 | 647 | setDelayedState = () => { 648 | this.setState({ value4: 'value--Albania' }); 649 | }; 650 | 651 | lastTime = new Date(); 652 | 653 | alterCountries = () => { 654 | this.lastTime = new Date(); 655 | this.setState({ 656 | countries: countries.map( 657 | country => 658 | country && { 659 | ...country, 660 | text: country.text && country.text + ' ' + Math.random().toFixed(2) 661 | } 662 | ) 663 | }); 664 | }; 665 | 666 | countDown = () => { 667 | this.setState({ 668 | countDown: this.state.countDown - 1 || 5 669 | }); 670 | }; 671 | 672 | toggleAsyncAlterCountries = () => { 673 | const asyncAlterCountries = !this.state.asyncAlterCountries; 674 | this.setState({ asyncAlterCountries, countDown: 5 }); 675 | 676 | clearTimeout(this.alterCountriesTimeout); 677 | clearTimeout(this.countDownTimeout); 678 | if (asyncAlterCountries) { 679 | this.alterCountriesTimeout = setInterval(this.alterCountries, 5000); 680 | this.countDownTimeout = setInterval(this.countDown, 1000); 681 | } 682 | }; 683 | 684 | render() { 685 | return ( 686 |
687 | 688 | {process.env.npm_package_name} 689 | v{process.env.npm_package_version} 690 | 691 |
{process.env.npm_package_description}
692 |
693 |
694 |
695 |
Autosize (inline):
696 |
697 | This text has no default width:{' '} 698 | , and this has 100px default width:{' '} 702 | 706 |
707 |
708 | {this.renderCode(code1and2, 'code1and2open')} 709 | 713 | {this.renderCode(code3, 'code3open')} 714 | 718 | {this.renderCode(code4, 'code4open')} 719 | 723 | {this.renderCode(code5, 'code5open')} 724 | 730 | 733 | 739 | 745 | 753 |
754 | } 755 | /> 756 | {this.renderCode(code6, 'code6open')} 757 | 761 | this.setState({ unmaskedValue7: value }) 762 | } 763 | /> 764 | 768 | {this.renderCode(code7, 'code7open')} 769 | 773 | this.setState({ unmaskedValue8: value }) 774 | } 775 | /> 776 | 780 | {this.renderCode(code8, 'code8open')} 781 | 785 | {this.renderCode(code9, 'code9open')} 786 | 790 | {this.renderCode(code10, 'code10open')} 791 | 792 |
793 | 794 | ); 795 | } 796 | 797 | renderCode(code, key) { 798 | return ( 799 |
800 |
801 | this.setState({ [key]: !this.state[key] })} 804 | > 805 | Show code 806 | 807 | 808 |
{code.replace(/^\n+/, '')}
809 |
810 |
811 |
812 | ); 813 | } 814 | 815 | handleValue1Change = value => this.setState({ value1: value }); 816 | 817 | handleValue2Change = value => this.setState({ value2: value }); 818 | 819 | handleValue3Change = value => this.setState({ value3: value }); 820 | 821 | handleValue4Change = value => this.setState({ value4: value }); 822 | 823 | handleValue5Change = value => this.setState({ value5: value }); 824 | 825 | handleValue6Change = value => this.setState({ value6: value }); 826 | 827 | handleValue7Change = value => this.setState({ value7: value }); 828 | 829 | handleValue8Change = value => this.setState({ value8: value }); 830 | 831 | handleValue9Change = value => this.setState({ value9: value }); 832 | 833 | handleValue10Change = value => this.setState({ value10: value }); 834 | } 835 | 836 | const styles = { 837 | wrapper: { 838 | height: '100vh', 839 | width: '80%', 840 | margin: '0 auto' 841 | }, 842 | header: {}, 843 | content: { 844 | paddingTop: '20px', 845 | paddingBottom: '300px' 846 | } 847 | }; 848 | -------------------------------------------------------------------------------- /demo/src/js/bootstrap-input-inline.css: -------------------------------------------------------------------------------- 1 | .inline-input { 2 | display: inline; 3 | white-space: nowrap; 4 | } 5 | 6 | .inline-input .input-group, 7 | .inline-input .input-group-addon { 8 | display: inline; 9 | } 10 | 11 | .inline-input .form-control, 12 | .inline-input .input-group .form-control { 13 | width: auto; 14 | display: inline; 15 | float: none; 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/js/countries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { label: 'No Country', text: '', value: null, static: true }, 3 | null, 4 | { text: 'Disabled Option', disabled: true, static: true }, 5 | null, 6 | { text: 'Afghanistan', value: 'value--Afghanistan' }, 7 | { text: 'Albania', value: 'value--Albania' }, 8 | { text: 'Algeria', value: 'value--Algeria' }, 9 | { text: 'Andorra', value: 'value--Andorra' }, 10 | { text: 'Angola', value: 'value--Angola' }, 11 | { text: 'Antigua & Deps', value: 'value--Antigua & Deps' }, 12 | { text: 'Argentina', value: 'value--Argentina' }, 13 | { text: 'Armenia', value: 'value--Armenia' }, 14 | { text: 'Australia', value: 'value--Australia' }, 15 | { text: 'Austria', value: 'value--Austria' }, 16 | { text: 'Azerbaijan', value: 'value--Azerbaijan' }, 17 | { text: 'Bahamas', value: 'value--Bahamas' }, 18 | { text: 'Bahrain', value: 'value--Bahrain' }, 19 | { text: 'Bangladesh', value: 'value--Bangladesh' }, 20 | { text: 'Barbados', value: 'value--Barbados' }, 21 | { text: 'Belarus', value: 'value--Belarus' }, 22 | { text: 'Belgium', value: 'value--Belgium' }, 23 | { text: 'Belize', value: 'value--Belize' }, 24 | { text: 'Benin', value: 'value--Benin' }, 25 | { text: 'Bhutan', value: 'value--Bhutan' }, 26 | { text: 'Bolivia', value: 'value--Bolivia' }, 27 | { text: 'Bosnia Herzegovina', value: 'value--Bosnia Herzegovina' }, 28 | { text: 'Botswana', value: 'value--Botswana' }, 29 | { text: 'Brazil', value: 'value--Brazil' }, 30 | { text: 'Brunei', value: 'value--Brunei' }, 31 | { text: 'Bulgaria', value: 'value--Bulgaria' }, 32 | { text: 'Burkina', value: 'value--Burkina' }, 33 | { text: 'Burundi', value: 'value--Burundi' }, 34 | { text: 'Cambodia', value: 'value--Cambodia' }, 35 | { text: 'Cameroon', value: 'value--Cameroon' }, 36 | { text: 'Canada', value: 'value--Canada' }, 37 | { text: 'Cape Verde', value: 'value--Cape Verde' }, 38 | { text: 'Central African Rep', value: 'value--Central African Rep' }, 39 | { text: 'Chad', value: 'value--Chad' }, 40 | { text: 'Chile', value: 'value--Chile' }, 41 | { text: 'China', value: 'value--China' }, 42 | { text: 'Colombia', value: 'value--Colombia' }, 43 | { text: 'Comoros', value: 'value--Comoros' }, 44 | { text: 'Congo', value: 'value--Congo' }, 45 | { text: 'Congo {Democratic Rep}', value: 'value--Congo {Democratic Rep}' }, 46 | { text: 'Costa Rica', value: 'value--Costa Rica' }, 47 | { text: 'Croatia', value: 'value--Croatia' }, 48 | { text: 'Cuba', value: 'value--Cuba' }, 49 | { text: 'Cyprus', value: 'value--Cyprus' }, 50 | { text: 'Czech Republic', value: 'value--Czech Republic' }, 51 | { text: 'Denmark', value: 'value--Denmark' }, 52 | { text: 'Djibouti', value: 'value--Djibouti' }, 53 | { text: 'Dominica', value: 'value--Dominica' }, 54 | { text: 'Dominican Republic', value: 'value--Dominican Republic' }, 55 | { text: 'East Timor', value: 'value--East Timor' }, 56 | { text: 'Ecuador', value: 'value--Ecuador' }, 57 | { text: 'Egypt', value: 'value--Egypt' }, 58 | { text: 'El Salvador', value: 'value--El Salvador' }, 59 | { text: 'Equatorial Guinea', value: 'value--Equatorial Guinea' }, 60 | { text: 'Eritrea', value: 'value--Eritrea' }, 61 | { text: 'Estonia', value: 'value--Estonia' }, 62 | { text: 'Ethiopia', value: 'value--Ethiopia' }, 63 | { text: 'Fiji', value: 'value--Fiji' }, 64 | { text: 'Finland', value: 'value--Finland' }, 65 | { text: 'France', value: 'value--France' }, 66 | { text: 'Gabon', value: 'value--Gabon' }, 67 | { text: 'Gambia', value: 'value--Gambia' }, 68 | { text: 'Georgia', value: 'value--Georgia' }, 69 | { text: 'Germany', value: 'value--Germany' }, 70 | { text: 'Ghana', value: 'value--Ghana' }, 71 | { text: 'Greece', value: 'value--Greece' }, 72 | { text: 'Grenada', value: 'value--Grenada' }, 73 | { text: 'Guatemala', value: 'value--Guatemala' }, 74 | { text: 'Guinea', value: 'value--Guinea' }, 75 | { text: 'Guinea-Bissau', value: 'value--Guinea-Bissau' }, 76 | { text: 'Guyana', value: 'value--Guyana' }, 77 | { text: 'Haiti', value: 'value--Haiti' }, 78 | { text: 'Honduras', value: 'value--Honduras' }, 79 | { text: 'Hungary', value: 'value--Hungary' }, 80 | { text: 'Iceland', value: 'value--Iceland' }, 81 | { text: 'India', value: 'value--India' }, 82 | { text: 'Indonesia', value: 'value--Indonesia' }, 83 | { text: 'Iran', value: 'value--Iran' }, 84 | { text: 'Iraq', value: 'value--Iraq' }, 85 | { text: 'Ireland {Republic}', value: 'value--Ireland {Republic}' }, 86 | { text: 'Israel', value: 'value--Israel' }, 87 | { text: 'Italy', value: 'value--Italy' }, 88 | { text: 'Ivory Coast', value: 'value--Ivory Coast' }, 89 | { text: 'Jamaica', value: 'value--Jamaica' }, 90 | { text: 'Japan', value: 'value--Japan' }, 91 | { text: 'Jordan', value: 'value--Jordan' }, 92 | { text: 'Kazakhstan', value: 'value--Kazakhstan' }, 93 | { text: 'Kenya', value: 'value--Kenya' }, 94 | { text: 'Kiribati', value: 'value--Kiribati' }, 95 | { text: 'Korea North', value: 'value--Korea North' }, 96 | { text: 'Korea South', value: 'value--Korea South' }, 97 | { text: 'Kosovo', value: 'value--Kosovo' }, 98 | { text: 'Kuwait', value: 'value--Kuwait' }, 99 | { text: 'Kyrgyzstan', value: 'value--Kyrgyzstan' }, 100 | { text: 'Laos', value: 'value--Laos' }, 101 | { text: 'Latvia', value: 'value--Latvia' }, 102 | { text: 'Lebanon', value: 'value--Lebanon' }, 103 | { text: 'Lesotho', value: 'value--Lesotho' }, 104 | { text: 'Liberia', value: 'value--Liberia' }, 105 | { text: 'Libya', value: 'value--Libya' }, 106 | { text: 'Liechtenstein', value: 'value--Liechtenstein' }, 107 | { text: 'Lithuania', value: 'value--Lithuania' }, 108 | { text: 'Luxembourg', value: 'value--Luxembourg' }, 109 | { text: 'Macedonia', value: 'value--Macedonia' }, 110 | { text: 'Madagascar', value: 'value--Madagascar' }, 111 | { text: 'Malawi', value: 'value--Malawi' }, 112 | { text: 'Malaysia', value: 'value--Malaysia' }, 113 | { text: 'Maldives', value: 'value--Maldives' }, 114 | { text: 'Mali', value: 'value--Mali' }, 115 | { text: 'Malta', value: 'value--Malta' }, 116 | { text: 'Marshall Islands', value: 'value--Marshall Islands' }, 117 | { text: 'Mauritania', value: 'value--Mauritania' }, 118 | { text: 'Mauritius', value: 'value--Mauritius' }, 119 | { text: 'Mexico', value: 'value--Mexico' }, 120 | { text: 'Micronesia', value: 'value--Micronesia' }, 121 | { text: 'Moldova', value: 'value--Moldova' }, 122 | { text: 'Monaco', value: 'value--Monaco' }, 123 | { text: 'Mongolia', value: 'value--Mongolia' }, 124 | { text: 'Montenegro', value: 'value--Montenegro' }, 125 | { text: 'Morocco', value: 'value--Morocco' }, 126 | { text: 'Mozambique', value: 'value--Mozambique' }, 127 | { text: 'Myanmar, {Burma}', value: 'value--Myanmar, {Burma}' }, 128 | { text: 'Namibia', value: 'value--Namibia' }, 129 | { text: 'Nauru', value: 'value--Nauru' }, 130 | { text: 'Nepal', value: 'value--Nepal' }, 131 | { text: 'Netherlands', value: 'value--Netherlands' }, 132 | { text: 'New Zealand', value: 'value--New Zealand' }, 133 | { text: 'Nicaragua', value: 'value--Nicaragua' }, 134 | { text: 'Niger', value: 'value--Niger' }, 135 | { text: 'Nigeria', value: 'value--Nigeria' }, 136 | { text: 'Norway', value: 'value--Norway' }, 137 | { text: 'Oman', value: 'value--Oman' }, 138 | { text: 'Pakistan', value: 'value--Pakistan' }, 139 | { text: 'Palau', value: 'value--Palau' }, 140 | { text: 'Panama', value: 'value--Panama' }, 141 | { text: 'Papua New Guinea', value: 'value--Papua New Guinea' }, 142 | { text: 'Paraguay', value: 'value--Paraguay' }, 143 | { text: 'Peru', value: 'value--Peru' }, 144 | { text: 'Philippines', value: 'value--Philippines' }, 145 | { text: 'Poland', value: 'value--Poland' }, 146 | { text: 'Portugal', value: 'value--Portugal' }, 147 | { text: 'Qatar', value: 'value--Qatar' }, 148 | { text: 'Romania', value: 'value--Romania' }, 149 | { text: 'Russian Federation', value: 'value--Russian Federation' }, 150 | { text: 'Rwanda', value: 'value--Rwanda' }, 151 | { text: 'St Kitts & Nevis', value: 'value--St Kitts & Nevis' }, 152 | { text: 'St Lucia', value: 'value--St Lucia' }, 153 | { 154 | text: 'Saint Vincent & the Grenadines', 155 | value: 'value--Saint Vincent & the Grenadines' 156 | }, 157 | { text: 'Samoa', value: 'value--Samoa' }, 158 | { text: 'San Marino', value: 'value--San Marino' }, 159 | { text: 'Sao Tome & Principe', value: 'value--Sao Tome & Principe' }, 160 | { text: 'Saudi Arabia', value: 'value--Saudi Arabia' }, 161 | { text: 'Senegal', value: 'value--Senegal' }, 162 | { text: 'Serbia', value: 'value--Serbia' }, 163 | { text: 'Seychelles', value: 'value--Seychelles' }, 164 | { text: 'Sierra Leone', value: 'value--Sierra Leone' }, 165 | { text: 'Singapore', value: 'value--Singapore' }, 166 | { text: 'Slovakia', value: 'value--Slovakia' }, 167 | { text: 'Slovenia', value: 'value--Slovenia' }, 168 | { text: 'Solomon Islands', value: 'value--Solomon Islands' }, 169 | { text: 'Somalia', value: 'value--Somalia' }, 170 | { text: 'South Africa', value: 'value--South Africa' }, 171 | { text: 'South Sudan', value: 'value--South Sudan' }, 172 | { text: 'Spain', value: 'value--Spain' }, 173 | { text: 'Sri Lanka', value: 'value--Sri Lanka' }, 174 | { text: 'Sudan', value: 'value--Sudan' }, 175 | { text: 'Suriname', value: 'value--Suriname' }, 176 | { text: 'Swaziland', value: 'value--Swaziland' }, 177 | { text: 'Sweden', value: 'value--Sweden' }, 178 | { text: 'Switzerland', value: 'value--Switzerland' }, 179 | { text: 'Syria', value: 'value--Syria' }, 180 | { text: 'Taiwan', value: 'value--Taiwan' }, 181 | { text: 'Tajikistan', value: 'value--Tajikistan' }, 182 | { text: 'Tanzania', value: 'value--Tanzania' }, 183 | { text: 'Thailand', value: 'value--Thailand' }, 184 | { text: 'Togo', value: 'value--Togo' }, 185 | { text: 'Tonga', value: 'value--Tonga' }, 186 | { text: 'Trinidad & Tobago', value: 'value--Trinidad & Tobago' }, 187 | { text: 'Tunisia', value: 'value--Tunisia' }, 188 | { text: 'Turkey', value: 'value--Turkey' }, 189 | { text: 'Turkmenistan', value: 'value--Turkmenistan' }, 190 | { text: 'Tuvalu', value: 'value--Tuvalu' }, 191 | { text: 'Uganda', value: 'value--Uganda' }, 192 | { text: 'Ukraine', value: 'value--Ukraine' }, 193 | { text: 'United Arab Emirates', value: 'value--United Arab Emirates' }, 194 | { text: 'United Kingdom', value: 'value--United Kingdom' }, 195 | { text: 'United States', value: 'value--United States' }, 196 | { text: 'Uruguay', value: 'value--Uruguay' }, 197 | { text: 'Uzbekistan', value: 'value--Uzbekistan' }, 198 | { text: 'Vanuatu', value: 'value--Vanuatu' }, 199 | { text: 'Vatican City', value: 'value--Vatican City' }, 200 | { text: 'Venezuela', value: 'value--Venezuela' }, 201 | { text: 'Vietnam', value: 'value--Vietnam' }, 202 | { text: 'Yemen', value: 'value--Yemen' }, 203 | { text: 'Zambia', value: 'value--Zambia' }, 204 | { text: 'Zimbabwe', value: 'value--Zimbabwe' } 205 | ]; 206 | -------------------------------------------------------------------------------- /demo/src/js/ie.css: -------------------------------------------------------------------------------- 1 | ::-ms-clear { 2 | display: none; 3 | } -------------------------------------------------------------------------------- /demo/src/js/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import DemoApp from './DemoApp'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /demo/src/js/pure.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const getDisplayName = c => c.displayName || c.name || 'Component'; 4 | 5 | export default WrappedComponent => { 6 | return class Pure extends Component { 7 | static displayName = `$pure(${getDisplayName(WrappedComponent)})`; 8 | 9 | shouldComponentUpdate(nextProps) { 10 | return ( 11 | !!Object.keys(nextProps).length !== Object.keys(this.props).length || 12 | !!Object.keys(nextProps).find(k => nextProps[k] !== this.props[k]) 13 | ); 14 | } 15 | 16 | render() { 17 | return ; 18 | } 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {%= o.htmlWebpackPlugin.options.package.name || '[[Package Name]]' %} 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-input-enhancements", 3 | "version": "0.7.6", 4 | "description": "Set of enhancements for input control (bootstrap-ready)", 5 | "scripts": { 6 | "test-jsdom": "jest --config __tests__/jest.jsdom.config.json", 7 | "test-node": "jest --config __tests__/jest.node.config.json", 8 | "test": "yarn test-jsdom && yarn test-node", 9 | "build": "rm -rf lib && NODE_ENV=production babel src --out-dir lib", 10 | "build-demo": "rm -rf demo/dist && NODE_ENV=production webpack -p", 11 | "stats": "webpack --profile --json > stats.json", 12 | "start": "webpack-dev-server", 13 | "lint": "eslint --max-warnings 0 src/*.js* src/**/*.js* demo/src/**/*.js* webpack.config.js", 14 | "format": "prettier-eslint --write --single-quote src/*.js* src/**/*.js* demo/src/**/*.js* webpack.config.js", 15 | "preversion": "yarn lint && yarn test", 16 | "version": "yarn build && yarn build-demo && git add -A .", 17 | "postversion": "git push && git push --tags", 18 | "gh": "git subtree push --prefix demo/dist origin gh-pages" 19 | }, 20 | "main": "lib/index.js", 21 | "repository": { 22 | "url": "https://github.com/alexkuz/react-input-enhancements" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "reactjs", 27 | "input", 28 | "autocomplete", 29 | "autosize", 30 | "typeahead", 31 | "mask", 32 | "dropdown", 33 | "combobox" 34 | ], 35 | "devDependencies": { 36 | "babel-core": "^6.26.0", 37 | "babel-loader": "^7.1.2", 38 | "babel-preset-env": "^1.6.1", 39 | "babel-preset-react": "^6.24.1", 40 | "babel-preset-stage-0": "^6.24.1", 41 | "css-loader": "^0.28.7", 42 | "html-webpack-plugin": "^2.22.0", 43 | "imports-loader": "^0.6.5", 44 | "jest": "^15.1.1", 45 | "postcss-loader": "^2.0.9", 46 | "pre-commit": "^1.1.3", 47 | "prettier-eslint-cli": "^4.7.0", 48 | "raf": "^3.4.0", 49 | "raw-loader": "^0.5.1", 50 | "react": "^16.2.0", 51 | "react-bootstrap": "^0.31.5", 52 | "react-dom": "^16.2.0", 53 | "react-scripts": "^1.0.17", 54 | "react-test-renderer": "^16.2.0", 55 | "react-transform-hmr": "^1.0.4", 56 | "style-loader": "^0.19.1", 57 | "webpack": "^3.10.0", 58 | "webpack-dev-server": "^2.9.7", 59 | "webpack-simple-progress-plugin": "^0.0.3" 60 | }, 61 | "peerDependencies": { 62 | "react": "^15.3.1 || ^16.0.0", 63 | "react-dom": "^15.3.1 || ^16.0.0" 64 | }, 65 | "dependencies": { 66 | "babel-runtime": "^6.11.6", 67 | "inline-style-prefixer": "^2.0.1", 68 | "lodash.sortby": "^4.7.0", 69 | "moment": "^2.14.1", 70 | "prop-types": "^15.6.0", 71 | "react-base16-styling": "^0.5.3", 72 | "react-day-picker-themeable": "^7.0.5" 73 | }, 74 | "author": "Alexander (http://kuzya.org/)", 75 | "license": "MIT", 76 | "pre-commit": [ 77 | "format", 78 | "test" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /src/.noderequirer.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": true 3 | } 4 | -------------------------------------------------------------------------------- /src/Autocomplete.jsx: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import * as shapes from './shapes'; 4 | import findMatchingTextIndex from './utils/findMatchingTextIndex'; 5 | import getInput from './utils/getInput'; 6 | import registerInput from './utils/registerInput'; 7 | import renderChild from './utils/renderChild'; 8 | 9 | function updateInputSelection(input, start, end) { 10 | input.setSelectionRange(start, end); 11 | } 12 | 13 | function updateInputNode(input, value) { 14 | input.value = value; 15 | } 16 | 17 | function setSelection(input, text, matchingText) { 18 | if (text === null) { 19 | updateInputNode(input, null); 20 | } else { 21 | updateInputNode(input, matchingText); 22 | if (text.length !== matchingText.length) { 23 | updateInputSelection(input, text.length, matchingText.length); 24 | } 25 | } 26 | } 27 | 28 | export default class Autocomplete extends PureComponent { 29 | constructor(props) { 30 | super(props); 31 | this.state = { 32 | matchingText: null, 33 | value: props.value 34 | }; 35 | } 36 | 37 | static propTypes = { 38 | getInputElement: PropTypes.func, 39 | value: PropTypes.string, 40 | options: PropTypes.arrayOf(shapes.ITEM_OR_STRING).isRequired 41 | }; 42 | 43 | componentWillReceiveProps(nextProps) { 44 | if ( 45 | this.props.value !== nextProps.value && 46 | nextProps.value !== this.state.value 47 | ) { 48 | this.setValue(nextProps.value, nextProps.options); 49 | } 50 | } 51 | 52 | componentWillUpdate(nextProps, nextState) { 53 | if ( 54 | this.props.options !== nextProps.options && 55 | this.props.value === nextProps.value 56 | ) { 57 | const match = findMatchingTextIndex(nextState.value, nextProps.options); 58 | const [, matchingText] = match; 59 | this.setState({ matchingText }); 60 | } 61 | } 62 | 63 | setValue(value, options) { 64 | if (value === this.state.value) { 65 | return; 66 | } 67 | const match = findMatchingTextIndex(value, options); 68 | const [, matchingText] = match; 69 | this.setState({ value, matchingText }); 70 | } 71 | 72 | componentDidUpdate() { 73 | const matchingText = this.state.matchingText || ''; 74 | const value = this.state.value || ''; 75 | 76 | if (matchingText && value.length !== matchingText.length) { 77 | const input = getInput(this); 78 | setSelection(input, this.state.value, this.state.matchingText); 79 | } 80 | } 81 | 82 | registerInput = input => registerInput(this, input); 83 | 84 | render() { 85 | const { children } = this.props; 86 | const { matchingText, value } = this.state; 87 | const inputProps = { 88 | value: matchingText || value, 89 | onKeyDown: this.handleKeyDown, 90 | onChange: this.handleChange 91 | }; 92 | 93 | return renderChild( 94 | children, 95 | inputProps, 96 | { matchingText, value }, 97 | this.registerInput 98 | ); 99 | } 100 | 101 | handleChange = e => { 102 | let value = e.target.value; 103 | 104 | this.setValue(value, this.props.options); 105 | 106 | if (this.props.onChange) { 107 | this.props.onChange(e); 108 | } 109 | }; 110 | 111 | handleKeyDown = e => { 112 | const keyMap = { 113 | Backspace: this.handleBackspaceKeyDown, 114 | Enter: this.handleEnterKeyDown 115 | }; 116 | 117 | if (keyMap[e.key]) { 118 | keyMap[e.key](e); 119 | } 120 | 121 | if (this.props.onKeyDown) { 122 | this.props.onKeyDown(e); 123 | } 124 | }; 125 | 126 | handleBackspaceKeyDown = () => { 127 | const input = getInput(this); 128 | if ( 129 | input.selectionStart !== input.selectionEnd && 130 | input.selectionEnd === input.value.length && 131 | input.selectionStart !== 0 132 | ) { 133 | const value = input.value.substr(0, input.selectionStart); 134 | this.setValue(value.substr(0, value.length - 1), this.props.options); 135 | updateInputNode(input, value); 136 | } 137 | }; 138 | 139 | handleEnterKeyDown = () => { 140 | const input = getInput(this); 141 | 142 | setSelection(input, this.state.matchingText, this.state.matchingText); 143 | input.blur(); 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /src/Autosize.jsx: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './utils/getComputedStyle'; 4 | import getInput from './utils/getInput'; 5 | import registerInput from './utils/registerInput'; 6 | import renderChild from './utils/renderChild'; 7 | 8 | const ALLOWED_CSS_PROPS = [ 9 | 'direction', 10 | 'fontFamily', 11 | 'fontKerning', 12 | 'fontSize', 13 | 'fontSizeAdjust', 14 | 'fontStyle', 15 | 'fontWeight', 16 | 'letterSpacing', 17 | 'lineHeight', 18 | 'padding', 19 | 'textAlign', 20 | 'textDecoration', 21 | 'textTransform', 22 | 'wordSpacing' 23 | ]; 24 | 25 | let sizersListEl = null; 26 | const sizerContainerStyle = { 27 | position: 'absolute', 28 | visibility: 'hidden', 29 | whiteSpace: 'nowrap', 30 | width: 'auto', 31 | minWidth: 'initial', 32 | maxWidth: 'initial', 33 | zIndex: 10000, 34 | left: -1000, 35 | top: 100 36 | }; 37 | 38 | export default class Autosize extends PureComponent { 39 | constructor(props) { 40 | super(props); 41 | this.state = { 42 | width: props.defaultWidth, 43 | defaultWidth: props.defaultWidth, 44 | value: props.value 45 | }; 46 | } 47 | 48 | static propTypes = { 49 | value: PropTypes.string, 50 | defaultWidth: PropTypes.number, 51 | getInputElement: PropTypes.func, 52 | getSizerContainer: PropTypes.func, 53 | padding: PropTypes.number 54 | }; 55 | 56 | static defaultProps = { 57 | getSizerContainer: () => document.body, 58 | padding: 0 59 | }; 60 | 61 | componentWillMount() { 62 | if (typeof document === 'undefined') { 63 | return; 64 | } 65 | 66 | if (!sizersListEl) { 67 | sizersListEl = document.createElement('div'); 68 | for (const [key, val] of Object.entries(sizerContainerStyle)) { 69 | sizersListEl.style[key] = val; 70 | } 71 | sizersListEl.style.whiteSpace = 'pre'; 72 | this.props.getSizerContainer().appendChild(sizersListEl); 73 | } 74 | 75 | this.sizerEl = document.createElement('span'); 76 | sizersListEl.appendChild(this.sizerEl); 77 | 78 | window.addEventListener('resize', this.handleWindownResize); 79 | } 80 | 81 | componentWillUnmount() { 82 | sizersListEl.removeChild(this.sizerEl); 83 | if (sizersListEl.childNodes.length === 0) { 84 | this.props.getSizerContainer().removeChild(sizersListEl); 85 | sizersListEl = null; 86 | } 87 | this.sizerEl = null; 88 | 89 | window.removeEventListener('resize', this.handleWindownResize); 90 | } 91 | 92 | componentDidMount() { 93 | if (typeof window === 'undefined') { 94 | return; 95 | } 96 | let defaultWidth = this.props.defaultWidth; 97 | 98 | if (defaultWidth === undefined) { 99 | const input = getInput(this); 100 | defaultWidth = input.offsetWidth; 101 | this.setDefaultWidth(defaultWidth); 102 | } 103 | 104 | this.updateWidth( 105 | this.props.value || this.props.placeholder, 106 | defaultWidth, 107 | this.props.padding 108 | ); 109 | } 110 | 111 | setDefaultWidth(defaultWidth) { 112 | this.setState({ defaultWidth }); 113 | } 114 | 115 | componentWillReceiveProps(nextProps) { 116 | if (nextProps.value !== this.props.value) { 117 | this.setState({ value: nextProps.value }); 118 | } 119 | } 120 | 121 | componentWillUpdate(nextProps, nextState) { 122 | if ( 123 | nextState.value !== this.state.value || 124 | nextProps.padding !== this.props.padding 125 | ) { 126 | this.updateWidth( 127 | nextState.value || nextProps.placeholder, 128 | nextState.defaultWidth, 129 | nextProps.padding 130 | ); 131 | } 132 | } 133 | 134 | registerInput = input => registerInput(this, input); 135 | 136 | updateWidth(value, defaultWidth, padding) { 137 | const input = getInput(this); 138 | const inputStyle = window.getComputedStyle(input, null); 139 | 140 | if (!value) { 141 | this.setState({ 142 | width: defaultWidth 143 | }); 144 | return; 145 | } 146 | 147 | for (const key in inputStyle) { 148 | if (ALLOWED_CSS_PROPS.indexOf(key) !== -1) { 149 | this.sizerEl.style[key] = inputStyle[key]; 150 | } 151 | } 152 | 153 | this.sizerEl.innerText = value; 154 | this.sizerEl.style.position = 'absolute'; 155 | 156 | this.setState({ 157 | width: Math.max(this.sizerEl.offsetWidth + padding + 1, defaultWidth) 158 | }); 159 | } 160 | 161 | render() { 162 | const { children, style, placeholder, value } = this.props; 163 | const { width } = this.state; 164 | const inputProps = { 165 | style: { 166 | ...(style || {}), 167 | ...(width ? { width } : {}) 168 | }, 169 | placeholder, 170 | value, 171 | onChange: this.handleChange 172 | }; 173 | 174 | return renderChild(children, inputProps, { width }, this.registerInput); 175 | } 176 | 177 | handleWindownResize = () => { 178 | this.updateWidth( 179 | this.state.value || this.props.placeholder, 180 | this.state.defaultWidth, 181 | this.props.padding 182 | ); 183 | }; 184 | 185 | handleChange = e => { 186 | const value = e.target.value; 187 | 188 | if (this.props.value === undefined) { 189 | this.setState({ value }); 190 | } 191 | 192 | if (this.props.onChange) { 193 | this.props.onChange(e); 194 | } 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /src/Combobox.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Autocomplete from './Autocomplete'; 4 | import Dropdown from './Dropdown'; 5 | import Autosize from './Autosize'; 6 | import renderChild from './utils/renderChild'; 7 | 8 | const CARET_PADDING = 15; 9 | 10 | export default class Combobox extends PureComponent { 11 | static propTypes = { 12 | autosize: PropTypes.bool, 13 | autocomplete: PropTypes.bool 14 | }; 15 | 16 | render() { 17 | const { autosize, autocomplete, children, ...props } = this.props; 18 | 19 | if (autosize && autocomplete) { 20 | return this.renderAutosizeAutocompleteDropdown(children, props); 21 | } else if (autosize) { 22 | return this.renderAutosizeDropdown(children, props); 23 | } else if (autocomplete) { 24 | return this.renderAutocompleteDropdown(children, props); 25 | } else { 26 | return this.renderDropdown(children, props); 27 | } 28 | } 29 | 30 | renderAutosizeAutocompleteDropdown(children, props) { 31 | return ( 32 | 33 | {(dropdownInputProps, { textValue }, registerInput) => ( 34 | 42 | {(inputProps, { matchingText }, registerInput) => ( 43 | 52 | {(autosizeInputProps, { width }, registerInput) => 53 | renderChild( 54 | children, 55 | { 56 | ...dropdownInputProps, 57 | ...inputProps, 58 | ...autosizeInputProps 59 | }, 60 | { matchingText, width }, 61 | registerInput 62 | ) 63 | } 64 | 65 | )} 66 | 67 | )} 68 | 69 | ); 70 | } 71 | 72 | renderAutosizeDropdown(children, props) { 73 | return ( 74 | 75 | {(inputProps, { textValue }, registerInput) => ( 76 | 85 | {(autosizeInputProps, { width }, registerInput) => 86 | renderChild( 87 | children, 88 | { ...inputProps, ...autosizeInputProps }, 89 | { width }, 90 | registerInput 91 | ) 92 | } 93 | 94 | )} 95 | 96 | ); 97 | } 98 | 99 | renderAutocompleteDropdown(children, props) { 100 | return ( 101 | 102 | {(dropdownInputProps, { textValue }, registerInput) => ( 103 | 111 | {(inputProps, { matchingText }, registerInput) => 112 | renderChild( 113 | children, 114 | { 115 | ...dropdownInputProps, 116 | ...inputProps 117 | }, 118 | { matchingText }, 119 | registerInput 120 | ) 121 | } 122 | 123 | )} 124 | 125 | ); 126 | } 127 | 128 | renderDropdown(children, props) { 129 | return ( 130 | 131 | {(inputProps, otherProps, registerInput) => 132 | renderChild(children, inputProps, otherProps, registerInput) 133 | } 134 | 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/DatePicker.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Children } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Mask from './Mask'; 4 | import InputPopup from './InputPopup'; 5 | import moment from 'moment'; 6 | import DayPicker, { DateUtils } from 'react-day-picker-themeable'; 7 | import MomentLocaleUtils from 'react-day-picker-themeable/moment'; 8 | import createStyling from './createStyling'; 9 | 10 | const VALIDATORS = { 11 | YYYY: () => false, 12 | MM: val => (parseInt(val, 10) > 12 ? '12' : false), 13 | ddd: () => {}, 14 | DD: val => (parseInt(val, 10) > 31 ? '31' : false) 15 | }; 16 | 17 | function getStateFromProps(value, props) { 18 | const date = moment( 19 | value === null ? undefined : value, 20 | value ? props.pattern : '', 21 | props.locale 22 | ); 23 | 24 | return { 25 | date: date.isValid() ? date : moment(undefined, '', props.locale), 26 | value, 27 | pattern: props.pattern.replace(/ddd/g, '\\d\\d\\d').replace(/[DMY]/g, '0') 28 | }; 29 | } 30 | 31 | export default class DatePicker extends PureComponent { 32 | popupEl = null; 33 | 34 | constructor(props) { 35 | super(props); 36 | this.state = getStateFromProps(props.value, props); 37 | this.styling = createStyling(props.theme); 38 | } 39 | 40 | static propTypes = { 41 | pattern: PropTypes.string, 42 | placeholder: PropTypes.string, 43 | onRenderCalendar: PropTypes.func, 44 | getInputElement: PropTypes.func, 45 | locale: PropTypes.string 46 | }; 47 | 48 | static defaultProps = { 49 | pattern: 'ddd DD/MM/YYYY', 50 | placeholder: moment().format('ddd DD/MM/YYYY'), 51 | todayButtonText: 'Go to Today', 52 | onRenderCalendar: options => 53 | options.popupShown && ( 54 |
60 | 63 | DateUtils.isSameDay(options.date.toDate(), day) 64 | } 65 | onDayClick={day => 66 | options.onSelect(moment(day, null, options.locale)) 67 | } 68 | onTodayButtonClick={options.onTodayButtonClick} 69 | locale={options.locale} 70 | localeUtils={MomentLocaleUtils} 71 | todayButton={options.todayButtonText} 72 | /> 73 |
74 | ), 75 | locale: 'en' 76 | }; 77 | 78 | componentWillUpdate(nextProps, nextState) { 79 | const value = 80 | nextProps.value !== this.props.value ? nextProps.value : nextState.value; 81 | const state = getStateFromProps(value, nextProps); 82 | 83 | if (state.value !== nextState.value) { 84 | this.setState(getStateFromProps(value, nextProps)); 85 | } 86 | } 87 | 88 | render() { 89 | const { 90 | children, 91 | placeholder, 92 | registerInput, 93 | getInputElement 94 | } = this.props; 95 | 96 | const child = (maskProps, otherProps, registerInput) => 97 | typeof children === 'function' 98 | ? children(maskProps, otherProps, registerInput) 99 | : React.cloneElement(Children.only(children), { 100 | ...maskProps, 101 | ...Children.only(children).props 102 | }); 103 | 104 | return ( 105 | 115 | {(maskProps, otherProps, registerInput) => ( 116 | { 127 | this.popupEl = c; 128 | }} 129 | > 130 | {(inputProps, otherProps, registerInput) => 131 | child(inputProps, otherProps, registerInput) 132 | } 133 | 134 | )} 135 | 136 | ); 137 | } 138 | 139 | handlePopupShownChange = popupShown => { 140 | this.setState({ popupShown }); 141 | }; 142 | 143 | handleIsActiveChange = isActive => { 144 | this.setState({ isActive }); 145 | }; 146 | 147 | handleChange = e => { 148 | this.setState(getStateFromProps(e.target.value, this.props)); 149 | 150 | if (this.props.onInputChange) { 151 | this.props.onInputChange(e); 152 | } 153 | }; 154 | 155 | handleValuePreUpdate = value => { 156 | if (this.props.onValuePreUpdate) { 157 | value = this.props.onValuePreUpdate(value); 158 | } 159 | const localeData = moment.localeData(this.props.locale); 160 | const days = localeData._weekdaysShort; 161 | 162 | return value.replace( 163 | RegExp(`(${days.join('|').replace('.', '\\.')})`, 'g'), 164 | 'ddd' 165 | ); 166 | }; 167 | 168 | handleValueUpdate = value => { 169 | const localeData = moment.localeData(this.props.locale); 170 | const state = getStateFromProps( 171 | value.replace(/ddd/g, localeData.weekdaysShort(this.state.date)), 172 | this.props 173 | ); 174 | 175 | return value.replace(/ddd/g, localeData.weekdaysShort(state.date)); 176 | }; 177 | 178 | renderPopup = (styling, isActive, popupShown) => { 179 | const { onRenderCalendar, locale, todayButtonText } = this.props; 180 | 181 | return onRenderCalendar({ 182 | styling, 183 | date: this.state.date, 184 | isActive, 185 | popupShown, 186 | onSelect: this.handleSelect, 187 | onTodayButtonClick: this.handleTodayButtonClick, 188 | locale, 189 | todayButtonText 190 | }); 191 | }; 192 | 193 | handleTodayButtonClick = () => { 194 | if (this.popupEl) { 195 | this.popupEl.focus(); 196 | } 197 | }; 198 | 199 | handleSelect = date => { 200 | const localeMoment = moment(date); 201 | localeMoment.locale(this.props.locale); 202 | const value = localeMoment.format(this.props.pattern); 203 | this.setState({ 204 | popupShown: false, 205 | isActive: false, 206 | ...getStateFromProps(value, this.props) 207 | }); 208 | this.props.onChange && this.props.onChange(date); 209 | }; 210 | 211 | handleValidate = (value, processedValue) => { 212 | const { pattern, emptyChar } = this.props; 213 | const re = RegExp(emptyChar, 'g'); 214 | let result = processedValue.result; 215 | 216 | Object.keys(VALIDATORS).forEach(format => { 217 | const pos = pattern.indexOf(format); 218 | if (pos !== -1) { 219 | let val = processedValue.result 220 | .substr(pos, format.length) 221 | .replace(re, ''); 222 | val = VALIDATORS[format](val); 223 | if (val) { 224 | result = 225 | result.substr(0, pos) + val + result.substr(pos + val.length); 226 | } 227 | } 228 | }); 229 | 230 | return { 231 | ...processedValue, 232 | result: this.handleValueUpdate(result) 233 | }; 234 | }; 235 | } 236 | -------------------------------------------------------------------------------- /src/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import * as shapes from './shapes'; 4 | import findMatchingTextIndex from './utils/findMatchingTextIndex'; 5 | import * as filters from './filters'; 6 | import InputPopup from './InputPopup'; 7 | import getOptionText from './utils/getOptionText'; 8 | import getOptionLabel from './utils/getOptionLabel'; 9 | import getOptionValue from './utils/getOptionValue'; 10 | import isStatic from './utils/isStatic'; 11 | import DropdownOption from './DropdownOption'; 12 | import createStyling from './createStyling'; 13 | import deprecated from './utils/deprecated'; 14 | import getInput from './utils/getInput'; 15 | import registerInput from './utils/registerInput'; 16 | 17 | function getOptionKey(opt, idx) { 18 | const value = getOptionValue(opt); 19 | 20 | return opt === null 21 | ? `option-separator-${idx}` 22 | : `option-${typeof value === 'string' ? value : getOptionText(opt) + idx}`; 23 | } 24 | 25 | function getSiblingIndex(idx, options, next) { 26 | if (idx === null) { 27 | idx = next ? -1 : options.length; 28 | } 29 | 30 | const step = next ? 1 : -1; 31 | 32 | for (let i = 0; i < options.length; i++) { 33 | const currentIdx = (idx + (i + 1) * step + options.length) % options.length; 34 | if (options[currentIdx] !== null && !options[currentIdx].disabled) { 35 | return currentIdx; 36 | } 37 | } 38 | 39 | return idx; 40 | } 41 | 42 | function getShownOptions(value, options, optionFilters) { 43 | return optionFilters.reduce((o, filter) => filter(o, value), options); 44 | } 45 | 46 | function findOptionIndex(options, option) { 47 | return Array.findIndex(options, opt => opt === option); 48 | } 49 | 50 | function getStateFromProps(props) { 51 | const value = props.value; 52 | const match = findMatchingTextIndex(value, props.options); 53 | const [selectedIndex, matchingText] = match; 54 | const shownOptions = getShownOptions( 55 | matchingText, 56 | props.options, 57 | props.optionFilters 58 | ); 59 | const highlightedIndex = findOptionIndex( 60 | shownOptions, 61 | props.options[selectedIndex] 62 | ); 63 | 64 | return { 65 | value: matchingText || null, 66 | isActive: false, 67 | listShown: false, 68 | selectedIndex, 69 | highlightedIndex, 70 | shownOptions 71 | }; 72 | } 73 | 74 | export default class Dropdown extends PureComponent { 75 | constructor(props) { 76 | super(props); 77 | 78 | this.state = getStateFromProps(props); 79 | this.styling = createStyling(props.theme); 80 | 81 | if (typeof props.onValueChange !== 'undefined') { 82 | deprecated( 83 | '`onValueChange` is deprecated, please use `onSelect` instead' 84 | ); 85 | } 86 | } 87 | 88 | static propTypes = { 89 | value: PropTypes.string, 90 | options: PropTypes.arrayOf(shapes.ITEM_OR_STRING), 91 | onRenderOption: PropTypes.func, 92 | onRenderList: PropTypes.func, 93 | optionFilters: PropTypes.arrayOf(PropTypes.func) 94 | }; 95 | 96 | static defaultProps = { 97 | onRenderOption: (styling, opt, highlighted, disabled) => 98 | opt !== null ? ( 99 |
100 | {getOptionLabel(opt, highlighted, disabled)} 101 |
102 | ) : ( 103 |
104 | ), 105 | 106 | onRenderList: (styling, isActive, listShown, children, header) => 107 | listShown && ( 108 |
114 | {header && ( 115 |
116 | {header} 117 |
118 | )} 119 |
120 | {children} 121 |
122 |
123 | ), 124 | 125 | onRenderListHeader: (allCount, shownCount, staticCount) => { 126 | if (allCount - staticCount < 20) return null; 127 | const allItems = `${allCount - staticCount} ${ 128 | allCount - staticCount === 1 ? 'item' : 'items' 129 | }`; 130 | return allCount === shownCount 131 | ? `${allItems} found` 132 | : `${shownCount - staticCount} of ${allItems} shown`; 133 | }, 134 | 135 | dropdownProps: {}, 136 | 137 | optionFilters: [ 138 | filters.filterByMatchingTextWithThreshold(20), 139 | filters.sortByMatchingText, 140 | filters.limitBy(100), 141 | filters.notFoundMessage('No matches found'), 142 | filters.filterRedudantSeparators 143 | ] 144 | }; 145 | 146 | componentWillUpdate(nextProps, nextState) { 147 | const { options, optionFilters } = nextProps; 148 | const optionsChanged = this.props.options !== options; 149 | 150 | if ( 151 | (nextProps.value && nextState.value === null) || 152 | this.props.value !== nextProps.value 153 | ) { 154 | const state = getStateFromProps(nextProps); 155 | 156 | if (state.value !== this.state.value || optionsChanged) { 157 | this.setState(state); 158 | } 159 | } else if (optionsChanged || this.props.optionFilters !== optionFilters) { 160 | const [highlightedIndex, shownOptions] = this.updateHighlightedIndex( 161 | nextState.value, 162 | options, 163 | optionFilters 164 | ); 165 | const selectedIndex = findOptionIndex( 166 | options, 167 | shownOptions[highlightedIndex] 168 | ); 169 | 170 | this.setState({ selectedIndex }); 171 | 172 | const state = getStateFromProps(nextProps); 173 | 174 | if (state.value !== this.state.value && !nextState.isActive) { 175 | this.setState(state); 176 | } 177 | } else if (this.state.isActive && !nextState.isActive) { 178 | this.setState({ 179 | value: getOptionText(nextProps.options[nextState.selectedIndex]) 180 | }); 181 | } 182 | } 183 | 184 | updateHighlightedIndex(value, options, optionFilters) { 185 | const shownOptions = getShownOptions(value, options, optionFilters); 186 | const match = findMatchingTextIndex(value, shownOptions, true); 187 | const [highlightedIndex] = match; 188 | 189 | this.setState({ highlightedIndex, shownOptions }); 190 | 191 | return [highlightedIndex, shownOptions]; 192 | } 193 | 194 | render() { 195 | const { dropdownProps, onRenderCaret, children } = this.props; 196 | 197 | const value = this.state.value === null ? '' : this.state.value; 198 | 199 | return ( 200 | 215 | {children} 216 | 217 | ); 218 | } 219 | 220 | registerInput = input => registerInput(this, input); 221 | 222 | renderPopup = (styling, isActive, popupShown) => { 223 | const { onRenderList, onRenderListHeader, options } = this.props; 224 | const { shownOptions } = this.state; 225 | 226 | return onRenderList( 227 | styling, 228 | isActive, 229 | popupShown, 230 | shownOptions.map(this.renderOption), 231 | onRenderListHeader( 232 | options.length, 233 | shownOptions.length, 234 | shownOptions.filter(isStatic).length 235 | ) 236 | ); 237 | }; 238 | 239 | renderOption = (opt, idx) => { 240 | const { onRenderOption } = this.props; 241 | const highlighted = idx === this.state.highlightedIndex; 242 | const disabled = opt && opt.disabled; 243 | 244 | return ( 245 | 250 | {onRenderOption(this.styling, opt, highlighted, disabled)} 251 | 252 | ); 253 | }; 254 | 255 | handleOptionClick(idx, e) { 256 | const option = this.state.shownOptions[idx]; 257 | 258 | if (!option || option.disabled) { 259 | e.preventDefault(); 260 | return; 261 | } 262 | 263 | this.setState( 264 | { 265 | listShown: false 266 | }, 267 | () => { 268 | this.selectOption(findOptionIndex(this.props.options, option), true); 269 | } 270 | ); 271 | } 272 | 273 | handleChange = e => { 274 | const { options, optionFilters } = this.props; 275 | const value = e.target.value; 276 | 277 | this.setState({ value }); 278 | this.updateHighlightedIndex(value, options, optionFilters); 279 | 280 | if (this.props.onChange) { 281 | this.props.onChange(e); 282 | } 283 | }; 284 | 285 | handleKeyDown = e => { 286 | const keyMap = { 287 | ArrowUp: this.handleArrowUpKeyDown, 288 | ArrowDown: this.handleArrowDownKeyDown, 289 | Enter: this.handleEnterKeyDown 290 | }; 291 | 292 | if (keyMap[e.key]) { 293 | keyMap[e.key](e); 294 | } 295 | 296 | if (this.props.onKeyDown) { 297 | this.props.onKeyDown(e); 298 | } 299 | }; 300 | 301 | handleArrowUpKeyDown = e => { 302 | const { highlightedIndex, shownOptions } = this.state; 303 | 304 | e.preventDefault(); 305 | 306 | this.setState({ 307 | highlightedIndex: getSiblingIndex(highlightedIndex, shownOptions, false) 308 | }); 309 | }; 310 | 311 | handleArrowDownKeyDown = e => { 312 | const { highlightedIndex, shownOptions } = this.state; 313 | 314 | e.preventDefault(); 315 | 316 | this.setState({ 317 | highlightedIndex: getSiblingIndex(highlightedIndex, shownOptions, true) 318 | }); 319 | }; 320 | 321 | handleEnterKeyDown = () => { 322 | const { highlightedIndex, shownOptions } = this.state; 323 | const option = shownOptions[highlightedIndex]; 324 | 325 | setTimeout(() => { 326 | this.selectOption(findOptionIndex(this.props.options, option), true); 327 | getInput(this).blur(); 328 | }); 329 | }; 330 | 331 | selectOption(index, fireOnSelect) { 332 | const { options, optionFilters } = this.props; 333 | const option = options[index]; 334 | const shownOptions = getShownOptions( 335 | getOptionText(option), 336 | options, 337 | optionFilters 338 | ); 339 | 340 | const onSelect = this.props.onSelect || this.props.onValueChange; 341 | 342 | this.setState({ 343 | value: getOptionText(option), 344 | highlightedIndex: findOptionIndex(shownOptions, option), 345 | selectedIndex: index, 346 | isActive: false, 347 | shownOptions 348 | }); 349 | if (fireOnSelect && onSelect) { 350 | onSelect(getOptionValue(option), getOptionText(option)); 351 | } 352 | } 353 | 354 | handleIsActiveChange = isActive => { 355 | this.setState({ isActive }); 356 | }; 357 | 358 | handlePopupShownChange = popupShown => { 359 | this.setState({ listShown: popupShown }); 360 | }; 361 | } 362 | -------------------------------------------------------------------------------- /src/DropdownOption.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { findDOMNode } from 'react-dom'; 4 | 5 | export default class DropdownOption extends PureComponent { 6 | static propTypes = { 7 | highlighted: PropTypes.bool, 8 | onMouseDown: PropTypes.func 9 | }; 10 | 11 | componentDidMount() { 12 | if (this.props.highlighted) { 13 | this.scrollToOption(); 14 | } 15 | } 16 | 17 | componentDidUpdate(prevProps) { 18 | if (!prevProps.highlighted && this.props.highlighted) { 19 | this.scrollToOption(); 20 | } 21 | } 22 | 23 | scrollToOption() { 24 | try { 25 | const optionEl = findDOMNode(this); 26 | if (optionEl) { 27 | const optionHeight = optionEl.offsetHeight; 28 | const listEl = optionEl.parentNode; 29 | const listHeight = listEl.clientHeight; 30 | listEl.scrollTop = optionEl.offsetTop - (listHeight - optionHeight) / 2; 31 | } 32 | } catch (e) {} 33 | } 34 | 35 | render() { 36 | return ( 37 |
{this.props.children}
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/InputPopup.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import renderChild from './utils/renderChild'; 4 | 5 | export default class InputPopup extends PureComponent { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | isActive: props.isActive, 11 | popupShown: props.popupShown, 12 | hover: false 13 | }; 14 | } 15 | 16 | static propTypes = { 17 | onRenderCaret: PropTypes.func, 18 | onRenderPopup: PropTypes.func, 19 | onIsActiveChange: PropTypes.func, 20 | onPopupShownChange: PropTypes.func, 21 | registerInput: PropTypes.func 22 | }; 23 | 24 | static defaultProps = { 25 | onRenderCaret: (styling, isActive, isHovered, children) => ( 26 |
27 | {children} 28 |
29 | ), 30 | 31 | onRenderPopup: () => {}, 32 | 33 | inputPopupProps: {} 34 | }; 35 | 36 | renderCaretSVG(styling, isActive, hovered, popupShown) { 37 | const svgStyling = styling( 38 | 'inputEnhancementsCaretSvg', 39 | isActive, 40 | hovered, 41 | popupShown 42 | ); 43 | return popupShown ? ( 44 | 45 | 46 | 47 | ) : ( 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | componentWillUnmount() { 55 | if (this.blurTimeout) { 56 | clearTimeout(this.blurTimeout); 57 | } 58 | } 59 | 60 | componentWillUpdate(nextProps) { 61 | if (nextProps.popupShown !== this.props.popupShown) { 62 | this.setState({ popupShown: nextProps.popupShown }); 63 | } 64 | 65 | if (nextProps.isActive !== this.props.isActive) { 66 | this.setState({ isActive: nextProps.isActive }); 67 | } 68 | } 69 | 70 | componentDidUpdate(prevProps, prevState) { 71 | if ( 72 | prevState.isActive !== this.state.isActive && 73 | this.props.onIsActiveChange 74 | ) { 75 | this.props.onIsActiveChange(this.state.isActive); 76 | } 77 | 78 | if ( 79 | prevState.popupShown !== this.state.popupShown && 80 | this.props.onPopupShownChange 81 | ) { 82 | this.props.onPopupShownChange(this.state.popupShown); 83 | } 84 | } 85 | 86 | render() { 87 | const { 88 | onRenderCaret, 89 | onRenderPopup, 90 | inputPopupProps, 91 | styling, 92 | popupRef, 93 | ...restProps 94 | } = this.props; 95 | const { isActive, hover, popupShown } = this.state; 96 | 97 | const caret = this.renderCaretSVG(styling, isActive, hover, popupShown); 98 | 99 | return ( 100 |
107 | {this.renderInput(styling, restProps)} 108 | {onRenderCaret(styling, isActive, hover, caret)} 109 | {onRenderPopup(styling, isActive, popupShown)} 110 |
111 | ); 112 | } 113 | 114 | renderInput(styling, restProps) { 115 | const { 116 | children, 117 | onInputFocus, 118 | onInputBlur, 119 | customProps, 120 | onChange, 121 | onInput, 122 | value, 123 | registerInput, 124 | placeholder 125 | } = restProps; 126 | const { isActive, hover, popupShown } = this.state; 127 | 128 | const inputProps = { 129 | ...styling('inputEnhancementsInput', isActive, hover, popupShown), 130 | value, 131 | placeholder, 132 | onFocus: onInputFocus, 133 | onBlur: onInputBlur, 134 | onChange, 135 | onInput, 136 | onMouseEnter: this.handleMouseEnter, 137 | onMouseLeave: this.handleMouseLeave, 138 | onKeyDown: this.handleKeyDown 139 | }; 140 | 141 | return renderChild(children, inputProps, customProps, registerInput); 142 | } 143 | 144 | handleMouseEnter = e => { 145 | this.setState({ hover: true }); 146 | 147 | if (this.props.onInputMouseEnter) { 148 | this.props.onInputMouseEnter(e); 149 | } 150 | }; 151 | 152 | handleMouseLeave = e => { 153 | this.setState({ hover: false }); 154 | 155 | if (this.props.onInputMouseLeave) { 156 | this.props.onInputMouseLeave(e); 157 | } 158 | }; 159 | 160 | handleKeyDown = e => { 161 | const keyMap = { 162 | Escape: this.handleEscapeKeyDown, 163 | Enter: this.handleEnterKeyDown 164 | }; 165 | 166 | if (keyMap[e.key]) { 167 | keyMap[e.key](e); 168 | } else { 169 | this.setState({ 170 | popupShown: true 171 | }); 172 | } 173 | 174 | if (this.props.onKeyDown) { 175 | this.props.onKeyDown(e); 176 | } 177 | }; 178 | 179 | handleEscapeKeyDown = () => { 180 | this.setState({ 181 | popupShown: false 182 | }); 183 | }; 184 | 185 | handleEnterKeyDown = () => { 186 | this.setState({ 187 | popupShown: false 188 | }); 189 | }; 190 | 191 | handleFocus = e => { 192 | if (this.blurTimeout) { 193 | clearTimeout(this.blurTimeout); 194 | this.blurTimeout = null; 195 | return; 196 | } 197 | 198 | this.setState({ 199 | isActive: true, 200 | popupShown: true 201 | }); 202 | 203 | if (this.props.onFocus) { 204 | this.props.onFocus(e); 205 | } 206 | }; 207 | 208 | handleBlur = e => { 209 | this.blurTimeout = setTimeout(() => { 210 | this.setState({ 211 | isActive: false, 212 | popupShown: false 213 | }); 214 | this.blurTimeout = null; 215 | }); 216 | 217 | if (this.props.onBlur) { 218 | this.props.onBlur(e); 219 | } 220 | }; 221 | } 222 | -------------------------------------------------------------------------------- /src/Mask.jsx: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import applyMaskToString from './applyMaskToString'; 4 | import getInput from './utils/getInput'; 5 | import registerInput from './utils/registerInput'; 6 | import renderChild from './utils/renderChild'; 7 | 8 | function getStateFromProps(value, props) { 9 | value = props.onValuePreUpdate(value); 10 | let processedValue = applyMaskToString(value, props.pattern, props.emptyChar); 11 | const validatedValue = props.onValidate(value, processedValue); 12 | if (validatedValue && validatedValue.result) { 13 | processedValue = validatedValue; 14 | } else if (validatedValue) { 15 | processedValue.isValid = false; 16 | } 17 | const state = processedValue.isValid 18 | ? { value: processedValue.result, lastIndex: processedValue.lastIndex } 19 | : {}; 20 | 21 | if (!processedValue.unmaskedValue && props.placeholder) { 22 | state.value = ''; 23 | } 24 | 25 | return [state, processedValue]; 26 | } 27 | 28 | export default class Mask extends PureComponent { 29 | constructor(props) { 30 | super(props); 31 | 32 | const value = props.value || ''; 33 | const [state] = getStateFromProps(value, props); 34 | this.state = { 35 | value, 36 | lastIndex: 0, 37 | ...state 38 | }; 39 | } 40 | 41 | static propTypes = { 42 | getInputElement: PropTypes.func, 43 | value: PropTypes.string, 44 | pattern: PropTypes.string.isRequired, 45 | emptyChar: PropTypes.string 46 | }; 47 | 48 | static defaultProps = { 49 | emptyChar: ' ', 50 | onValidate: () => {}, 51 | onValuePreUpdate: v => v 52 | }; 53 | 54 | componentWillReceiveProps(nextProps) { 55 | if ( 56 | this.props.pattern !== nextProps.pattern || 57 | this.props.value !== nextProps.value || 58 | this.props.emptyChar !== nextProps.emptyChar 59 | ) { 60 | this.setValue(nextProps.value, nextProps); 61 | } 62 | } 63 | 64 | setValue(value, props) { 65 | const [state, processedValue] = getStateFromProps(value, props); 66 | 67 | if (processedValue.isValid) { 68 | this.setState(state, () => this.setSelectionRange(this.state.lastIndex)); 69 | } else { 70 | this.setSelectionRange(this.state.lastIndex); 71 | } 72 | 73 | return processedValue; 74 | } 75 | 76 | setSelectionRange(lastIndex) { 77 | const input = getInput(this); 78 | if (input === document.activeElement) { 79 | input.setSelectionRange(lastIndex, lastIndex); 80 | } 81 | } 82 | 83 | registerInput = input => registerInput(this, input); 84 | 85 | render() { 86 | const { children, placeholder } = this.props; 87 | const { value } = this.state; 88 | const inputProps = { 89 | value, 90 | placeholder, 91 | onInput: this.handleInput, 92 | onChange: () => {} 93 | }; 94 | 95 | return renderChild(children, inputProps, { value }, this.registerInput); 96 | } 97 | 98 | // works better for IE than onChange 99 | handleInput = e => { 100 | const value = e.target.value; 101 | 102 | if (this.props.value === undefined) { 103 | const processedValue = this.setValue(value, this.props); 104 | if (!processedValue.isValid) { 105 | e.preventDefault(); 106 | return; 107 | } 108 | 109 | e.target.value = processedValue.result; 110 | 111 | if (this.props.onUnmaskedValueChange) { 112 | this.props.onUnmaskedValueChange(processedValue.unmaskedValue); 113 | } 114 | } 115 | 116 | if (this.props.onChange) { 117 | this.props.onChange(e); 118 | } 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /src/applyMaskToString.js: -------------------------------------------------------------------------------- 1 | export default function applyMaskToString(string, pattern, emptyChar) { 2 | let result = ''; 3 | let stringIndex = 0; 4 | let lastIndex = 0; 5 | let i; 6 | 7 | string = string.replace(new RegExp(emptyChar, 'g'), ''); 8 | for (i = 0; i < pattern.length; i++) { 9 | const patternChar = pattern[i]; 10 | if (patternChar === '\\') { 11 | string = string.replace(pattern[++i], ''); 12 | } else if (patternChar !== '0' && patternChar !== 'a') { 13 | string = string.replace(patternChar, ''); 14 | } 15 | } 16 | 17 | for (i = 0; i < pattern.length; i++) { 18 | const patternChar = pattern[i]; 19 | const stringChar = 20 | stringIndex < string.length ? string[stringIndex] : emptyChar; 21 | if (stringIndex < string.length) { 22 | lastIndex = result.length + 1; 23 | } 24 | 25 | switch (patternChar) { 26 | case 'a': 27 | if (!/^[a-zA-Z]$/.test(stringChar) && stringChar !== emptyChar) { 28 | return { 29 | result, 30 | unmaskedValue: string, 31 | isValid: false 32 | }; 33 | } 34 | result += stringChar; 35 | stringIndex++; 36 | break; 37 | 38 | case '0': 39 | if (!/^[0-9]$/.test(stringChar) && stringChar !== emptyChar) { 40 | return { 41 | result, 42 | unmaskedValue: string, 43 | isValid: false 44 | }; 45 | } 46 | result += stringChar; 47 | stringIndex++; 48 | break; 49 | 50 | case '\\': 51 | if (++i < pattern.length) { 52 | result += pattern[i]; 53 | if (stringChar === pattern[i]) { 54 | stringIndex++; 55 | } 56 | } 57 | break; 58 | 59 | default: 60 | result += pattern[i]; 61 | if (stringChar === pattern[i]) { 62 | stringIndex++; 63 | } 64 | break; 65 | } 66 | } 67 | 68 | return { 69 | result, 70 | unmaskedValue: string, 71 | isValid: stringIndex >= string.length, 72 | lastIndex 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/createStyling.js: -------------------------------------------------------------------------------- 1 | import { createStyling } from 'react-base16-styling'; 2 | import defaultTheme from './themes/default'; 3 | import Prefixer from 'inline-style-prefixer'; 4 | 5 | let prefixerInstance; 6 | if (typeof window !== 'undefined' && window.navigator) { 7 | prefixerInstance = new Prefixer(window.navigator); 8 | } else { 9 | prefixerInstance = new Prefixer({ 10 | userAgent: 11 | 'Node.js (darwin; U; rv:v4.3.1) AppleWebKit/537.36 (KHTML, like Gecko)' 12 | }); 13 | } 14 | const prefixer = prefixerInstance.prefix.bind(prefixerInstance); 15 | 16 | const navButtonImg = type => 17 | ({ 18 | /* eslint-disable max-len */ 19 | prev: 20 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAwCAYAAAB5R9gVAAAABGdBTUEAALGPC/xhBQAAAVVJREFUWAnN2G0KgjAYwPHpGfRkaZeqvgQaK+hY3SUHrk1YzNLay/OiEFp92I+/Mp2F2Mh2lLISWnflFjzH263RQjzMZ19wgs73ez0o1WmtW+dgA01VxrE3p6l2GLsnBy1VYQOtVSEH/atCCgqpQgKKqYIOiq2CBkqtggLKqQIKgqgCBjpJ2Y5CdJ+zrT9A7HHSTA1dxUdHgzCqJIEwq0SDsKsEg6iqBIEoq/wEcVRZBXFV+QJxV5mBtlDFB5VjYTaGZ2sf4R9PM7U9ZU+lLuaetPP/5Die3ToO1+u+MKtHs06qODB2zBnI/jBd4MPQm1VkY79Tb18gB+C62FdBFsZR6yeIo1YQiLJWMIiqVjQIu1YSCLNWFgijVjYIuhYYCKoWKAiiFgoopxYaKLUWOii2FgkophYp6F3r42W5A9s9OcgNvva8xQaysKXlFytoqdYmQH6tF3toSUo0INq9AAAAAElFTkSuQmCC', 21 | next: 22 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAwCAYAAAB5R9gVAAAABGdBTUEAALGPC/xhBQAAAXRJREFUWAnN119ugjAcwPHWzJ1gnmxzB/BBE0n24m4xfNkTaOL7wOtsl3AXMMb+Vjaa1BG00N8fSEibPpAP3xAKKs2yjzTPH9RAjhEo9WzPr/Vm8zgE0+gXATAxxuxtqeJ9t5tIwv5AtQAApsfT6TPdbp+kUBcgVwvO51KqVhMkXKsVJFXrOkigVhCIs1Y4iKlWZxB1rX4gwlpRIIpa8SDkWmggrFq4IIRaJKCYWnSgnrXIQV1r8YD+1Vrn+bReagysIFfLABRt31v8oBu1xEBttfRbltmfjgEcWh9snUS2kNdBK6WN1vrOWxObWsz+fjxevsxmB1GQDfINWiev83nhaoiB/CoOU438oPrhXS0WpQ9xc1ZQWxWHqUYe0I0qrKCQKjygDlXIQV2r0IF6ViEBxVTBBSFUQQNhVYkHIVeJAtkNsbQ7c1LtzP6FsObhb2rCKv7NBIGoq4SDmKoEgTirXAcJVGkFSVVpgoSrXICGUMUH/QBZNSUy5XWUhwAAAABJRU5ErkJggg==' 23 | /* eslint-enable max-len */ 24 | }[type]); 25 | 26 | const dayStyle = mod => 27 | ({ 28 | today: { 29 | boxShadow: '0px 0px 0 1px #FFFFFF, 0px 0px 0 2px #4A90E2', 30 | fontWeight: '500' 31 | }, 32 | disabled: { 33 | color: '#dce0e0', 34 | cursor: 'default', 35 | backgroundColor: '#FFFFFF' 36 | }, 37 | outside: { 38 | cursor: 'default', 39 | color: '#dce0e0', 40 | backgroundColor: '#FFFFFF' 41 | }, 42 | sunday: { 43 | backgroundColor: '#f7f8f8' 44 | }, 45 | selected: { 46 | color: '#FFF', 47 | backgroundColor: '#4A90E2' 48 | } 49 | }[mod]); 50 | 51 | function getStylingFromBase16() { 52 | return { 53 | dayPicker: ({ style }) => ({ 54 | style: { 55 | ...style, 56 | ...prefixer({ 57 | display: 'inline-block', 58 | fontSize: 14 59 | }) 60 | }, 61 | className: '' 62 | }), 63 | dayPickerWrapper: ({ style }) => ({ 64 | style: { 65 | ...style, 66 | ...prefixer({ 67 | position: 'relative', 68 | userSelect: 'none', 69 | paddingBottom: '1rem', 70 | flexDirection: 'row' 71 | }) 72 | }, 73 | className: '' 74 | }), 75 | dayPickerMonthWrapper: ({ style }) => ({ 76 | style: { 77 | ...style, 78 | ...prefixer({ 79 | display: 'table-row-group' 80 | }) 81 | }, 82 | className: '' 83 | }), 84 | dayPickerMonths: ({ style }) => ({ 85 | style: { 86 | ...style, 87 | ...prefixer({ 88 | display: 'flex', 89 | flexWrap: 'wrap', 90 | justifyContent: 'center' 91 | }) 92 | }, 93 | className: '' 94 | }), 95 | dayPickerMonth: ({ style }) => ({ 96 | style: { 97 | ...style, 98 | ...prefixer({ 99 | display: 'table', 100 | width: '26rem', 101 | borderSpacing: '0.2rem', 102 | userSelect: 'none', 103 | margin: '0 1rem', 104 | marginTop: '1rem' 105 | }) 106 | }, 107 | className: '' 108 | }), 109 | dayPickerNavBar: ({ style }) => ({ 110 | style: { 111 | ...style 112 | // position: 'absolute', 113 | // left: '0', 114 | // right: '0', 115 | // padding: '1rem 0.5rem' 116 | }, 117 | className: '' 118 | }), 119 | dayPickerNavButton: ({ style }, type, shouldShow, isHovered) => ({ 120 | style: { 121 | ...style, 122 | ...prefixer({ 123 | marginRight: type === 'prev' ? '2.5rem' : 0, 124 | position: 'absolute', 125 | cursor: 'pointer', 126 | top: '1rem', 127 | right: '1.5rem', 128 | marginTop: '2px', 129 | width: '2rem', 130 | height: '2rem', 131 | display: shouldShow ? 'inline-block' : 'none', 132 | backgroundSize: '50%', 133 | backgroundRepeat: 'no-repeat', 134 | backgroundPosition: 'center', 135 | backgroundImage: `url("${navButtonImg(type)}")`, 136 | opacity: isHovered ? 1 : 0.8 137 | }) 138 | }, 139 | className: '' 140 | }), 141 | dayPickerCaption: ({ style }) => ({ 142 | style: { 143 | ...style, 144 | ...prefixer({ 145 | padding: '0 0.5rem', 146 | display: 'table-caption', 147 | textAlign: 'left', 148 | marginBottom: '0.5rem' 149 | }) 150 | }, 151 | className: '' 152 | }), 153 | dayPickerCaptionInner: ({ style }) => ({ 154 | style: { 155 | ...style, 156 | ...prefixer({ 157 | fontSize: '1.6rem', 158 | fontWeight: '500' 159 | }) 160 | }, 161 | className: '' 162 | }), 163 | dayPickerWeekdays: ({ style }) => ({ 164 | style: { 165 | ...style, 166 | marginTop: '1rem', 167 | display: 'table-header-group' 168 | }, 169 | className: '' 170 | }), 171 | dayPickerWeekdaysRow: ({ style }) => ({ 172 | style: { 173 | ...style, 174 | ...prefixer({ 175 | display: 'table-row' 176 | }) 177 | }, 178 | className: '' 179 | }), 180 | dayPickerWeekday: ({ style }) => ({ 181 | style: { 182 | ...style, 183 | ...prefixer({ 184 | display: 'table-cell', 185 | padding: '0.5rem', 186 | textAlign: 'center', 187 | cursor: 'pointer', 188 | verticalAlign: 'middle', 189 | outline: 'none', 190 | width: '14.3%' 191 | }) 192 | }, 193 | className: '' 194 | }), 195 | dayPickerWeekdayAbbr: ({ style }) => ({ 196 | style: { 197 | ...style, 198 | ...prefixer({ 199 | textDecoration: 'none', 200 | border: 0 201 | }) 202 | }, 203 | className: '' 204 | }), 205 | dayPickerWeekNumber: ({ style }) => ({ 206 | style: { 207 | ...style, 208 | ...prefixer({ 209 | display: 'table-cell', 210 | padding: '0.5rem', 211 | textAlign: 'right', 212 | verticalAlign: 'middle', 213 | minWidth: '1rem', 214 | fontSize: '0.75em', 215 | cursor: 'pointer', 216 | borderRight: '1px solid #eaecec' 217 | }) 218 | }, 219 | className: '' 220 | }), 221 | dayPickerWeek: ({ style }) => ({ 222 | style: { 223 | ...style, 224 | ...prefixer({ 225 | display: 'table-row' 226 | }) 227 | }, 228 | className: '' 229 | }), 230 | dayPickerFooter: ({ style }) => ({ 231 | style: { 232 | ...style, 233 | ...prefixer({ 234 | paddingTop: '0.5rem', 235 | textAlign: 'center' 236 | }) 237 | }, 238 | className: '' 239 | }), 240 | dayPickerTodayButton: ({ style }) => ({ 241 | style: { 242 | ...style, 243 | ...prefixer({ 244 | border: 'none', 245 | backgroundImage: 'none', 246 | boxShadow: 'none', 247 | cursor: 'pointer', 248 | fontSize: '0.875em' 249 | }) 250 | }, 251 | className: '' 252 | }), 253 | dayPickerDay: ({ style, className }, day, modifiers, isHovered) => ({ 254 | style: { 255 | ...style, 256 | display: 'table-cell', 257 | outline: 'none', 258 | backgroundColor: isHovered ? '#F0F0F0' : '#FFFFFF', 259 | padding: '0.5rem', 260 | borderRadius: '10rem', 261 | textAlign: 'center', 262 | cursor: 'pointer', 263 | verticalAlign: 'middle', 264 | ...Object.keys(modifiers).reduce( 265 | (s, mod) => ({ 266 | ...s, 267 | ...dayStyle(mod) 268 | }), 269 | {} 270 | ) 271 | }, 272 | className: '' 273 | }), 274 | 275 | inputEnhancementsListHeader: prefixer({ 276 | flex: '0 0 auto', 277 | minHeight: '3rem', 278 | fontSize: '0.8em', 279 | color: '#999999', 280 | backgroundColor: '#FAFAFA', 281 | padding: '0.5rem 1rem', 282 | borderBottom: '1px solid #DDDDDD' 283 | }), 284 | inputEnhancementsListOptions: prefixer({ 285 | flex: '1 1 auto', 286 | overflowY: 'auto', 287 | // fix for IE 288 | // https://connect.microsoft.com/IE/feedback/details/802625 289 | maxHeight: '27rem' 290 | }), 291 | inputEnhancementsOption: ({ style }, highlighted, disabled, hovered) => ({ 292 | style: { 293 | ...style, 294 | ...(disabled 295 | ? { 296 | padding: '1rem 1.5rem', 297 | cursor: 'pointer', 298 | color: '#999999' 299 | } 300 | : highlighted 301 | ? { 302 | padding: '1rem 1.5rem', 303 | cursor: 'pointer', 304 | color: '#FFFFFF', 305 | backgroundColor: '#3333FF' 306 | } 307 | : { 308 | padding: '1rem 1.5rem', 309 | cursor: 'pointer', 310 | backgroundColor: hovered ? '#3333FF' : '#FFFFFF' 311 | }) 312 | } 313 | }), 314 | inputEnhancementsSeparator: { 315 | margin: '0.5rem 0', 316 | width: '100%', 317 | height: '1px', 318 | borderTop: '1px solid #DDDDDD' 319 | }, 320 | 321 | inputEnhancementsPopupWrapper: { 322 | position: 'relative', 323 | display: 'inline-block' 324 | }, 325 | inputEnhancementsCaret: { 326 | position: 'absolute', 327 | right: '5px', 328 | top: 0, 329 | paddingTop: '5px', 330 | verticalAlign: 'middle', 331 | paddingLeft: '3px', 332 | width: '10px' 333 | }, 334 | inputEnhancementsCaretSvg: ({ style }, isActive, hovered) => ({ 335 | style: { 336 | ...style, 337 | ...prefixer({ 338 | display: 'inline-block', 339 | opacity: hovered || isActive ? 1 : 0, 340 | transition: 'opacity 0.15s linear, transform 0.15s linear', 341 | transform: hovered || isActive ? 'translateY(0)' : 'translateY(5px)' 342 | }) 343 | } 344 | }), 345 | inputEnhancementsPopup: prefixer({ 346 | display: 'flex', 347 | position: 'absolute', 348 | left: 0, 349 | top: '100%', 350 | zIndex: 10000, 351 | maxHeight: '36rem', 352 | minWidth: '22rem', 353 | backgroundColor: '#FFFFFF', 354 | boxShadow: '1px 1px 4px rgba(100, 100, 100, 0.3)', 355 | flexDirection: 'column' 356 | }), 357 | inputEnhancementsInput: { 358 | paddingRight: '15px' 359 | } 360 | }; 361 | } 362 | 363 | export default createStyling(getStylingFromBase16, { 364 | defaultBase16: defaultTheme 365 | }); 366 | -------------------------------------------------------------------------------- /src/filters/filterByMatchingTextWithThreshold.js: -------------------------------------------------------------------------------- 1 | import getOptionText from '../utils/getOptionText'; 2 | import isStatic from '../utils/isStatic'; 3 | 4 | export default function filterByMatchingTextWithThreshold(threshold) { 5 | return (options, value) => { 6 | if (!value || (threshold && options.length < threshold)) return options; 7 | value = value.toLowerCase(); 8 | 9 | return options.filter(opt => { 10 | return ( 11 | isStatic(opt) || 12 | getOptionText(opt) 13 | .toLowerCase() 14 | .indexOf(value) !== -1 15 | ); 16 | }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/filters/filterRedudantSeparators.js: -------------------------------------------------------------------------------- 1 | export default function filterRedudantSeparators(options) { 2 | const length = options.length; 3 | 4 | return options.filter( 5 | (opt, idx) => 6 | opt !== null || 7 | (idx > 0 && idx !== length - 1 && options[idx - 1] !== null) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | export filterByMatchingTextWithThreshold from './filterByMatchingTextWithThreshold'; 2 | export limitBy from './limitBy'; 3 | export sortByMatchingText from './sortByMatchingText'; 4 | export filterRedudantSeparators from './filterRedudantSeparators'; 5 | export notFoundMessage from './notFoundMessage'; 6 | -------------------------------------------------------------------------------- /src/filters/limitBy.js: -------------------------------------------------------------------------------- 1 | import isStatic from '../utils/isStatic'; 2 | 3 | export default function limitBy(limit) { 4 | return options => options.slice(0, limit + options.filter(isStatic).length); 5 | } 6 | -------------------------------------------------------------------------------- /src/filters/notFoundMessage.js: -------------------------------------------------------------------------------- 1 | import isStatic from '../utils/isStatic'; 2 | 3 | function getEmptyOption(message) { 4 | return { label: message, static: true, disabled: true }; 5 | } 6 | 7 | export default function notFoundMessage(message, ignoreStatic) { 8 | return (options, value) => { 9 | if (!ignoreStatic) { 10 | return options.length === 0 && value 11 | ? [getEmptyOption(message)] 12 | : options; 13 | } 14 | 15 | const staticOptions = options.filter(isStatic); 16 | 17 | return options.length === staticOptions.length && value 18 | ? [...staticOptions, getEmptyOption(message)] 19 | : options; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/filters/sortByMatchingText.js: -------------------------------------------------------------------------------- 1 | import sort from 'lodash.sortby'; 2 | import getOptionText from '../utils/getOptionText'; 3 | import isStatic from '../utils/isStatic'; 4 | 5 | export default function sortByMatchingText(options, value) { 6 | value = value && value.toLowerCase(); 7 | 8 | return sort(options, opt => { 9 | if (isStatic(opt)) { 10 | return 0; 11 | } 12 | 13 | const text = getOptionText(opt).toLowerCase(); 14 | const matching = text.indexOf(value) === 0; 15 | return matching ? 1 : 2; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export Autosize from './Autosize'; 2 | export Autocomplete from './Autocomplete'; 3 | export Dropdown from './Dropdown'; 4 | export Combobox from './Combobox'; 5 | export Mask from './Mask'; 6 | export DatePicker from './DatePicker'; 7 | -------------------------------------------------------------------------------- /src/shapes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | const { shape, oneOfType, string, any } = PropTypes; 3 | 4 | export const ITEM = shape({ 5 | text: string, 6 | label: any, 7 | value: any 8 | }); 9 | 10 | export const ITEM_OR_STRING = oneOfType([ITEM, string]); 11 | -------------------------------------------------------------------------------- /src/themes/default.js: -------------------------------------------------------------------------------- 1 | export default { 2 | scheme: 'default', 3 | author: '', 4 | base00: '#002b36', 5 | base01: '#073642', 6 | base02: '#586e75', 7 | base03: '#657b83', 8 | base04: '#839496', 9 | base05: '#93a1a1', 10 | base06: '#eee8d5', 11 | base07: '#fdf6e3', 12 | base08: '#dc322f', 13 | base09: '#cb4b16', 14 | base0A: '#b58900', 15 | base0B: '#859900', 16 | base0C: '#2aa198', 17 | base0D: '#268bd2', 18 | base0E: '#6c71c4', 19 | base0F: '#d33682' 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/deprecated.js: -------------------------------------------------------------------------------- 1 | const WARNED = []; 2 | 3 | export default function deprecated(message) { 4 | if (WARNED.indexOf(message) === -1) { 5 | console.warn(message); // eslint-disable-line no-console 6 | WARNED.push(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/findMatchingTextIndex.js: -------------------------------------------------------------------------------- 1 | import getOptionText from './getOptionText'; 2 | 3 | const toLower = (val = '') => 4 | (val === null ? '' : val).toString().toLowerCase(); 5 | 6 | export default function findMatchingTextIndex(value, options, allMatches) { 7 | const lowerText = toLower(value); 8 | 9 | const foundOptions = options.reduce((opts, opt, idx) => { 10 | if (opt && opt.disabled) { 11 | return opts; 12 | } 13 | 14 | const optValue = 15 | opt && opt.hasOwnProperty('value') 16 | ? opt.value 17 | : typeof opt === 'string' ? opt : null; 18 | const optText = getOptionText(opt); 19 | const matchPosition = toLower(optText).indexOf(lowerText); 20 | 21 | if ( 22 | (optValue === value && opt !== null) || 23 | (optText && 24 | lowerText && 25 | (allMatches ? matchPosition !== -1 : matchPosition === 0)) 26 | ) { 27 | return [ 28 | ...opts, 29 | [idx, optText, optValue, matchPosition, optText.toLowerCase()] 30 | ]; 31 | } 32 | 33 | return opts; 34 | }, []); 35 | 36 | foundOptions.sort((a, b) => { 37 | return a[3] - b[3] || (a[4] > b[4] ? 1 : -1); 38 | }); 39 | 40 | return foundOptions.length ? foundOptions[0] : [null, null, null]; 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/getComputedStyle.js: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/jonathantneal/Polyfills-for-IE8/blob/master/getComputedStyle.js 2 | 3 | typeof window !== 'undefined' && 4 | !('getComputedStyle' in window) && 5 | (window.getComputedStyle = (function(window) { 6 | function getPixelSize(element, style, property, fontSize) { 7 | const sizeWithSuffix = style[property]; 8 | const size = parseFloat(sizeWithSuffix); 9 | const suffix = sizeWithSuffix.split(/\d/)[0]; 10 | let rootSize; 11 | 12 | fontSize = 13 | fontSize != null 14 | ? fontSize 15 | : /%|em/.test(suffix) && element.parentElement 16 | ? getPixelSize( 17 | element.parentElement, 18 | element.parentElement.currentStyle, 19 | 'fontSize', 20 | null 21 | ) 22 | : 16; 23 | rootSize = 24 | property === 'fontSize' 25 | ? fontSize 26 | : /width/i.test(property) 27 | ? element.clientWidth 28 | : element.clientHeight; 29 | 30 | return suffix === 'em' 31 | ? size * fontSize 32 | : suffix === 'in' 33 | ? size * 96 34 | : suffix === 'pt' 35 | ? size * 96 / 72 36 | : suffix === '%' ? size / 100 * rootSize : size; 37 | } 38 | 39 | function setShortStyleProperty(style, property) { 40 | const borderSuffix = property === 'border' ? 'Width' : ''; 41 | const t = property + 'Top' + borderSuffix; 42 | const r = property + 'Right' + borderSuffix; 43 | const b = property + 'Bottom' + borderSuffix; 44 | const l = property + 'Left' + borderSuffix; 45 | 46 | style[property] = (((style[t] === style[r]) === style[b]) === style[l] 47 | ? [style[t]] 48 | : style[t] === style[b] && style[l] === style[r] 49 | ? [style[t], style[r]] 50 | : style[l] === style[r] 51 | ? [style[t], style[r], style[b]] 52 | : [style[t], style[r], style[b], style[l]] 53 | ).join(' '); 54 | } 55 | 56 | function CSSStyleDeclaration(element) { 57 | const currentStyle = element.currentStyle; 58 | const style = this; 59 | const fontSize = getPixelSize(element, currentStyle, 'fontSize', null); 60 | 61 | for (const property in currentStyle) { 62 | if ( 63 | /width|height|margin.|padding.|border.+W/.test(property) && 64 | style[property] !== 'auto' 65 | ) { 66 | style[property] = 67 | getPixelSize(element, currentStyle, property, fontSize) + 'px'; 68 | } else if (property === 'styleFloat') { 69 | style['float'] = currentStyle[property]; 70 | } else { 71 | style[property] = currentStyle[property]; 72 | } 73 | } 74 | 75 | setShortStyleProperty(style, 'margin'); 76 | setShortStyleProperty(style, 'padding'); 77 | setShortStyleProperty(style, 'border'); 78 | 79 | style.fontSize = fontSize + 'px'; 80 | 81 | return style; 82 | } 83 | 84 | CSSStyleDeclaration.prototype = { 85 | constructor: CSSStyleDeclaration, 86 | getPropertyPriority() {}, 87 | getPropertyValue(prop) { 88 | return window[prop] || ''; 89 | }, 90 | item() {}, 91 | removeProperty() {}, 92 | setProperty() {}, 93 | getPropertyCSSValue() {} 94 | }; 95 | 96 | function getComputedStyle(element) { 97 | return new CSSStyleDeclaration(element); 98 | } 99 | 100 | return getComputedStyle; 101 | })(window)); 102 | -------------------------------------------------------------------------------- /src/utils/getInput.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import deprecated from './deprecated'; 3 | 4 | export default function getInput(cmp) { 5 | if (cmp.props.getInputElement) { 6 | return cmp.props.getInputElement(); 7 | } 8 | 9 | if (cmp.input) { 10 | return cmp.input; 11 | } 12 | 13 | // eslint-disable-next-line 14 | deprecated( 15 | 'Automatic input resolving is deprecated: please provide input instance via `registerInput`' 16 | ); 17 | 18 | const el = ReactDOM.findDOMNode(cmp); 19 | return el.tagName === 'INPUT' ? el : el.getElementsByTagName('INPUT')[0]; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/getOptionLabel.js: -------------------------------------------------------------------------------- 1 | export default function getOptionLabel(opt, highlighted) { 2 | return typeof opt === 'string' || !opt 3 | ? opt 4 | : typeof opt.label === 'function' 5 | ? opt.label(opt, highlighted) 6 | : opt.label || opt.text || opt.value; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/getOptionText.js: -------------------------------------------------------------------------------- 1 | export default function getOptionText(opt) { 2 | if (!opt) return ''; 3 | 4 | const text = Array.find( 5 | [opt, opt.text, opt.label, opt.value], 6 | value => typeof value === 'string' || typeof value === 'number' 7 | ); 8 | 9 | return typeof text === 'number' ? text.toString() : text || ''; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/getOptionValue.js: -------------------------------------------------------------------------------- 1 | export default function getOptionValue(opt) { 2 | return typeof opt === 'string' || !opt ? opt : opt.value; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/isStatic.js: -------------------------------------------------------------------------------- 1 | export default function isStatic(opt) { 2 | return opt === null || (opt && opt.static === true); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/registerInput.js: -------------------------------------------------------------------------------- 1 | export default function registerInput(cmp, input) { 2 | cmp.input = input; 3 | 4 | if (typeof cmp.props.registerInput === 'function') { 5 | cmp.props.registerInput(input); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/renderChild.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | 3 | export default function renderChild( 4 | children, 5 | inputProps, 6 | otherProps, 7 | registerInput 8 | ) { 9 | if (typeof children === 'function') { 10 | return children(inputProps, otherProps, registerInput); 11 | } else { 12 | const input = Children.only(children); 13 | 14 | let props = { 15 | ...inputProps, 16 | ...input.props 17 | }; 18 | 19 | if (props.style) { 20 | props = { 21 | ...props, 22 | style: { 23 | ...(inputProps.style || {}), 24 | ...(input.props.style || {}) 25 | } 26 | }; 27 | } 28 | 29 | return React.cloneElement(input, props); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var SimpleProgressPlugin = require('webpack-simple-progress-plugin'); 5 | 6 | var config = { 7 | devServerPort: 3000 8 | }; 9 | 10 | var isProduction = process.env.NODE_ENV === 'production'; 11 | 12 | module.exports = { 13 | devtool: 'eval', 14 | entry: isProduction 15 | ? ['./demo/src/js/index'] 16 | : [ 17 | 'webpack-dev-server/client?http://localhost:' + config.devServerPort, 18 | 'webpack/hot/only-dev-server', 19 | './demo/src/js/index' 20 | ], 21 | output: { 22 | path: path.resolve(__dirname, 'demo/dist'), 23 | filename: 'js/bundle.js' 24 | }, 25 | plugins: [ 26 | new HtmlWebpackPlugin({ 27 | inject: true, 28 | template: 'demo/src/index.html', 29 | env: process.env 30 | }), 31 | new webpack.DefinePlugin({ 32 | 'process.env': [ 33 | 'NODE_ENV', 34 | 'npm_package_version', 35 | 'npm_package_name', 36 | 'npm_package_description', 37 | 'npm_package_homepage' 38 | ].reduce(function(env, key) { 39 | env[key] = JSON.stringify(process.env[key]); 40 | return env; 41 | }, {}) 42 | }), 43 | new webpack.NoErrorsPlugin(), 44 | new SimpleProgressPlugin() 45 | ].concat(isProduction ? [] : [new webpack.HotModuleReplacementPlugin()]), 46 | resolve: { 47 | extensions: ['.js', '.jsx'], 48 | modules: ['node_modules', path.join(__dirname, 'src')] 49 | }, 50 | module: { 51 | loaders: [ 52 | { 53 | test: /\.jsx?$/, 54 | loaders: ['babel-loader'], 55 | include: [ 56 | path.join(__dirname, 'src'), 57 | path.join(__dirname, 'demo/src/js') 58 | ] 59 | }, 60 | { 61 | test: /\.css$/, 62 | loaders: ['style-loader', 'css-loader?-minimize', 'postcss-loader'] 63 | } 64 | ] 65 | }, 66 | devServer: isProduction 67 | ? undefined 68 | : { 69 | contentBase: 'demo/dist', 70 | quiet: false, 71 | port: config.devServerPort, 72 | hot: true, 73 | stats: { 74 | chunkModules: false, 75 | colors: true 76 | }, 77 | historyApiFallback: true 78 | }, 79 | node: { 80 | fs: 'empty' 81 | } 82 | }; 83 | --------------------------------------------------------------------------------