├── .gitignore
├── jest-preprocessor.js
├── example
├── README.md
├── css
│ └── style.css
├── index.html
├── package.json
└── src
│ └── js
│ ├── ChooseCity.js
│ └── main.js
├── LICENSE
├── src
├── __tests__
│ ├── DropdownInput-shallow-test.js
│ └── DropdownInput-deep-test.js
└── DropdownInput.js
├── package.json
├── README.md
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | bundle.js
3 | bundle.min.js
--------------------------------------------------------------------------------
/jest-preprocessor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var ReactTools = require('react-tools');
3 | module.exports = {
4 | process: function(src) {
5 | return ReactTools.transform(src);
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | React-Dropdown-Input Example
2 | ============================
3 |
4 | See this live at:
5 |
6 | [http://racingtadpole.github.io/react-dropdown-input/example/](http://racingtadpole.github.io/react-dropdown-input/example/)
7 |
8 | Or, clone this repo and build it from source with:
9 |
10 | cd example
11 | npm install
12 | npm run build
13 | python -m SimpleHTTPServer # eg. serves at localhost:8000
14 |
15 |
--------------------------------------------------------------------------------
/example/css/style.css:
--------------------------------------------------------------------------------
1 | .dropdown-input.dropdown-menu>li>a:hover, .dropdown-input.dropdown-menu>li>a:focus {
2 | color:#333;
3 | background-color:#fff;
4 | }
5 | .dropdown-input.dropdown-menu>.active>a:hover, .dropdown-input.dropdown-menu>.active>a:focus {
6 | text-decoration:none;
7 | color:#fff;
8 | background-color:#337ab7;
9 | }
10 | .dropdown-input.dropdown-menu>.disabled>a {
11 | color: #999;
12 | font-style: italic;
13 | }
14 | .dropdown-input.dropdown-menu>.active.disabled>a {
15 | text-decoration:none;
16 | color: #999;
17 | background-color:#fff;
18 | }
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Dropdown Input Example 1
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 RacingTadpole
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 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-dropdown-input-example",
3 | "version": "0.1.0",
4 | "description": "Example of react-dropdown-input",
5 | "homepage": "http://racingtadpole.github.io/react-dropdown-input/example/",
6 | "main": "index.html",
7 | "dependencies": {
8 | "react": ">=0.13",
9 | "react-bootstrap": ">=0.18.0",
10 | "react-dropdown-input": "../"
11 | },
12 | "devDependencies": {
13 | "browserify": "^6.2.0",
14 | "reactify": "^0.15.2",
15 | "uglify-js": "^2.4.19"
16 | },
17 | "scripts": {
18 | "build": "browserify src/js/*.js | uglifyjs -cm > bundle.min.js",
19 | "test": "echo 'No tests yet' && exit 1"
20 | },
21 | "browserify": {
22 | "transform": [
23 | "reactify"
24 | ]
25 | },
26 | "author": "Arthur Street (http://racingtadpole.com/)",
27 | "license": "MIT",
28 | "private": "true",
29 | "keywords": [
30 | "react",
31 | "ecosystem-react",
32 | "react-component",
33 | "bootstrap",
34 | "combobox",
35 | "autocomplete",
36 | "example"
37 | ],
38 | "//": [
39 | "to manually rebuild after a change in the react-dropdown-input module, you can use:",
40 | "cd .. ; npm run build && cd example && rm -rf node_modules/react-dropdown-input/ && npm install && npm run build"
41 | ]
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/example/src/js/ChooseCity.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var DropdownInput = require('react-dropdown-input');
4 |
5 | var ChooseCity =
6 | React.createClass({
7 |
8 | propTypes: {
9 | lookupText: React.PropTypes.string,
10 | options: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired
11 | },
12 |
13 | getInitialState: function() {
14 | return {cityText: null};
15 | },
16 |
17 | handleSelect: function(choice) {
18 | // returns choice.value and choice.index
19 | if (choice.index>=0) {
20 | this.setState({cityText: choice.value + ' is a nice choice'});
21 | } else {
22 | this.setState({cityText: choice.value + ' isn\'t on the list!'});
23 | }
24 | },
25 |
26 | render: function() {
27 | return (
28 |
29 |
37 |
38 |
{this.state.cityText}
39 |
40 |
41 | );
42 | }
43 | });
44 |
45 | module.exports = ChooseCity;
46 |
--------------------------------------------------------------------------------
/example/src/js/main.js:
--------------------------------------------------------------------------------
1 | //
2 | // main.js
3 | //
4 | 'use strict';
5 |
6 | var React = require('react');
7 | var ChooseCity = require('./ChooseCity');
8 |
9 | var names = [
10 | 'Sydney',
11 | 'Melbourne',
12 | 'Brisbane',
13 | 'Perth',
14 | 'Adelaide',
15 | 'Gold Coast',
16 | 'Newcastle',
17 | 'Canberra',
18 | 'Sunshine Coast',
19 | 'Wollongong',
20 | 'Hobart',
21 | 'Geelong',
22 | 'Townsville',
23 | 'Cairns',
24 | 'Darwin',
25 | 'Toowoomba',
26 | 'Ballarat',
27 | 'Bendigo',
28 | 'Launceston',
29 | 'Albury-Wodonga',
30 | 'Mackay',
31 | 'Rockhampton',
32 | 'Bundaberg',
33 | 'Bunbury',
34 | 'Coffs Harbour',
35 | 'Wagga Wagga',
36 | 'Hervey Bay',
37 | 'Mildura',
38 | 'Shepparton',
39 | 'Gladstone',
40 | 'Port Macquarie',
41 | 'Tamworth',
42 | 'Traralgon',
43 | 'Orange',
44 | 'Geraldton',
45 | 'Bowral',
46 | 'Dubbo',
47 | 'Nowra',
48 | 'Bathurst',
49 | 'Warrnambool',
50 | 'Kalgoorlie',
51 | 'Busselton',
52 | 'Albany',
53 | 'Warragul',
54 | 'Devonport'
55 | ].sort();
56 |
57 | React.render(
58 |
68 | , document.getElementById('example')
69 | );
70 |
--------------------------------------------------------------------------------
/src/__tests__/DropdownInput-shallow-test.js:
--------------------------------------------------------------------------------
1 | //
2 | // In these tests, we mock React-Bootstrap, so we can only check it has the right components
3 | //
4 |
5 | /*global jest, describe, it, expect */
6 | 'use strict';
7 |
8 | jest.dontMock('../DropdownInput');
9 | var React = require('react/addons');
10 | var DropdownInput = require('../DropdownInput');
11 | var ReactBootstrap = require('react-bootstrap');
12 | var TestUtils = React.addons.TestUtils;
13 |
14 | describe('DropdownInput', function() {
15 |
16 | var names = ['Aab', 'abcd', 'Cde', 'Def', 'Deb', 'Eabc'];
17 | var menuClassName = 'jest-test';
18 | var id = 'test-id';
19 | var element = ();
20 | var result;
21 |
22 | beforeEach(function() {
23 | var shallowRenderer = TestUtils.createRenderer();
24 | shallowRenderer.render(element);
25 | result = shallowRenderer.getRenderOutput();
26 | });
27 |
28 | it('contains the right components', function() {
29 | // there should be one Input component and one DropdownMenu component
30 | // for now, assume in that order - TODO: generalize
31 | var child0 = result.props.children[0];
32 | var child1 = result.props.children[1];
33 | expect(child0.type).toEqual(ReactBootstrap.Input);
34 | expect(child1.type).toEqual(ReactBootstrap.DropdownMenu);
35 | // also check the dropdown menu has been passed the right class name
36 | expect(child1.props.className).toEqual(menuClassName);
37 | });
38 |
39 | it('contains the right menu items', function() {
40 | var child1 = result.props.children[1];
41 | var child1props = child1.props.children[0]; // not very general
42 | expect(child1props[0].props.children).toContain(names[0]);
43 | });
44 |
45 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-dropdown-input",
3 | "version": "0.1.11",
4 | "description": "Displays a Bootstrap Input element with a DropdownMenu of possible options (similar to a combobox)",
5 | "homepage": "http://racingtadpole.github.io/react-dropdown-input/",
6 | "main": "index.js",
7 | "dependencies": {
8 | "classnames": ">=1.1.4",
9 | "react": ">=0.12",
10 | "react-bootstrap": ">=0.18.0"
11 | },
12 | "devDependencies": {
13 | "babel": "^4.7.16",
14 | "babel-eslint": "^2.0.2",
15 | "eslint": "^0.18.0",
16 | "eslint-plugin-react": "^2.0.2",
17 | "jest-cli": "0.2.1",
18 | "react-tools": "^0.13.1"
19 | },
20 | "jest": {
21 | "scriptPreprocessor": "/jest-preprocessor.js",
22 | "unmockedModulePathPatterns": ["/node_modules/react"]
23 | },
24 | "//": [
25 | "to run tests, I use node 0.10.x and jest-cli 0.2.1 as jest 0.4.0 doesn't work on my Mac OS with node v0.12.x",
26 | "but to build, I use node 0.12.x because babel won't run on node 0.10.x"
27 | ],
28 | "scripts": {
29 | "build": "npm run lint && node_modules/.bin/babel src/DropdownInput.js > index.js",
30 | "lint": "./node_modules/eslint/bin/eslint.js src/DropdownInput.js example/src/js/ChooseCity.js",
31 | "test": "jest",
32 | "test-debug": "node-debug --nodejs --harmony ./node_modules/jest-cli/bin/jest.js --runInBand"
33 | },
34 | "author": "Arthur Street (http://racingtadpole.com/)",
35 | "license": "MIT",
36 | "repository": {
37 | "type": "git",
38 | "url": "https://github.com/RacingTadpole/react-dropdown-input.git"
39 | },
40 | "bugs": {
41 | "url": "https://github.com/RacingTadpole/react-dropdown-input/issues"
42 | },
43 | "keywords": [
44 | "react",
45 | "ecosystem-react",
46 | "react-component",
47 | "bootstrap",
48 | "combobox",
49 | "autocomplete"
50 | ],
51 | "eslintConfig": {
52 | "parser": "babel-eslint",
53 | "plugins": [
54 | "react"
55 | ],
56 | "ecmaFeatures": {
57 | "jsx": true
58 | },
59 | "env": {
60 | "node": true,
61 | "browser": true
62 | },
63 | "rules": {
64 | "react/display-name": 0,
65 | "react/jsx-quotes": 1,
66 | "react/jsx-no-undef": 1,
67 | "react/jsx-sort-props": 0,
68 | "react/jsx-uses-react": 1,
69 | "react/jsx-uses-vars": 1,
70 | "react/no-did-mount-set-state": 1,
71 | "react/no-did-update-set-state": 1,
72 | "react/no-multi-comp": 1,
73 | "react/no-unknown-property": 1,
74 | "react/prop-types": 2,
75 | "react/react-in-jsx-scope": 1,
76 | "react/self-closing-comp": 1,
77 | "react/wrap-multilines": 1,
78 | "quotes": [
79 | 1,
80 | "single",
81 | "avoid-escape"
82 | ]
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/__tests__/DropdownInput-deep-test.js:
--------------------------------------------------------------------------------
1 | //
2 | // In these tests, we do NOT mock React-Bootstrap, so we can check it has the right behaviour
3 | //
4 |
5 | /*global jest, describe, it, expect, beforeEach */
6 | 'use strict';
7 |
8 | jest.dontMock('../DropdownInput');
9 | jest.dontMock('react-bootstrap');
10 | jest.dontMock('classnames');
11 | var React = require('react/addons');
12 | var DropdownInput = require('../DropdownInput');
13 | var ReactBootstrap = require('react-bootstrap');
14 | var TestUtils = React.addons.TestUtils;
15 |
16 | describe('DropdownInput', function() {
17 |
18 | var names = ['Aab', 'abcd', 'Cde', 'Def', 'Deb', 'Eabc'];
19 | var menuClassName = 'jest-test';
20 | var id = 'test-id';
21 | var element = ();
22 | var renderedItem;
23 |
24 | beforeEach(function() {
25 | renderedItem = TestUtils.renderIntoDocument(element);
26 | });
27 |
28 | it('contains an initially empty input tag', function() {
29 | var input = TestUtils.findRenderedDOMComponentWithTag(renderedItem, 'input').getDOMNode();
30 | expect(input.textContent).toEqual('');
31 | });
32 |
33 | it('contains the menu class and items', function() {
34 | var menu = TestUtils.findRenderedDOMComponentWithClass(renderedItem, menuClassName).getDOMNode();
35 | var items = menu.children;
36 | for (var i=0; i < items.length; i++) {
37 | expect(items[i].textContent).toEqual(names[i]);
38 | }
39 | });
40 |
41 | it('adjusts the menu when the input changes', function() {
42 | var txt = 'a';
43 | var input = TestUtils.findRenderedDOMComponentWithTag(renderedItem, 'input').getDOMNode();
44 | //TestUtils.Simulate.keyDown(input, {key: 'a'}); // this doesn't work
45 | TestUtils.Simulate.change(input, {target: {value: txt}});
46 | expect(input.value).toEqual(txt);
47 | // did the menu respond appropriately?
48 | var menu = TestUtils.findRenderedDOMComponentWithClass(renderedItem, menuClassName).getDOMNode();
49 | var items = menu.children;
50 | var matchingNames = names.filter(function(n) { return n.indexOf(txt)>=0; });
51 | expect(items.length).toEqual(matchingNames.length);
52 | for (var i=0; i < items.length; i++) {
53 | expect(items[i].textContent).toEqual(matchingNames[i]);
54 | }
55 | });
56 |
57 | it('maxes out with a final message', function() {
58 | var max = 3;
59 | var element = ();
60 | var renderedItem = TestUtils.renderIntoDocument(element);
61 | var menu = TestUtils.findRenderedDOMComponentWithClass(renderedItem, menuClassName).getDOMNode();
62 | var items = menu.children;
63 | expect(items.length).toEqual(max);
64 | expect(items[max - 1].textContent).toEqual('+' + (names.length - max + 1) + ' more');
65 | });
66 |
67 | it('still shows all options if length == max', function() {
68 | var max = names.length;
69 | var element = ();
70 | var renderedItem = TestUtils.renderIntoDocument(element);
71 | var menu = TestUtils.findRenderedDOMComponentWithClass(renderedItem, menuClassName).getDOMNode();
72 | var items = menu.children;
73 | expect(items.length).toEqual(max);
74 | expect(items[max - 1].textContent).toEqual(names[names.length - 1]);
75 | });
76 |
77 | });
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.org/package/react-dropdown-input)
2 |
3 | React-Dropdown-Input
4 | ====================
5 |
6 | React-dropdown-input displays a "combobox" for [React](http://facebook.github.io/react/).
7 | More explicitly, it shows a text input box; once the user starts to type, a dropdown menu
8 | opens with matching options. The user can choose one of those options either by clicking one,
9 | or by using the arrow keys and hitting Enter.
10 |
11 | It is styled with bootstrap, using the [React-Bootstrap](http://react-bootstrap.github.io/) package; it actually displays a `ReactBootstrap.Input` element
12 | with a `ReactBootstrap.DropdownMenu` of possible options.
13 |
14 | ## Demo
15 |
16 | [http://racingtadpole.github.io/react-dropdown-input/example/](http://racingtadpole.github.io/react-dropdown-input/example/)
17 |
18 | ## Installation
19 |
20 | npm install react-dropdown-input --save
21 |
22 | ## Sample Usage
23 |
24 | var searchNames = ['Sydney', 'Melbourne', 'Brisbane',
25 | 'Adelaide', 'Perth', 'Hobart'];
26 | //...
27 |
34 |
35 | ## In more detail...
36 |
37 | The options are simply passed as a javascript array (or [immutablejs](http://facebook.github.io/immutable-js/) object)
38 | to the `options` prop.
39 |
40 | Supply one or both of these callbacks: `onSelect` & `onChange`.
41 |
42 | - `onSelect` fires when an option is clicked, or when Enter is pressed.
43 | It passes the object:
44 |
45 | { value: input text,
46 | index: option index, or -1 if user entered their own text and pressed Enter
47 | }
48 | - `onChange` fires whenever the input text value changes, either due to a click or typing.
49 | It passes the object:
50 |
51 | { value: input text }
52 |
53 | Other props you can pass:
54 |
55 | - `filter`: a function that determines which options to show, given the input text
56 | (see `defaultFilter` in the code for the default).
57 | - `menuClassName`: a class for the menu, which you need for the css styling below;
58 | eg. 'dropdown-input'.
59 | - `max`: the maximum number of options to display.
60 | - `maxText`: text of a disabled MenuItem to show at the end of a list, if the max is exceeded
61 | replaces '#' with the number not shown; defaults to '+# more not shown'.
62 |
63 | You can also pass `` all the properties that `` allows,
64 | eg. `ButtonAfter`.
65 |
66 | IMPORTANT NOTE ABOUT CSS
67 | ------------------------
68 |
69 | You need to turn off Bootstrap's hover highlighting css for this element;
70 | we do it manually using the active class instead. You may also need to re-enable
71 | the hover highlighting on the active class. Eg. in sass, add this:
72 |
73 | .dropdown-input.dropdown-menu > li > a {
74 | &:hover,
75 | &:focus {
76 | color: $dropdown-link-color; // #333
77 | background-color: $dropdown-bg; // #fff
78 | }
79 | }
80 | .dropdown-input.dropdown-menu > .active > a {
81 | &:hover,
82 | &:focus {
83 | text-decoration: none;
84 | color: $dropdown-link-hover-color; // #fff
85 | background-color: $dropdown-link-hover-bg; // #337ab7
86 | }
87 | }
88 |
89 | If you're showing `maxText`, you may also want to make sure it can't be selected too:
90 |
91 | .dropdown-input.dropdown-menu>.active.disabled>a {
92 | text-decoration: none;
93 | color: $dropdown-link-disabled-color; // #777
94 | background-color: $dropdown-bg; // #fff
95 | }
96 |
97 | ## Release History
98 |
99 | * 0.1.0 Initial release
100 | * 0.1.1 Point to js (not jsx), update README
101 | * 0.1.2 Update example
102 | * 0.1.3 Align package.json version number with git tag
103 | * 0.1.4 Added maxText property
104 | * 0.1.5 Added eslint to dev
105 | * 0.1.6 Corrected number not shown
106 | * 0.1.7 Don't pass options and menuClassName props through to Input
107 | * 0.1.8 Added working tests using jest
108 | * 0.1.9 Use self-closing tag in ReadMe
109 | * 0.1.10 Remove extra comments, rename var as DropdownInput
110 | * 0.1.11 Prevent form submission if open
111 |
--------------------------------------------------------------------------------
/src/DropdownInput.js:
--------------------------------------------------------------------------------
1 | //
2 | // react-dropdown-input
3 | // Displays a ReactBootstrap.Input element
4 | // with a ReactBootstrap.DropdownMenu of possible options.
5 | //
6 |
7 | 'use strict';
8 |
9 | var React = require('react/addons');
10 | var ReactBootstrap = require('react-bootstrap');
11 | var joinClasses = require('react/lib/joinClasses');
12 | var cx = require('classnames');
13 |
14 | var BootstrapMixin = ReactBootstrap.BootstrapMixin;
15 | var DropdownStateMixin = ReactBootstrap.DropdownStateMixin;
16 | var DropdownMenu = ReactBootstrap.DropdownMenu;
17 | var Input = ReactBootstrap.Input;
18 | var MenuItem = ReactBootstrap.MenuItem;
19 |
20 | var defaultMaxText = '+# more not shown';
21 |
22 | var defaultFilter = function(filterText, optionName) { // also optionIndex as third arg
23 | return (optionName.toLowerCase().indexOf(filterText.toLowerCase()) >= 0);
24 | };
25 |
26 | var genLength = function(list) {
27 | // deal with both regular arrays and immutablejs objects, which have .count() instead of length
28 | return (typeof list.count !== 'undefined' ? list.count() : list.length);
29 | };
30 |
31 | var genGet = function(list, i) {
32 | // deal with both regular arrays and immutablejs objects, which have list.get(i) instead of list[i]
33 | return (typeof list.get !== 'undefined' ? list.get(i) : list[i]);
34 | };
35 |
36 | var caseInsensIndexOf = function(list, str) {
37 | var lowerList = list.map(function(item) { return item.toLowerCase(); });
38 | return lowerList.indexOf(str.toLowerCase());
39 | };
40 |
41 |
42 | var DropdownInput = React.createClass({
43 |
44 | mixins: [BootstrapMixin, DropdownStateMixin],
45 |
46 | propTypes: {
47 | pullRight: React.PropTypes.bool,
48 | dropup: React.PropTypes.bool,
49 | defaultValue: React.PropTypes.string,
50 | menuClassName: React.PropTypes.string,
51 | max: React.PropTypes.number,
52 | maxText: React.PropTypes.string,
53 | onChange: React.PropTypes.func,
54 | onSelect: React.PropTypes.func,
55 | navItem: React.PropTypes.bool,
56 | options: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired,
57 | filter: React.PropTypes.func,
58 | // the rest are to make eslint happy
59 | id: React.PropTypes.string,
60 | className: React.PropTypes.string,
61 | bsSize: React.PropTypes.string
62 | },
63 |
64 | getInitialState: function () {
65 | return {
66 | value: this.props.defaultValue || '',
67 | activeIndex: -1
68 | };
69 | },
70 |
71 | filteredOptions: function() {
72 | var filter = this.props.filter || defaultFilter;
73 | return this.props.options.filter(filter.bind(undefined, this.state.value));
74 | },
75 |
76 | cappedLength: function(options) {
77 | var total = genLength(options);
78 | if (total>this.props.max) {
79 | // if it exceeded the max, we took an extra one off
80 | total = this.props.max - 1;
81 | }
82 | return total;
83 | },
84 |
85 | render: function () {
86 | var classes = {
87 | 'dropdown': true,
88 | 'open': this.state.open,
89 | 'dropup': this.props.dropup
90 | };
91 | // you can provide a filter prop, which is a function(filterText, optionName, optionIndex) which should
92 | // return true to show option with the given name and index, given the input filterText.
93 | var filteredOptions = this.filteredOptions();
94 | var numFiltered = genLength(filteredOptions);
95 | var maxMenuItem = null;
96 | var maxText = typeof this.props.maxText === 'undefined' ? defaultMaxText : this.props.maxText;
97 | if (this.props.max && numFiltered > this.props.max) {
98 | // take an extra one off, to leave space for the maxText
99 | filteredOptions = filteredOptions.slice(0, this.props.max - 1);
100 | maxText = maxText.replace('#', (numFiltered - this.props.max + 1));
101 | maxMenuItem = this.renderAsMenuItem(maxText, this.props.max, null, true);
102 | }
103 | var dropdown = null;
104 | if (numFiltered>0) {
105 | dropdown = (
112 | {filteredOptions.map(this.renderAsMenuItem)}
113 | {maxMenuItem}
114 | );
115 | }
116 | return (
117 |
118 |
135 | {dropdown}
136 |
137 | );
138 | },
139 |
140 | renderAsMenuItem: function(item, index, options, disabled) {
141 | var start = item.toLowerCase().indexOf(this.state.value.toLowerCase()),
142 | end = start + this.state.value.length,
143 | part1 = item.slice(0, start),
144 | part2 = item.slice(start, end),
145 | part3 = item.slice(end);
146 | var classes = cx({active: this.state.activeIndex===index, disabled: disabled===true});
147 | if (disabled) {
148 | // don't highlight parts of disabled items, eg. the maxText
149 | part1 = item;
150 | part2 = null;
151 | part3 = null;
152 | }
153 | return (
154 |
161 | );
162 | },
163 |
164 | handleInputChange: function(e) {
165 | // the user changed the input text
166 | this.setState({value: e.target.value, activeIndex: -1});
167 | this.setDropdownState(true);
168 | // fire the supplied onChange event.
169 | this.sendChange({value: e.target.value});
170 | },
171 |
172 | handleKeyDown: function(e) {
173 | // catch arrow keys and the Enter key
174 | var filteredOptions = this.filteredOptions();
175 | var numOptions = this.cappedLength(filteredOptions);
176 | var newName;
177 | switch(e.keyCode){
178 |
179 | case 38: // up arrow
180 | if (this.state.activeIndex>0) {
181 | this.setState({activeIndex: this.state.activeIndex-1});
182 | } else {
183 | this.setState({activeIndex: numOptions-1});
184 | }
185 | break;
186 |
187 | case 40: // down arrow
188 | this.setState({activeIndex: (this.state.activeIndex+1) % numOptions});
189 | break;
190 |
191 | case 13: // enter
192 | var newIndex = caseInsensIndexOf(this.props.options, this.state.value); // may need this
193 | if (this.state.open) {
194 | e.preventDefault();
195 | }
196 | if (this.state.activeIndex >= 0 && this.state.activeIndex < numOptions) {
197 | newIndex = this.state.activeIndex;
198 | newName = genGet(filteredOptions, this.state.activeIndex);
199 | this.setDropdownState(false);
200 | } else if (this.state.activeIndex === -1 && newIndex >= 0) {
201 | newName = genGet(this.props.options, newIndex);
202 | this.setDropdownState(false);
203 | } else {
204 | newIndex = this.state.activeIndex;
205 | newName = this.state.value;
206 | }
207 | this.sendSelect({value: newName, index: newIndex});
208 | this.sendChange({value: newName});
209 | this.setState({value: newName, activeIndex: -1});
210 | break;
211 |
212 | }
213 | },
214 |
215 | handleMouseEnter: function(index) {
216 | // when the mouse enters a dropdown menu item, set the active item to the item
217 | this.setState({activeIndex: index});
218 | },
219 |
220 | handleDropdownClick: function (e) {
221 | e.preventDefault();
222 |
223 | this.setDropdownState(!this.state.open);
224 | },
225 |
226 | handleOptionSelect: function(key, name) {
227 | // the user clicked on a dropdown menu item
228 | this.setDropdownState(false);
229 | this.sendSelect({value: name, index: this.state.activeIndex});
230 | this.sendChange({value: name});
231 | this.setState({value: name, activeIndex: -1});
232 | },
233 |
234 | sendChange: function(e) {
235 | if (this.props.onChange) {
236 | this.props.onChange(e);
237 | }
238 | },
239 |
240 | sendSelect: function(e) {
241 | if (this.props.onSelect) {
242 | this.props.onSelect(e);
243 | }
244 | }
245 |
246 |
247 | });
248 |
249 | module.exports = DropdownInput;
250 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | //
2 | // react-dropdown-input
3 | // Displays a ReactBootstrap.Input element
4 | // with a ReactBootstrap.DropdownMenu of possible options.
5 | //
6 |
7 | "use strict";
8 |
9 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
10 |
11 | var React = require("react/addons");
12 | var ReactBootstrap = require("react-bootstrap");
13 | var joinClasses = require("react/lib/joinClasses");
14 | var cx = require("classnames");
15 |
16 | var BootstrapMixin = ReactBootstrap.BootstrapMixin;
17 | var DropdownStateMixin = ReactBootstrap.DropdownStateMixin;
18 | var DropdownMenu = ReactBootstrap.DropdownMenu;
19 | var Input = ReactBootstrap.Input;
20 | var MenuItem = ReactBootstrap.MenuItem;
21 |
22 | var defaultMaxText = "+# more not shown";
23 |
24 | var defaultFilter = function defaultFilter(filterText, optionName) {
25 | // also optionIndex as third arg
26 | return optionName.toLowerCase().indexOf(filterText.toLowerCase()) >= 0;
27 | };
28 |
29 | var genLength = function genLength(list) {
30 | // deal with both regular arrays and immutablejs objects, which have .count() instead of length
31 | return typeof list.count !== "undefined" ? list.count() : list.length;
32 | };
33 |
34 | var genGet = function genGet(list, i) {
35 | // deal with both regular arrays and immutablejs objects, which have list.get(i) instead of list[i]
36 | return typeof list.get !== "undefined" ? list.get(i) : list[i];
37 | };
38 |
39 | var caseInsensIndexOf = function caseInsensIndexOf(list, str) {
40 | var lowerList = list.map(function (item) {
41 | return item.toLowerCase();
42 | });
43 | return lowerList.indexOf(str.toLowerCase());
44 | };
45 |
46 | var DropdownInput = React.createClass({
47 | displayName: "DropdownInput",
48 |
49 | mixins: [BootstrapMixin, DropdownStateMixin],
50 |
51 | propTypes: {
52 | pullRight: React.PropTypes.bool,
53 | dropup: React.PropTypes.bool,
54 | defaultValue: React.PropTypes.string,
55 | menuClassName: React.PropTypes.string,
56 | max: React.PropTypes.number,
57 | maxText: React.PropTypes.string,
58 | onChange: React.PropTypes.func,
59 | onSelect: React.PropTypes.func,
60 | navItem: React.PropTypes.bool,
61 | options: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired,
62 | filter: React.PropTypes.func,
63 | // the rest are to make eslint happy
64 | id: React.PropTypes.string,
65 | className: React.PropTypes.string,
66 | bsSize: React.PropTypes.string
67 | },
68 |
69 | getInitialState: function getInitialState() {
70 | return {
71 | value: this.props.defaultValue || "",
72 | activeIndex: -1
73 | };
74 | },
75 |
76 | filteredOptions: function filteredOptions() {
77 | var filter = this.props.filter || defaultFilter;
78 | return this.props.options.filter(filter.bind(undefined, this.state.value));
79 | },
80 |
81 | cappedLength: function cappedLength(options) {
82 | var total = genLength(options);
83 | if (total > this.props.max) {
84 | // if it exceeded the max, we took an extra one off
85 | total = this.props.max - 1;
86 | }
87 | return total;
88 | },
89 |
90 | render: function render() {
91 | var classes = {
92 | dropdown: true,
93 | open: this.state.open,
94 | dropup: this.props.dropup
95 | };
96 | // you can provide a filter prop, which is a function(filterText, optionName, optionIndex) which should
97 | // return true to show option with the given name and index, given the input filterText.
98 | var filteredOptions = this.filteredOptions();
99 | var numFiltered = genLength(filteredOptions);
100 | var maxMenuItem = null;
101 | var maxText = typeof this.props.maxText === "undefined" ? defaultMaxText : this.props.maxText;
102 | if (this.props.max && numFiltered > this.props.max) {
103 | // take an extra one off, to leave space for the maxText
104 | filteredOptions = filteredOptions.slice(0, this.props.max - 1);
105 | maxText = maxText.replace("#", numFiltered - this.props.max + 1);
106 | maxMenuItem = this.renderAsMenuItem(maxText, this.props.max, null, true);
107 | }
108 | var dropdown = null;
109 | if (numFiltered > 0) {
110 | dropdown = React.createElement(
111 | DropdownMenu,
112 | {
113 | className: this.props.menuClassName,
114 | ref: "menu",
115 | "aria-labelledby": this.props.id,
116 | pullRight: this.props.pullRight,
117 | key: 1,
118 | onSelect: null },
119 | filteredOptions.map(this.renderAsMenuItem),
120 | maxMenuItem
121 | );
122 | }
123 | return React.createElement(
124 | "div",
125 | { className: joinClasses(this.props.className, cx(classes)) },
126 | React.createElement(Input, _extends({}, this.props, {
127 | menuClassName: null,
128 | options: null,
129 | type: "text",
130 | bsSize: this.props.bsSize,
131 | ref: "dropdownInput",
132 | onClick: this.handleDropdownClick,
133 | key: 0,
134 | navDropdown: this.props.navItem,
135 | navItem: null,
136 | pullRight: null,
137 | onSelect: null,
138 | onChange: this.handleInputChange,
139 | onKeyDown: this.handleKeyDown,
140 | dropup: null,
141 | value: this.state.value })),
142 | dropdown
143 | );
144 | },
145 |
146 | renderAsMenuItem: function renderAsMenuItem(item, index, options, disabled) {
147 | var start = item.toLowerCase().indexOf(this.state.value.toLowerCase()),
148 | end = start + this.state.value.length,
149 | part1 = item.slice(0, start),
150 | part2 = item.slice(start, end),
151 | part3 = item.slice(end);
152 | var classes = cx({ active: this.state.activeIndex === index, disabled: disabled === true });
153 | if (disabled) {
154 | // don't highlight parts of disabled items, eg. the maxText
155 | part1 = item;
156 | part2 = null;
157 | part3 = null;
158 | }
159 | return React.createElement(
160 | MenuItem,
161 | {
162 | key: index,
163 | onSelect: this.handleOptionSelect.bind(this, index, item),
164 | className: classes,
165 | onMouseEnter: this.handleMouseEnter.bind(this, index) },
166 | part1,
167 | React.createElement(
168 | "b",
169 | null,
170 | part2
171 | ),
172 | part3
173 | );
174 | },
175 |
176 | handleInputChange: function handleInputChange(e) {
177 | // the user changed the input text
178 | this.setState({ value: e.target.value, activeIndex: -1 });
179 | this.setDropdownState(true);
180 | // fire the supplied onChange event.
181 | this.sendChange({ value: e.target.value });
182 | },
183 |
184 | handleKeyDown: function handleKeyDown(e) {
185 | // catch arrow keys and the Enter key
186 | var filteredOptions = this.filteredOptions();
187 | var numOptions = this.cappedLength(filteredOptions);
188 | var newName;
189 | switch (e.keyCode) {
190 |
191 | case 38:
192 | // up arrow
193 | if (this.state.activeIndex > 0) {
194 | this.setState({ activeIndex: this.state.activeIndex - 1 });
195 | } else {
196 | this.setState({ activeIndex: numOptions - 1 });
197 | }
198 | break;
199 |
200 | case 40:
201 | // down arrow
202 | this.setState({ activeIndex: (this.state.activeIndex + 1) % numOptions });
203 | break;
204 |
205 | case 13:
206 | // enter
207 | var newIndex = caseInsensIndexOf(this.props.options, this.state.value); // may need this
208 | if (this.state.open) {
209 | e.preventDefault();
210 | }
211 | if (this.state.activeIndex >= 0 && this.state.activeIndex < numOptions) {
212 | newIndex = this.state.activeIndex;
213 | newName = genGet(filteredOptions, this.state.activeIndex);
214 | this.setDropdownState(false);
215 | } else if (this.state.activeIndex === -1 && newIndex >= 0) {
216 | newName = genGet(this.props.options, newIndex);
217 | this.setDropdownState(false);
218 | } else {
219 | newIndex = this.state.activeIndex;
220 | newName = this.state.value;
221 | }
222 | this.sendSelect({ value: newName, index: newIndex });
223 | this.sendChange({ value: newName });
224 | this.setState({ value: newName, activeIndex: -1 });
225 | break;
226 |
227 | }
228 | },
229 |
230 | handleMouseEnter: function handleMouseEnter(index) {
231 | // when the mouse enters a dropdown menu item, set the active item to the item
232 | this.setState({ activeIndex: index });
233 | },
234 |
235 | handleDropdownClick: function handleDropdownClick(e) {
236 | e.preventDefault();
237 |
238 | this.setDropdownState(!this.state.open);
239 | },
240 |
241 | handleOptionSelect: function handleOptionSelect(key, name) {
242 | // the user clicked on a dropdown menu item
243 | this.setDropdownState(false);
244 | this.sendSelect({ value: name, index: this.state.activeIndex });
245 | this.sendChange({ value: name });
246 | this.setState({ value: name, activeIndex: -1 });
247 | },
248 |
249 | sendChange: function sendChange(e) {
250 | if (this.props.onChange) {
251 | this.props.onChange(e);
252 | }
253 | },
254 |
255 | sendSelect: function sendSelect(e) {
256 | if (this.props.onSelect) {
257 | this.props.onSelect(e);
258 | }
259 | }
260 |
261 | });
262 |
263 | module.exports = DropdownInput;
264 |
265 |
--------------------------------------------------------------------------------