├── .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 [](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 |
51 | `;
52 |
53 | exports[`Combobox renders correctly with function child 1`] = `
54 |
103 | `;
104 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/DatePicker.test.js.snap:
--------------------------------------------------------------------------------
1 | exports[`DatePicker renders correctly with element child 1`] = `
2 |
55 | `;
56 |
57 | exports[`DatePicker renders correctly with function child 1`] = `
58 |
111 | `;
112 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/Dropdown.test.js.snap:
--------------------------------------------------------------------------------
1 | exports[`Dropdown renders correctly with element child 1`] = `
2 |
55 | `;
56 |
57 | exports[`Dropdown renders correctly with function child 1`] = `
58 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------